@protobase/cli 0.68.2 → 0.68.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.
- package/dist/cli/package.js +1 -1
- package/package.json +8 -8
- package/skills/protobase/references/features/user-tables.md +1 -0
- package/skills/protobase/references/guides/import-records.md +13 -3
- package/skills/protobase/references/reference/ui-config.md +1 -1
- package/skills/protobase/references/reference/user-tables.md +50 -2
package/dist/cli/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.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,13 +43,13 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@better-auth/core": "^1.7.12",
|
|
45
45
|
"@hono/node-server": "^2.1.3",
|
|
46
|
-
"@protobase/config": "0.68.
|
|
47
|
-
"@protobase/layout": "0.68.
|
|
48
|
-
"@protobase/query": "0.68.
|
|
49
|
-
"@protobase/schema": "0.68.
|
|
50
|
-
"@protobase/server": "0.68.
|
|
51
|
-
"@protobase/ui": "0.68.
|
|
52
|
-
"@protobase/workflows": "0.68.
|
|
46
|
+
"@protobase/config": "0.68.3",
|
|
47
|
+
"@protobase/layout": "0.68.3",
|
|
48
|
+
"@protobase/query": "0.68.3",
|
|
49
|
+
"@protobase/schema": "0.68.3",
|
|
50
|
+
"@protobase/server": "0.68.3",
|
|
51
|
+
"@protobase/ui": "0.68.3",
|
|
52
|
+
"@protobase/workflows": "0.68.3",
|
|
53
53
|
"@react-email/render": "^2.1.0",
|
|
54
54
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
55
55
|
"@sentry/vite-plugin": "^11.10.0",
|
|
@@ -7,5 +7,6 @@ Tables an app's users add from the admin UI, by hand or from a file, and how a d
|
|
|
7
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
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.
|
|
9
9
|
- [Adding a table in the admin app](../reference/user-tables.md#in-the-admin-app) (beta): People who may add tables find Tables in the admin app, make a table with its fields and add fields later, on a computer or a phone. The new table appears in the sidebar at once.
|
|
10
|
+
- [A table from a file](../reference/user-tables.md#a-table-from-a-file) (beta): An Excel, CSV or JSON file becomes a new table: each column a field typed from what its cells hold, each row a record. Values are never guessed: `007`, `1,5` and `03/04/2026` stay text, and an empty cell stays empty.
|
|
10
11
|
- [Taking a table over as code](../reference/cli.md#eject) (beta): `protobase eject` turns a table someone added in the admin app into ordinary config and a migration in the project, so a developer can customise it like any other resource. Its records stay where they are.
|
|
11
12
|
- [Tables per organization](../reference/user-tables.md#organizations) (beta): In an app with organizations, a table someone adds belongs to their organization. Only its owners and admins, and superusers working in it, see the table and its records.
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Importing records from a spreadsheet
|
|
4
4
|
|
|
5
|
-
Bring a spreadsheet of companies into the admin app with the list's Import, update them from another, and turn Import off where it does not belong.
|
|
5
|
+
Bring a spreadsheet of companies into the admin app with the list's Import, update them from another, make a new table from a file, and turn Import off where it does not belong.
|
|
6
6
|
|
|
7
|
-
Rijnland's sales team keeps a spreadsheet of companies they met at a trade fair, and wants them in the ERP example (`tests/apps/erp`) without typing each one. Every list offers **Import** to a user who may create its records, so there is nothing to build: this guide walks through an import, an update from a second file, and turning Import off. The [reference](../reference/ui-config.md#importing-records) has every rule.
|
|
7
|
+
Rijnland's sales team keeps a spreadsheet of companies they met at a trade fair, and wants them in the ERP example (`tests/apps/erp`) without typing each one. Every list offers **Import** to a user who may create its records, so there is nothing to build: this guide walks through an import, an update from a second file, a new table from a file, and turning Import off. The [reference](../reference/ui-config.md#importing-records) has every rule.
|
|
8
8
|
|
|
9
9
|
## 1. Import new records
|
|
10
10
|
|
|
@@ -33,7 +33,17 @@ A second sheet has the companies' VAT numbers, by name:
|
|
|
33
33
|
|
|
34
34
|
Choose **Update** under **Rows**, then **Find records by** Name and **Found in column** Name. Name is a filterable field, so the import can look the companies up; a name two companies share is a problem on its row rather than a guess. A row whose name finds no company is not imported; **Create or update** would create it instead.
|
|
35
35
|
|
|
36
|
-
## 3.
|
|
36
|
+
## 3. Make a new table from a file
|
|
37
|
+
|
|
38
|
+
The team also keeps a sheet of trade fair contacts that fits no list the app has. In an app with [user tables](../reference/user-tables.md) on, someone who may add tables opens **Tables**, presses **From a file** and chooses it:
|
|
39
|
+
|
|
40
|
+
| Name | Visits | Met on | Phone |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| Hoogland Fietsen | 3 | 2026-03-01 | 0612345678 |
|
|
43
|
+
|
|
44
|
+
Each column proposes a field, typed only from what its cells hold: Visits becomes a whole number and Met on a date, while Phone stays text, as `0612345678` starts with a zero a number would drop. A value such as `1,5` or `03/04/2026`, which could be read two ways, keeps its column text too. Give the table a singular label, such as Contact, change any name, label or type, or leave a column out, and press **Make table**. The table and its fields are made in one go, the rows are written as an import writes them, and you land on the new table's list. The [reference](../reference/user-tables.md#a-table-from-a-file) has every rule.
|
|
45
|
+
|
|
46
|
+
## 4. Turn Import off
|
|
37
47
|
|
|
38
48
|
When a list's records should never come from a file, such as invoices that only orders should make, turn its Import off in the view:
|
|
39
49
|
|
|
@@ -578,7 +578,7 @@ view<typeof auditLog>('audit_log').export(false)
|
|
|
578
578
|
|
|
579
579
|
## Importing records
|
|
580
580
|
|
|
581
|
-
A list page offers **Import** beside **New** to a user who may create the resource's records; `.import(false)` on the view turns it off. It opens a dialog that creates or updates records from an Excel workbook (`.xlsx`), a CSV file (UTF-8, its delimiter a comma, semicolon or tab, found from the first line) or a JSON file (`.json`, UTF-8) that holds an array of objects. See [Importing records from a spreadsheet](../guides/import-records.md) for a walk through.
|
|
581
|
+
A list page offers **Import** beside **New** to a user who may create the resource's records; `.import(false)` on the view turns it off, and `.import('table')` makes it [a new table from the file](user-tables.md#a-table-from-a-file) instead, as the **Tables** list of [user tables](user-tables.md) does. It opens a dialog that creates or updates records from an Excel workbook (`.xlsx`), a CSV file (UTF-8, its delimiter a comma, semicolon or tab, found from the first line) or a JSON file (`.json`, UTF-8) that holds an array of objects. See [Importing records from a spreadsheet](../guides/import-records.md) for a walk through.
|
|
582
582
|
|
|
583
583
|
```ts
|
|
584
584
|
view<typeof invoices>('invoices').import(false)
|
|
@@ -120,7 +120,7 @@ Building an index holds the table's write lock, and every statement runs within
|
|
|
120
120
|
## In the admin app
|
|
121
121
|
|
|
122
122
|
:::caution[Beta]
|
|
123
|
-
A table is made first and given its fields one by one,
|
|
123
|
+
A table is made first and given its fields one by one, or [made from a file](#a-table-from-a-file) with all of them.
|
|
124
124
|
:::
|
|
125
125
|
|
|
126
126
|
People who may add tables find **Tables** in the admin app, make a table with its fields and add fields later, on a computer or a phone. The new table appears in the sidebar at once.
|
|
@@ -133,7 +133,55 @@ The screens are the admin app's standard list, record page and create forms over
|
|
|
133
133
|
- **A new field.** The form asks for the label, the name, the type, chosen by its name in words, such as **Long text** or **Yes or no**, and the position, and for a decimal number its digits and decimals. Every field users add is optional, so the form does not ask.
|
|
134
134
|
- **What changes later.** On a table's page, its labels and its title field, the name of one of its text fields; on a field's page, its label and position. The name, the type and a decimal's digits and decimals stay as made.
|
|
135
135
|
|
|
136
|
-
<gap>A table cannot be made together with its fields in one form because a create form that adds related records is not implemented; each field is added from the table's page.</gap>
|
|
136
|
+
<gap>A table cannot be made together with its fields in one form because a create form that adds related records is not implemented; each field is added from the table's page, or all of them at once from a file.</gap>
|
|
137
|
+
|
|
138
|
+
## A table from a file
|
|
139
|
+
|
|
140
|
+
:::caution[Beta]
|
|
141
|
+
A file becomes one new table, without relations between its columns or choices read from repeated values. A JSON file is read whole, so a phone reads a smaller one than a computer.
|
|
142
|
+
:::
|
|
143
|
+
|
|
144
|
+
An Excel, CSV or JSON file becomes a new table: each column a field typed from what its cells hold, each row a record. Values are never guessed: `007`, `1,5` and `03/04/2026` stay text, and an empty cell stays empty.
|
|
145
|
+
|
|
146
|
+
On the **Tables** list, **From a file** opens the [Import](ui-config.md#importing-records) dialog for a new table, to anyone who may create tables and their fields. It reads the same files as Import: an Excel workbook (`.xlsx`, a sheet of your choice), a CSV file in UTF-8, or a JSON file holding a list of objects, with the first row or the objects' keys naming the columns. It works the same on a phone, in the installed web app and on a computer, in [English and Dutch](languages.md).
|
|
147
|
+
|
|
148
|
+
1. **Read.** The whole file is read once, in a Web Worker, before anything is made. A CSV and a sheet stream in 500 rows at a time and only what each column's type needs is kept, so their length never adds to the memory it takes; a workbook's shared texts and a JSON file are held whole, so their size is bounded by the memory the browser gives the page, which on a phone is less. A file too large to read stops here with an error, and nothing is made.
|
|
149
|
+
2. **Review.** The table is named after the file, its plural label the file's name; give it a singular label. Each column proposes a field, its label the header and its name the header in snake_case, such as `customer_email` for **Customer email**. A header with no letters or digits is named by its column, `column_c`; two headers that give one name, or a name a table's field always has (`id`, `etag`, `organization_id`) or SQL reserves (`order`), get `_2`, `_3` and so on, so no two columns ever become one field. The first text field is the title. Rename and relabel any field, leave a column out, or change its type to a less specific one, such as a whole number to a decimal number or to text. Names follow the [rules of every table](#the-schemas-and-the-migration), and the dialog says which break them before anything is sent.
|
|
150
|
+
3. **Make.** **Make table** creates the table and all its fields, then its title, in one [batch write](#adding-tables-and-fields), so a refused name leaves no table behind. The rows are then written as Import writes them, in batches of 100 that each pass the server's validation and access checks. When every row is written you land on the new table's list; otherwise the summary counts what was written and downloads the rows not imported with the reasons. The table and the rows already written stay.
|
|
151
|
+
|
|
152
|
+
<gap>A file cannot become several tables, relations between its columns or a choice field read from repeated values because splitting a file, finding relations and user tables with choice fields are not implemented.</gap>
|
|
153
|
+
|
|
154
|
+
What a column's cells say decides its type, and nothing else does. Every non-empty cell must be written the same way, or the column is text:
|
|
155
|
+
|
|
156
|
+
| Every value is | The field |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| a whole number without leading zeros or a plus sign, such as `12` or `-3` | **Whole number** (`integer`), or a **Decimal number** of scale 0 once one is past its range |
|
|
159
|
+
| a number with a point, such as `1.5`, among whole numbers or not | **Decimal number**, with as many decimals as the longest and room for at least 15 digits before the point |
|
|
160
|
+
| `true` or `false`, in any case, or a workbook's TRUE and FALSE | **Yes or no** |
|
|
161
|
+
| an ISO date, such as `2026-03-01`, or a workbook date | **Date** |
|
|
162
|
+
| an ISO date and time, such as `2026-03-01T14:30:00Z`, or a workbook date and time | **Date and time**; a time without a zone is read in your browser's time zone, as Import reads one |
|
|
163
|
+
| a JSON file's nested object or array, beside any other value | **JSON**, holding each value as the file states it |
|
|
164
|
+
| anything else, such as `007`, `+31`, `1,5`, `1.234,5` or `03/04/2026`, or a mix | **Text**, or **Long text** once a value is over 500 characters |
|
|
165
|
+
|
|
166
|
+
A number written with a comma, a thousands separator or a date with slashes could be read more than one way, so it stays text rather than being parsed by a guess. An empty cell, or one of spaces, is empty in the record, and a column of only empty cells is text.
|
|
167
|
+
|
|
168
|
+
The same reading is in `@protobase/schema` for your own code, such as an import of your own or a tool that proposes tables:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { columnTypeReader, fieldNamesOf, inferColumnType, tableNameOf, widerFieldTypes } from '@protobase/schema'
|
|
172
|
+
|
|
173
|
+
inferColumnType(['12', '-3', null]) // { type: 'integer' }
|
|
174
|
+
inferColumnType(['1.5', '20']) // { type: 'decimal', precision: 16, scale: 1 }
|
|
175
|
+
inferColumnType(['007', '1,5']) // { type: 'text' }
|
|
176
|
+
fieldNamesOf(['Name', 'name', 'ID', 'Order']) // ['name', 'name_2', 'id_2', 'order_2']
|
|
177
|
+
tableNameOf('Trade fair 2026.xlsx', ['trade_fair_2026']) // 'trade_fair_2026_2'
|
|
178
|
+
widerFieldTypes('integer') // ['integer', 'decimal', 'text', 'long_text']
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- **`inferColumnType(cells)`** gives a column's type from all its cells; **`columnTypeReader()`** reads them one at a time with `add(cell)` and gives it with `type()`, keeping no cell, and with `decimal()` the decimal number that would hold them. A cell is text, a number, a boolean, `{ date }` for a workbook's date or time (`2026-03-01` or `2026-03-01T14:30:00`), `{ json }` for a JSON file's nested value, or `null`.
|
|
182
|
+
- **`fieldNamesOf(headers)`** and **`tableNameOf(fileName, taken, maxBytes)`** give the names as above; with organizations a table's name is at most 42 bytes.
|
|
183
|
+
- **`widerFieldTypes(type)`** lists the types a column may change to, its own first.
|
|
184
|
+
- **`userTableNameIssue(name, kind, maxBytes)`** says why a table or field may not have a name: `form`, `reserved_word` or `reserved_field`, or nothing when it may.
|
|
137
185
|
|
|
138
186
|
## Taking a table over as code
|
|
139
187
|
|