@protobase/cli 0.67.2 → 0.67.3

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.
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.67.2";
2
+ var version = "0.67.3";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.67.2",
3
+ "version": "0.67.3",
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.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",
46
+ "@protobase/config": "0.67.3",
47
+ "@protobase/layout": "0.67.3",
48
+ "@protobase/query": "0.67.3",
49
+ "@protobase/schema": "0.67.3",
50
+ "@protobase/server": "0.67.3",
51
+ "@protobase/ui": "0.67.3",
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,5 @@
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. Every server process serves a new table within a second of it being added.
7
+ - [Tables defined in the database](../reference/user-tables.md#tables-defined-in-the-database) (beta): 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.
8
+ - [Adding tables and fields](../reference/user-tables.md#adding-tables-and-fields) (beta): Someone with the right capability creates a table with its fields, or adds a field to it, through the app's API. It is created in the database at once, with an index for every field but JSON so its list can be filtered by it, and sorted by it unless it is long text, and only admins can see it until access is widened.
@@ -8,15 +8,15 @@ An app's resources usually come from its code: a resource in `config/` over a ta
8
8
 
9
9
  ## Tables defined in the database
10
10
 
11
- :::caution[In development]
12
- The server serves the tables defined in the database, but no screen or API adds one yet.
11
+ :::caution[Beta]
12
+ Tables are added through the API; the admin app has no screens for them yet, and an app with organizations cannot use them.
13
13
  :::
14
14
 
15
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
- <gap>Users cannot add a table yet because the screens and the API that create one are not implemented.</gap>
19
+ <gap>Users cannot add a table from the admin app yet because its screens for tables and fields are not implemented.</gap>
20
20
 
21
21
  ### The switch
22
22
 
@@ -34,7 +34,7 @@ The tables users add and their definitions live in two schemas of the app's data
34
34
 
35
35
  | Schema | What it holds |
36
36
  | --- | --- |
37
- | `table_definitions` | `tables`, one row per table users add, with its singular and plural label, its title field and a version, and `fields`, each table's fields in order with their label, type and whether they are required |
37
+ | `table_definitions` | `tables`, one row per table users add, with its singular and plural label, its title field and a version, 1 when it is added, and `fields`, each table's fields in order with their label, type and whether they are required |
38
38
  | `user_tables` | the tables themselves, each with an `id bigint generated always as identity` primary key and a column per field |
39
39
 
40
40
  With `userTables: true`, [`protobase migration`](cli.md#migration) writes a migration that adds both schemas, the two definition tables and the `pg_trgm` extension when it is missing, and grants the app's database role CREATE on `user_tables` alone, and writes nothing once the database has them. Apply it with the project's other migrations; the server never creates them itself. At start, `dev` and `serve` stop while one is missing; `PROTOBASE_STARTUP_CHECKS=false` skips that check in `serve`, as it skips the others.
@@ -43,7 +43,7 @@ A table's name and each field's name are lowercase letters, digits and `_`, star
43
43
 
44
44
  | Type | Column |
45
45
  | --- | --- |
46
- | `text` | `text` |
46
+ | `text` | `text`, at most 500 characters |
47
47
  | `long_text` | `text`, edited in a box that grows with it |
48
48
  | `integer` | `integer` |
49
49
  | `decimal` | `numeric`, with the definition's precision and scale |
@@ -68,7 +68,7 @@ When the server starts, and whenever the definitions change, it builds a resourc
68
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
69
 
70
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.
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`, as the [API](#adding-tables-and-fields) does; adding and removing a definition change the version on their own.
72
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
73
 
74
74
  Measured with six fields per table on one machine running Postgres and the processes:
@@ -81,3 +81,42 @@ Measured with six fields per table on one machine running Postgres and the proce
81
81
  | 500 | about 2.2 s | about 4.5 s |
82
82
 
83
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.
84
+
85
+ ## Adding tables and fields
86
+
87
+ :::caution[Beta]
88
+ Tables and fields are added through the API alone, and are never renamed, retyped or removed.
89
+ :::
90
+
91
+ Someone with the right capability creates a table with its fields, or adds a field to it, through the app's API. It is created in the database at once, with an index for every field but JSON so its list can be filtered by it, and sorted by it unless it is long text, and only admins can see it until access is widened.
92
+
93
+ With `userTables: true`, the API has two resources over the definitions, with the standard addresses and routes of the [API](api.md#routes):
94
+
95
+ | Resource | A record | Fields |
96
+ | --- | --- | --- |
97
+ | `user_tables` | a table, at `/user_tables/<id>` | `name`, `singular_label`, `plural_label`, `title_field`; `id` and `version` are read-only |
98
+ | `user_table_fields` | a field of a table, at `/user_table_fields/<table_id>,<name>` | `table_id`, `name`, `label`, `type` (one of the [types](#the-schemas-and-the-migration)), `position` (the order of the fields), `precision` and `scale` (a decimal's alone) |
99
+
100
+ Make a table with its fields in one [batch write](api.md#batch-writes): the table, each field pointing at it, then the title field, which names a field that exists. Nothing is created unless all of it is:
101
+
102
+ ```json
103
+ POST /api/v1:batchWrite
104
+ {
105
+ "ops": [
106
+ { "op": "create", "resource": "user_tables", "ref": "notes", "data": { "name": "notes", "singular_label": "Note", "plural_label": "Notes" } },
107
+ { "op": "create", "resource": "user_table_fields", "data": { "table_id": { "$ref": "notes" }, "name": "title", "label": "Title", "type": "text", "position": 1 } },
108
+ { "op": "create", "resource": "user_table_fields", "data": { "table_id": { "$ref": "notes" }, "name": "price", "label": "Price", "type": "decimal", "precision": 12, "scale": 2, "position": 2 } },
109
+ { "op": "update", "resource": "user_tables", "key": { "$ref": "notes" }, "etag": "*", "data": { "title_field": "title" } }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ The answer has the table's record, so a client links to the new table at `/notes`. Add a field later with `POST /api/v1/user_table_fields`.
115
+
116
+ - **What is created.** A table is `user_tables.<name>` with its `id bigint generated always as identity` key. A field is a column of its type that may be empty and has no default, so the records already there read it as empty, with a btree index, or a `pg_trgm` one for `long_text`, whose values outgrow a btree entry; a `json` field gets none. The title field gets the `pg_trgm` index search needs. The rows, the table or column, its indexes and the version are written in one transaction: anything refused leaves none of them.
117
+ - **What is refused.** A name outside the rules above, a word SQL reserves such as `select`, a field named `id` or `etag`, the name of a resource in code, of one Protobase adds or of a table `user_tables` already has, a field marked required, and a title that is not a text field of the table, each as a field error. A table whose name a resource in code has now, which is how a table is taken over as code, takes no new field.
118
+ - **What changes later.** The labels, the title field and the fields' order. A name, a type, a decimal's precision and scale never change, and no table or field is deleted. <gap>A table or field cannot be renamed, retyped or removed because changing the columns of a table users add is not implemented.</gap>
119
+ - **Who may.** With the project's [roles](access.md), the capabilities `user_tables.create` and `user_table_fields.create`, which only `*` or a grant that names them holds, never a wildcard such as `*.create`, like [resources that need named grants](access.md#resources-that-need-named-grants); reading and changing the labels need `.read` and `.update` the same way. Without roles, admins. Each new table is its admins' alone, as [each one served](#how-each-one-is-served) is.
120
+ - **The rest of the app.** The process that made the change serves it before it answers, and every other process [within a second](#how-a-change-reaches-every-process). While a deploy freezes writes, a create answers as any write does then.
121
+
122
+ Building an index holds the table's write lock, and every statement runs within the request's statement timeout (1 s by default). <gap>Adding a field to a table with very many records cannot finish when building its index takes longer than the statement timeout, because building it outside the request is not implemented; the field is then not added.</gap>