@rebasepro/types 0.24.0 → 0.24.1-canary.gcb918fc

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.
@@ -303,6 +303,23 @@ export interface UserCreationPrepareResult {
303
303
  /** Whether an invitation was sent (only relevant when hookHandledEmail is true). */
304
304
  invitationSent: boolean;
305
305
  }
306
+ /**
307
+ * What the door creating a user knows about its caller, for
308
+ * `AuthAdapter.prepareUserCreation()`.
309
+ *
310
+ * @group Auth
311
+ */
312
+ export interface UserCreationOptions {
313
+ /**
314
+ * Whether the caller may waive the password policy with
315
+ * `skipPasswordPolicy: true` in the body. The doors set it for an
316
+ * administrator (the service key signs in as one) and for nobody else, so
317
+ * an adapter that reads the flag is taking an administrator at their word —
318
+ * an import of accounts whose passwords were set under another system's
319
+ * rules. Absent, the flag waives nothing.
320
+ */
321
+ mayWaivePasswordPolicy?: boolean;
322
+ }
306
323
  /**
307
324
  * What a create body for an auth collection may name beyond the collection's
308
325
  * own declared fields — see `AuthAdapter.describeUserCreationContract()`.
@@ -322,6 +339,16 @@ export interface UserCreationWriteContract {
322
339
  * deletes it before the row is ever built.
323
340
  */
324
341
  extraFields: string[];
342
+ /**
343
+ * Each body's `prepareUserCreation` touches nothing but its own result —
344
+ * no hook, no email, no write — so a bulk create may prepare several at
345
+ * once. The built-in adapter's path with no `onCreateUser`, no
346
+ * `onAdminCreateUser` and the default `hashPassword` is: it hashes a
347
+ * password and normalizes an email, and a 1,000-row bulk spent half a
348
+ * minute hashing one password at a time. Leave it unset when a hook runs:
349
+ * preparations then run one at a time, in the order the rows were given.
350
+ */
351
+ independent?: boolean;
325
352
  }
326
353
  /**
327
354
  * Result of `AuthAdapter.finalizeUserCreation()`.
@@ -505,11 +532,15 @@ export interface AuthAdapter {
505
532
  *
506
533
  * If not implemented, the collection saves values as-is (no password hashing).
507
534
  *
535
+ * A chosen `password` is held to the password policy unless the body says
536
+ * `skipPasswordPolicy: true` and `options.mayWaivePasswordPolicy` is set.
537
+ *
508
538
  * @param values - Raw request body from the client.
509
539
  * @param collectionAuth - The parsed `AuthCollectionConfig` from the collection (if `auth` is an object).
540
+ * @param options - What the door knows about its caller.
510
541
  * @returns Processed values ready for `driver.save()`, plus metadata for the post-save step.
511
542
  */
512
- prepareUserCreation?(values: Record<string, unknown>, collectionAuth?: unknown): Promise<UserCreationPrepareResult>;
543
+ prepareUserCreation?(values: Record<string, unknown>, collectionAuth?: unknown, options?: UserCreationOptions): Promise<UserCreationPrepareResult>;
513
544
  /**
514
545
  * Describe what a create body for this auth collection is allowed to name,
515
546
  * so unknown-field validation can run on it.
@@ -6,6 +6,7 @@ import type { AuthAdapter } from "./auth_adapter.js";
6
6
  import type { HistoryConfig } from "../controllers/client.js";
7
7
  import type { ChannelBusSetting } from "./channel_bus.js";
8
8
  import type { SchemaEditingAdmin } from "./schema_editing.js";
9
+ import type { AccessModel } from "./scopes.js";
9
10
  /**
10
11
  * Abstract database connection interface.
11
12
  * Represents a connection to any database system.
@@ -252,6 +253,52 @@ export interface RealtimeChannelsConfig {
252
253
  * Defaults to 1000. `REALTIME_MAX_SUBSCRIPTIONS_PER_SOCKET` wins over it.
253
254
  */
254
255
  maxSubscriptionsPerSocket?: number;
