@protobase/cli 0.66.14 → 0.66.16
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 +7 -7
- package/skills/protobase/references/features/api-functions.md +4 -2
- package/skills/protobase/references/features/lists.md +1 -1
- package/skills/protobase/references/features/rest-api.md +3 -3
- package/skills/protobase/references/guides/api-functions.md +17 -1
- package/skills/protobase/references/guides/import-records.md +2 -0
- package/skills/protobase/references/reference/api.md +7 -3
- package/skills/protobase/references/reference/auth.md +1 -1
- package/skills/protobase/references/reference/cli.md +1 -1
- package/skills/protobase/references/reference/email.md +4 -0
- package/skills/protobase/references/reference/ui-config.md +4 -4
package/dist/cli/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.66.
|
|
3
|
+
"version": "0.66.16",
|
|
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.
|
|
47
|
-
"@protobase/layout": "0.66.
|
|
48
|
-
"@protobase/query": "0.66.
|
|
49
|
-
"@protobase/schema": "0.66.
|
|
50
|
-
"@protobase/server": "0.66.
|
|
51
|
-
"@protobase/ui": "0.66.
|
|
46
|
+
"@protobase/config": "0.66.16",
|
|
47
|
+
"@protobase/layout": "0.66.16",
|
|
48
|
+
"@protobase/query": "0.66.16",
|
|
49
|
+
"@protobase/schema": "0.66.16",
|
|
50
|
+
"@protobase/server": "0.66.16",
|
|
51
|
+
"@protobase/ui": "0.66.16",
|
|
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",
|
|
@@ -7,8 +7,10 @@ Endpoints of your own beside the REST API.
|
|
|
7
7
|
- [API functions](../guides/api-functions.md#a-first-function): Each file in `functions/` becomes an endpoint for signed-in callers, reloaded while you work.
|
|
8
8
|
- [Records as the caller](../guides/api-functions.md#reading-and-writing-records-as-the-caller): Functions read and write records with the caller's access, validation and hooks.
|
|
9
9
|
- [Input checks](../guides/api-functions.md#checking-input): Request bodies are checked with zod, Valibot or another Standard Schema validator.
|
|
10
|
-
- [Roles for functions](../guides/api-functions.md#roles): A function can be limited to roles or a capability.
|
|
10
|
+
- [Roles for functions](../guides/api-functions.md#roles): A function can be limited to roles or a capability; in an app with roles, one with neither is open only to roles that hold everything.
|
|
11
11
|
- [Public functions and webhooks](../guides/api-functions.md#public-functions-and-webhooks): A function can be open to anyone, with helpers that check webhook signatures.
|
|
12
12
|
- [WebSockets](../guides/api-functions.md#websockets): A function can accept a WebSocket, and the caller is checked again every minute.
|
|
13
|
+
- [Calling a function](../guides/api-functions.md#calling-a-function): The typed client's `invoke` calls a function with the signed-in user's token, from an action or any page.
|
|
14
|
+
- [Work after the response](../guides/api-functions.md#work-after-the-response): A function can answer at once and finish its work afterwards, which a graceful stop waits for.
|
|
13
15
|
- [Hono apps](../guides/api-functions.md#hono-apps): A function with several routes can be a Hono app.
|
|
14
|
-
- [Long queries](../guides/api-functions.md#long-queries): A function or job that does bulk work may run queries for up to 30 seconds, on a connection kept for it.
|
|
16
|
+
- [Long queries](../guides/api-functions.md#long-queries): A function or job that does bulk work may run queries for up to 30 seconds, on a connection kept for it, and functions and jobs always leave a connection to page requests.
|
|
@@ -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
|
|
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.
|
|
@@ -6,7 +6,7 @@ The API every resource gets, with filters, writes, errors and the typed client.
|
|
|
6
6
|
|
|
7
7
|
- [Generated REST API](../reference/api.md#routes): List, get, create, update and delete endpoints for every resource, following the AIP API design guidelines.
|
|
8
8
|
- [OpenAPI and reference](../reference/api.md#meta-and-openapi-per-caller): An OpenAPI 3.1 document and an API reference page, each showing only what the caller may see and do.
|
|
9
|
-
- [Filters](../reference/api.md#filters): AIP-160 filters and ordering, with
|
|
9
|
+
- [Filters](../reference/api.md#filters): AIP-160 filters and ordering, with word search across a resource's search fields, lists of values and relative dates.
|
|
10
10
|
- [Counts, facets and charts](../reference/api.md#routes): Endpoints for value counts, grouped sums and averages, records over time, histograms and paging to any row.
|
|
11
11
|
- [Map queries](../reference/api.md#maps): The records inside a map's view, or counts per area when there are many.
|
|
12
12
|
- [Related names and photos](../reference/api.md#related-names): A list response can carry the names of related records and each record's first photos.
|
|
@@ -14,11 +14,11 @@ The API every resource gets, with filters, writes, errors and the typed client.
|
|
|
14
14
|
- [Minimal responses](../reference/api.md#writes): A create or update can answer without the record, to save the transfer.
|
|
15
15
|
- [Batch writes](../reference/api.md#batch-writes): Up to 500 creates, updates, deletes and reorders across resources in one transaction.
|
|
16
16
|
- [Record permissions](../reference/api.md#record-permissions): Responses say which records and fields the caller may change.
|
|
17
|
-
- [Scan guard](../reference/api.md#safety):
|
|
17
|
+
- [Scan guard](../reference/api.md#safety): Lists, searches and facets on a column no index serves are refused, at any table size, with the index to create.
|
|
18
18
|
- [Statement timeouts](../reference/api.md#safety): Every request's queries stop after 1 second by default, with an error that names the request, and reads run read-only.
|
|
19
19
|
- [Errors](../reference/api.md#errors): Every error is a problem document with a stable type and per-field details.
|
|
20
20
|
- [Audit events](../reference/api.md#audit-events): Reveals of sensitive values and organization changes are published as audit events.
|
|
21
|
-
- [Error reporting](../reference/api.md#error-reporting): Server and admin app errors go to a Sentry-compatible error tracker when a DSN is set.
|
|
21
|
+
- [Error reporting](../reference/api.md#error-reporting) (beta): Server and admin app errors go to a Sentry-compatible error tracker when a DSN is set.
|
|
22
22
|
- [JWT authentication](../reference/api.md#authentication): The API accepts tokens from another identity provider, or a check of your own.
|
|
23
23
|
- [CORS](../reference/api.md#cors): Websites on other origins can call the API once their origins are listed.
|
|
24
24
|
- [Typed client](../reference/client.md): A typed fetch client and React hooks for the API.
|
|
@@ -291,6 +291,21 @@ The ticket only lets the socket open, and anyone who holds it may open one with
|
|
|
291
291
|
|
|
292
292
|
`protobase dev` closes every open socket with `1012` when you edit the project, so it reconnects to the rebuilt API. `protobase serve` closes them with `1001` when it stops, and drops any still open after 5 seconds. Clients that reconnect on these codes keep working across both.
|
|
293
293
|
|
|
294
|
+
## Work after the response
|
|
295
|
+
|
|
296
|
+
`waitUntil(promise)` from `@protobase/server` lets a function answer at once and finish its work afterwards, such as a slow call to another API:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { defineFunction, waitUntil } from '@protobase/server'
|
|
300
|
+
|
|
301
|
+
export default defineFunction((_request, { session }) => {
|
|
302
|
+
waitUntil(fetch('https://hooks.example.com/notify', { method: 'POST', body: JSON.stringify({ user: session.user.id }) }))
|
|
303
|
+
return new Response(null, { status: 202 })
|
|
304
|
+
})
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`protobase serve` waits for that work when it stops gracefully; work still running when the host kills the process is lost, and an error it throws is printed, since no response waits for it. On Protobase Cloud's Starter plan an app stops 30 seconds after its last request, with 10 seconds to finish, so move longer work into a [job](../reference/jobs.md).
|
|
308
|
+
|
|
294
309
|
## Secrets and other APIs
|
|
295
310
|
|
|
296
311
|
```ts title="functions/notify-customer.ts"
|
|
@@ -406,6 +421,7 @@ export default defineFunction({ longQueries: true }, async (request, { db }) =>
|
|
|
406
421
|
|
|
407
422
|
- **Up to 30 seconds** per query, the most any query of an app may run. Work longer than that belongs in steps, such as a [job](../reference/jobs.md) per batch, which takes `longQueries` too.
|
|
408
423
|
- **One connection kept for it.** Under `protobase dev` and `serve`, those queries run on a connection of their own, which asks for the longer timeout when it logs in, so long work never takes the connections pages need; a second long call waits for it. A PgBouncer of your own in front of the database must list `statement_timeout` in `track_extra_parameters` to pass that on; Protobase Cloud's does.
|
|
424
|
+
- **Room for pages.** Under `protobase dev` and `serve`, function calls and jobs together hold at most one connection less than [`PROTOBASE_DB_CONNECTIONS`](../reference/cli.md#serve), so a long import always leaves one to page and API requests; a call past that waits for a connection of its own.
|
|
409
425
|
- **`records` keep the API's limit,** [`statementTimeoutMs`](../reference/api.md#safety).
|
|
410
426
|
- **A query past its limit** is stopped, and the call answers `504 statement-timeout` naming the call, its limit and the way out, such as `POST /api/functions/import ran a query longer than the database's statement timeout, so the database stopped it. Make the query faster, such as with an index or a narrower filter, or give it longQueries: true, whose queries may run up to 30 s.` It goes to the app's error hook, `hooks/error.ts`, and the [error tracker](../reference/api.md#error-reporting).
|
|
411
427
|
|
|
@@ -422,7 +438,7 @@ export default defineFunction({ longQueries: true }, async (request, { db }) =>
|
|
|
422
438
|
- **At `/api/sb/functions/v1/<name>` too,** so `supabase.functions.invoke` callers do not change. Only a function with `supabase: true` answers there, and any other function answers `404`. There a protected function lets in a request with the app's publishable or secret key in the `apikey` header, and a user token of [`/api/sb/auth/v1`](../reference/auth.md#supabase-js-and-supabasessr) as the bearer, if any, as Supabase's `verify_jwt` does. A public function lets in anyone. The keys are the app's [API keys](../reference/supabase-rest.md#keys-and-roles): without them, only public functions answer there. As on Supabase, the request's path starts at the function's name (`/<name>/more/parts`), and a CORS preflight reaches the function, which answers it.
|
|
423
439
|
- **Its context there** is `{ name, db, caller() }`, where `caller()` resolves to who the key and token make the request: `{ role: 'anon' }` for the publishable key alone, `{ role: 'app' }` for the secret key, or `{ role: 'authenticated', claims }`. There is no `session` and no `records`. The publishable key alone lets callers in, so give `supabase: true` only to functions written for Supabase's `verify_jwt`. Together with `roles` or `capability`, which the address cannot check, it stops startup.
|
|
424
440
|
- **Supabase's variables:** `protobase dev` sets `SUPABASE_URL` to the app's address with `/api/sb`, where supabase-js finds it, `SUPABASE_ANON_KEY` and `SUPABASE_PUBLISHABLE_KEY` to the publishable key and `SUPABASE_PUBLISHABLE_KEYS` to `{"default":<publishable key>}`, `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_SECRET_KEY` to the secret key and `SUPABASE_SECRET_KEYS` to `{"default":<secret key>}`, each only when the app has that key, `SUPABASE_JWKS_URL` to the key set of `/api/sb/auth/v1` when the app has sign-in, and `SUPABASE_DB_URL` to the database. A variable set in `.env` wins. Under `protobase serve`, set them where the host keeps its settings.
|
|
425
|
-
-
|
|
441
|
+
- **[`waitUntil(promise)`](#work-after-the-response)** from `@protobase/server` takes the place of `EdgeRuntime.waitUntil`.
|
|
426
442
|
|
|
427
443
|
## What functions are not
|
|
428
444
|
|
|
@@ -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.
|
|
@@ -211,7 +211,7 @@ The API sends no CORS headers until it has something to allow: other websites in
|
|
|
211
211
|
|
|
212
212
|
## Errors
|
|
213
213
|
|
|
214
|
-
Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase:problem:<slug>`), `title`, `status`, `detail`, `instance`. Filter and `order_by` errors add `parameter` and `errors: [{ code, message, hint, span: { start, end } }]`; invalid parameters add `errors: [{ parameter, message }]`. A create or update whose body fails the checks is `invalid-record` with `errors: [{ field, code, message, params }]`, `code` one of `required`, `invalid-value`, `read-only`, `unknown-field` and `invalid-record` (a `.validate()` rule); `params` is the limit an invalid value broke, such as `{ min }`, `{ max }` or a decimal's `{ precision, scale }`. Database constraint failures map to `409` or `400` without the raw message. A create or update that writes the position of a resource with `after` so a record ranks at or above one it depends on is a `409` `ranks-above-dependency`, as in a [`reorder`](#batch-writes). A create that repeats the values of a resource's [`.openExisting()`](data-config.md#opening-an-existing-record) unique key is a `409` `conflict` with `existing: { key }`, the key of the record holding them as its address writes it, only when the caller may read that record; otherwise it is the plain `409`, as any unique conflict is. A delete that other records still point at is a `409` `conflict` with `blockedBy: { resource, field? }`: their resource, when the caller may list it, and its relation field pointing at the record, when they may filter by it, so a client can open those records; the admin app's toast names them, such as "Orders still point at this company.", with a link to that list. Postgres names one table of them only, so `blockedBy` holds one resource
|
|
214
|
+
Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase:problem:<slug>`), `title`, `status`, `detail`, `instance`. Filter and `order_by` errors add `parameter` and `errors: [{ code, message, hint, span: { start, end } }]`; invalid parameters add `errors: [{ parameter, message }]`. A create or update whose body fails the checks is `invalid-record` with `errors: [{ field, code, message, params }]`, `code` one of `required`, `invalid-value`, `read-only`, `unknown-field` and `invalid-record` (a `.validate()` rule); `params` is the limit an invalid value broke, such as `{ min }`, `{ max }` or a decimal's `{ precision, scale }`. Database constraint failures map to `409` or `400` without the raw message. A create or update that writes the position of a resource with `after` so a record ranks at or above one it depends on is a `409` `ranks-above-dependency`, as in a [`reorder`](#batch-writes). A create that repeats the values of a resource's [`.openExisting()`](data-config.md#opening-an-existing-record) unique key is a `409` `conflict` with `existing: { key }`, the key of the record holding them as its address writes it, only when the caller may read that record; otherwise it is the plain `409`, as any unique conflict is. A delete that other records still point at is a `409` `conflict` with `blockedBy: { resource, field? }`: their resource, when the caller may list it, and its relation field pointing at the record, when they may filter by it, so a client can open those records; the admin app's toast names them, such as "Orders still point at this company.", with a link to that list. Postgres names one table of them only, so `blockedBy` holds one resource. A write that Postgres rolls back because it locked the same records as another write in the opposite order (a deadlock) is a `409` `conflict` too: nothing was written, so send it again. Anything unexpected is a `500` with a fixed detail, reported to the app's error hook (`hooks/error.ts`, `createAdmin`'s `options.onUnhandledError`) and, when it is set, the [error tracker](#error-reporting).
|
|
215
215
|
|
|
216
216
|
| Status | Slugs |
|
|
217
217
|
| --- | --- |
|
|
@@ -219,13 +219,17 @@ Every error is `application/problem+json` (RFC 9457) with `type` (`urn:protobase
|
|
|
219
219
|
| 401 | `unauthenticated` |
|
|
220
220
|
| 403 | `access-denied`, `tenant-required` |
|
|
221
221
|
| 404 | `not-found` |
|
|
222
|
-
| 409 | `conflict
|
|
222
|
+
| 409 | `conflict`: a unique key, a record other records point at, or a deadlock with another write, which may be sent again |
|
|
223
223
|
| 412 / 428 | `precondition-failed` / `precondition-required` |
|
|
224
224
|
| 503 | `read-only`: the database refuses writes, as during a deploy's write freeze; send the write again after `Retry-After`. `connections-busy`: every database connection stayed busy for 2 seconds (see [Safety](#safety)) |
|
|
225
225
|
| 504 | `statement-timeout`: a query ran past its limit (see [Safety](#safety)) |
|
|
226
226
|
|
|
227
227
|
### Error reporting
|
|
228
228
|
|
|
229
|
+
:::caution[Beta]
|
|
230
|
+
<gap>An error raised only inside a plugin built by an earlier `build-plugin` cannot be reported because marking a plugin's scripts when the app loads them is not implemented; build the plugin again.</gap>
|
|
231
|
+
:::
|
|
232
|
+
|
|
229
233
|
An app can send its errors to an error tracker of its own: [Sentry](https://sentry.io) or one that speaks its protocol, such as [Bugsink](https://www.bugsink.com) or [GlitchTip](https://glitchtip.com). It is off until a DSN, the address the tracker gives a project, is set in `errorReporting` or `PROTOBASE_SENTRY_DSN`; without one nothing is loaded or sent.
|
|
230
234
|
|
|
231
235
|
```ts
|
|
@@ -235,6 +239,6 @@ export default defineConfig({
|
|
|
235
239
|
```
|
|
236
240
|
|
|
237
241
|
- **Server:** every `500` with the request it failed, and every `504 statement-timeout` and `503 connections-busy` with the request it names, under `dev`, `serve` and the runtime, besides the default export of `hooks/error.ts` or the console. Under `serve`, the runtime and `worker`, an uncaught exception or unhandled rejection too, which then still ends the process with exit code 1.
|
|
238
|
-
- **Admin app:** uncaught errors and the errors of components that fail to draw. `/api/meta` names the DSN to every signed-in caller, as a browser app always shows its own. Only the app's own errors are sent: `build`, `build-shell` and `build-plugin` mark every script they write as the app's, and an error none of whose stack frames is in a marked script, such as one raised by a browser extension or a script the browser injects, is dropped. Under `dev`, whose scripts are not built, every error is sent.
|
|
242
|
+
- **Admin app:** uncaught errors and the errors of components that fail to draw. `/api/meta` names the DSN to every signed-in caller, as a browser app always shows its own. Only the app's own errors are sent: `build`, `build-shell` and `build-plugin` mark every script they write as the app's, and an error none of whose stack frames is in a marked script, such as one raised by a browser extension or a script the browser injects, is dropped. Under `dev`, whose scripts are not built, every error is sent.
|
|
239
243
|
- Errors only, with no performance tracing. Before an event leaves, the values of authorization headers, cookies, passwords, tokens and other credentials, in fields and in URLs, are replaced by `[Filtered]`.
|
|
240
244
|
- `environment` names the deployment in the tracker; without it the tracker's default, `production`. `errorReporting: false` turns reporting off whatever the environment says. A DSN that is no `http(s)` URL with a key and a project id stops startup.
|
|
@@ -249,7 +249,7 @@ export default { send: async ({ to, subject, text }) => { /* deliver it */ } } s
|
|
|
249
249
|
|
|
250
250
|
- The email names the app and gives the link, as a button and in its plain-text part, that it works for 1 hour and once, and that it can be ignored (see [Protobase's own mail](email.md#protobases-own-mail)).
|
|
251
251
|
- The SMTP client greets the server with the sender's domain (`admin.example.com` above), not the container's `[127.0.0.1]`; a `?name=` on `PROTOBASE_SMTP_URL` overrides it.
|
|
252
|
-
- A request answers the same whether or not the address has an account, and the mail goes out after the answer, so neither the response nor its timing tells. A send that fails is logged by Better Auth.
|
|
252
|
+
- A request answers the same whether or not the address has an account, and the mail goes out after the answer, so neither the response nor its timing tells. A send that fails is logged by Better Auth; one the server does not finish within 30 seconds fails with `MAIL_SERVER_TIMEOUT` (see [When the server does not answer](email.md#when-the-server-does-not-answer)), so the failure is in the log while an emailed code still works.
|
|
253
253
|
- Requests are limited to 3 per minute and client. Setting a new password ends all of the user's sessions; the new password needs 12 characters like any other.
|
|
254
254
|
- `redirectTo` must be on a trusted origin (the app's `url`, one of its [other origins](#several-origins) or `auth.trustedOrigins`); the admin app sends its own page with a `password-reset` marker, so the link opens the set-password page there.
|
|
255
255
|
- The link is on the origin the reset was asked on, when that is one of the app's.
|
|
@@ -148,7 +148,7 @@ One port, two processes: Vite serves the admin app from `@protobase/ui`'s `src/a
|
|
|
148
148
|
- **Login:** real sign-in only, with the config's `auth`, on by default, or its `authenticate` for tokens of an auth server elsewhere (see [protobase.config.ts](config.md) and [Login with Better Auth](auth.md)). A project with sign-in off and no authenticator fails at startup with a message pointing there, as does one with sign-in on and no `PROTOBASE_SECRET`, one whose auth store has not applied every migration of this version, naming them (see [`auth migration`](#auth-migration)), one with a resource over a table of the auth store, and one with a resource whose [events](events.md) or [history](history.md) lack their triggers, or whose [comments](comments.md) lack their table, or whose [rollup and formula fields](data-config.md#rollups-and-formulas) lack their columns or triggers, which it checks again on every reload (see [`migration`](#migration)), and one with an [AI field or action](ai.md#the-model) and no model key; there is no development login. The `login` line of the startup message says how to sign in: while the auth store has no users, it prints the exact command that creates the first admin, such as `bun run protobase users create you@example.com --generate-password` (the package manager that started `dev`, `npx` otherwise). Against a database that has users already, such as an app's hosted one, sign in as one of them or add one with a new email; its own password is set there. A `warning` line follows for each way the auth store's tables differ from what the app's sign-in needs, such as a column dropped by hand: the one place Protobase compares them, since `serve` reads the store's record of [migrations](auth.md#migrations) alone.
|
|
149
149
|
- **Mobile releases:** see [below](#mobile-releases).
|
|
150
150
|
- **Jobs:** with [jobs](jobs.md) in the config it runs them in the API's process, once the jobs schema checks out (see [`migration`](#migration)); an edited job runs its new code from the next job on. `--no-jobs` runs none of them and delivers none of the app's [events](events.md), webhooks or pushes, for a dev server on the database of a hosted app that already runs them; jobs are still enqueued, for that app to run, and the startup message says `jobs off`.
|
|
151
|
-
- **Tunnels and other hosts:** Vite only answers `localhost` by default. `--allowed-hosts .trycloudflare.com,app.example.com` adds Host headers (a leading dot matches all subdomains). Hot reload needs no extra setting: the Vite client takes protocol, host and port from the page, so behind an HTTPS tunnel it connects over `wss` on 443 as long as the tunnel forwards WebSockets.
|
|
151
|
+
- **Tunnels and other hosts:** Vite only answers `localhost` by default. `--allowed-hosts .trycloudflare.com,app.example.com` adds Host headers (a leading dot matches all subdomains). The API's WebSockets, such as an [API function](../guides/api-functions.md#websockets)'s, pass the same check: an upgrade with another Host header is refused with `403`. Hot reload needs no extra setting: the Vite client takes protocol, host and port from the page, so behind an HTTPS tunnel it connects over `wss` on 443 as long as the tunnel forwards WebSockets.
|
|
152
152
|
- **Project discovery**, by convention: `config/index.ts` exports the resources (and views), and every `config/*/ui.ts` export that is not already exported by the index is added as a view. Each `functions/<name>.ts` (or `functions/<name>/index.ts`) is an [API function](../guides/api-functions.md) at `/api/functions/<name>`. `protobase.config.ts` (or `protobase.config.json`) in the project root describes the rest as data, and the jobs, hooks and other code it names are found by their paths; see [protobase.config.ts](config.md). Without it, the app runs on the defaults.
|
|
153
153
|
- **Project UI:** the app imports `protobase.ui.tsx` from the project root, with hot reload, when it exists (see [Custom components](custom-components.md)): the same contract as the plugin `protobase build` makes of it, built into the dev page instead. Layout files are checked as they load, as `protobase build` checks them.
|
|
154
154
|
- **Theme:** the page links `/protobase.theme.css`, which the dev server answers from the project's [theme](theme.md), checked as the build checks it, on every request, so an edit shows on the next reload. It links `/protobase.favicon.svg` too, the project's [favicon](theme.md#logo-and-favicon) or Protobase's, and `/protobase.apple-touch-icon.png`, which it answers with the PNG [icons](app-identity.md#the-icons) the project has.
|
|
@@ -28,6 +28,10 @@ PROTOBASE_MAIL_FROM=noreply@shop.example.com
|
|
|
28
28
|
|
|
29
29
|
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
30
|
|
|
31
|
+
### When the server does not answer
|
|
32
|
+
|
|
33
|
+
A send through `PROTOBASE_SMTP_URL` gets 30 seconds in all: looking up the server, connecting to each of its addresses, its greeting, TLS, the login and the message. One that has not finished by then rejects with a `MailServerTimeoutError` (exported by `@protobase/server`) whose `code` is `MAIL_SERVER_TIMEOUT`, as when a firewall drops the mail port's packets or the server never greets; it is not retried. Options on the URL, such as `?connectionTimeout=`, cannot lift the limit. A server that answers and refuses keeps its own error code, such as `EAUTH` for a refused login or `EENVELOPE` for a refused sender. <gap>A send that ran out cannot close its connection at the limit because cancelling a send in progress is not implemented in the SMTP client; the client's own timers, 10 seconds for each of the server's addresses, close it soon after.</gap>
|
|
34
|
+
|
|
31
35
|
## Protobase's own mail
|
|
32
36
|
|
|
33
37
|
The auth emails (sign-in, two-factor and sign-up codes, the note to an address that asked to sign up while it has an account, password reset, invitations and staff sign-in notices) and the email to someone a [comment mentions](comments.md#mentions) are built with React Email's components: a heading, the message, one code or button, and a footer naming the app.
|
|
@@ -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`)
|
|
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.
|
|
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
|
|
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,
|
|
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
|