@protobase/cli 0.67.1 → 0.67.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/package.js +1 -1
- package/dist/dev/entry.js +2 -3
- package/dist/project/project-admin.js +8 -6
- package/dist/project/project-user-tables.js +3 -8
- package/dist/project/served-admin.js +29 -0
- package/dist/serve/lifecycle.js +2 -3
- package/package.json +7 -7
- package/skills/protobase/references/features/user-tables.md +1 -1
- package/skills/protobase/references/reference/user-tables.md +22 -5
package/dist/cli/package.js
CHANGED
package/dist/dev/entry.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { buildProject } from "../config/build-project.js";
|
|
2
2
|
import { projectIcons } from "../build/app-icons.js";
|
|
3
3
|
import { projectErrorReporter } from "../project/error-reporter.js";
|
|
4
|
-
import {
|
|
5
|
-
import { projectUserTables } from "../project/project-user-tables.js";
|
|
4
|
+
import { servedAdmin } from "../project/served-admin.js";
|
|
6
5
|
import { devJobs } from "./dev-jobs.js";
|
|
7
6
|
import { devRealtimePrune } from "./dev-prune.js";
|
|
8
7
|
import { devDatabase, loadDevConfig } from "./project.js";
|
|
@@ -56,7 +55,7 @@ const ui = {
|
|
|
56
55
|
fieldTypes: {},
|
|
57
56
|
icons: projectIcons(projectDir)
|
|
58
57
|
};
|
|
59
|
-
const { admin, jobs } =
|
|
58
|
+
const { admin, jobs } = await servedAdmin(project, db, ui, reporter, upgradeWebSocket);
|
|
60
59
|
if (process.env.PROTOBASE_DEV_JOBS !== "false") await devJobs(db, jobs, admin.decryptField, project.mailer);
|
|
61
60
|
devRealtimePrune(admin);
|
|
62
61
|
var entry_default = withDevServiceWorker(new Hono(), Boolean(project.notifications)).route("/", admin);
|
|
@@ -7,11 +7,15 @@ import { configExports, createAdmin } from "@protobase/server";
|
|
|
7
7
|
* project's own and the delivery of its events and history. With a `reporter`, every error the API turns into a 500,
|
|
8
8
|
* and every stopped query, goes to the error tracker too, besides the project's `onUnhandledError` or the console. With
|
|
9
9
|
* the host's `upgradeWebSocket`, it serves `/api/sb/realtime/v1` too. With `userTables`, the tables users add serve
|
|
10
|
-
* beside the config's resources (see `
|
|
10
|
+
* beside the config's resources (see `readUserTables`). `report` is what the admin does with an unhandled error.
|
|
11
11
|
*/
|
|
12
12
|
const projectAdmin = (project, db, ui, reporter, upgradeWebSocket, userTables) => {
|
|
13
13
|
const { resources, views, pages, userMenu, bottomBars, subscriptions } = configExports(project.config);
|
|
14
14
|
const unhandled = project.options?.onUnhandledError ?? ((error) => console.error(error));
|
|
15
|
+
const report = (error, request) => {
|
|
16
|
+
reporter?.report(error, request);
|
|
17
|
+
unhandled(error, request);
|
|
18
|
+
};
|
|
15
19
|
const admin = createAdmin({
|
|
16
20
|
resources,
|
|
17
21
|
views,
|
|
@@ -44,15 +48,13 @@ const projectAdmin = (project, db, ui, reporter, upgradeWebSocket, userTables) =
|
|
|
44
48
|
...upgradeWebSocket && { upgradeWebSocket },
|
|
45
49
|
options: {
|
|
46
50
|
...project.options,
|
|
47
|
-
onUnhandledError:
|
|
48
|
-
reporter?.report(error, request);
|
|
49
|
-
unhandled(error, request);
|
|
50
|
-
}
|
|
51
|
+
onUnhandledError: report
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
return {
|
|
54
55
|
admin,
|
|
55
|
-
jobs: [...project.jobs, ...admin.jobs]
|
|
56
|
+
jobs: [...project.jobs, ...admin.jobs],
|
|
57
|
+
report
|
|
56
58
|
};
|
|
57
59
|
};
|
|
58
60
|
//#endregion
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { userTableResources } from "@protobase/server";
|
|
2
2
|
//#region src/project/project-user-tables.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
* when the definitions are in the database. Each definition that does not fit the database is printed as a warning and
|
|
6
|
-
* left out, so the rest of the app serves.
|
|
7
|
-
*/
|
|
8
|
-
const projectUserTables = async (project, db) => {
|
|
9
|
-
if (!project.userTables) return;
|
|
3
|
+
/** The tables users add, read from their definitions; each one that does not fit the database is printed as a warning and left out, so the rest of the app serves. */
|
|
4
|
+
const readUserTables = async (db) => {
|
|
10
5
|
const tables = await userTableResources(db);
|
|
11
6
|
for (const problem of tables.problems) console.warn(problem);
|
|
12
7
|
return tables;
|
|
13
8
|
};
|
|
14
9
|
//#endregion
|
|
15
|
-
export {
|
|
10
|
+
export { readUserTables };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { projectAdmin } from "./project-admin.js";
|
|
2
|
+
import { readUserTables } from "./project-user-tables.js";
|
|
3
|
+
import { readDefinitionsVersion, reloadingAdmin } from "@protobase/server";
|
|
4
|
+
//#region src/project/served-admin.ts
|
|
5
|
+
/**
|
|
6
|
+
* The admin a serving process answers with (`projectAdmin`). With `userTables`, it is rebuilt when the tables users add
|
|
7
|
+
* change (`reloadingAdmin`); the version is read before the tables, so a change between the two reads rebuilds again.
|
|
8
|
+
*/
|
|
9
|
+
const servedAdmin = async (project, db, ui, reporter, upgradeWebSocket) => {
|
|
10
|
+
if (!project.userTables) return projectAdmin(project, db, ui, reporter, upgradeWebSocket);
|
|
11
|
+
const version = await readDefinitionsVersion(db);
|
|
12
|
+
const tables = await readUserTables(db);
|
|
13
|
+
const built = projectAdmin(project, db, ui, reporter, upgradeWebSocket, tables);
|
|
14
|
+
const admin = reloadingAdmin({
|
|
15
|
+
first: built.admin,
|
|
16
|
+
version,
|
|
17
|
+
readVersion: () => readDefinitionsVersion(db),
|
|
18
|
+
readTables: () => readUserTables(db),
|
|
19
|
+
build: (changed) => projectAdmin(project, db, ui, reporter, upgradeWebSocket, changed).admin,
|
|
20
|
+
report: built.report,
|
|
21
|
+
...project.options?.statementTimeoutMs !== void 0 && { statementTimeoutMs: project.options.statementTimeoutMs }
|
|
22
|
+
});
|
|
23
|
+
return {
|
|
24
|
+
...built,
|
|
25
|
+
admin
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { servedAdmin };
|
package/dist/serve/lifecycle.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { protobaseVersion } from "../version/version.js";
|
|
2
2
|
import { buildProject } from "../config/build-project.js";
|
|
3
3
|
import { openAppDatabase } from "../project/pg-db.js";
|
|
4
|
-
import {
|
|
5
|
-
import { projectUserTables } from "../project/project-user-tables.js";
|
|
4
|
+
import { servedAdmin } from "../project/served-admin.js";
|
|
6
5
|
import { apiWebSocketServer } from "../project/api-websocket-server.js";
|
|
7
6
|
import { withClientAddress } from "./client-address.js";
|
|
8
7
|
import { createServeApp } from "./app.js";
|
|
@@ -50,7 +49,7 @@ const startServe = async ({ config, env, openDb = (url) => openAppDatabase(pg, u
|
|
|
50
49
|
if (env.startupChecks && project.notifications) await checkNotificationsSchema(db, project.notifications, project.auth);
|
|
51
50
|
if (env.startupChecks && project.status) await checkStatusSchema(db, project.status, project.auth);
|
|
52
51
|
if (env.startupChecks && project.userTables) await checkUserTablesSchema(db);
|
|
53
|
-
const { admin, jobs: definitions } =
|
|
52
|
+
const { admin, jobs: definitions } = await servedAdmin(project, db, ui, reporter, upgradeWebSocket);
|
|
54
53
|
if (env.startupChecks && definitions.length > 0) await checkJobsSchema(db);
|
|
55
54
|
const jobs = definitions.length > 0 ? await startJobs({
|
|
56
55
|
db,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.67.
|
|
3
|
+
"version": "0.67.2",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.67.
|
|
47
|
-
"@protobase/layout": "0.67.
|
|
48
|
-
"@protobase/query": "0.67.
|
|
49
|
-
"@protobase/schema": "0.67.
|
|
50
|
-
"@protobase/server": "0.67.
|
|
51
|
-
"@protobase/ui": "0.67.
|
|
46
|
+
"@protobase/config": "0.67.2",
|
|
47
|
+
"@protobase/layout": "0.67.2",
|
|
48
|
+
"@protobase/query": "0.67.2",
|
|
49
|
+
"@protobase/schema": "0.67.2",
|
|
50
|
+
"@protobase/server": "0.67.2",
|
|
51
|
+
"@protobase/ui": "0.67.2",
|
|
52
52
|
"@react-email/render": "^2.1.0",
|
|
53
53
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
54
54
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
|
|
5
5
|
Tables an app's users add from the admin UI, by hand or from a file, and how a developer takes them over as code.
|
|
6
6
|
|
|
7
|
-
- [Tables defined in the database](../reference/user-tables.md#tables-defined-in-the-database) (in development): An app can keep tables its users add in its own database, beside the resources in its code. Each one is served like any other resource, at its own address with its list, record page, search and API.
|
|
7
|
+
- [Tables defined in the database](../reference/user-tables.md#tables-defined-in-the-database) (in development): An app can keep tables its users add in its own database, beside the resources in its code. Each one is served like any other resource, at its own address with its list, record page, search and API. Every server process serves a new table within a second of it being added.
|
|
@@ -9,17 +9,15 @@ An app's resources usually come from its code: a resource in `config/` over a ta
|
|
|
9
9
|
## Tables defined in the database
|
|
10
10
|
|
|
11
11
|
:::caution[In development]
|
|
12
|
-
The server serves the tables defined in the database, but no screen or API adds one yet
|
|
12
|
+
The server serves the tables defined in the database, but no screen or API adds one yet.
|
|
13
13
|
:::
|
|
14
14
|
|
|
15
|
-
An app can keep tables its users add in its own database, beside the resources in its code. Each one is served like any other resource, at its own address with its list, record page, search and API.
|
|
15
|
+
An app can keep tables its users add in its own database, beside the resources in its code. Each one is served like any other resource, at its own address with its list, record page, search and API. Every server process serves a new table within a second of it being added.
|
|
16
16
|
|
|
17
17
|
<gap>Tables users add cannot be used in an app with organizations because keeping their definitions and records per organization is not implemented.</gap>
|
|
18
18
|
|
|
19
19
|
<gap>Users cannot add a table yet because the screens and the API that create one are not implemented.</gap>
|
|
20
20
|
|
|
21
|
-
<gap>A definition added while the server runs cannot be served before the server starts again because picking up new definitions while serving is not implemented.</gap>
|
|
22
|
-
|
|
23
21
|
### The switch
|
|
24
22
|
|
|
25
23
|
User tables are off by default. Turn them on in `protobase.config.ts`:
|
|
@@ -56,7 +54,7 @@ A table's name and each field's name are lowercase letters, digits and `_`, star
|
|
|
56
54
|
|
|
57
55
|
### How each one is served
|
|
58
56
|
|
|
59
|
-
When the server starts, it builds a resource from each definition and serves it at `/<table>` and `/<table>/<key>`, with its list, record page, sidebar entry and REST API, and in `/meta`, as the resources in code are. The labels of the definition are the names the admin app shows.
|
|
57
|
+
When the server starts, and whenever the definitions change, it builds a resource from each definition and serves it at `/<table>` and `/<table>/<key>`, with its list, record page, sidebar entry and REST API, and in `/meta`, as the resources in code are. The labels of the definition are the names the admin app shows.
|
|
60
58
|
|
|
61
59
|
- **Admins only.** Each table starts readable and writable by admins alone, and is left out of role wildcards such as `*.read`, like [resources that need named grants](access.md#resources-that-need-named-grants). Anyone else does not see it.
|
|
62
60
|
- **Filters, sorts and search follow the indexes.** A field can be filtered or sorted by only where an index of its table serves it, and the table is searched on its title field only where a `pg_trgm` index serves that field, as the [scan guard](config/api.md#scanguard) requires.
|
|
@@ -64,3 +62,22 @@ When the server starts, it builds a resource from each definition and serves it
|
|
|
64
62
|
- **Code wins.** A resource in code with the name of a user table, or a resource Protobase adds, is served instead of the definition, which is then ignored with a warning. This is how a table users added is taken over as code.
|
|
65
63
|
|
|
66
64
|
[`protobase doctor`](cli.md#doctor) checks each definition against the database as it checks the resources in code, and reports each one it leaves out as an error. [`protobase scaffold`](cli.md#scaffold) leaves out the definitions and the tables in `user_tables` that no resource in code claims, and writes and updates the config of one that a resource claims.
|
|
65
|
+
|
|
66
|
+
### How a change reaches every process
|
|
67
|
+
|
|
68
|
+
A definition added, changed or removed is served by every process of `serve` and `dev` within a second, with no restart or deploy. Each process rebuilds its API from the resources in code and the definitions as they are now, and the admin app reads `/meta` again once `X-Meta-Version` changes, as after any change of the app.
|
|
69
|
+
|
|
70
|
+
- **When a process looks.** `/meta` reads the version of the definitions before it answers. Any other request reads it at most once a second, and once more when its answer would be not found or invalid, as for a resource or field the process does not know yet; it is then answered by the API built anew. The process that makes a change rebuilds at once. A process that receives no request reads nothing, so a database that scales to zero stays asleep.
|
|
71
|
+
- **What a look costs.** The version is one read of `table_definitions.tables`, under a millisecond. Each change of a definition or its fields must raise the definition's `version`; adding and removing a definition change the version on their own.
|
|
72
|
+
- **What a rebuild costs.** A rebuild reads the definitions and the indexes of each table, then builds the API, once per process: requests that need it meanwhile wait for that one rebuild. Requests already running finish on the API they started with, and an open WebSocket keeps the API it opened with. <gap>A channel socket opened before a rebuild cannot hear a broadcast sent through a socket opened after it, in the same process and without a realtime relay, until it reconnects, because moving open sockets to the rebuilt API is not implemented.</gap> <gap>A request whose body is not JSON, such as a file upload, that names a table added under a second ago cannot be answered by the rebuilt API because replaying a streamed body is not implemented; it is answered not found, and works on the next try.</gap>
|
|
73
|
+
|
|
74
|
+
Measured with six fields per table on one machine running Postgres and the processes:
|
|
75
|
+
|
|
76
|
+
| Tables users add | Rebuild, one process | Rebuild, each of four processes at once |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| 10 | under 0.1 s | under 0.1 s |
|
|
79
|
+
| 50 | about 0.1 s | about 0.4 s |
|
|
80
|
+
| 200 | about 0.7 s | about 1.4 s |
|
|
81
|
+
| 500 | about 2.2 s | about 4.5 s |
|
|
82
|
+
|
|
83
|
+
- **A rebuild that fails.** When the API does not build with the new definitions, the process reports why through `onUnhandledError`, naming what it refused, and keeps serving the API it had, with the resources in code. A definition that does not fit the database is left out with a warning, as at start.
|