256
+ /**
257
+ * How many sockets one user (one identity: a signed-in user, an API key's
258
+ * user, an impersonated user) may hold open on one server instance. The
259
+ * next one's `AUTHENTICATE` is refused with `TOO_MANY_SOCKETS` and the
260
+ * socket closed with 4002; the SDK keeps retrying with backoff, so it gets
261
+ * in once another closes. A positive whole number, checked at boot.
262
+ * Unset (the default) is no ceiling. `REALTIME_MAX_SOCKETS_PER_USER` wins
263
+ * over it.
264
+ */
265
+ maxSocketsPerUser?: number;
266
+ /**
267
+ * The longest a change waits for the refetch that delivers it, in ms. A
268
+ * refetch waits for 300 ms without a change, and at most this long under a
269
+ * steady stream of writes. Defaults to 1000. A positive whole number,
270
+ * checked at boot. `REALTIME_REFETCH_MAX_WAIT_MS` wins over it.
271
+ */
272
+ refetchMaxWaitMs?: number;
273
+ /**
274
+ * How long a big list subscription's total may be reused before it is
275
+ * counted again, in ms. Unset (the default), every frame counts. Frames
276
+ * that reuse it carry `meta.totalAsOf`. `REALTIME_RECOUNT_INTERVAL_MS`
277
+ * wins over it.
278
+ */
279
+ recountIntervalMs?: number;
280
+ /**
281
+ * The smallest last total at which {@link recountIntervalMs} applies;
282
+ * smaller lists are counted on every frame. Defaults to 100 000.
283
+ * `REALTIME_RECOUNT_MIN_ROWS` wins over it.
284
+ */
285
+ recountMinRows?: number;
286
+ /**
287
+ * How changes are captured for realtime. `auto` (the default) installs
288
+ * database-level capture where the connection can and falls back to
289
+ * announcing the API's own writes where it cannot; `trigger` asks for
290
+ * capture and warns when it is refused; `wal` is `trigger` until WAL
291
+ * streaming ships; `off` announces the API's own writes only.
292
+ * `REALTIME_CDC` wins over it.
293
+ */
294
+ cdc?: "auto" | "wal" | "trigger" | "off";
295
+ /**
296
+ * The URL path the realtime socket accepts upgrades on. Defaults to the
297
+ * one the server derives from where it is mounted — `/` for a server at
298
+ * `/api`, which is where the SDK dials. Set it when clients dial elsewhere
299
+ * (a `websocketUrl` with a path). See {@link RealtimeSocketOptions.path}.
300
+ */
301
+ path?: string;
255
302
  }
256
303
  /**
257
304
  * Abstract realtime provider interface.
@@ -445,8 +492,19 @@ export interface SqlScriptResult {
445
492
  tables: SqlScriptTable[];
446
493
  /** The last statement's command: `SELECT`, `UPDATE`, `CREATE TABLE` … */
447
494
  command?: string;
448
- /** How many rows the last statement returned or changed, when its command reports a count. */
495
+ /**
496
+ * How many rows the last statement returned or changed, when its command
497
+ * reports a count — or, for a {@link truncated} result, how many it
498
+ * returned, of which {@link rows} holds the first.
499
+ */
449
500
  rowCount?: number;
501
+ /**
502
+ * The last statement returned more than a run keeps, and {@link rows}
503
+ * holds only its first rows. The Studio console's door keeps at most
504
+ * 10,000 rows, or 32 Mi characters of values, of one result: a run that
505
+ * held every row of a million-row table took the server down with it.
506
+ */
507
+ truncated?: boolean;
450
508
  /** What the database said while the script ran, in order. */
451
509
  notices: SqlScriptNotice[];
452
510
  /**
@@ -457,6 +515,22 @@ export interface SqlScriptResult {
457
515
  */
458
516
  effectiveRole?: string;
459
517
  }
