@protobase/cli 0.66.15 → 0.67.0

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.66.15";
2
+ var version = "0.67.0";
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.66.15",
3
+ "version": "0.67.0",
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.66.15",
47
- "@protobase/layout": "0.66.15",
48
- "@protobase/query": "0.66.15",
49
- "@protobase/schema": "0.66.15",
50
- "@protobase/server": "0.66.15",
51
- "@protobase/ui": "0.66.15",
46
+ "@protobase/config": "0.67.0",
47
+ "@protobase/layout": "0.67.0",
48
+ "@protobase/query": "0.67.0",
49
+ "@protobase/schema": "0.67.0",
50
+ "@protobase/server": "0.67.0",
51
+ "@protobase/ui": "0.67.0",
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",
@@ -27,4 +27,4 @@ The list page of each resource, with its columns, filters, charts, search, expor
27
27
  - [Favorites](../reference/ui-config.md#favorites): A heart on each card saves the record to the user's favorites, which a list page can show.
28
28
  - [Sign-in prompts](../reference/ui-config.md#sign-in-prompts): Visitors who are not signed in see what signing in gives them, in place of a list or when they press favorite or save search.
29
29
  - [Export to Excel and CSV](../reference/ui-config.md#exports): Any list, or the selected rows, is saved as an Excel or CSV file with the columns the user picks.
30
- - [Import from a spreadsheet](../reference/ui-config.md#importing-records): Records are created or updated from Excel or CSV. Every row is checked before anything is saved, and refused rows can be downloaded with the reasons.
30
+ - [Import from a file](../reference/ui-config.md#importing-records): Records are created or updated from Excel, CSV or JSON. Every row is checked before anything is saved, and refused rows can be downloaded to fix.
@@ -15,6 +15,8 @@ Save the spreadsheet as it is (`.xlsx`), or as **CSV UTF-8**. Its first row name
15
15
  | Hoogland Fietsen | Leiden | NL | Lead | inkoop@hoogland.test |
16
16
  | De Spaak | Delft | NL | lead | |
17
17
 
18
+ Data exported from another tool as JSON imports the same way: an array of objects, whose keys name the columns, such as `[{"Name": "Hoogland Fietsen", "City": "Leiden"}]`.
19
+
18
20
  Open **Companies**, press **Import** and choose the file. Each column whose header is a field's label or name is mapped already: Country is the label the companies' view gives `country_code`, a relation whose column holds each country's key. Map other columns, or leave them at **Do not import**. Set **Numbers** and **Dates** to how the file writes them. Fields a new company needs that no column fills are named above **Check rows**, which waits for them.
19
21
 
20
22
  **Check rows** reads the whole file and writes nothing. It shows the first rows as they will be saved and lists each row with a problem, such as a country no record has or a status that is none of Lead, Active and Dormant (`lead` is fine: case and spacing do not count). Those rows are skipped. An empty Email is saved empty; an empty cell is never filled with a default.
@@ -232,7 +232,7 @@ The page is checked like any other, and the account blocks are allowed on it alo
232
232
  With mail configured, the sign-in page offers "Forgot password?": the person enters their email, gets a link, and chooses a new password on the page the link opens. It is Better Auth's own reset: `POST /api/auth/request-password-reset` with `{ email, redirectTo }`, the emailed link `GET /api/auth/reset-password/<token>`, which sends the browser back to `redirectTo` with `?token=...` (or `?error=INVALID_TOKEN`), and `POST /api/auth/reset-password` with `{ token, newPassword }`.
233
233
 
234
234
  ```sh
235
- PROTOBASE_SMTP_URL=smtps://user:password@smtp.example.com:465 # or smtp://...:587, STARTTLS when the server offers it
235
+ PROTOBASE_SMTP_URL=smtps://user:password@smtp.example.com:465 # or smtp://...:587, which requires STARTTLS unless the URL ends in ?requireTLS=false
236
236
  PROTOBASE_MAIL_FROM=noreply@admin.example.com
237
237
  ```
238
238
 
@@ -403,7 +403,7 @@ Serves the API of a bundle from `protobase build` and owns the process around it
403
403
  | `PROTOBASE_DB_CONNECTIONS` | the connections that pool opens, such as the database's limit for the app; default 10. Sign-in's store shares them. [API functions](../guides/api-functions.md) and [jobs](jobs.md) hold at most one less, so a long import leaves a connection to page and API requests; those with [`longQueries`](../guides/api-functions.md#long-queries) take theirs from one more connection, kept for them. A query waits at most 2 seconds for a free connection, then fails with `503 connections-busy` ([Safety](api.md#safety)); a new connection may take up to 30 seconds to open, such as while a database that scaled to zero wakes |
404
404
  | `PORT` | default 8787 |
405
405
  | `REQUEST_LOG` | `true` logs every request; default `false` |
406
- | `PROTOBASE_SMTP_URL` | the SMTP server for password-reset mail, emailed sign-in and two-factor codes and the app's own [email](email.md), credential included: `smtp://user:password@host:587` (STARTTLS when offered) or `smtps://...:465`; without it they are off ([Password reset](auth.md#password-reset), [Emailed sign-in codes](auth.md#emailed-sign-in-codes)) |
406
+ | `PROTOBASE_SMTP_URL` | the SMTP server for password-reset mail, emailed sign-in and two-factor codes and the app's own [email](email.md), credential included: `smtp://user:password@host:587` (STARTTLS required, `?requireTLS=false` sends in plaintext, see [Email](email.md#configuration)) or `smtps://...:465`; without it they are off ([Password reset](auth.md#password-reset), [Emailed sign-in codes](auth.md#emailed-sign-in-codes)) |
407
407
  | `PROTOBASE_MAIL_FROM` | the sender of that mail, a plain address such as `noreply@admin.example.com`; set with `PROTOBASE_SMTP_URL` or not at all |
408
408
  | `PROTOBASE_APP_NAME` | the app's name, such as `Property Finder`: what its auth emails, its sidebar, its browser tabs and the [installed app](installable-app.md) call it, and the sender's display name of its mail, over the config's [`name`](config/app.md#name) |
409
409
  | `PROTOBASE_SIGN_IN_ISSUER`, `PROTOBASE_SIGN_IN_CLIENT_ID`, `PROTOBASE_SIGN_IN_CLIENT_SECRET` | an OpenID Connect provider people who have an account can sign in with; all three or none. `PROTOBASE_SIGN_IN_PROVIDER` and `PROTOBASE_SIGN_IN_NAME` are optional ([Platform sign-in provider](auth.md#platform-sign-in-provider)) |
@@ -10,7 +10,7 @@ An app sends mail through one SMTP server: Protobase's own mail ([password reset
10
10
 
11
11
  | Variable | |
12
12
  | --- | --- |
13
- | `PROTOBASE_SMTP_URL` | The SMTP server, credential included: `smtp://user:password@host:587` (STARTTLS when the server offers it) or `smtps://user:password@host:465` (TLS from the start). |
13
+ | `PROTOBASE_SMTP_URL` | The SMTP server, credential included: `smtp://user:password@host:587` (STARTTLS required) or `smtps://user:password@host:465` (TLS from the start). |
14
14
  | `PROTOBASE_MAIL_FROM` | The sender, a plain address such as `noreply@shop.example.com`. The client greets the server with its domain; a `?name=` on the URL overrides that. |
15
15
  | `PROTOBASE_APP_NAME` | Optional: the app's name, such as `Shop`, which mail clients show as the sender's name (`Shop <noreply@shop.example.com>`). It overrides the config's [`name`](config/app.md#name). Without either the sender has no name. |
16
16
 
@@ -22,10 +22,12 @@ Set both or neither. One without the other, a URL that is not `smtp://` or `smtp
22
22
  docker run -d -p 8025:8025 -p 1025:1025 axllent/mailpit # its inbox is http://localhost:8025
23
23
 
24
24
  # .env
25
- PROTOBASE_SMTP_URL=smtp://localhost:1025
25
+ PROTOBASE_SMTP_URL=smtp://localhost:1025?requireTLS=false
26
26
  PROTOBASE_MAIL_FROM=noreply@shop.example.com
27
27
  ```
28
28
 
29
+ Over `smtp://` the client always upgrades the connection with STARTTLS before it logs in or sends anything, so nobody between the app and the server reads the server's password or the mail, such as a sign-in code or a reset link. A server that does not offer STARTTLS, or an answer from which someone on the way removed it, fails the send, with the code `ETLS` when the server refuses the upgrade. `?requireTLS=false` on the URL is the only way to send in plaintext, for a server that has no TLS, such as the mail catcher above or a server on the same host. `smtps://` uses TLS from the start.
30
+
29
31
  A project that delivers its mail another way exports a mailer, `{ send({ to, subject, text, html, headers }) }`, as the default of `auth/mailer.ts`: sign-in and the jobs (`mail` in their context) both send with it. `auth: { mailer: false }` turns sign-in's mail off whatever the environment says. A host that runs jobs itself passes a mailer to `startJobs`. A template's `send` takes one too, see [Templates](#templates).
30
32
 
31
33
  ### When the server does not answer
@@ -577,16 +577,16 @@ view<typeof auditLog>('audit_log').export(false)
577
577
 
578
578
  ## Importing records
579
579
 
580
- 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`) or a CSV file (UTF-8, its delimiter a comma, semicolon or tab, found from the first line). See [Importing records from a spreadsheet](../guides/import-records.md) for a walk through.
580
+ 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
581
 
582
582
  ```ts
583
583
  view<typeof invoices>('invoices').import(false)
584
584
  ```
585
585
 
586
- 1. **File.** The first row names the columns; a workbook with several sheets asks which one to read. A Web Worker reads the file, a CSV and a sheet's rows as they stream in, so a large file neither holds up the page nor is held whole in memory, and the file itself never goes to the server: only the writes of its rows do.
587
- 2. **Columns.** A column whose header is a field's label or name, ignoring case and spacing, fills that field; each mapping can be changed, or the column skipped. The fields are those the import can write: not read-only, not the tenant, not JSON or files, and relations to records with a key of one field. The fields a new record needs (not optional, without a default) that no column fills are listed, and a create waits for them. A relation's column holds the related record's key, or the value of a field the user picks under it, such as a customer's email. **Numbers** and **Dates** say how the file writes what text leaves open: `1,234.56` or `1.234,56`, the day or the month first; they start from the browser's language. Dates written as `2026-12-31` read the same either way, and a time without a zone is the browser's.
586
+ 1. **File.** The first row names the columns; a workbook with several sheets asks which one to read. In a JSON file each object is a row and the columns are the objects' keys, in the order they first appear; a key an object lacks is an empty cell. Strings, numbers and booleans are kept as the file states them, so `"007"` stays text, and a nested object or array is a JSON value. A whole number past 2^53 keeps its digits as text where the browser gives JSON parsing the number's source text. <gap>Import cannot keep every digit of a whole number past 2^53 in a browser without JSON source text access because a JSON parser of its own is not implemented.</gap> A file that is not an array of objects is refused. <gap>Import cannot read JSON Lines or an array inside a JSON object because readers for those shapes are not implemented.</gap> A Web Worker reads the file, so a large file does not hold up the page, and the file itself never goes to the server: only the writes of its rows do. A CSV and a sheet's rows are read as they stream in, so they are never held whole in memory; a JSON file is read whole, so its size is bounded by the browser's memory.
587
+ 2. **Columns.** A column whose header is a field's label or name, ignoring case and spacing, fills that field; each mapping can be changed, or the column skipped. The fields are those the import can write: not read-only, not the tenant, not files, JSON fields only from a JSON file, and relations to records with a key of one field. The fields a new record needs (not optional, without a default) that no column fills are listed, and a create waits for them. A relation's column holds the related record's key, or the value of a field the user picks under it, such as a customer's email. **Numbers** and **Dates** say how the file writes what text leaves open: `1,234.56` or `1.234,56`, the day or the month first; they start from the browser's language. Dates written as `2026-12-31` read the same either way, and a time without a zone is the browser's.
588
588
  3. **Rows.** **Create** makes a record of each row. A user who may also update records can choose **Update**, which finds each row's record by a column the user picks, holding its key or a field no two records share (filterable, not sensitive), and skips a row that finds none, or **Create or update**, which creates that record instead.
589
- 4. **Check.** The whole file is read and every row converted before anything is written: the first rows as they will be saved, and each row's problems, also as a file to download. Nothing is guessed: an empty cell is `null`, never the field's default; numbers and dates in the chosen formats, true or false (`true`, `false`, `yes`, `no`, `1`, `0`), enum values by their [value labels](#names-and-columns) or values, and related records found by exactly one match convert, and anything else is a problem on its row. The import skips those rows.
589
+ 4. **Check.** The whole file is read and every row converted before anything is written: the first rows as they will be saved, and each row's problems, also as a file to download. Nothing is guessed: an empty cell is `null`, never the field's default; numbers and dates in the chosen formats, true or false (`true`, `false`, `yes`, `no`, `1`, `0`), enum values by their [value labels](#names-and-columns) or values, related records found by exactly one match, and a JSON file's values in a JSON field as they are convert, and anything else is a problem on its row, such as a nested value mapped to a field that is not JSON. The import skips those rows.
590
590
  5. **Import.** The rows are written in batches of 100 with [`POST /api/v1:batchWrite`](api.md#batch-writes), so the server validates each and checks the user's access as it does a single write; an update is sent with the ETag `*`. Each batch is a transaction of its own, so an import is not all or nothing: a row the server refuses is set aside with the server's reason, and the rest of its batch is sent again. **Cancel import** stops between two batches, keeping what was written, and so does an error such as an expired session. The summary counts the rows created, updated, skipped (with problems in the file) and failed (refused by the server), and downloads the rows not imported as a CSV, the file's own columns followed by each row's line in the file and its problems, to fix and import again.
591
591
 
592
592
  ## Create pages