@fabrika/db 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/orm-types.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  // may not call is an `Unavailable` tuple: calling it fails at the call, and the message names the fix.
4
4
 
5
5
  import type { Selectable, Updateable } from 'kysely'
6
+ import type { RowRef } from './capability'
6
7
  import type { ConditionTuple } from './description'
8
+ import type { FileReference } from './file-references'
7
9
  import type {
8
10
  ActionOf,
9
11
  ApplicationTableName,
@@ -37,7 +39,7 @@ type ModeOf<P, T> = PolicyOf<P, T> extends { readonly scope: infer S } ? S exten
37
39
  : S extends { readonly child: string } ? 'child'
38
40
  : S extends { readonly app: string } ? 'app'
39
41
  : S extends { readonly reference: string } ? 'reference'
40
- : 'inbox'
42
+ : never
41
43
  : never
42
44
 
43
45
  /** The actions a table's `actions.appRead` or `actions.appCount` lists; `never` when it lists none. */
@@ -45,8 +47,6 @@ type AppActionsOf<P, T, F extends 'appRead' | 'appCount'> = PolicyOf<P, T> exten
45
47
  { readonly actions: { readonly [K in F]: readonly (infer A extends string)[] } } ? A
46
48
  : never
47
49
 
48
- type ReadActionOf<P, T> = PolicyOf<P, T> extends { readonly scope: { readonly read: infer A extends string } } ? A : ActionOf<P, T, 'read'>
49
-
50
50
  /** Each column that declares its own read action, by column. */
51
51
  type ColumnReadsOf<P, T> = PolicyOf<P, T> extends { readonly columns: infer C } ? {
52
52
  readonly [K in keyof C as C[K] extends { readonly read: string } ? K : never]: C[K] extends { readonly read: infer A extends string } ? A : never
@@ -74,7 +74,7 @@ type ColumnWriteAction<C> = { [K in keyof C]: C[K] extends { readonly write: inf
74
74
 
75
75
  /** One table, as every handle reads it. */
76
76
  export interface IndexedTable {
77
- readonly mode: 'dimension' | 'child' | 'app' | 'reference' | 'inbox'
77
+ readonly mode: 'dimension' | 'child' | 'app' | 'reference'
78
78
  /** The dimension a dimension table owns. */
79
79
  readonly dimension: string
80
80
  /** The dimension its rows are listed and inserted in: the container's, or a top-level table's own. */
@@ -101,6 +101,8 @@ export interface IndexedTable {
101
101
  /** The dimensions whose `Within` lists the table: every dimension of its chain, and a top-level table's own. */
102
102
  readonly reach: string
103
103
  readonly versioned: boolean
104
+ /** A queue table: its rows are claimed and finished through `row.ref`. */
105
+ readonly queued: boolean
104
106
  }
105
107
 
106
108
  type IndexedTableOf<DB, P, T extends keyof P & keyof DB & string, V = unknown> = {
@@ -109,22 +111,23 @@ type IndexedTableOf<DB, P, T extends keyof P & keyof DB & string, V = unknown> =
109
111
  readonly list: ListDimensionOf<P, T>
110
112
  readonly parent: ParentOf<P, T>
111
113
  readonly actions: {
112
- readonly read: ReadActionOf<P, T>
114
+ readonly read: ActionOf<P, T, 'read'>
113
115
  readonly insert: ActionOf<P, T, 'insert'>
114
116
  readonly update: ActionOf<P, T, 'update'>
115
117
  readonly delete: ActionOf<P, T, 'delete'>
116
118
  }
117
119
  readonly appRead: AppActionsOf<P, T, 'appRead'>
118
120
  readonly appCount: AppActionsOf<P, T, 'appCount'>
119
- readonly row: RowType<DB[T]>
121
+ readonly row: WithFiles<RowType<DB[T]>, FileColumnsOf<P, T>>
120
122
  readonly reads: ColumnReadsOf<P, T>
121
- readonly insert: InsertValues<DB, P, T>
122
- readonly update: UpdateValues<DB, P, T>
123
- readonly writes: UpdatesOf<DB, P, T>
123
+ readonly insert: WithFiles<InsertValues<DB, P, T>, FileColumnsOf<P, T>>
124
+ readonly update: WithFiles<UpdateValues<DB, P, T>, FileColumnsOf<P, T>>
125
+ readonly writes: { readonly [A in keyof UpdatesOf<DB, P, T>]: WithFiles<UpdatesOf<DB, P, T>[A], FileColumnsOf<P, T>> }
124
126
  readonly chain: ChainColumns<P, T, V>
125
127
  readonly refs: DimensionOf<P, T> | ChainDimensions<P, T, V>
126
128
  readonly reach: ReachOf<P, T, V>
127
129
  readonly versioned: PolicyOf<P, T> extends { readonly version: string } ? true : false
130
+ readonly queued: PolicyOf<P, T> extends { readonly queue: object } ? true : false
128
131
  }
129
132
 
130
133
  type OwnerTable<P, D> = { [K in keyof P]: P[K] extends { readonly scope: { readonly dimension: D } } ? K : never }[keyof P]
@@ -162,6 +165,13 @@ type ReachOf<P, T, V> = [ContainerOf<P, T>] extends [never] ? DimensionOf<P, T>
162
165
  */
163
166
  type RowType<R> = [R] extends [Selectable<R>] ? [Selectable<R>] extends [R] ? R : Selectable<R> : Selectable<R>
164
167
 
168
+ /** The file columns of table `T`'s policy (`files: { <column>: '<kind>' }`). */
169
+ type FileColumnsOf<P, T> = PolicyOf<P, T> extends { readonly files: infer F } ? keyof F & string : never
170
+
171
+ /** A row or write of a table with file columns: each holds a file reference, `null` where the column is nullable. */
172
+ type WithFiles<R, Files extends string> = [Files] extends [never] ? R
173
+ : { [K in keyof R]: K extends Files ? (null extends R[K] ? FileReference | null : FileReference) : R[K] }
174
+
165
175
  type ReferenceTableName<P> = { [K in keyof P]: P[K] extends { readonly scope: { readonly reference: string } } ? K : never }[keyof P] & string
166
176
 
167
177
  /**
@@ -225,6 +235,66 @@ export type Saved<E extends IndexedTable> = E['versioned'] extends true ? { read
225
235
  /** A versioned table's update and remove take `{ expectVersion }`; another table's take nothing. */
226
236
  export type VersionArguments<E extends IndexedTable> = E['versioned'] extends true ? [options: { readonly expectVersion: number }] : []
227
237
 
238
+ /** The values an insert takes: the columns of the table's `insert` list. */
239
+ type InsertValuesOf<E extends IndexedTable> = { [C in keyof E['insert']]: E['insert'][C] }
240
+
241
+ /** `onConflict` of an insert: the columns of a unique index the migrations create, and what a conflict does. */
242
+ export interface InsertConflict<E extends IndexedTable, M extends 'nothing' | 'update'> {
243
+ readonly onConflict: { readonly columns: readonly (keyof E['row'] & string)[]; readonly do: M }
244
+ }
245
+
246
+ /**
247
+ * An insert: with `onConflict`, a conflict of its columns writes nothing (`'nothing'`, which answers `{ inserted: false }`)
248
+ * or updates the conflicting row's update columns (`'update'`, which also needs the table's update action).
249
+ */
250
+ export interface InsertMethod<E extends IndexedTable> {
251
+ (values: InsertValuesOf<E>, options: InsertConflict<E, 'nothing'>): Promise<(Saved<E> & { readonly inserted: true }) | { readonly inserted: false }>
252
+ (values: InsertValuesOf<E>, options: InsertConflict<E, 'update'>): Promise<Saved<E> & { readonly inserted: boolean }>
253
+ // Last, so `Parameters` and `ReturnType`, which read the last signature, read the plain insert.
254
+ (values: InsertValuesOf<E>): Promise<Saved<E>>
255
+ }
256
+
257
+ /** What a claim takes: how many rows to lease, at most `MAX_LIST_LIMIT`, and conditions on the columns its action reads. */
258
+ export interface ClaimOptions<Row> {
259
+ readonly limit: number
260
+ readonly where?: readonly ConditionTuple<Row>[]
261
+ }
262
+
263
+ /** A claimed row: the columns of the claim's action, its new `version`, and `ref`, which `tx.db.on(ref)` finishes it through. */
264
+ export type ClaimedRow<T extends string, E extends IndexedTable, A extends string> =
265
+ & RowFor<E, A>
266
+ & { readonly version: number; readonly ref: RowRef<T, A> }
267
+
268
+ type ClaimRefusal<K extends DataKind> = K extends 'query' ? 'claim leases rows, so it runs only in a job'
269
+ : 'claim leases rows in a transaction of its own: a job calls it on ctx.db before ctx.transaction, never inside one or in a request'
270
+
271
+ /** `claim` of a queue table, in a job, for a capability of its update action. */
272
+ type Claim<T extends string, E extends IndexedTable, A extends string, K extends DataKind> = K extends 'job'
273
+ ? E['queued'] extends true ? [A] extends [E['actions']['update']] ? (options: ClaimOptions<RowFor<E, A>>) => Promise<ClaimedRow<T, E, A>[]>
274
+ : Unavailable<`a claim of ${T} needs its update action '${E['actions']['update'] & string}', and this capability holds '${A}'`>
275
+ : Unavailable<`${T} is not a queue table: declare queue: { attempts, due } in its policy`>
276
+ : Unavailable<ClaimRefusal<K>>
277
+
278
+ type JobWrite = Unavailable<'a job writes inside ctx.transaction((tx) => …): use tx.db there'>
279
+
280
+ /**
281
+ * `tx.db.on(row.ref)`: one claimed row, finished in the job's transaction. `update` writes the claim action's columns and,
282
+ * with `done: true`, clears `due`; `remove` needs the table's delete action; `requeue` makes the row due again at `at`
283
+ * (now by default) with no attempts.
284
+ */
285
+ export interface ClaimedRowHandle<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
286
+ readonly update: K extends 'mutation' ? (
287
+ values: { [C in keyof UpdateValuesOf<E, A>]: UpdateValuesOf<E, A>[C] },
288
+ options: { readonly expectVersion: number; readonly done?: boolean },
289
+ ) => Promise<Saved<E>>
290
+ : JobWrite
291
+ readonly remove: K extends 'mutation' ? [A] extends [E['actions']['delete']] ? (options: { readonly expectVersion: number }) => Promise<void>
292
+ : [E['actions']['delete']] extends [never] ? Unavailable<`${T} allows no remove: its policy declares no delete action`>
293
+ : Unavailable<`remove of a claimed ${T} row needs its delete action '${E['actions']['delete'] & string}', and this row was claimed with '${A}'`>
294
+ : JobWrite
295
+ readonly requeue: K extends 'mutation' ? (options?: { readonly at?: Date | string }) => Promise<Saved<E>> : JobWrite
296
+ }
297
+
228
298
  /** The actions table `E` names: its read, insert, update and delete actions, and each column's own write action. */
229
299
  type NamedActions<E extends IndexedTable> = (E['actions'][keyof E['actions']] | keyof E['writes']) & string
230
300
 
@@ -305,7 +375,7 @@ export interface TargetRow<T extends string, E extends IndexedTable, A extends s
305
375
  * by the scope's value.
306
376
  */
307
377
  export interface OwnerRow<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
308
- readonly insert: Write<T, E, 'insert', A, K, (values: { [C in keyof E['insert']]: E['insert'][C] }) => Promise<Saved<E>>>
378
+ readonly insert: Write<T, E, 'insert', A, K, InsertMethod<E>>
309
379
  }
310
380
 
311
381
  /** A dimension no table owns has no row of its own; its subtree tables are still reachable. */
@@ -320,7 +390,7 @@ export interface UnownedTarget<D extends string> {
320
390
  export interface ChildRows<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
321
391
  readonly list: Read<T, E, A, (options?: ListOptions<RowFor<E, A>>) => Promise<ListPage<RowFor<E, A>>>>
322
392
  readonly get: Read<T, E, A, (key: string) => Promise<RowFor<E, A>>>
323
- readonly insert: Write<T, E, 'insert', A, K, (values: { [C in keyof E['insert']]: E['insert'][C] }) => Promise<Saved<E>>>
393
+ readonly insert: Write<T, E, 'insert', A, K, InsertMethod<E>>
324
394
  readonly update: Update<
325
395
  T,
326
396
  E,
@@ -334,7 +404,7 @@ export interface ChildRows<T extends string, E extends IndexedTable, A extends s
334
404
  /** A dimension table the target contains: listed in it, and inserted into it by a mutation of its insert action. */
335
405
  export interface ContainedRows<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
336
406
  readonly list: Read<T, E, A, (options?: ListOptions<RowFor<E, A>>) => Promise<ListPage<RowFor<E, A>>>>
337
- readonly insert: Write<T, E, 'insert', A, K, (values: { [C in keyof E['insert']]: E['insert'][C] }) => Promise<Saved<E>>>
407
+ readonly insert: Write<T, E, 'insert', A, K, InsertMethod<E>>
338
408
  }
339
409
 
340
410
  type SubtreeTables<Ix extends AnyDbIndex, D extends string, A extends string, K extends DataKind, F extends TargetForm> =
@@ -350,8 +420,9 @@ export type ScopeTarget<Ix extends AnyDbIndex, D extends string, A extends strin
350
420
  & ([OwnerOf<Ix, D>] extends [never] ? UnownedTarget<D> : TargetRow<OwnerOf<Ix, D>, Ix['tables'][OwnerOf<Ix, D>], A, K, F>)
351
421
  & SubtreeTables<Ix, D, A, K, F>
352
422
 
353
- /** A table listed in an `over` endpoint's dimension, read through its `Within`. */
354
- export interface WithinRows<T extends string, E extends IndexedTable, A extends string> {
423
+ /** A table listed in an `over` endpoint's dimension, read through its `Within`; a job claims a queue table's rows through it. */
424
+ export interface WithinRows<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
425
+ readonly claim: Claim<T, E, A, K>
355
426
  readonly list: A extends E['actions']['read'] ? (options?: ListOptions<RowFor<E, A>>) => Promise<ListPage<RowFor<E, A>>>
356
427
  : Unavailable<ListRefusal<T, E, A>>
357
428
  readonly count: A extends E['actions']['read'] ? (options?: CountOptions<RowFor<E, A>>) => Promise<number> : Unavailable<ListRefusal<T, E, A>>
@@ -369,8 +440,8 @@ type ListRefusal<T extends string, E extends IndexedTable, A extends string> = `
369
440
  * `ctx.db.within` of an `over` endpoint: every table whose containment chain holds its dimension (tasks two levels
370
441
  * down through their stamped `org_id`), and a top-level table its own; child tables included.
371
442
  */
372
- export type WithinTables<Ix extends AnyDbIndex, D extends string, A extends string> = {
373
- readonly [T in WithinIn<Ix, D>]: WithinRows<T, Ix['tables'][T], A>
443
+ export type WithinTables<Ix extends AnyDbIndex, D extends string, A extends string, K extends DataKind> = {
444
+ readonly [T in WithinIn<Ix, D>]: WithinRows<T, Ix['tables'][T], A, K>
374
445
  }
375
446
 
376
447
  /** An application table under the capability of an `app` endpoint of action `A`, in a site of kind `K`. */
@@ -387,7 +458,9 @@ export interface ApplicationRows<T extends string, E extends IndexedTable, A ext
387
458
  readonly getForUpdate: K extends 'mutation' ? Read<T, E, A, (key: string) => Promise<RowFor<E, A>>>
388
459
  : K extends 'job' ? Unavailable<'a job writes inside ctx.transaction((tx) => …): use tx.db there'>
389
460
  : Unavailable<'getForUpdate locks the row, so it runs only in a mutation: declare the endpoint with r.mutation'>
390
- readonly insert: Write<T, E, 'insert', A, K, (values: { [C in keyof E['insert']]: E['insert'][C] }) => Promise<Saved<E>>>
461
+ /** Leases due rows of a queue table: a job's drain, before its transactions. */
462
+ readonly claim: Claim<T, E, A, K>
463
+ readonly insert: Write<T, E, 'insert', A, K, InsertMethod<E>>
391
464
  readonly update: Update<
392
465
  T,
393
466
  E,
@@ -398,6 +471,59 @@ export interface ApplicationRows<T extends string, E extends IndexedTable, A ext
398
471
  readonly remove: Write<T, E, 'delete', A, K, (key: string, ...version: VersionArguments<E>) => Promise<void>>
399
472
  }
400
473
 
474
+ /**
475
+ * A public procedure's insert: it answers the new row's key (and version), which the data layer generated, and reads
476
+ * no column back; a conflict of `onConflict`'s columns writes nothing.
477
+ */
478
+ export interface PublicInsertMethod<E extends IndexedTable> {
479
+ (values: InsertValuesOf<E>, options: InsertConflict<E, 'nothing'>): Promise<(Saved<E> & { readonly inserted: true }) | { readonly inserted: false }>
480
+ (values: InsertValuesOf<E>): Promise<Saved<E>>
481
+ }
482
+
483
+ type PublicRefusal<Op extends string> = Unavailable<`a public procedure does not ${Op} a row: it inserts, and reads under the table's read action`>
484
+
485
+ /** A public procedure updates and removes only a versioned row, by an id a verified signed token of its request carries. */
486
+ type PublicVersioned<T extends string, E extends IndexedTable, Member> = E['versioned'] extends true ? Member
487
+ : Unavailable<`a public procedure updates and removes only a versioned row: declare version in the policy of ${T}`>
488
+
489
+ /**
490
+ * An application table of a public procedure of action `A`, in a site of kind `K`: read under its read action only,
491
+ * inserted with nothing read back, and updated or removed only by the id a verified signed token of the request carries,
492
+ * under `expectVersion`; never locked or claimed.
493
+ */
494
+ export interface PublicApplicationRows<T extends string, E extends IndexedTable, A extends string, K extends DataKind> {
495
+ readonly list: ApplicationRows<T, E, A, K>['list']
496
+ readonly count: ApplicationRows<T, E, A, K>['count']
497
+ readonly aggregate: ApplicationRows<T, E, A, K>['aggregate']
498
+ readonly get: A extends E['actions']['read'] ? (key: string) => Promise<RowFor<E, A>> : Unavailable<ListRefusal<T, E, A>>
499
+ readonly getForUpdate: PublicRefusal<'lock'>
500
+ readonly claim: PublicRefusal<'claim'>
501
+ /** By the id a verified signed token of the request carries; answers only the new version. */
502
+ readonly update: PublicVersioned<
503
+ T,
504
+ E,
505
+ Update<
506
+ T,
507
+ E,
508
+ A,
509
+ K,
510
+ (
511
+ key: string,
512
+ values: { [C in keyof UpdateValuesOf<E, A>]: UpdateValuesOf<E, A>[C] },
513
+ options: { readonly expectVersion: number },
514
+ ) => Promise<{ readonly version: number }>
515
+ >
516
+ >
517
+ /** By the id a verified signed token of the request carries. */
518
+ readonly remove: PublicVersioned<T, E, Write<T, E, 'delete', A, K, (key: string, options: { readonly expectVersion: number }) => Promise<void>>>
519
+ readonly insert: Write<T, E, 'insert', A, K, PublicInsertMethod<E>>
520
+ }
521
+
522
+ /** `ctx.db.app('<action>')` of a public procedure: the application tables only, each as `PublicApplicationRows`. */
523
+ export type PublicApplicationTables<Ix extends AnyDbIndex, A extends string, K extends DataKind> = {
524
+ readonly [T in Ix['applications']]: PublicApplicationRows<T, Ix['tables'][T], A, K>
525
+ }
526
+
401
527
  /**
402
528
  * A dimension or child table under an `app` endpoint of action `A`: every row read-only, with the columns `A` reads
403
529
  * (`RowFor`), or only how many rows match. A count-only action has no `aggregate`: a group or a minimum is a value.
@@ -477,30 +603,28 @@ export type UnscopedMode = 'read' | 'write'
477
603
  export type UnscopedDeclaration<Table extends string, Mode extends UnscopedMode> = { readonly [T in Table]?: Mode }
478
604
 
479
605
  /**
480
- * The tables of `U` whose mode may be `'write'` that no site writes: a reference table and a form's inbox. A union or
481
- * optional mode that admits `'write'` counts, because the declaration it types may hold it.
606
+ * The tables of `U` whose mode may be `'write'` that no site writes: a reference table. A union or optional mode that
607
+ * admits `'write'` counts, because the declaration it types may hold it.
482
608
  */
483
609
  type ReadOnlyWritten<Ix extends AnyDbIndex, U> = {
484
- [T in keyof U & TableName<Ix>]: 'write' extends U[T] ? Ix['tables'][T]['mode'] extends 'reference' | 'inbox' ? T : never : never
610
+ [T in keyof U & TableName<Ix>]: 'write' extends U[T] ? Ix['tables'][T]['mode'] extends 'reference' ? T : never : never
485
611
  }[keyof U & TableName<Ix>]
486
612
 
487
- type ReadOnlyMessage<T extends string, Mode> = Mode extends 'reference'
488
- ? `${T} is a reference table, which no site writes: its rows come from a migration, so declare unscoped: { ${T}: 'read' }`
489
- : `${T} is a form's inbox, which only its form writes: declare unscoped: { ${T}: 'read' }`
613
+ type ReadOnlyMessage<T extends string> =
614
+ `${T} is a reference table, which no site writes: its rows come from a migration, so declare unscoped: { ${T}: 'read' }`
490
615
 
491
- /** An `unscoped` declaration's `'write'` of a read-only table, refused at its own line. */
616
+ /** An `unscoped` declaration's `'write'` of a reference table, refused at its own line. */
492
617
  export type ReadOnlyUnscoped<Ix extends AnyDbIndex, U> = [ReadOnlyWritten<Ix, U>] extends [never] ? unknown
493
- : { readonly [T in ReadOnlyWritten<Ix, U>]: Unavailable<ReadOnlyMessage<T, Ix['tables'][T]['mode']>> }
618
+ : { readonly [T in ReadOnlyWritten<Ix, U>]: Unavailable<ReadOnlyMessage<T>> }
494
619
 
495
- type KeylessMessage<T extends string, Mode> = Mode extends 'reference' ? `${T} is a reference table, which has no key: read it with list`
496
- : `${T} is a form's inbox, which has no key: read it with list`
620
+ type KeylessMessage<T extends string> = `${T} is a reference table, which has no key: read it with list`
497
621
 
498
622
  /** A table a site declared `unscoped`: its scope predicate is `true`, and `'read'` writes nothing. */
499
623
  export interface UnscopedRows<T extends string, E extends IndexedTable, M extends UnscopedMode, K extends DataKind> {
500
624
  list(options?: ListOptions<TableRow<E>>): Promise<ListPage<TableRow<E>>>
501
625
  count(options?: CountOptions<TableRow<E>>): Promise<number>
502
626
  aggregate<const S extends AggregateSpec<TableRow<E>>>(spec: S): Promise<AggregateRow<TableRow<E>, S>[]>
503
- readonly get: E['mode'] extends 'reference' | 'inbox' ? Unavailable<KeylessMessage<T, E['mode']>> : (key: string) => Promise<TableRow<E>>
627
+ readonly get: E['mode'] extends 'reference' ? Unavailable<KeylessMessage<T>> : (key: string) => Promise<TableRow<E>>
504
628
  readonly insert: UnscopedWrite<T, M, K, 'insert', (values: { [C in keyof UnscopedInsert<E>]: UnscopedInsert<E>[C] }) => Promise<Saved<E>>>
505
629
  readonly update: UnscopedWrite<
506
630
  T,