518
+ /**
519
+ * A query's result being read a batch at a time — see `SQLAdmin.openSqlCursor`.
520
+ *
521
+ * @group Admin
522
+ */
523
+ export interface SqlCursor {
524
+ /** The result's column names, in order. */
525
+ readonly columns: string[];
526
+ /**
527
+ * The next rows, each value the text the database wrote for it and `null`
528
+ * for NULL — `null` once there are none left.
529
+ */
530
+ read(): Promise<(string | null)[][] | null>;
531
+ /** End the read and give the session back. Safe to call more than once. */
532
+ close(): Promise<void>;
533
+ }
460
534
  /**
461
535
  * Administrative operations for SQL-based databases (PostgreSQL, MySQL, etc.).
462
536
  * Used by the SQL Editor, RLS Editor, and schema browser.
@@ -472,12 +546,19 @@ export interface SQLAdmin {
472
546
  * before it is reused. For SQL a person wrote (the Studio editor), which
473
547
  * may `SET ROLE` and must not leave that on a pooled connection the
474
548
  * server's own queries use next.
549
+ *
550
+ * `maxResultBytes` refuses a result whose rows take more than that many
551
+ * bytes, with an error whose `code` is `RESULT_TOO_LARGE`, rather than
552
+ * return it — never a part of it. On a session of its own the read stops
553
+ * there; elsewhere the rows are measured once read. The WebSocket
554
+ * `EXECUTE_SQL` door sets it for every caller.
475
555
  */
476
556
  executeSql(sql: string, options?: {
477
557
  database?: string;
478
558
  role?: string;
479
559
  params?: unknown[];
480
560
  isolateSession?: boolean;
561
+ maxResultBytes?: number;
481
562
  }): Promise<Record<string, unknown>[]>;
482
563
  /**
483
564
  * Run a script a person wrote — the Studio SQL console — and describe what
@@ -490,11 +571,35 @@ export interface SQLAdmin {
490
571
  * no other. A script that leaves a transaction open is refused, and the
491
572
  * transaction rolled back: nothing outlives the run to be committed or
492
573
  * rolled back by a later one.
574
+ *
575
+ * Aborting `signal` cancels the run: the statement in progress is stopped
576
+ * (`pg_cancel_backend` on a Postgres server; the in-process development
577
+ * database cannot be interrupted, and the run ends on its own), and the
578
+ * promise rejects.
493
579
  */
494
580
  runSqlScript?(sql: string, options?: {
495
581
  database?: string;
496
582
  role?: string;
583
+ signal?: AbortSignal;
497
584
  }): Promise<SqlScriptResult>;
585
+ /**
586
+ * Read the whole result of ONE query a person wrote, a batch at a time —
587
+ * the Studio SQL console's "Download full result as CSV", which the
588
+ * console's 10,000-row cap would otherwise leave no way to get.
589
+ *
590
+ * Through a server-side cursor, in a read-only transaction, on a session
591
+ * of its own, as `role`: a write is refused by the database, and a second
592
+ * statement by the protocol. Each batch is read when the previous one has
593
+ * been consumed, so memory holds one batch whatever the result's size;
594
+ * every read runs under the session's `statement_timeout`. The session is
595
+ * reset and returned when the cursor is closed — which the caller must do,
596
+ * on success, failure or a reader that went away.
597
+ */
598
+ openSqlCursor?(sql: string, options?: {
599
+ database?: string;
600
+ role?: string;
601
+ batchSize?: number;
602
+ }): Promise<SqlCursor>;
498
603
  /**
499
604
  * Fetch the available databases on the server.
500
605
  */
@@ -520,6 +625,21 @@ export interface SQLAdmin {
520
625
  * Fetch the current database name.
521
626
  */
522
627
  fetchCurrentDatabase?(): Promise<string | undefined>;
628
+ /**
629
+ * Hold a `LISTEN` on `channel`, on a connection of its own that stays open
630
+ * and comes back after a drop, and call `onNotify` with each payload — what
631
+ * wakes the job worker when another process enqueues, instead of a poll
632
+ * every two seconds.
633
+ *
634
+ * Resolves once listening, with the stop. Rejects when no such connection
635
+ * can be had — an in-process database, a first connect that fails — so the
636
+ * caller can fall back to polling. `onReconnect` is called each time a
637
+ * dropped connection is listening again: whatever was sent meanwhile is
638
+ * lost. `channel` must be a plain identifier.
639
+ */
640
+ listen?(channel: string, onNotify: (payload: string) => void, options?: {
641
+ onReconnect?: () => void;
642
+ }): Promise<() => Promise<void>>;
523
643
  }
