@rebasepro/types 0.19.1 → 0.19.2-canary.g09316f6
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/controllers/data.d.ts +515 -14
- package/dist/controllers/data_driver.d.ts +166 -4
- package/dist/controllers/storage.d.ts +30 -0
- package/dist/index.es.js +163 -3
- package/dist/index.es.js.map +1 -1
- package/dist/types/collections.d.ts +59 -0
- package/dist/types/filter-operators.d.ts +37 -6
- package/dist/types/index.d.ts +1 -0
- package/dist/types/policy.d.ts +39 -1
- package/dist/types/properties.d.ts +205 -8
- package/dist/types/relations.d.ts +71 -0
- package/dist/types/schema_editing.d.ts +6 -0
- package/dist/types/tenancy.d.ts +147 -0
- package/dist/types/websockets.d.ts +37 -0
- package/dist/users/user.d.ts +21 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import type { Relation } from "./relations.js";
|
|
|
6
6
|
import type { SecurityRule } from "./security_rules.js";
|
|
7
7
|
import type { SearchConfig } from "./search.js";
|
|
8
8
|
import type { CollectionIndex } from "./indexes.js";
|
|
9
|
+
import type { CollectionTenantConfig } from "./tenancy.js";
|
|
9
10
|
/**
|
|
10
11
|
* Base interface containing all driver-agnostic collection properties.
|
|
11
12
|
* Use {@link PostgresCollectionConfig} or {@link FirebaseCollectionConfig} for
|
|
@@ -149,6 +150,12 @@ export interface BaseCollectionConfig<M extends Record<string, unknown> = Record
|
|
|
149
150
|
/**
|
|
150
151
|
* User id of the owner of this collection. This is used only by plugins, or if you
|
|
151
152
|
* are writing custom code
|
|
153
|
+
*
|
|
154
|
+
* **Admin form only — not enforced by the API or the database.** The
|
|
155
|
+
* collection editor stamps it on a collection it creates and shows it
|
|
156
|
+
* beside the name; nothing on the request path consults it. It is not an
|
|
157
|
+
* ownership check, and a collection with somebody else's id here is served
|
|
158
|
+
* to exactly the same callers as one with none.
|
|
152
159
|
*/
|
|
153
160
|
ownerId?: string;
|
|
154
161
|
/**
|
|
@@ -290,6 +297,58 @@ export interface PostgresCollectionConfig<M extends Record<string, unknown> = Re
|
|
|
290
297
|
* rather than silently ignored.
|
|
291
298
|
*/
|
|
292
299
|
indexes?: readonly CollectionIndex<Extract<keyof M, string>>[];
|
|
300
|
+
/**
|
|
301
|
+
* Turn `delete` into "stamp a timestamp", and hide stamped rows from reads.
|
|
302
|
+
*
|
|
303
|
+
* With this on, a delete — single, bulk or through a nested path — sets the
|
|
304
|
+
* field to `now()` instead of issuing a `DELETE`, and every read filters
|
|
305
|
+
* `<field> IS NULL` by default: `find`, `findById`, `count`, aggregates, the
|
|
306
|
+
* realtime refetch, and the loading of this collection through a relation.
|
|
307
|
+
* A restore is an ordinary update setting the field back to `null`. A real
|
|
308
|
+
* `DELETE` is still available as `delete(…, { hard: true })` / `?hard=true`,
|
|
309
|
+
* and needs exactly the same permission an ordinary delete does — it is the
|
|
310
|
+
* same operation, and gating it separately would be a second access-control
|
|
311
|
+
* surface for one verb.
|
|
312
|
+
*
|
|
313
|
+
* `true` uses `deletedAt` (column `deleted_at`). The object form renames the
|
|
314
|
+
* field. **Either way the collection must declare that property itself**, as
|
|
315
|
+
* a `date` — this flag says what a column *means*, it does not conjure the
|
|
316
|
+
* column into existence. A config that turns it on without the property is
|
|
317
|
+
* refused at boot rather than at the first delete, because the failure would
|
|
318
|
+
* otherwise land on a caller trying to remove a row.
|
|
319
|
+
*
|
|
320
|
+
* The hooks do not change: `beforeDelete` can still veto and `afterDelete`
|
|
321
|
+
* still fires. From the application's point of view the row was deleted;
|
|
322
|
+
* how the table records that is this flag's business.
|
|
323
|
+
*
|
|
324
|
+
* Postgres-only, like {@link SearchConfig}.
|
|
325
|
+
*/
|
|
326
|
+
softDelete?: boolean | {
|
|
327
|
+
/**
|
|
328
|
+
* The `date` property that records the deletion. Defaults to
|
|
329
|
+
* `deletedAt`.
|
|
330
|
+
*/
|
|
331
|
+
field?: string;
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Scope every row of this collection to a tenant.
|
|
335
|
+
*
|
|
336
|
+
* One declaration replaces the four hand-written pieces a tenant-scoped
|
|
337
|
+
* table used to need — the `NOT NULL` column, the RLS rule, the value
|
|
338
|
+
* stamped on insert, and the index — and keeps them in agreement, because
|
|
339
|
+
* they are all derived from this.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* tenant: { field: "orgId", from: { claim: "org_id" } }
|
|
343
|
+
* ```
|
|
344
|
+
*
|
|
345
|
+
* The property must already be declared: this says what a column *means*,
|
|
346
|
+
* it does not create one. Postgres-only, like {@link SearchConfig} — RLS is
|
|
347
|
+
* what enforces the boundary.
|
|
348
|
+
*
|
|
349
|
+
* @see CollectionTenantConfig
|
|
350
|
+
*/
|
|
351
|
+
tenant?: CollectionTenantConfig<M>;
|
|
293
352
|
}
|
|
294
353
|
/**
|
|
295
354
|
* A collection backed by Firebase / Firestore.
|
|
@@ -37,16 +37,36 @@
|
|
|
37
37
|
* @module
|
|
38
38
|
*/
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Where NULLs sort relative to real values on one key.
|
|
41
|
+
*
|
|
42
|
+
* Absent means the convention Postgres itself applies and the driver writes
|
|
43
|
+
* out: `NULLS LAST` ascending, `NULLS FIRST` descending. That convention was
|
|
44
|
+
* hardcoded and unstateable — a "newest first" list put every row with no date
|
|
45
|
+
* at the very top, and the only way out was to add a `is-not-null` filter and
|
|
46
|
+
* lose those rows entirely.
|
|
47
|
+
*
|
|
48
|
+
* The keyset comparison honours whatever is chosen here, so a cursor over a
|
|
49
|
+
* nullable key stays correct under either placement.
|
|
50
|
+
*
|
|
51
|
+
* @group Models
|
|
52
|
+
*/
|
|
53
|
+
export type NullsPlacement = "first" | "last";
|
|
54
|
+
/**
|
|
55
|
+
* Canonical sort representation: `[fieldName, direction]`, optionally with a
|
|
56
|
+
* {@link NullsPlacement}.
|
|
41
57
|
*
|
|
42
58
|
* Used in `FindParams.orderBy`, `collection.sort`, and `FilterPreset.sort`.
|
|
43
|
-
* The colon-string form (`"field:direction"`)
|
|
44
|
-
* boundary, handled by `serializeOrderBy` /
|
|
45
|
-
* `@rebasepro/common`.
|
|
59
|
+
* The colon-string form (`"field:direction"`, or `"field:direction:nulls"`)
|
|
60
|
+
* exists only at the HTTP wire boundary, handled by `serializeOrderBy` /
|
|
61
|
+
* `deserializeOrderBy` in `@rebasepro/common`.
|
|
62
|
+
*
|
|
63
|
+
* The third slot is optional so every `[field, direction]` written before it
|
|
64
|
+
* existed is still exactly this type, and every `const [field, direction] =`
|
|
65
|
+
* destructure still reads what it always read.
|
|
46
66
|
*
|
|
47
67
|
* @group Models
|
|
48
68
|
*/
|
|
49
|
-
export type OrderByTuple<Key extends string = string> = [Key, "asc" | "desc"];
|
|
69
|
+
export type OrderByTuple<Key extends string = string> = [Key, "asc" | "desc", NullsPlacement?];
|
|
50
70
|
/**
|
|
51
71
|
* One sort key, or several applied in order of significance.
|
|
52
72
|
*
|
|
@@ -83,7 +103,7 @@ export type SortKey<Key extends string = string> = Key | RelationAggregateSort;
|
|
|
83
103
|
*
|
|
84
104
|
* @group Models
|
|
85
105
|
*/
|
|
86
|
-
export type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, "asc" | "desc"];
|
|
106
|
+
export type OrderBySortTuple<Key extends string = string> = [SortKey<Key>, "asc" | "desc", NullsPlacement?];
|
|
87
107
|
/**
|
|
88
108
|
* The aggregate functions a relation sort can apply.
|
|
89
109
|
*
|
|
@@ -307,6 +327,17 @@ export declare const REST_TO_CANONICAL: Readonly<Record<RestFilterOp, WhereFilte
|
|
|
307
327
|
* Codecs normalize the value of these conditions to `null`.
|
|
308
328
|
*/
|
|
309
329
|
export declare const NULL_OPS: ReadonlySet<WhereFilterOp>;
|
|
330
|
+
/**
|
|
331
|
+
* Operators whose operand is a **list** of values rather than one value.
|
|
332
|
+
*
|
|
333
|
+
* On the wire that list is always parenthesised — `in.(draft,review)` — which
|
|
334
|
+
* is what lets the REST codec tell `?status=in.(a,b)` (the operator) from
|
|
335
|
+
* `?status=in.progress` (a value that happens to start with an operator's
|
|
336
|
+
* name). See `deserializeSingle` in `@rebasepro/common`.
|
|
337
|
+
*
|
|
338
|
+
* @group Models
|
|
339
|
+
*/
|
|
340
|
+
export declare const LIST_OPS: ReadonlySet<WhereFilterOp>;
|
|
310
341
|
/**
|
|
311
342
|
* Every canonical operator, in a stable order. Useful for engine capability
|
|
312
343
|
* declarations ({@link DataSourceCapabilities.filterOperators}) and for
|
package/dist/types/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./relations.js";
|
|
|
10
10
|
export * from "./policy.js";
|
|
11
11
|
export * from "./rls-functions.js";
|
|
12
12
|
export * from "./security_rules.js";
|
|
13
|
+
export * from "./tenancy.js";
|
|
13
14
|
export * from "./entity_callbacks.js";
|
|
14
15
|
export * from "./websockets.js";
|
|
15
16
|
export * from "./backend.js";
|
package/dist/types/policy.d.ts
CHANGED
|
@@ -236,7 +236,7 @@ export interface RawPolicyExpression {
|
|
|
236
236
|
* An operand referenced by a {@link ComparePolicyExpression}.
|
|
237
237
|
* @group Models
|
|
238
238
|
*/
|
|
239
|
-
export type PolicyOperand = FieldPolicyOperand | OuterFieldPolicyOperand | LiteralPolicyOperand | AuthUidPolicyOperand | AuthRolesPolicyOperand;
|
|
239
|
+
export type PolicyOperand = FieldPolicyOperand | OuterFieldPolicyOperand | LiteralPolicyOperand | AuthUidPolicyOperand | AuthRolesPolicyOperand | AuthClaimPolicyOperand;
|
|
240
240
|
/** A column value on the row being evaluated. @group Models */
|
|
241
241
|
export interface FieldPolicyOperand {
|
|
242
242
|
kind: "field";
|
|
@@ -271,6 +271,43 @@ export interface AuthUidPolicyOperand {
|
|
|
271
271
|
export interface AuthRolesPolicyOperand {
|
|
272
272
|
kind: "authRoles";
|
|
273
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* A named claim on the caller's session token — compiles to
|
|
276
|
+
* `NULLIF(rebase.jwt() ->> '<name>', '')`.
|
|
277
|
+
*
|
|
278
|
+
* The operand multi-tenancy is built on, and the reason it is an operand rather
|
|
279
|
+
* than a {@link RawPolicyExpression}: a claim arrives as **text**, and the
|
|
280
|
+
* column it is compared against usually is not. `org_id = rebase.jwt() ->>
|
|
281
|
+
* 'org_id'` on a `uuid` column is not a policy that denies — it is
|
|
282
|
+
* `CREATE POLICY` failing with "operator does not exist: uuid = text", and a
|
|
283
|
+
* table left with RLS enabled and no policy denies every row. Casting the
|
|
284
|
+
* *column* to text instead compiles, but takes the index off the one predicate
|
|
285
|
+
* that is ANDed into every read of the table.
|
|
286
|
+
*
|
|
287
|
+
* As an operand the compiler can see both sides: it casts the claim to the
|
|
288
|
+
* column's type, guarded so a malformed claim denies rather than raising
|
|
289
|
+
* `invalid input syntax` on every query, and the column keeps its index.
|
|
290
|
+
*
|
|
291
|
+
* An absent claim, and a claim set to the empty string, are both NULL — and a
|
|
292
|
+
* comparison against NULL is never true, so a caller carrying no claim sees no
|
|
293
|
+
* rows rather than all of them.
|
|
294
|
+
*
|
|
295
|
+
* Only *custom* claims are reachable. `uid`, `roles`, `aal` and `isAnonymous`
|
|
296
|
+
* are identity claims written after the custom ones when a token is minted,
|
|
297
|
+
* precisely so a claims hook cannot assert them; they have their own operands
|
|
298
|
+
* ({@link AuthUidPolicyOperand}, {@link AuthRolesPolicyOperand}) and naming one
|
|
299
|
+
* here is refused.
|
|
300
|
+
*
|
|
301
|
+
* Postgres-authoritative: the JavaScript evaluator reports *unknown* rather
|
|
302
|
+
* than reproducing Postgres's cast semantics (uuid case folding, numeric
|
|
303
|
+
* widening) a second time and getting them subtly wrong.
|
|
304
|
+
* @group Models
|
|
305
|
+
*/
|
|
306
|
+
export interface AuthClaimPolicyOperand {
|
|
307
|
+
kind: "authClaim";
|
|
308
|
+
/** The claim's name on the token, e.g. `"org_id"`. */
|
|
309
|
+
name: string;
|
|
310
|
+
}
|
|
274
311
|
/** @group Models */
|
|
275
312
|
export declare const policy: {
|
|
276
313
|
true: () => TruePolicyExpression;
|
|
@@ -294,4 +331,5 @@ export declare const policy: {
|
|
|
294
331
|
literal: (value: string | number | boolean | null) => LiteralPolicyOperand;
|
|
295
332
|
authUid: () => AuthUidPolicyOperand;
|
|
296
333
|
authRoles: () => AuthRolesPolicyOperand;
|
|
334
|
+
authClaim: (name: string) => AuthClaimPolicyOperand;
|
|
297
335
|
};
|
|
@@ -250,6 +250,28 @@ export type InferEntityType<P extends Properties> = {
|
|
|
250
250
|
} & {
|
|
251
251
|
-readonly [K in OptionalPropertyKeys<P>]?: InferPropertyType<P[K]>;
|
|
252
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* Per-field read and write permission, by application role.
|
|
255
|
+
*
|
|
256
|
+
* An omitted list is not an empty one, and the difference is the whole type:
|
|
257
|
+
* *omitted* delegates to the row — everyone the collection's security rules let
|
|
258
|
+
* read (or write) the row gets the field; *`[]`* is nobody, through the API, at
|
|
259
|
+
* any privilege. `["editor"]` is everyone holding `editor`, plus `admin`.
|
|
260
|
+
*
|
|
261
|
+
* @see BaseProperty.access
|
|
262
|
+
*/
|
|
263
|
+
export interface FieldAccess {
|
|
264
|
+
/**
|
|
265
|
+
* Roles that may read the field. Omitted = everyone the collection's RLS
|
|
266
|
+
* lets read the row. `[]` = nobody through the API.
|
|
267
|
+
*/
|
|
268
|
+
read?: readonly string[];
|
|
269
|
+
/**
|
|
270
|
+
* Roles that may write the field. Omitted = everyone the collection's RLS
|
|
271
|
+
* lets write the row. `[]` = nobody through the API.
|
|
272
|
+
*/
|
|
273
|
+
write?: readonly string[];
|
|
274
|
+
}
|
|
253
275
|
export interface BaseProperty<CustomProps = unknown> {
|
|
254
276
|
/**
|
|
255
277
|
* The label the admin panel shows for this field — a column header, a form
|
|
@@ -311,13 +333,56 @@ export interface BaseProperty<CustomProps = unknown> {
|
|
|
311
333
|
* This is a server-side guarantee, unlike `admin.hideFromCollection`, which
|
|
312
334
|
* only stops the admin panel from *rendering* a field and leaves it in the
|
|
313
335
|
* JSON payload.
|
|
336
|
+
*
|
|
337
|
+
* Sugar for `access: { read: [], write: [] }` — the two are one mechanism,
|
|
338
|
+
* not two, and declaring both on the same property is refused at boot. Write
|
|
339
|
+
* whichever reads better: the flag says "this is the server's column", the
|
|
340
|
+
* empty lists say the same thing in the vocabulary of {@link FieldAccess}.
|
|
314
341
|
*/
|
|
315
342
|
excludeFromApi?: boolean;
|
|
343
|
+
/**
|
|
344
|
+
* Who may read and who may write this one field.
|
|
345
|
+
*
|
|
346
|
+
* Row access is the collection's `securityRules`; this is the field inside
|
|
347
|
+
* the row. A caller the row's policies let through still does not receive a
|
|
348
|
+
* field their roles cannot read — it is *absent* from the response rather
|
|
349
|
+
* than `null`, so a client cannot tell a withheld value from a stored one by
|
|
350
|
+
* its shape — and a write naming a field their roles cannot write is a 400
|
|
351
|
+
* (`FIELD_NOT_WRITABLE`), never a silently dropped key.
|
|
352
|
+
*
|
|
353
|
+
* Roles are Rebase application roles, the same ones `policy.rolesOverlap`
|
|
354
|
+
* compiles against and the same list `rebase.roles()` reads inside a policy:
|
|
355
|
+
* whatever the call context carries as `user.roles`. `admin` satisfies any
|
|
356
|
+
* non-empty list, mirroring the `rolesOverlap(['admin'])` arm every baseline
|
|
357
|
+
* policy carries — a field-level rule must not lock an administrator out of
|
|
358
|
+
* their own data, and `rebase.dataAsAdmin` holds that role.
|
|
359
|
+
*
|
|
360
|
+
* The enforcement point is the API boundary. In-process writes through
|
|
361
|
+
* `rebase.data` / `rebase.dataAsAdmin` and the framework's own auth paths do
|
|
362
|
+
* not pass through it — the same exemption `excludeFromApi` has always had,
|
|
363
|
+
* and the reason it is possible to store a password hash at all.
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```ts
|
|
367
|
+
* salary: {
|
|
368
|
+
* type: "number",
|
|
369
|
+
* // Readable by HR and by admins; writable by nobody through the API.
|
|
370
|
+
* access: { read: ["hr"], write: [] }
|
|
371
|
+
* }
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
access?: FieldAccess;
|
|
316
375
|
/**
|
|
317
376
|
* Use this to define dynamic properties that change based on certain conditions
|
|
318
377
|
* or on the entity's values. For example, you can make a field read-only if
|
|
319
378
|
* another field has a certain value.
|
|
320
379
|
* This function receives the same props as a `PropertyBuilder` and should return a partial `Property` object.
|
|
380
|
+
*
|
|
381
|
+
* **Admin form only — not enforced by the API or the database.** The
|
|
382
|
+
* function is bundled into the panel and called while a form renders. A
|
|
383
|
+
* write that never goes through a form never goes through it, so a rule
|
|
384
|
+
* that must hold for every caller belongs in {@link validation}, which the
|
|
385
|
+
* server checks, or in a security rule, which the database enforces.
|
|
321
386
|
*/
|
|
322
387
|
dynamicProps?: (props: PropertyBuilderProps) => Partial<Property>;
|
|
323
388
|
/**
|
|
@@ -328,6 +393,11 @@ export interface BaseProperty<CustomProps = unknown> {
|
|
|
328
393
|
* - Edited via the collection editor UI
|
|
329
394
|
* - Evaluated at runtime like property builders
|
|
330
395
|
*
|
|
396
|
+
* **Admin form only — not enforced by the API or the database.** Like
|
|
397
|
+
* {@link dynamicProps}, these are evaluated while a form renders and shape
|
|
398
|
+
* what the panel offers. `conditions.required` does not make a column
|
|
399
|
+
* `NOT NULL` and does not make a write fail.
|
|
400
|
+
*
|
|
331
401
|
* @see PropertyConditions for available condition options
|
|
332
402
|
* @see https://jsonlogic.com/ for JSON Logic syntax
|
|
333
403
|
*/
|
|
@@ -367,12 +437,21 @@ export interface StringProperty extends BaseProperty {
|
|
|
367
437
|
*
|
|
368
438
|
* You can set this to `"manual"` for a user-defined ID, or specify a generation strategy:
|
|
369
439
|
* 'uuid' -> Drizzle `.defaultRandom()` (Postgres gen_random_uuid())
|
|
370
|
-
*
|
|
371
|
-
*
|
|
440
|
+
* Or any other string to act as a raw SQL default expression, written either
|
|
441
|
+
* bare (`gen_random_uuid()::text`) or in template form
|
|
442
|
+
* (``isId: "sql`my_id()`"``) — the wrapper is markup and is stripped before
|
|
443
|
+
* the SQL is written. The function has to exist: nothing here creates it.
|
|
444
|
+
*
|
|
445
|
+
* `"cuid"` is **refused on Postgres**, at config load. It emitted
|
|
446
|
+
* `DEFAULT cuid()` against a function Rebase has never created — not by a
|
|
447
|
+
* generator, not at boot, not in a migration — so the column has never had a
|
|
448
|
+
* working default and the first insert relying on it failed with
|
|
449
|
+
* `function cuid() does not exist`. Use `"uuid"`, or supply your own SQL
|
|
450
|
+
* expression above and create the function in a migration.
|
|
372
451
|
*
|
|
373
452
|
* On the UI side, the field automatically gets disabled on new entities if a string strategy is provided.
|
|
374
453
|
*/
|
|
375
|
-
isId?: boolean | "manual" | "uuid" |
|
|
454
|
+
isId?: boolean | "manual" | "uuid" | string;
|
|
376
455
|
/**
|
|
377
456
|
* You can use the enum values providing a map of possible
|
|
378
457
|
* exclusive values the property can take, mapped to the label that it is
|
|
@@ -382,6 +461,10 @@ export interface StringProperty extends BaseProperty {
|
|
|
382
461
|
* colors). If you need to ensure the order of the elements, you can pass
|
|
383
462
|
* a `Map` instead of a plain object.
|
|
384
463
|
*
|
|
464
|
+
* On a SQL store this compiles to a Postgres enum type named
|
|
465
|
+
* `<table>_<column>`, so the ids are the type's labels. An empty list is
|
|
466
|
+
* refused at config load: `CREATE TYPE … AS ENUM ()` is not valid SQL, and
|
|
467
|
+
* the column would reference a type nothing creates.
|
|
385
468
|
*/
|
|
386
469
|
enum?: EnumValues;
|
|
387
470
|
/**
|
|
@@ -396,6 +479,11 @@ export interface StringProperty extends BaseProperty {
|
|
|
396
479
|
* provider (e.g. the ID in your `users` table).
|
|
397
480
|
* You can also use a property builder to specify the user path dynamically
|
|
398
481
|
* based on other values of the entity.
|
|
482
|
+
*
|
|
483
|
+
* **Admin form only — not enforced by the API or the database.** The column
|
|
484
|
+
* is an ordinary string; there is no foreign key to the users table and
|
|
485
|
+
* nothing resolves or checks the id on the way in. To make the link real,
|
|
486
|
+
* declare a `relation` to the auth collection instead.
|
|
399
487
|
*/
|
|
400
488
|
userSelect?: boolean;
|
|
401
489
|
/**
|
|
@@ -411,6 +499,29 @@ export interface StringProperty extends BaseProperty {
|
|
|
411
499
|
* renders it — as a link, an image, a video — is `admin.urlPreview`.
|
|
412
500
|
*/
|
|
413
501
|
url?: boolean;
|
|
502
|
+
/**
|
|
503
|
+
* Stamp this column with the **uid of the acting user**, on creation only
|
|
504
|
+
* (`user_on_create`) or on every write including creation
|
|
505
|
+
* (`user_on_update`). The `created_by` / `updated_by` twin of
|
|
506
|
+
* {@link DateProperty.autoValue}, which has always done the same for
|
|
507
|
+
* timestamps.
|
|
508
|
+
*
|
|
509
|
+
* The value is taken from the call context, not from the request body: a
|
|
510
|
+
* caller cannot claim to be somebody else by sending the field, because the
|
|
511
|
+
* driver overwrites whatever arrived. That is the whole point — a column
|
|
512
|
+
* whose value the caller supplies is not an audit column.
|
|
513
|
+
*
|
|
514
|
+
* With no acting user (an anonymous request, a service token, a seed
|
|
515
|
+
* script) the column is set to `null`. If it is also
|
|
516
|
+
* `validation: { required: true }`, that is a 400 rather than a null: a
|
|
517
|
+
* collection that demands to know who wrote a row is a collection that
|
|
518
|
+
* cannot accept an anonymous write.
|
|
519
|
+
*
|
|
520
|
+
* There is no foreign key here. The uid is a string, the user store may be
|
|
521
|
+
* another database entirely, and a deleted user must not take their audit
|
|
522
|
+
* trail with them — declare a `relation` if you want the join.
|
|
523
|
+
*/
|
|
524
|
+
autoValue?: "user_on_create" | "user_on_update";
|
|
414
525
|
}
|
|
415
526
|
export interface NumberProperty extends BaseProperty {
|
|
416
527
|
type: "number";
|
|
@@ -423,6 +534,27 @@ export interface NumberProperty extends BaseProperty {
|
|
|
423
534
|
* If not provided, integer fields (where validation.integer is true or isId is true) default to `integer`, others to `numeric`.
|
|
424
535
|
*/
|
|
425
536
|
columnType?: "integer" | "real" | "double precision" | "numeric" | "bigint" | "serial" | "bigserial";
|
|
537
|
+
/**
|
|
538
|
+
* Total significant digits for a `numeric` column — `NUMERIC(precision, scale)`.
|
|
539
|
+
*
|
|
540
|
+
* Money is the case this exists for. Without it a price lands as an
|
|
541
|
+
* *unbounded* `NUMERIC`, which stores `19.999999999999998` as faithfully as
|
|
542
|
+
* `19.99` and cannot be tightened afterwards without rewriting the column,
|
|
543
|
+
* so the rounding rule ends up living in whichever caller last touched the
|
|
544
|
+
* value. `precision: 10, scale: 2` makes the database the one that decides.
|
|
545
|
+
*
|
|
546
|
+
* Read only when the column is `numeric` — either declared
|
|
547
|
+
* ({@link NumberProperty.columnType}) or arrived at by default, which is
|
|
548
|
+
* what a non-integer `number` gets. Ignored on `integer`, `real`,
|
|
549
|
+
* `double precision` and the serial types, which have no modifier.
|
|
550
|
+
*
|
|
551
|
+
* Changing it on a live column is an `ALTER COLUMN … TYPE`, so `db push`
|
|
552
|
+
* plans it and the boot-time ensure reports it rather than applying it —
|
|
553
|
+
* the same treatment every other type change gets.
|
|
554
|
+
*/
|
|
555
|
+
precision?: number;
|
|
556
|
+
/** Digits after the decimal point. Requires {@link NumberProperty.precision}. */
|
|
557
|
+
scale?: number;
|
|
426
558
|
/**
|
|
427
559
|
* Rules for validating this property
|
|
428
560
|
*/
|
|
@@ -434,14 +566,27 @@ export interface NumberProperty extends BaseProperty {
|
|
|
434
566
|
* UI behavior: Field value cannot be changed after creation.
|
|
435
567
|
*
|
|
436
568
|
* You can set this to `"manual"` for a user-defined ID, or specify a generation strategy:
|
|
437
|
-
* 'increment' -> PostgreSQL `GENERATED BY DEFAULT AS IDENTITY
|
|
438
|
-
* Or any other
|
|
569
|
+
* 'increment' -> PostgreSQL `INTEGER GENERATED BY DEFAULT AS IDENTITY`.
|
|
570
|
+
* Or any other string to act as a raw SQL default expression, bare or in
|
|
571
|
+
* template form (``isId: "sql`nextval('s')`"``).
|
|
572
|
+
*
|
|
573
|
+
* {@link NumberProperty.columnType} is **not read** beside
|
|
574
|
+
* `isId: "increment"`: an increment key is always INTEGER, because every
|
|
575
|
+
* foreign key and junction column that points at a numeric primary key is
|
|
576
|
+
* INTEGER, and a wider key would be referenced by narrower columns. Config
|
|
577
|
+
* validation warns when the two are written together.
|
|
439
578
|
*/
|
|
440
579
|
isId?: boolean | "manual" | "increment" | string;
|
|
441
580
|
/**
|
|
442
581
|
* You can use the enum values providing a map of possible
|
|
443
582
|
* exclusive values the property can take, mapped to the label that it is
|
|
444
583
|
* displayed in the dropdown.
|
|
584
|
+
*
|
|
585
|
+
* Unlike a string enum, this creates **no Postgres enum type**: the column
|
|
586
|
+
* stays NUMERIC or INTEGER, so the values are offered by the panel and are
|
|
587
|
+
* not enforced by the database. (One used to be created, referenced by
|
|
588
|
+
* nothing, and every `db push` planned a DROP for it.) An empty list is
|
|
589
|
+
* refused at config load.
|
|
445
590
|
*/
|
|
446
591
|
enum?: EnumValues;
|
|
447
592
|
}
|
|
@@ -567,10 +712,18 @@ export interface DateProperty extends BaseProperty {
|
|
|
567
712
|
* Set the granularity of the field to a date or date + time.
|
|
568
713
|
* Defaults to `date_time`.
|
|
569
714
|
*
|
|
715
|
+
* **Admin form only — not enforced by the API or the database.** It picks
|
|
716
|
+
* the picker. The column is whatever {@link columnType} says, and the API
|
|
717
|
+
* accepts a full timestamp either way — narrowing the widget does not
|
|
718
|
+
* narrow the value.
|
|
570
719
|
*/
|
|
571
720
|
mode?: "date" | "date_time";
|
|
572
721
|
/**
|
|
573
722
|
* Timezone string to evaluate the date in.
|
|
723
|
+
*
|
|
724
|
+
* **Admin form only — not enforced by the API or the database.** It is the
|
|
725
|
+
* zone the panel reads and writes the value in; what is stored is a
|
|
726
|
+
* `timestamptz`, which has no zone of its own.
|
|
574
727
|
*/
|
|
575
728
|
timezone?: string;
|
|
576
729
|
/**
|
|
@@ -875,7 +1028,11 @@ export interface PropertyValidationSchema {
|
|
|
875
1028
|
*/
|
|
876
1029
|
required?: boolean;
|
|
877
1030
|
/**
|
|
878
|
-
* Customize the required message when the property is not set
|
|
1031
|
+
* Customize the required message when the property is not set.
|
|
1032
|
+
*
|
|
1033
|
+
* **Admin form only — not enforced by the API or the database.** It is the
|
|
1034
|
+
* sentence the panel shows under the field; an API rejection carries its
|
|
1035
|
+
* own error code and message.
|
|
879
1036
|
*/
|
|
880
1037
|
requiredMessage?: string;
|
|
881
1038
|
/**
|
|
@@ -888,6 +1045,11 @@ export interface PropertyValidationSchema {
|
|
|
888
1045
|
* once per entry in the parent `ArrayProperty`. It has no effect if this
|
|
889
1046
|
* property is not a child of an `ArrayProperty`. It works on direct
|
|
890
1047
|
* children of an `ArrayProperty` or first level children of `MapProperty`
|
|
1048
|
+
*
|
|
1049
|
+
* **Admin form only — not enforced by the API or the database.** Unlike
|
|
1050
|
+
* {@link unique}, which compiles to a constraint, this one is checked as
|
|
1051
|
+
* the form is filled in: the column holds a JSON array, and nothing in
|
|
1052
|
+
* Postgres is looking inside it.
|
|
891
1053
|
*/
|
|
892
1054
|
uniqueInArray?: boolean;
|
|
893
1055
|
}
|
|
@@ -947,11 +1109,24 @@ export interface StringPropertyValidationSchema extends PropertyValidationSchema
|
|
|
947
1109
|
*
|
|
948
1110
|
* A transform, not a check: it changes the value that is written, which is
|
|
949
1111
|
* what makes it the fix for "the same tag twice, one with a trailing space".
|
|
1112
|
+
*
|
|
1113
|
+
* **Admin form only — not enforced by the API or the database.** The panel
|
|
1114
|
+
* applies it on its way to the API; a value written any other way arrives
|
|
1115
|
+
* as it was sent. A transform that must always happen is a `beforeSave`
|
|
1116
|
+
* callback, which runs on the server for every write.
|
|
950
1117
|
*/
|
|
951
1118
|
trim?: boolean;
|
|
952
|
-
/**
|
|
1119
|
+
/**
|
|
1120
|
+
* Lowercase the value before saving. A transform, like {@link trim}.
|
|
1121
|
+
*
|
|
1122
|
+
* **Admin form only — not enforced by the API or the database.**
|
|
1123
|
+
*/
|
|
953
1124
|
lowercase?: boolean;
|
|
954
|
-
/**
|
|
1125
|
+
/**
|
|
1126
|
+
* Uppercase the value before saving. A transform, like {@link trim}.
|
|
1127
|
+
*
|
|
1128
|
+
* **Admin form only — not enforced by the API or the database.**
|
|
1129
|
+
*/
|
|
955
1130
|
uppercase?: boolean;
|
|
956
1131
|
}
|
|
957
1132
|
/**
|
|
@@ -1008,6 +1183,10 @@ export type StorageConfig = {
|
|
|
1008
1183
|
* Advanced image resizing and cropping configuration.
|
|
1009
1184
|
* Applied before upload to optimize storage and bandwidth.
|
|
1010
1185
|
* Only applies to image MIME types: image/jpeg, image/png, image/webp
|
|
1186
|
+
*
|
|
1187
|
+
* **Admin form only — not enforced by the API or the database.** The
|
|
1188
|
+
* resizing happens in the browser, before the bytes are sent. An upload
|
|
1189
|
+
* that does not go through the panel is stored at its original size.
|
|
1011
1190
|
*/
|
|
1012
1191
|
imageResize?: ImageResize;
|
|
1013
1192
|
/**
|
|
@@ -1028,6 +1207,9 @@ export type StorageConfig = {
|
|
|
1028
1207
|
* - `{propertyKey}` - ID of this property
|
|
1029
1208
|
* - `{path}` - Path of this entity
|
|
1030
1209
|
*
|
|
1210
|
+
* **Admin form only — not enforced by the API or the database.** The panel's
|
|
1211
|
+
* uploader resolves it; `client.storage.upload()` names its own `key`.
|
|
1212
|
+
*
|
|
1031
1213
|
* @param context
|
|
1032
1214
|
*/
|
|
1033
1215
|
fileName?: string | ((context: UploadedFileContext) => string | Promise<string>);
|
|
@@ -1043,6 +1225,12 @@ export type StorageConfig = {
|
|
|
1043
1225
|
* - `{entityId}` - ID of the entity
|
|
1044
1226
|
* - `{propertyKey}` - ID of this property
|
|
1045
1227
|
* - `{path}` - Path of this entity
|
|
1228
|
+
*
|
|
1229
|
+
* **Admin form only — not enforced by the API or the database.** Required
|
|
1230
|
+
* on the type because a file field in the panel has to put the object
|
|
1231
|
+
* somewhere, but it is the panel's uploader that resolves it. Nothing on
|
|
1232
|
+
* the server confines an upload to this prefix — that is what a storage
|
|
1233
|
+
* authorization rule is for.
|
|
1046
1234
|
*/
|
|
1047
1235
|
storagePath: string | ((context: UploadedFileContext) => string);
|
|
1048
1236
|
/**
|
|
@@ -1072,17 +1260,26 @@ export type StorageConfig = {
|
|
|
1072
1260
|
/**
|
|
1073
1261
|
* Use this callback to process the file before uploading it to the storage.
|
|
1074
1262
|
* If nothing is returned, the file is uploaded as it is.
|
|
1263
|
+
*
|
|
1264
|
+
* **Admin form only — not enforced by the API or the database.** It runs in
|
|
1265
|
+
* the browser, on the file the reader picked.
|
|
1266
|
+
*
|
|
1075
1267
|
* @param file
|
|
1076
1268
|
*/
|
|
1077
1269
|
processFile?: (file: File) => Promise<File> | undefined;
|
|
1078
1270
|
/**
|
|
1079
1271
|
* Postprocess the saved value (storage path or URL)
|
|
1080
1272
|
* after it has been resolved.
|
|
1273
|
+
*
|
|
1274
|
+
* **Admin form only — not enforced by the API or the database.**
|
|
1081
1275
|
*/
|
|
1082
1276
|
postProcess?: (pathOrUrl: string) => Promise<string>;
|
|
1083
1277
|
/**
|
|
1084
1278
|
* You can use this prop in order to provide a custom preview URL.
|
|
1085
1279
|
* Useful when the file's path is different from the original field value
|
|
1280
|
+
*
|
|
1281
|
+
* **Admin form only — not enforced by the API or the database.** It changes
|
|
1282
|
+
* what the panel renders, never what is stored.
|
|
1086
1283
|
*/
|
|
1087
1284
|
previewUrl?: (fileName: string) => string;
|
|
1088
1285
|
};
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import type { AnyCollectionConfig } from "./collections.js";
|
|
2
|
+
import type { Properties } from "./properties.js";
|
|
2
3
|
/**
|
|
3
4
|
* @group Models
|
|
4
5
|
*/
|
|
5
6
|
export type OnAction = "cascade" | "restrict" | "no action" | "set null" | "set default";
|
|
7
|
+
/**
|
|
8
|
+
* The key a junction row's own columns are carried under, in both directions.
|
|
9
|
+
*
|
|
10
|
+
* A read that includes a `manyToMany` relation serves each related row with its
|
|
11
|
+
* link's columns nested here — `{ id: 5, name: "ts", _pivot: { role: "owner" } }`
|
|
12
|
+
* — and a membership write may name the same key on an element to state what
|
|
13
|
+
* the link should hold. One constant because the two have to be the same word:
|
|
14
|
+
* a wire name that differs between the read and the write it round-trips
|
|
15
|
+
* through is a shape no client can echo back.
|
|
16
|
+
*
|
|
17
|
+
* Leading underscore, like `_matches`: it reads as metadata about the row
|
|
18
|
+
* rather than as one of its columns. A payload property may not be named
|
|
19
|
+
* `_pivot` either — `checkJunctionPayload` refuses it — so the key means one
|
|
20
|
+
* thing wherever it appears.
|
|
21
|
+
*
|
|
22
|
+
* @group Models
|
|
23
|
+
*/
|
|
24
|
+
export declare const JUNCTION_PIVOT_KEY = "_pivot";
|
|
6
25
|
/**
|
|
7
26
|
* What kind of link a relation is.
|
|
8
27
|
*
|
|
@@ -183,6 +202,52 @@ export interface ManyToManyRelation extends RelationBase {
|
|
|
183
202
|
sourceColumn?: string;
|
|
184
203
|
/** Junction column holding the **target's** key. */
|
|
185
204
|
targetColumn?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Extra columns the junction row carries, declared exactly like a
|
|
207
|
+
* collection's properties.
|
|
208
|
+
*
|
|
209
|
+
* A membership is often not only a membership. "This user is in that
|
|
210
|
+
* organisation" is really "…as an `owner`, since March"; "this tag is
|
|
211
|
+
* on that post" is really "…in third place". Until this existed the
|
|
212
|
+
* junction was two key columns and nothing else, so the role and the
|
|
213
|
+
* position had to become a collection of their own — which is a
|
|
214
|
+
* different data model, a different set of policies and a different
|
|
215
|
+
* URL, for what is still one link.
|
|
216
|
+
*
|
|
217
|
+
* The properties are read by the same planner that reads a
|
|
218
|
+
* collection's, so a payload column gets the type, `NOT NULL`,
|
|
219
|
+
* `DEFAULT`, `UNIQUE` and enum type it would get on a table. What it
|
|
220
|
+
* does **not** get is `indexes` (declared per collection, and no
|
|
221
|
+
* collection declares a junction), `search`, `vector`, or anything a
|
|
222
|
+
* relation would put on it — a payload property may not be a
|
|
223
|
+
* `relation`, a `reference` or a `vector`, and config validation
|
|
224
|
+
* refuses one that is.
|
|
225
|
+
*
|
|
226
|
+
* On the wire the values travel under {@link JUNCTION_PIVOT_KEY}: a
|
|
227
|
+
* read serves `{ …target, _pivot: { role } }`, and a membership write
|
|
228
|
+
* accepts `{ id, _pivot: { role } }` beside the bare ids.
|
|
229
|
+
*
|
|
230
|
+
* ```ts
|
|
231
|
+
* members: {
|
|
232
|
+
* kind: "manyToMany",
|
|
233
|
+
* target: () => users,
|
|
234
|
+
* through: {
|
|
235
|
+
* table: "org_members",
|
|
236
|
+
* properties: {
|
|
237
|
+
* role: { type: "string", enum: ["owner", "admin", "member"],
|
|
238
|
+
* defaultValue: "member", validation: { required: true } },
|
|
239
|
+
* joinedAt: { type: "date", autoValue: "on_create" }
|
|
240
|
+
* }
|
|
241
|
+
* }
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* Both sides of the same junction may declare it, and both must agree:
|
|
246
|
+
* `resolveJunctionSpecs` refuses two declarations of the same payload
|
|
247
|
+
* key that do not describe the same column, because only one of them
|
|
248
|
+
* could ever be created.
|
|
249
|
+
*/
|
|
250
|
+
properties?: Properties;
|
|
186
251
|
};
|
|
187
252
|
}
|
|
188
253
|
/**
|
|
@@ -351,6 +416,12 @@ export interface ResolvedManyToMany extends ResolvedRelationBase {
|
|
|
351
416
|
table: string;
|
|
352
417
|
sourceColumn: string;
|
|
353
418
|
targetColumn: string;
|
|
419
|
+
/**
|
|
420
|
+
* The payload columns as authored, or `{}` when there are none —
|
|
421
|
+
* never `undefined`, so a consumer reads one shape.
|
|
422
|
+
* See {@link ManyToManyRelation.through}.
|
|
423
|
+
*/
|
|
424
|
+
properties: Properties;
|
|
354
425
|
};
|
|
355
426
|
}
|
|
356
427
|
/** @group Models */
|