mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-05-04 23:49:58 -05:00
Docs: Group API documentation - make "extending" an overview that ingrates more intentionally with its sibling "Record API" and "JS API" docs. #97
This commit is contained in:
@@ -101,13 +101,17 @@ export default defineConfig({
|
||||
slug: "documentation/auth",
|
||||
},
|
||||
{
|
||||
label: "APIs",
|
||||
label: "Endpoints",
|
||||
items: [
|
||||
{
|
||||
slug: "documentation/record_apis",
|
||||
label: "Overview",
|
||||
slug: "documentation/apis_overview",
|
||||
},
|
||||
{
|
||||
slug: "documentation/js_apis",
|
||||
slug: "documentation/apis_record",
|
||||
},
|
||||
{
|
||||
slug: "documentation/apis_js",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -123,9 +127,6 @@ export default defineConfig({
|
||||
{
|
||||
slug: "documentation/production",
|
||||
},
|
||||
{
|
||||
slug: "documentation/extending",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: JS/TS APIs
|
||||
---
|
||||
|
||||
import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
On startup TrailBase will automatically load any JavaScript and TypeScript
|
||||
files in `traildepot/scripts`.
|
||||
This can be used to implement arbitrary HTTP APIs using custom handlers.
|
||||
|
||||
## Example HTTP Endpoint
|
||||
|
||||
The following example illustrates a few things:
|
||||
|
||||
* How to register a parameterized route with `{table}`.
|
||||
* How to implement a handler that returns `text/plain` content. There is also
|
||||
`jsonHandler` and `htmlHandler`.
|
||||
* How to query the database.
|
||||
* How to return an error.
|
||||
|
||||
```js
|
||||
import {
|
||||
addRoute,
|
||||
query,
|
||||
stringHandler,
|
||||
HttpError,
|
||||
StatusCodes
|
||||
} from "../trailbase.js";
|
||||
|
||||
addRoute("GET", "/test/{table}", stringHandler(async (req) => {
|
||||
const table = req.params["table"];
|
||||
if (table) {
|
||||
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
|
||||
return `entries: ${rows[0][0]}`;
|
||||
}
|
||||
|
||||
throw new HttpError(
|
||||
StatusCodes.BAD_REQUEST, "Missing '?table=' search query param");
|
||||
}));
|
||||
```
|
||||
|
||||
More examples can be found in the repository in
|
||||
`client/testfixture/scripts/index.ts`.
|
||||
|
||||
## Runtime Details
|
||||
|
||||
At its heart, TrailBase's runtime is a pool of V8 worker threads - called
|
||||
*isolates* - alongside a runtime that supports basic tasks such as file I/O, web
|
||||
requests, timers, etc.
|
||||
While it uses Deno's V8-integration under the hood, it is *not* a full Node.js
|
||||
runtime, at least for now.
|
||||
|
||||
<Aside type="note" title="State Sharing">
|
||||
Different *isolates* do not share state, i.e. you cannot use global state to
|
||||
reliably share state across requests. Instead, state can be persisted using
|
||||
the database.
|
||||
</Aside>
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
title: Endpoints
|
||||
description: Collocating your custom business logic
|
||||
---
|
||||
|
||||
import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
When building any connected application, eventually you'll want to call an API
|
||||
endpoint:
|
||||
|
||||
1. to [read/write some data](#data-apis), or
|
||||
2. to run some [server-side application logic](#custom-endpoints-inside-trailbase).
|
||||
|
||||
Especially for application logic, where and how it should run depends on your
|
||||
app, your environment, and external requirements.
|
||||
|
||||
For example, for rich client-side applications - such as mobile, desktop, and
|
||||
progressive web apps - it is often a good idea to run inside the user's device.
|
||||
It is cheap, snappy, privacy-friendly and able to run offline or on a spotty
|
||||
connection.
|
||||
That said, there are very valid reasons to not run everything in an untrusted,
|
||||
resource and battery-limited, hard to update sandbox.
|
||||
|
||||
## Data APIs
|
||||
|
||||
Data APIs are limited APIs that let you perform a common operations on your
|
||||
data: **read/list**, **create**, **update** and **delete**.
|
||||
|
||||
In the simplest case, you can use TrailBase's [record
|
||||
APIs](/documentation/apis_record/) to define limited-access APIs over your
|
||||
`TABLE`s and `VIEW`s.
|
||||
If you require more flexibility, the following provides an overview of ways to
|
||||
run arbitrary logic.
|
||||
|
||||
|
||||
## Custom Endpoints inside TrailBase
|
||||
|
||||
There are several ways to integrate custom logic into TrailBase, however you
|
||||
can also run logic outside in your [own backend](#bring-your-own-backend).
|
||||
|
||||
Within TrailBase you have the following options:
|
||||
|
||||
1. [JS & TS handlers](#javascript--typescript-handlers),
|
||||
2. [Stored database procedures](#stored-procedures),
|
||||
3. [Native Rust handlers](#rust-handlers).
|
||||
|
||||
|
||||
### JavaScript & TypeScript Handlers
|
||||
|
||||
TrailBase's built-in runtime enables wiring custom HTTP endpoints using full
|
||||
ES6 JavaScript and/or TypeScript.
|
||||
For more context, check out the dedicated [docs](/documentation/apis_js/).
|
||||
|
||||
### Stored Procedures
|
||||
|
||||
Unlike Postgres or MySQL, SQLite does not support stored procedures out of the
|
||||
box.
|
||||
TrailBase makes Sqlean's
|
||||
[user-defined functions](https://github.com/nalgeon/sqlean/blob/main/docs/define.md)
|
||||
available to fill the gap.
|
||||
They can be accessed from record APIs through `VIEW`s or custom handlers.
|
||||
|
||||
<Aside type="note" title="Portability">
|
||||
Sqlean can be used with any SQLite client, thus avoiding lock-in.
|
||||
</Aside>
|
||||
|
||||
{/* Is this too obtuse to actually be useful?
|
||||
### SQLite Extensions and Virtual Tables
|
||||
|
||||
Likely the most bespoke approach is to expose your functionality as a custom
|
||||
SQLite extension or module similar to how TrailBase extends SQLite itself.
|
||||
|
||||
This approach can be somewhat limiting in terms of dependencies you have
|
||||
access to and things you can do especially for extensions. Modules are quite a bit
|
||||
more flexible but also involved.
|
||||
Take a look at [SQLite's list](https://www.sqlite.org/vtablist.html) and
|
||||
[osquery](https://osquery.readthedocs.io/en/stable/) to get a sense of what's
|
||||
possible.
|
||||
|
||||
Besides their limitations, major advantages of using extensions or
|
||||
modules are:
|
||||
* you have extremely low-overhead access to your data,
|
||||
* extensions and modules can also be used by services accessing the
|
||||
underlying SQLite databases.
|
||||
*/}
|
||||
|
||||
### Rust Handlers
|
||||
|
||||
The Rust APIs aren't yet stable and fairly undocumented.
|
||||
That said, similar to using PocketBase as a Go framework, you can build your
|
||||
own TrailBase binary and register custom
|
||||
[axum](https://github.com/tokio-rs/axum) HTTP handlers written in rust with the
|
||||
main application router, see `/examples/custom-binary`.
|
||||
|
||||
<Aside type="note" title="API Stability">
|
||||
the Rust APIs are subject to change. However, we will rely on semantic
|
||||
versioning to communicate breaking changes explicitly.
|
||||
</Aside>
|
||||
|
||||
|
||||
## Bring your own Backend
|
||||
|
||||
The most flexible and de-coupled way of running your own code is to deploy a
|
||||
separate service in front of or alongside TrailBase.
|
||||
This gives you full control over your destiny: language, runtime, scaling,
|
||||
deployment, etc.
|
||||
|
||||
TrailBase is designed with the explicit goal of running along a sea of other
|
||||
services.
|
||||
Its stateless tokens using asymmetric crypto make it easy for other resource
|
||||
servers to hermetically authenticate your users.
|
||||
TrailBase's APIs can be accessed transitively, simply by forwarding users'
|
||||
[auth tokens](/documentation/auth/) [^1].
|
||||
|
||||
Alternatively, for more of a side-car setup you can fall back to accessing the
|
||||
SQLite database directly, both for data access and schema alterations[^2].
|
||||
|
||||
---
|
||||
|
||||
[^1]:
|
||||
We would like to add service accounts in the future to authorize privileged
|
||||
services independent from user-provided tokens or using fake user-accounts
|
||||
for services.
|
||||
|
||||
[^2]:
|
||||
SQLite is running in WAL mode, which allows for parallel reads and
|
||||
concurrent writes. That said, when possible you should probably use the APIs
|
||||
since falling back to raw database access is a priviledge practically reserved
|
||||
to processes with access to a shared file-system.
|
||||
@@ -1,105 +0,0 @@
|
||||
---
|
||||
title: Custom Logic
|
||||
description: Collocating your custom business logic
|
||||
---
|
||||
|
||||
import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
This article explores different ways to integrate your App with TrailBase,
|
||||
extend it and your custom logic.
|
||||
|
||||
The question of where code should run weighs heavily on the web: push
|
||||
everything to the server, more to the client or even the edge?
|
||||
Answering this question is s a lot simpler for rich client-side applications
|
||||
such as mobile, desktop, and progressive web apps or SPAs where the inclination
|
||||
is to run on the users device providing privacy friendly, snappy interactivity
|
||||
and offline capabilities.
|
||||
There are perfectly good reasons to not run everything in an untrusted, battery
|
||||
limited, SEO unfriendly client-side sandbox but the overall need for
|
||||
server-side execution is greatly reduced.
|
||||
**It's rich client-side apps where application servers like TrailBase can shine
|
||||
providing common server-side functionality and strategic extension points**.
|
||||
|
||||
|
||||
## Bring your own Backend
|
||||
|
||||
The most flexible and likewise de-coupled way of running your own code is to
|
||||
deploy a separate service alongside TrailBase. This gives you full control over
|
||||
your destiny: runtime, scaling, deployment, etc.
|
||||
|
||||
TrailBase is designed with the explicit goal of running along a sea of other
|
||||
services.
|
||||
Its stateless tokens using asymmetric crypto make it easy for other resource
|
||||
servers to hermetically authenticate your users.
|
||||
TrailBase's APIs can be accessed transitively, simply by forwarding user
|
||||
tokens [^1].
|
||||
Alternatively, you can fall back to raw SQLite for reads, writes and even
|
||||
schema alterations[^2].
|
||||
|
||||
## Custom APIs in TrailBase
|
||||
|
||||
TrailBase provides a couple of ways to embed custom logic and provide custom APIs endpoints:
|
||||
|
||||
1. Rust HTTP handlers using Axum,
|
||||
2. JS/TS handlers [APIs](/documentation/js_apis/),
|
||||
3. Stored database procedures,
|
||||
3. SQLite extensions and modules (virtual tables).
|
||||
|
||||
<Aside type="note" title="Rust Handlers">
|
||||
the Rust APIs are subject to change. However, we will rely on semantic
|
||||
versioning to communicate breaking changes explicitly.
|
||||
</Aside>
|
||||
|
||||
### Using ES6 JavaScript & TypeScript
|
||||
|
||||
You can write custom HTTP endpoints using both full ES6 JavaScript and/or
|
||||
TypeScript. TrailBase will transpile your code on the fly and execute it on a
|
||||
speedy V8-engine, the same engine found across Chrome, node.js and deno.
|
||||
More information can be found in the [API docs](/documentation/js_apis/).
|
||||
|
||||
### Using Rust
|
||||
|
||||
The Rust APIs aren't yet stable and fairly undocumented.
|
||||
That said, similar to using PocketBase as a Go framework, you can build your
|
||||
own TrailBase binary and register custom Axum handlers written in rust with the
|
||||
main application router, see `/examples/custom-binary`.
|
||||
|
||||
### Stored Procedures
|
||||
|
||||
Unlike Postgres or MySQL, SQLite does not support stored procedures out of the
|
||||
box.
|
||||
However, TrailBase has integrated sqlean's
|
||||
[user-defined functions](https://github.com/nalgeon/sqlean/blob/main/docs/define.md)
|
||||
to fill the gap. You can easily adopt SQLean in your own backends avoiding
|
||||
lock-in.
|
||||
|
||||
### SQLite Extensions and Modules a.k.a. Virtual Tables
|
||||
|
||||
Likely the most bespoke approach is to expose your functionality as a custom
|
||||
SQLite extension or module similar to how TrailBase extends SQLite itself.
|
||||
|
||||
This approach can be somewhat limiting in terms of dependencies you have
|
||||
access to and things you can do especially for extensions. Modules are quite a bit
|
||||
more flexible but also involved.
|
||||
Take a look at [SQLite's list](https://www.sqlite.org/vtablist.html) and
|
||||
[osquery](https://osquery.readthedocs.io/en/stable/) to get a sense of what's
|
||||
possible.
|
||||
|
||||
Besides their limitations, major advantages of using extensions or
|
||||
modules are:
|
||||
* you have extremely low-overhead access to your data,
|
||||
* extensions and modules can also be used by services accessing the
|
||||
underlying SQLite databases.
|
||||
|
||||
---
|
||||
|
||||
[^1]:
|
||||
We would like to add service accounts in the future to authorize privileged
|
||||
services independent from user-provided tokens or using fake user-accounts
|
||||
for services.
|
||||
|
||||
[^2]:
|
||||
SQLite is running in WAL mode, which allows for parallel reads and
|
||||
concurrent writes. That said, when possible you should probably use the APIs
|
||||
since falling back to raw database access is a priviledge practically reserved
|
||||
to processes with access to a shared file-system.
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: JS/TS APIs
|
||||
---
|
||||
|
||||
import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
You can place JavaScript and TypeScript into `traildepot/scripts` and TrailBase
|
||||
will automatically load them on startup.
|
||||
For now we support custom HTTP handlers letting you register routes, act on
|
||||
requests, query the database and build arbitrary responses.
|
||||
|
||||
## Runtime
|
||||
|
||||
Before we jump into details, let's quickly talk about the runtime itself. At
|
||||
its heart, it's a pool of V8-js-engines alongside a runtime that supports basic
|
||||
tasks such as file I/O, web requests, timers, etc.
|
||||
However, it is *not* a complete Node.js runtime, at least for now, since it
|
||||
would pull in a lot of extra dependencies.
|
||||
Note further, that the pool of workers/isolates does not share state, i.e. you
|
||||
cannot use global state to reliably share state across requests. You should
|
||||
rely on the database for persisting and sharing state.
|
||||
|
||||
## HTTP Endpoints
|
||||
|
||||
The following example illustrates a few things:
|
||||
|
||||
* How to register a parameterized route with `:table`.
|
||||
* How to implement a handler that returns `text/plain` content. There is also
|
||||
`jsonHandler` and `htmlHandler`.
|
||||
* How to query the database.
|
||||
* How to return an error.
|
||||
|
||||
```js
|
||||
import {
|
||||
addRoute,
|
||||
query,
|
||||
stringHandler,
|
||||
HttpError,
|
||||
StatusCodes
|
||||
} from "../trailbase.js";
|
||||
|
||||
addRoute("GET", "/test/{table}", stringHandler(async (req) => {
|
||||
const table = req.params["table"];
|
||||
if (table) {
|
||||
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
|
||||
return `entries: ${rows[0][0]}`;
|
||||
}
|
||||
|
||||
throw new HttpError(
|
||||
StatusCodes.BAD_REQUEST, "Missing '?table=' search query parm");
|
||||
}));
|
||||
```
|
||||
|
||||
More examples can be found in the repository in
|
||||
`client/testfixture/scripts/index.ts`.
|
||||
@@ -69,7 +69,7 @@ export interface Article {
|
||||
|
||||
TrailBase also supports generating type-safe bindings for columns containing
|
||||
JSON data and enforcing a specific JSON schema, see
|
||||
[here](/documentation/record_apis/#custom-json-schemas).
|
||||
[here](/documentation/apis_record/#custom-json-schemas).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user