524
644
  /**
525
645
  * Administrative operations for document-based databases (MongoDB, Firestore, etc.).
@@ -778,6 +898,24 @@ export interface RealtimeSocketOptions {
778
898
  } | {
779
899
  message: string;
780
900
  }>;
901
+ /**
902
+ * Verify a session token presented to `AUTHENTICATE` with the backend's own
903
+ * keys, when the socket has no auth adapter to ask: who it names, or null
904
+ * when it does not verify. The server passes its backend's; absent, the
905
+ * driver falls back to the process default.
906
+ */
907
+ verifyAccessToken?: (token: string) => Promise<{
908
+ uid: string;
909
+ roles?: string[];
910
+ isAnonymous?: boolean;
911
+ claims?: Record<string, unknown>;
912
+ } | null>;
913
+ /**
914
+ * The backend's access model — what a session's roles hold on the socket's
915
+ * admin frames. The server passes its backend's; absent, the driver falls
916
+ * back to the process default.
917
+ */
918
+ accessModel?: AccessModel;
781
919
  /**
782
920
  * The largest frame accepted, in bytes — the data API's body limit. `0` or
783
921
  * less for none. Defaults to the server's default body limit.
@@ -799,6 +937,29 @@ export interface RealtimeSocketOptions {
799
937
  * identity that has since signed out, been revoked, demoted or deleted.
800
938
  */
801
939
  identityRecheckIntervalMs?: number;
940
+ /**
941
+ * How often a socket that asked for heartbeats (`?rebase_caps=heartbeat`)
942
+ * is sent one, in ms (default 30 000). The SDK redials after two missed.
943
+ */
944
+ heartbeatIntervalMs?: number;
945
+ /**
946
+ * How many sockets one identity may hold open on this server; the next
947
+ * one's `AUTHENTICATE` is refused with `TOO_MANY_SOCKETS` and the socket
948
+ * closed (4002). Unset for no ceiling. See `realtime.maxSocketsPerUser`.
949
+ */
950
+ maxSocketsPerUser?: number;
951
+ /**
952
+ * The URL path the socket accepts upgrades on (default `/`). The SDK dials
953
+ * its `baseUrl` with no extra segment, so for a server mounted at `/api`
954
+ * this is `/`, and for one at `/app/api` it is `/app`. A trailing `/` is
955
+ * ignored, and so is the query string.
956
+ *
957
+ * An upgrade to any other path is not this socket's: it is left untouched
958
+ * for another `upgrade` listener on the same `http.Server` — a host app's
959
+ * own WebSocket server — or, when the socket's listener is the only one,
960
+ * answered `404` and closed, so the client is not left hanging.
961
+ */
962
+ path?: string;
802
963
  }
803
964
  /**
804
965
  * A `BackendBootstrapper` encapsulates all driver-specific initialization logic.
@@ -876,8 +1037,11 @@ export interface BackendBootstrapper {
876
1037
  */
877
1038
  initializeRealtime?(config: unknown, driverResult: InitializedDriver): Promise<RealtimeProvider | undefined>;
878
1039
  /**
879
- * Mount any driver-specific HTTP routes (e.g., custom admin endpoints).
880
- * Called after all drivers are initialized.
1040
+ * Never called: the server mounts no route on a bootstrapper's behalf. A
1041
+ * driver that needs HTTP routes exports a route factory, and the app
1042
+ * mounts it where it chooses.
1043
+ *
1044
+ * @deprecated Never called. Will be removed in a future release.
881
1045
  */
882
1046
  mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;
883
1047
  /**
@@ -1029,7 +1193,7 @@ export interface InitializedDriver {
1029
1193
  connection?: DatabaseConnection;
1030
1194
  /**
1031
1195
  * Opaque handle that the bootstrapper can use in subsequent hooks
1032
- * (e.g., `initializeAuth`, `mountRoutes`) to access driver internals.
1196
+ * (e.g., `initializeAuth`, `getAdmin`) to access driver internals.
1033
1197
  * Not used by the coordinator.
1034
1198
  */
1035
1199
  internals?: unknown;
@@ -87,6 +87,25 @@ export interface BackupScheduleStatus {
87
87
  */
88
88
  historyError?: string;
89
89
  }
90
+ /**
91
+ * A short-lived link to one backup's bytes, from `POST /admin/backups/download-url`.
92
+ *
93
+ * The URL needs no `Authorization` header — the token in it stands in for one,
94
+ * for this backup only — so a browser can be sent to it and stream the dump to
95
+ * disk instead of holding it in the page.
96
+ */
97
+ export interface BackupDownloadLink {
98
+ /**
99
+ * `…/admin/backups/download?key=…&token=…`. The server answers it as a
100
+ * path on itself; the SDK's `downloadUrl()` answers it absolute, on the
101
+ * client's own base URL.
102
+ */
103
+ url: string;
104
+ /** The token in {@link url}, on its own. */
105
+ token: string;
106
+ /** Seconds until the link stops working (60). */
107
+ expiresIn: number;
108
+ }
90
109
  /** What the backup listing (`GET /admin/backups`) answers. */
91
110
  export interface BackupListing {
92
111
  /** Backups at the destination, newest first. */
@@ -1,4 +1,5 @@
1
1
  import type { RebaseServerClient } from "../controllers/client.js";
2
+ import type { RegisteredDatabase } from "./register.js";
2
3
  /**
3
4
  * Cron Job type definitions for Rebase.
4
5
  *
@@ -10,7 +11,7 @@ import type { RebaseServerClient } from "../controllers/client.js";
10
11
  * A cron job definition file exports this shape as its default export.
11
12
  * See the example cron files in `app/backend/crons/` for usage.
12
13
  */
13
- export interface CronJobDefinition {
14
+ export interface CronJobDefinition<DB = RegisteredDatabase> {
14
15
  schedule: string;
15
16
  /**
16
17
  * IANA time zone the schedule is read in, e.g. `"Europe/Madrid"`.
@@ -84,20 +85,21 @@ export interface CronJobDefinition {
84
85
  * Receives a context object with the data driver and logger.
85
86
  * May return arbitrary JSON-serialisable data stored in the log.
86
87
  */
87
- handler: (ctx: CronJobContext) => Promise<unknown> | unknown;
88
+ handler: (ctx: CronJobContext<DB>) => Promise<unknown> | unknown;
88
89
  }
89
90
  /**
90
91
  * Context passed to each cron handler invocation.
91
92
  */
92
- export interface CronJobContext {
93
+ export interface CronJobContext<DB = RegisteredDatabase> {
93
94
  /** The job's unique ID (derived from filename). */
94
95
  jobId: string;
95
96
  /** The current scheduled tick timestamp. */
96
97
  scheduledAt: Date;
97
98
  /**
98
- * A simple logger scoped to this job run. Each line is kept with the
99
- * run's log entry and also written to the server log at `info`, with a
100
- * `cron` field holding the job ID.
99
+ * A simple logger scoped to this job run. Each line is written to the
100
+ * server log at `info`, with a `cron` field holding the job ID, and kept
101
+ * with the run's log entry — the first 500 lines or 64 KB of them,
102
+ * whichever comes first, then one line saying how many more there were.
101
103
  */
102
104
  log: (...args: unknown[]) => void;
103
105
  /**
@@ -156,7 +158,7 @@ export interface CronJobContext {
156
158
  * }
157
159
  * });
158
160
  */
159
- rebase: RebaseServerClient;
161
+ rebase: RebaseServerClient<DB>;
160
162
  }
161
163
  export type CronJobRunState = "idle" | "running" | "success" | "error" | "disabled";
162
164
  /**
@@ -244,3 +246,16 @@ export interface CronJobLogEntry {
244
246
  /** Whether this was a manual trigger. */
245
247
  manual?: boolean;
246
248
  }
249
+ /**
250
+ * The answer to a manual trigger made with `?async=1`
251
+ * (`client.cron.triggerJob(id, { async: true })`): the run has started and
252
+ * carries on after the answer. Its outcome is in the run history, as the entry
253
+ * with `manual: true` and this `startedAt`.
254
+ */
255
+ export interface CronJobStarted {
256
+ accepted: true;
257
+ /** ISO timestamp the run started at — the `startedAt` of its log entry. */
258
+ startedAt: string;
259
+ /** The job's status as the run began. */
260
+ job: CronJobStatus;
261
+ }
@@ -171,9 +171,11 @@ export interface DatabaseAdapter {
171
171
  */
172
172
  getAdmin?(driverResult: InitializedDriver): DatabaseAdmin | undefined;
173
173
  /**
174
- * Mount any database-specific HTTP routes (e.g., custom admin endpoints).
174
+ * Never called: the server mounts no route on an adapter's behalf. A
175
+ * driver that needs HTTP routes exports a route factory, and the app
176
+ * mounts it where it chooses.
175
177
  *
176
- * Called after all adapters are initialized.
178
+ * @deprecated Never called. Will be removed in a future release.
177
179
  */
178
180
  mountRoutes?(app: unknown, basePath: string, driverResult: InitializedDriver): void;
179
181
  /**
@@ -196,8 +198,29 @@ export interface DatabaseAdapterInitConfig {
196
198
  * name none when this is the default.
197
199
  */
198
200
  dataSourceKey?: string;
199
- /** The shared collection registry to register into. */
200
- collectionRegistry: CollectionRegistryInterface;
201
+ /**
202
+ * The backend's shared collection registry. No bundled driver reads it —
203
+ * each builds and returns its own on `InitializedDriver.collectionRegistry`
204
+ * — so a caller opening a driver on its own (a script, a test) leaves it out.
205
+ */
206
+ collectionRegistry?: CollectionRegistryInterface;
207
+ /**
208
+ * Whether opening the driver may change the database (default `true`).
209
+ *
210
+ * The server's boot owns its schema, so the driver provisions what it needs
211
+ * while it opens: the restricted role requests run as under RLS, the
212
+ * branch-metadata table, channel-history tables, change-capture triggers.
213
+ *
214
+ * `false` opens it with no DDL of any kind — what a script, a REPL or a
215
+ * test that only reads and writes rows wants, and what a process that does
216
+ * not own the schema must ask for. A connection that would bypass
217
+ * row-level security still sends a user's requests through the restricted
218
+ * role; when an earlier boot never created that role, those requests fail
219
+ * rather than run unfiltered. Change capture is not installed
220
+ * (`realtime.provision` reads as `false`); whether change events are
221
+ * consumed is still `realtime.subscribe`.
222
+ */
223
+ provision?: boolean;
201
224
  /**
202
225
  * Whether this driver should describe its own schema.
203
226
  *
@@ -251,9 +274,12 @@ export interface DatabaseAdapterInitConfig {
251
274
  * runtime otherwise refuses to boot without.
252
275
  *
253
276
  * Absent means both — every caller that predates this field is a
254
- * single-process deployment, where both are true.
277
+ * single-process deployment, where both are true. `false` is a process
278
+ * that takes no part in realtime at all: nothing installed, nothing
279
+ * consumed, and its writes not announced to the processes that serve
280
+ * realtime (a script, a REPL).
255
281
  */
256
- realtime?: {
282
+ realtime?: false | {
257
283
  /**
258
284
  * Consume change events: open the `LISTEN` connection, start CDC or the
259
285
  * app-level fallback. False for a process that serves no websockets.
@@ -32,6 +32,17 @@ export interface Entity<M extends Record<string, unknown> = Record<string, unkno
32
32
  * and a record fetched by id never has one.
33
33
  */
34
34
  searchMatches?: SearchMatch[];
35
+ /**
36
+ * The version of the row this entity was read at: the `ETag` the read
37
+ * reported, for a `findById`, a `PATCH` answer or a live row frame. A
38
+ * form saves with it as `If-Match`, so a write made against a version
39
+ * someone has since replaced is refused instead of reverting their change.
40
+ *
41
+ * Absent for a row from a list, from a cache, or from a server or data
42
+ * source that does not report versions — a save then is unconditional,
43
+ * as it always was.
44
+ */
45
+ etag?: string;
35
46
  /**
36
47
  * Which driver this entity belongs to (e.g., 'postgres', 'firestore').
37
48
  * If not specified, the default driver is assumed.
@@ -27,8 +27,20 @@ export interface EntityHistoryEntry {
27
27
  action: "create" | "update" | "delete";
28
28
  /** Which fields changed. `null` for creates and deletes. */
29
29
  changed_fields: string[] | null;
30
+ /** The row after the change; for a delete, the row that was deleted. */
30
31
  values: Record<string, unknown> | null;
32
+ /**
33
+ * On an update, what the changed fields held before it. `null` for creates
34
+ * and deletes. Which fields it holds depends on {@link format}.
35
+ */
31
36
  previous_values: Record<string, unknown> | null;
37
+ /**
38
+ * How the entry was stored. Absent or `1`: `previous_values` is the whole
39
+ * row before the change. `2`: it holds the `changed_fields` only — a field
40
+ * that is absent from it was absent before — and every other field is
41
+ * unchanged, so it is in `values`.
42
+ */
43
+ format?: 1 | 2;
32
44
  updated_by: string | null;
33
45
  /** ISO-8601. A driver storing a native date converts on read. */
34
46
  updated_at: string;
@@ -46,7 +58,9 @@ export interface RecordHistoryParams {
46
58
  updatedBy?: string | null;
47
59
  }
48
60
  /**
49
- * How much history to keep. Pruning runs per row after each write.
61
+ * How much history to keep. Both rules are applied to a row after each write
62
+ * to it; `ttlDays` is also applied to every row by an hourly sweep, so the
63
+ * history of a row nobody writes again (a deleted one) still ages out.
50
64
  * @group Backend
51
65
  */
52
66
  export interface HistoryRetentionConfig {
@@ -35,3 +35,4 @@ export * from "./project_manifest.js";
35
35
  export * from "./collection_contract.js";
36
36
  export * from "./schema_version.js";
37
37
  export * from "./storage_authorize.js";
38
+ export * from "./register.js";
@@ -83,6 +83,14 @@ export interface RebaseBackendAppConfig {
83
83
  * its default export. Default `collections/users`.
84
84
  */
85
85
  usersCollection?: string;
86
+ /**
87
+ * Type server-side data from the generated `Database`. When `true`, every
88
+ * `rebase generate-sdk` — `rebase dev`'s included, and the first one on a
89
+ * fresh clone — writes the types and the `Register` declaration to
90
+ * `<config>/generated/`. `rebase generate-sdk --register` sets it.
91
+ * Default `false`.
92
+ */
93
+ registerDatabase?: boolean;
86
94
  /**
87
95
  * `runtime: "custom"` only. Dockerfile path relative to the repository root.
88
96
  * Default `Dockerfile`.
@@ -5,6 +5,12 @@ import type { ColorKey, ColorScheme } from "./chips.js";
5
5
  import type { AuthState } from "../controllers/auth_state.js";
6
6
  import type { AfterReadProps, BeforeSaveProps } from "./entity_callbacks.js";
7
7
  import type { User } from "../users/index.js";
8
+ /**
9
+ * A SQL expression, written in template form: ``"sql`gen_random_uuid()`"``.
10
+ * The `sql` and backticks are markup; the text between them is the SQL.
11
+ * @group Entity properties
12
+ */
13
+ export type SqlExpression = `sql\`${string}\``;
8
14
  /**
9
15
  * Callbacks/Hooks for individual property fields
10
16
  * @group Entity properties
@@ -372,6 +378,25 @@ export interface BaseProperty<CustomProps = unknown> {
372
378
  * ```
373
379
  */
374
380
  access?: FieldAccess;
381
+ /**
382
+ * The database fills this column when a create leaves it empty: a
383
+ * `NOT NULL DEFAULT <expression>` column such as `gen_random_uuid()`,
384
+ * `nextval(…)` or `now()`, whose value no property can state.
385
+ *
386
+ * A `validation.required` field with it may be left out of a create, by
387
+ * the admin form and by the API alike, and a create that sends it as `null`
388
+ * leaves it to the database rather than storing `NULL`. It is still
389
+ * required when a stored record is edited. The generated SDK's `Insert`
390
+ * type makes the field optional. Defaults to `false`.
391
+ *
392
+ * Give the expression, in the template form `isId` takes
393
+ * (``databaseDefault: "sql`gen_random_uuid()`"``), and the schema planner
394
+ * writes it as the column's `DEFAULT`, so `rebase db push` keeps it. `true`
395
+ * declares a DEFAULT someone else wrote and the planner does not write
396
+ * one, so a push plans dropping it. "Import from table" and
397
+ * `rebase schema introspect` set the expression they read.
398
+ */
399
+ databaseDefault?: boolean | SqlExpression;
375
400
  /**
376
401
  * Use this to define dynamic properties that change based on certain conditions
377
402
  * or on the entity's values. For example, you can make a field read-only if
@@ -421,8 +446,9 @@ export interface StringProperty extends BaseProperty {
421
446
  */
422
447
  defaultValue?: string;
423
448
  /**
424
- * Optional database column type. If not set, it defaults to `varchar` or `uuid` depending on `isId` configuration.
425
- * Use `text` for strings with unbound length, `char` for fixed-length strings, or `varchar` for variable-length strings with a limit.
449
+ * Optional database column type. If not set, it is `text` — or `uuid` for an `isId: "uuid"` key.
450
+ * `validation.max` does not bound a `text` column; declare `varchar` (or `char`) to get a
451
+ * width, which is then taken from `validation.max` (255 when it states none).
426
452
  */
427
453
  columnType?: "varchar" | "text" | "char" | "uuid";
428
454
  /**
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The project-wide registry of generated types, filled by module augmentation.
3
+ *
4
+ * Empty by default, and every server-side data surface is untyped while it is:
5
+ * `context.data`, `rebase.dataAsAdmin`, a cron's `ctx.rebase` and a function's
6
+ * `ctx.rebase` accept any collection by any name and hand back open records.
7
+ *
8
+ * A project that generated its types with `rebase generate-sdk` registers its
9
+ * `Database` once, in any file the TypeScript program includes:
10
+ *
11
+ * ```ts
12
+ * // src/rebase-env.d.ts — or the top of any module file
13
+ * import type { Database } from "./generated/database.types";
14
+ *
15
+ * declare module "@rebasepro/types" {
16
+ * interface Register {
17
+ * database: Database;
18
+ * }
19
+ * }
20
+ * ```
21
+ *
22
+ * From then on every one of those surfaces is typed from it: rows, filters,
23
+ * sorts, `include`, and the inputs `create` and `update` accept. Nothing about
24
+ * the runtime changes; the registration is a declaration.
25
+ *
26
+ * A single function or cron can still name its own type instead —
27
+ * `defineFunction<Database>(…)`, `defineCron<Database>({…})` — which wins over
28
+ * the registration.
29
+ *
30
+ * @group Data
31
+ */
32
+ export interface Register {
33
+ }
34
+ /**
35
+ * The `Database` a project registered on {@link Register}, or `unknown` when it
36
+ * registered none — which is what keeps an unregistered project's server-side
37
+ * data untyped, exactly as it was before the registry existed.
38
+ *
39
+ * @group Data
40
+ */
41
+ export type RegisteredDatabase = Register extends {
42
+ database: infer DB;
43
+ } ? DB : unknown;