@fabrika/db 0.8.2 → 0.9.0-cloudflare.2
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/package.json +44 -12
- package/src/admissions.ts +78 -12
- package/src/application-rows.ts +108 -0
- package/src/capability.ts +5 -0
- package/src/codegen-sqlite.ts +154 -0
- package/src/codegen.ts +59 -9
- package/src/database.ts +208 -56
- package/src/errors.ts +1 -0
- package/src/file-columns.ts +4 -1
- package/src/framework-tables.ts +28 -8
- package/src/framework.ts +10 -2
- package/src/guard.ts +8 -0
- package/src/index.ts +9 -1
- package/src/job-leases.ts +157 -37
- package/src/links.ts +8 -1
- package/src/migrations-sqlite.ts +132 -0
- package/src/nodes.ts +45 -9
- package/src/orm.ts +25 -15
- package/src/policy.ts +8 -2
- package/src/postgres.ts +26 -0
- package/src/registry.ts +8 -2
- package/src/sql.ts +15 -7
- package/src/sqlite-bun.ts +122 -0
- package/src/sqlite-migrate.ts +242 -0
- package/src/sqlite.ts +224 -0
- package/src/store.ts +336 -62
- package/src/stored-files.ts +149 -46
- package/src/tables.ts +16 -0
- package/src/temporal.ts +115 -9
- package/src/testing-shared.ts +733 -0
- package/src/testing-sqlite.ts +513 -0
- package/src/testing.ts +170 -482
- package/types/src/admissions.d.ts +21 -0
- package/types/src/application-rows.d.ts +58 -0
- package/types/src/builtins.d.ts +42 -0
- package/types/src/capability.d.ts +119 -0
- package/types/src/codegen-sqlite.d.ts +6 -0
- package/types/src/codegen.d.ts +82 -0
- package/types/src/database.d.ts +179 -0
- package/types/src/description.d.ts +109 -0
- package/types/src/errors.d.ts +98 -0
- package/types/src/file-columns.d.ts +37 -0
- package/types/src/file-references.d.ts +27 -0
- package/types/src/framework-tables.d.ts +59 -0
- package/types/src/framework.d.ts +37 -0
- package/types/src/guard.d.ts +23 -0
- package/types/src/identifier.d.ts +4 -0
- package/types/src/index.d.ts +10 -0
- package/types/src/job-leases.d.ts +93 -0
- package/types/src/links.d.ts +13 -0
- package/types/src/migrations-sqlite.d.ts +4 -0
- package/types/src/migrations.d.ts +25 -0
- package/types/src/nodes.d.ts +63 -0
- package/types/src/orm-types.d.ts +638 -0
- package/types/src/orm.d.ts +77 -0
- package/types/src/policy.d.ts +354 -0
- package/types/src/postgres.d.ts +12 -0
- package/types/src/production.d.ts +5 -0
- package/types/src/registry.d.ts +40 -0
- package/types/src/sql.d.ts +59 -0
- package/types/src/sqlite-bun.d.ts +13 -0
- package/types/src/sqlite-migrate.d.ts +28 -0
- package/types/src/sqlite.d.ts +24 -0
- package/types/src/store.d.ts +345 -0
- package/types/src/stored-files.d.ts +98 -0
- package/types/src/tables.d.ts +21 -0
- package/types/src/temporal.d.ts +33 -0
- package/types/src/testing-shared.d.ts +201 -0
- package/types/src/testing-sqlite.d.ts +22 -0
- package/types/src/testing.d.ts +35 -0
- package/types/src/vocabulary-tables.d.ts +225 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the layer refused a capability, as a closed catalog: a server log names the reason and its fixed text, never the
|
|
3
|
+
* message, which can name a table, a dimension or a value.
|
|
4
|
+
*/
|
|
5
|
+
export declare const CAPABILITY_ERRORS: Readonly<{
|
|
6
|
+
wrong_capability: "the operation takes another capability than it was given: another kind (a Ref, a Within or an app capability) or dimension";
|
|
7
|
+
wrong_action: "the capability holds another action than the table names for the operation";
|
|
8
|
+
out_of_reach: "the table does not lie in the capability's dimension, or the capability does not reach it";
|
|
9
|
+
unenforceable: "the table carries no column for a dimension of the capability's chain, so the layer cannot enforce it";
|
|
10
|
+
undeclared_operation: "the table's policy declares no action for the operation, or handlers do not write the table";
|
|
11
|
+
table_kind: "the operation needs another kind of table (a dimension, a child or an application table)";
|
|
12
|
+
within_size: "a Within carries more scope values than one statement binds";
|
|
13
|
+
layer_unbound: "the data layer was used before the framework connected it at boot, or connected a second time";
|
|
14
|
+
}>;
|
|
15
|
+
export type CapabilityErrorReason = keyof typeof CAPABILITY_ERRORS;
|
|
16
|
+
/** A capability of the wrong kind, dimension or action for the operation, or the layer used outside its binding. */
|
|
17
|
+
export declare class CapabilityError extends Error {
|
|
18
|
+
name: string;
|
|
19
|
+
/** What a log may say; the message is for the author's own run. */
|
|
20
|
+
readonly reason: CapabilityErrorReason;
|
|
21
|
+
constructor(reason: CapabilityErrorReason, message: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Why the layer refused, as a closed catalog: a server log names the reason and its fixed text, never the message,
|
|
25
|
+
* which can quote a value the request sent.
|
|
26
|
+
*/
|
|
27
|
+
export declare const QUERY_REFUSALS: {
|
|
28
|
+
readonly invalid_argument: "a data-layer call took an argument of another shape than its type declares";
|
|
29
|
+
readonly invalid_value: "a value cannot be bound: an invalid or out-of-range date, or a non-finite number";
|
|
30
|
+
readonly invalid_name: "a configured name (application, schema, table or file column) breaks its naming rule";
|
|
31
|
+
readonly unknown_column: "a column the table does not declare, or one this operation may not set";
|
|
32
|
+
readonly unknown_table: "a table src/db/tables.ts does not declare, or one no store, join or locator reaches";
|
|
33
|
+
readonly framework_table: "a framework table or a catalog name: application data reaches only the application's own tables";
|
|
34
|
+
readonly row_address: "a row is addressed otherwise than its table declares (its Ref, its parent Ref and key, or its key)";
|
|
35
|
+
readonly version_required: "the table has a version column, so update and remove take expectVersion";
|
|
36
|
+
readonly read_only: "a read-only request, query or table was asked to write or lock a row";
|
|
37
|
+
readonly ended: "the transaction, invocation or handle has ended or belongs to other work";
|
|
38
|
+
readonly statement_failed: "a statement of this transaction failed, so nothing it wrote commits";
|
|
39
|
+
readonly unexpected_row: "the database answered a row in another shape than its migration declares";
|
|
40
|
+
readonly statement_shape: "the compiled statement has a form the guard does not admit";
|
|
41
|
+
readonly scope_predicate: "the scope predicate is missing or is not a top-level conjunct";
|
|
42
|
+
readonly list_page: "a list limit, cursor or order outside what a page admits";
|
|
43
|
+
readonly undeclared_reach: "the site reaches data it did not declare (sql or unscoped, or their mode)";
|
|
44
|
+
readonly sql_template: "ctx.sql was not called as one tagged template whose values are interpolated whole";
|
|
45
|
+
readonly sql_statement: "ctx.sql runs one statement of an admitted kind, and none inside another";
|
|
46
|
+
readonly sql_read_only: "sql: 'read' runs queries only; declare sql: 'write' on a mutation, a job or a webhook";
|
|
47
|
+
readonly sql_name: "ctx.sql names a framework, catalog or foreign-schema object, or writes a name in U& form";
|
|
48
|
+
readonly sql_function: "ctx.sql calls a function that is not a reviewed built-in";
|
|
49
|
+
readonly sql_outside_transaction: "a job runs ctx.sql only inside ctx.transaction((tx) => …), as tx.sql";
|
|
50
|
+
readonly sql_scope: "ctx.sql.scope(…) stands alone, joined by AND, in a WHERE or JOIN … ON clause of its own invocation";
|
|
51
|
+
readonly sql_dialect: "ctx.sql runs only on Postgres; an application on SQLite (Cloudflare) reads and writes through ctx.db";
|
|
52
|
+
readonly file_scan: "a scan verdict outside the scan job's lease";
|
|
53
|
+
readonly claim_site: "claim leases rows in a transaction of its own: a job calls it on ctx.db, never inside ctx.transaction or a request";
|
|
54
|
+
readonly on_conflict: "an insert's onConflict names no column of the table, a column the insert leaves empty, or another action";
|
|
55
|
+
readonly public_reach: "a public procedure updates, removes, locks or claims no row, and an insert's conflict writes nothing";
|
|
56
|
+
readonly file_value: "a file column takes a file reference this request holds and may read (an incoming file, a put, or a read under the table's read action) of the column's kind, or null";
|
|
57
|
+
readonly file_scope: "a file is referenced from another top-level scope than the one its first reference set";
|
|
58
|
+
};
|
|
59
|
+
export type QueryRefusal = keyof typeof QUERY_REFUSALS;
|
|
60
|
+
/** A query description or an operation tree outside what the layer allows. */
|
|
61
|
+
export declare class QueryRefusedError extends Error {
|
|
62
|
+
name: string;
|
|
63
|
+
/** What a log may say; the message is for the author's own run and may quote a value. */
|
|
64
|
+
readonly reason: QueryRefusal;
|
|
65
|
+
constructor(reason: QueryRefusal, message: string);
|
|
66
|
+
}
|
|
67
|
+
/** A `ctx.sql` row its row schema refuses: the handler's bug. The message names the column, never its value. */
|
|
68
|
+
export declare class SqlRowError extends Error {
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
71
|
+
/** A table policy that cannot be enforced as written. */
|
|
72
|
+
export declare class PolicyError extends Error {
|
|
73
|
+
name: string;
|
|
74
|
+
}
|
|
75
|
+
/** The addressed object does not exist in the capability's scope (HTTP 404). */
|
|
76
|
+
export declare class NotFoundError extends Error {
|
|
77
|
+
name: string;
|
|
78
|
+
}
|
|
79
|
+
/** The object exists, but its version is not the one the caller expected (HTTP 409). */
|
|
80
|
+
export declare class ConflictError extends Error {
|
|
81
|
+
name: string;
|
|
82
|
+
}
|
|
83
|
+
/** A job attempt wrote after it lost its lease (timed out, taken over or expired); nothing was written. */
|
|
84
|
+
export declare class LeaseLostError extends Error {
|
|
85
|
+
name: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A statement the database refused or could not run, as the layer reports it: its SQLSTATE only. The driver's message,
|
|
89
|
+
* detail and hint can quote the whole failing row, columns the capability may not read included, so none is kept.
|
|
90
|
+
*/
|
|
91
|
+
export declare class DatabaseError extends Error {
|
|
92
|
+
name: string;
|
|
93
|
+
/** The five-character SQLSTATE, or `null` when the statement never reached the database (a lost connection). */
|
|
94
|
+
readonly sqlState: string | null;
|
|
95
|
+
constructor(sqlState: string | null);
|
|
96
|
+
}
|
|
97
|
+
/** The driver's failure as a `DatabaseError`; the driver's error object goes no further. */
|
|
98
|
+
export declare function databaseError(error: unknown): DatabaseError;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Session } from './database';
|
|
2
|
+
import type { Primitive } from './description';
|
|
3
|
+
import type { TableModel } from './policy';
|
|
4
|
+
/** Where a site's file references belong, and what claiming a pending one needs. */
|
|
5
|
+
export interface FileBinding {
|
|
6
|
+
/** The work a reference of this site belongs to: one request, or one job attempt. */
|
|
7
|
+
readonly holder: object;
|
|
8
|
+
/** The id a pending file must carry to be claimed here: the request (or attempt) that staged it. */
|
|
9
|
+
readonly request: string;
|
|
10
|
+
/** How long a pending file may wait for its first reference. */
|
|
11
|
+
readonly graceMs: number;
|
|
12
|
+
/** Whether files of `kind` wait for a clean scan: a claimed one is `quarantined` until then. */
|
|
13
|
+
scans(kind: string): boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Puts a file reference in each file column of `rows`, read with `action` in `session`: one statement loads every
|
|
17
|
+
* file's metadata. Without a binding (a store's own runner) the columns keep their ids.
|
|
18
|
+
*/
|
|
19
|
+
export declare function answerFiles<R>(model: TableModel, binding: FileBinding | undefined, session: Session, rows: R[], action: string): Promise<R[]>;
|
|
20
|
+
/**
|
|
21
|
+
* A write's value for a file column: `null`, or the id of a file reference of the column's kind. A string id, a copied
|
|
22
|
+
* object or a reference of another kind is refused; whether the site holds it is checked where the write runs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fileValue(model: TableModel, column: string, value: unknown): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* The top-level scope a row of `model` lies in, from the values it is written with: `<dimension>:<value>`, or `null` for
|
|
27
|
+
* an application table, which sets no scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare function rowScope(model: TableModel, values: ReadonlyMap<string, Primitive | null>): string | null | undefined;
|
|
30
|
+
/** The top-level scope of the row whose key is `key`, read in `session`: what an update's file references are stamped with. */
|
|
31
|
+
export declare function storedRowScope(model: TableModel, session: Session, key: string, keyValue: string): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* The file references of a write, in `session` (its transaction), after its statement: each file is locked `for share`
|
|
34
|
+
* while its state is checked, a pending one this request staged within the grace is claimed, and the first reference
|
|
35
|
+
* from a dimensional table stamps its scope; a reference from another top-level scope is refused.
|
|
36
|
+
*/
|
|
37
|
+
export declare function referenceFiles(model: TableModel, binding: FileBinding | undefined, session: Session, values: ReadonlyMap<string, Primitive | null>, scope: () => Promise<string | null>): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StoredFileMeta, StoredFileStatus } from './stored-files';
|
|
2
|
+
/** What a handler sees of a stored file: its metadata, never its object. */
|
|
3
|
+
export interface FileReference {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly name: string | null;
|
|
6
|
+
readonly type: string;
|
|
7
|
+
readonly size: number;
|
|
8
|
+
readonly status: StoredFileStatus;
|
|
9
|
+
}
|
|
10
|
+
/** Whose a reference is, and what it lets that work do. */
|
|
11
|
+
export interface FileHolding {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly kind: string;
|
|
14
|
+
/** The work that holds it: one request, or one job attempt. */
|
|
15
|
+
readonly holder: object;
|
|
16
|
+
/** Read under an action that reads the file (or received, or put): it may be written into a column, served and opened. */
|
|
17
|
+
readonly readable: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A reference to a file for `holder`; only `@fabrika/db` and the framework mint one. A `readable` one also lets its
|
|
21
|
+
* holder write the file into a column, serve it and open it, through any reference to the same file.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fileReference(meta: StoredFileMeta, holder: object, readable: boolean): FileReference;
|
|
24
|
+
/** What a reference holds, or `undefined` for any other value: a structural copy, a string id or a forged object. */
|
|
25
|
+
export declare function fileHoldingOf(value: unknown): FileHolding | undefined;
|
|
26
|
+
/** Whether `holder` received, put, or read under a read action the file `id`. */
|
|
27
|
+
export declare function holdsFile(holder: object, id: string): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Session } from './database';
|
|
2
|
+
import type { DataLayerRuntime } from './store';
|
|
3
|
+
/** One webhook binding: the scope value it reaches, the provider account it accepts, and its signing secret. */
|
|
4
|
+
export interface WebhookBinding {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
/** The webhook mount's name. */
|
|
7
|
+
readonly webhook: string;
|
|
8
|
+
readonly scope: {
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly value: string;
|
|
11
|
+
};
|
|
12
|
+
/** The provider account: for GitHub, `installation:<id>` or `repository:<id>`, which a delivery must name. */
|
|
13
|
+
readonly account: string;
|
|
14
|
+
readonly secret: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NewWebhookBinding extends WebhookBinding {
|
|
17
|
+
readonly createdBy: string;
|
|
18
|
+
readonly createdAt: number;
|
|
19
|
+
}
|
|
20
|
+
export interface WebhookTables {
|
|
21
|
+
/** Records a binding, in `using` when given: the transaction of the procedure that binds. */
|
|
22
|
+
createBinding(binding: NewWebhookBinding, using?: Session): Promise<void>;
|
|
23
|
+
/** The binding `id` of `webhook`, or `null`. */
|
|
24
|
+
binding(webhook: string, id: string): Promise<WebhookBinding | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Replaces the secret of the binding when it belongs to `webhook` and `scope`, in `using` when given: a provider that
|
|
27
|
+
* issues its own signing secret shows it only after the binding's URL is registered. `false` when there was none.
|
|
28
|
+
*/
|
|
29
|
+
replaceSecret(webhook: string, id: string, scope: {
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly value: string;
|
|
32
|
+
}, secret: string, using?: Session): Promise<boolean>;
|
|
33
|
+
/** Removes the binding when it belongs to `webhook` and `scope`, in `using` when given; `false` when there was none. */
|
|
34
|
+
removeBinding(webhook: string, id: string, scope: {
|
|
35
|
+
readonly type: string;
|
|
36
|
+
readonly value: string;
|
|
37
|
+
}, using?: Session): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Claims a delivery key for `leaseMs`: `null` when the binding completed it within the retention window, or another
|
|
40
|
+
* copy holds an unexpired claim. A claim whose lease expired is taken over. Keys of the binding older than
|
|
41
|
+
* `retainedSince` are dropped first, so the table stays bounded.
|
|
42
|
+
*/
|
|
43
|
+
claimDelivery(bindingId: string, deliveryKey: string, now: number, leaseMs: number, retainedSince: number): Promise<DeliveryClaim | null>;
|
|
44
|
+
/**
|
|
45
|
+
* Completes a claimed delivery in `using`, the transaction its handler wrote in, so the work and the completion
|
|
46
|
+
* commit together; `false` when another copy took the claim over meanwhile, and that transaction must not commit.
|
|
47
|
+
* Without `using` (a handler with no transaction) it completes on its own.
|
|
48
|
+
*/
|
|
49
|
+
completeDelivery(claim: DeliveryClaim, using?: Session): Promise<boolean>;
|
|
50
|
+
/** Gives a claim back after its handler failed, so the provider's redelivery runs it again at once. */
|
|
51
|
+
releaseDelivery(claim: DeliveryClaim): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
/** One copy's claim on a delivery: its lease end fences the completion against a copy that took the claim over. */
|
|
54
|
+
export interface DeliveryClaim {
|
|
55
|
+
readonly bindingId: string;
|
|
56
|
+
readonly key: string;
|
|
57
|
+
readonly leaseUntil: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function webhookTables(runtime: () => DataLayerRuntime): WebhookTables;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DataAccess } from './orm';
|
|
2
|
+
import { type FrameworkTables, type Locator, type ProvisionalBinding } from './registry';
|
|
3
|
+
import type { DataLayerRuntime } from './store';
|
|
4
|
+
export type { Admissions } from './admissions';
|
|
5
|
+
export type { LeaseFence } from './capability';
|
|
6
|
+
export type { InvocationTransaction, Session } from './database';
|
|
7
|
+
export { CAPABILITY_ERRORS, CapabilityError, type CapabilityErrorReason, LeaseLostError, QUERY_REFUSALS, type QueryRefusal, QueryRefusedError, } from './errors';
|
|
8
|
+
export type { FileBinding } from './file-columns';
|
|
9
|
+
export { type FileHolding, fileHoldingOf, type FileReference, fileReference, holdsFile } from './file-references';
|
|
10
|
+
export type { DeliveryClaim, NewWebhookBinding, WebhookBinding, WebhookTables } from './framework-tables';
|
|
11
|
+
export type { JobLeases, JobLeaseState, JobOutcome, OverdueClaim } from './job-leases';
|
|
12
|
+
export type { DataAccess, DataHandles, DataSite } from './orm';
|
|
13
|
+
export type { FrameworkTables, Locator, ProvisionalBinding } from './registry';
|
|
14
|
+
export type { SqlMode, SqlSite, SqlTag, SqlValue } from './sql';
|
|
15
|
+
export type { FileColumnRef, QuarantinedStoredFile, ServedFile, StagedFile, StoredFileMeta, StoredFiles, StoredFileStatus } from './stored-files';
|
|
16
|
+
/** Whether `value` is an object `scoped(tables)` created, rather than a look-alike with the same fields. */
|
|
17
|
+
export declare function isScopedDatabase(value: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Connects the layer `scoped(tables)` created to `runtime`, once, and answers its locator, which nothing else can
|
|
20
|
+
* reach. Throws for any other value and for a layer that is already connected.
|
|
21
|
+
*/
|
|
22
|
+
export declare function bindScopedDatabase(value: unknown, runtime: DataLayerRuntime): Locator;
|
|
23
|
+
/**
|
|
24
|
+
* Connects the layer like `bindScopedDatabase`, and answers with its locator the `release` that disconnects it again:
|
|
25
|
+
* a boot that fails releases the layer, so a later boot of the same application binds it. Throws as that does.
|
|
26
|
+
*/
|
|
27
|
+
export declare function bindScopedDatabaseProvisionally(value: unknown, runtime: DataLayerRuntime): ProvisionalBinding;
|
|
28
|
+
/**
|
|
29
|
+
* The framework's own writes over the layer `scoped(tables)` created: the webhook tables, the job leases and the
|
|
30
|
+
* admission counters. Each operation needs the layer bound first. Throws for any other value.
|
|
31
|
+
*/
|
|
32
|
+
export declare function frameworkTablesOf(value: unknown): FrameworkTables;
|
|
33
|
+
/**
|
|
34
|
+
* The mini-ORM of the layer `scoped(tables)` created: the invocation transactions and the `ctx.db` handles the
|
|
35
|
+
* framework builds per site. Each operation needs the layer bound first. Throws for any other value.
|
|
36
|
+
*/
|
|
37
|
+
export declare function dataAccessOf(value: unknown): DataAccess;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OperationNode } from 'kysely';
|
|
2
|
+
export type RootKind = 'SelectQueryNode' | 'InsertQueryNode' | 'UpdateQueryNode' | 'DeleteQueryNode';
|
|
3
|
+
export interface ForeignKeyPair {
|
|
4
|
+
readonly childColumn: string;
|
|
5
|
+
readonly parentColumn: string;
|
|
6
|
+
}
|
|
7
|
+
export interface GuardContext {
|
|
8
|
+
readonly root: RootKind;
|
|
9
|
+
readonly table: string;
|
|
10
|
+
/** The children a select may join, each with the composite foreign key its ON must equal. */
|
|
11
|
+
readonly joins: Readonly<Record<string, readonly ForeignKeyPair[]>>;
|
|
12
|
+
/** The scope predicate; it must be the WHERE itself or its top-level right conjunct. Absent for an insert only. */
|
|
13
|
+
readonly scope: OperationNode | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* A select that locks its rows: `share` is the layer's check of a mutation's destination, `update` a mutation's
|
|
16
|
+
* `getForUpdate` of its own target, `claim` a queue claim's `for update skip locked`. No other lock is admitted.
|
|
17
|
+
*/
|
|
18
|
+
readonly lock?: 'share' | 'update' | 'claim';
|
|
19
|
+
/** An insert that does nothing on a conflict of its listed columns: `on conflict (columns) do nothing`, and no other. */
|
|
20
|
+
readonly onConflict?: 'nothing';
|
|
21
|
+
}
|
|
22
|
+
/** Refuses any operation tree outside the allowlist. Runs on the final tree, immediately before it is compiled. */
|
|
23
|
+
export declare function assertAllowedTree(root: OperationNode, context: GuardContext): void;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** `^[a-z_][a-z0-9_]*$`, at most 63 characters (Postgres truncates longer names). */
|
|
2
|
+
export declare function isPlainIdentifier(value: unknown): value is string;
|
|
3
|
+
/** A plain identifier, or two joined by one dot (`note_comments.body`, the alias of a joined column). */
|
|
4
|
+
export declare function isPlainAlias(value: unknown): value is string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type { AppCapability, AppGrant, CapabilityAuthority, CapabilityGrant, ContainerGrant, LeaseFence, Ref, RefGrant, RowGrant, RowRef, Within, WithinGrant, } from './capability';
|
|
2
|
+
export { MAX_WITHIN_VALUES } from './capability';
|
|
3
|
+
export { applicationSchema, Database, type DatabaseEngine, type Executor, type SqlDialect, type TransactionOptions, type TransactionRunner, } from './database';
|
|
4
|
+
export type { AllowedOperator, ComparisonOperator, ConditionTuple, ListOperator, NoColumns, NullOperator, PatternOperator, Primitive, ScopedSelect, } from './description';
|
|
5
|
+
export { CAPABILITY_ERRORS, CapabilityError, type CapabilityErrorReason, DatabaseError, LeaseLostError, PolicyError, QUERY_REFUSALS, type QueryRefusal, QueryRefusedError, } from './errors';
|
|
6
|
+
export type { AnyDbIndex, ChildRows, ContainedRows, CountOptions, DataKind, DbIndex, IndexedTable, ListOptions, ListPage, NoUnscoped, OrderTerm, OwnerRow, ReadOnlyUnscoped, ReferenceRows, ReferenceTables, RowFor, Saved, ScopeTarget, TargetForm, TargetRow, Unavailable, UnownedTarget, UnscopedDeclaration, UnscopedInsert, UnscopedMode, UnscopedRows, UnscopedTables, VersionArguments, WithinRows, WithinTables, } from './orm-types';
|
|
7
|
+
export { type ChildPolicy, type ChildScope, type ColumnList, defineTables, type DimensionPolicy, type DimensionScope, type InsertValues, isTables, readTables, type RecordActions, type ReferencePolicy, type ReferenceScope, type RowOf, type TableActions, type TableInsert, Tables, type TableUpdate, type UpdateValues, type VersionColumn, type VocabularyTables, } from './policy';
|
|
8
|
+
export { type AggregateRow, type AggregateSpec, type ChildListScope, type DataLayer, type DataLayerRuntime, scoped, type ScopedDatabase, } from './store';
|
|
9
|
+
export { type TablePolicies, tablePolicies } from './tables';
|
|
10
|
+
export type { ColumnTypes, DefineVocabularyPolicies, NormalizedPolicies, TableVocabulary, TypedColumnList, VocabularyAction, VocabularyChildScope, VocabularyContainer, VocabularyContainerOf, VocabularyDimension, VocabularyDimensionScope, VocabularyPolicyMap, VocabularyTablePolicy, } from './vocabulary-tables';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LeaseFence } from './capability';
|
|
2
|
+
import { type Session, type SqlDialect } from './database';
|
|
3
|
+
import type { DataLayerRuntime } from './store';
|
|
4
|
+
export type JobOutcome = 'succeeded' | 'failed';
|
|
5
|
+
export interface JobLeaseState {
|
|
6
|
+
readonly generation: number;
|
|
7
|
+
/** The attempt holding the lease, or `null` when it is free. */
|
|
8
|
+
readonly holder: string | null;
|
|
9
|
+
readonly expiresAt: Date;
|
|
10
|
+
/** The holding attempt's deadline: no write of it commits after this. */
|
|
11
|
+
readonly attemptDeadline: Date;
|
|
12
|
+
/** When the job was first seen in this schema: the baseline of a job that never succeeded. */
|
|
13
|
+
readonly firstSeenAt: Date;
|
|
14
|
+
readonly lastStartedAt: Date | null;
|
|
15
|
+
readonly lastFinishedAt: Date | null;
|
|
16
|
+
readonly lastStatus: JobOutcome | null;
|
|
17
|
+
readonly lastSucceededAt: Date | null;
|
|
18
|
+
readonly lastDurationMs: number | null;
|
|
19
|
+
readonly lastMissedReportedAt: Date | null;
|
|
20
|
+
}
|
|
21
|
+
/** A job overdue by twice its period, claimed for one report. */
|
|
22
|
+
export interface OverdueClaim {
|
|
23
|
+
readonly lastSucceededAt: Date | null;
|
|
24
|
+
readonly firstSeenAt: Date;
|
|
25
|
+
}
|
|
26
|
+
export interface JobLeases {
|
|
27
|
+
/** The database's clock. */
|
|
28
|
+
now(): Promise<Date>;
|
|
29
|
+
/** Records that the job exists (its first-seen time), once; later calls change nothing. */
|
|
30
|
+
register(job: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Frees a lease whose holder never finished before it expired (the run crashed) and records the run as failed.
|
|
33
|
+
* `true` for the one caller that freed it.
|
|
34
|
+
*/
|
|
35
|
+
reapCrashed(job: string): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* A free lease for `holder`, under a new generation, for `leaseMs`; its attempt may write for `attemptMs`. `null`
|
|
38
|
+
* while another run holds it.
|
|
39
|
+
*/
|
|
40
|
+
acquire(job: string, holder: string, attemptMs: number, leaseMs: number): Promise<LeaseFence | null>;
|
|
41
|
+
/**
|
|
42
|
+
* `acquire` of a registered job that never waits for a lock: `null` at once while another run holds the lease or an
|
|
43
|
+
* attempt's transaction holds its row. A kick takes the lease only this way, on a connection requests share.
|
|
44
|
+
*/
|
|
45
|
+
tryAcquire(job: string, holder: string, attemptMs: number, leaseMs: number): Promise<LeaseFence | null>;
|
|
46
|
+
/**
|
|
47
|
+
* Hands a live lease to the next attempt under a new generation, so no write of the previous attempt commits from
|
|
48
|
+
* now on. `null` when `fence` no longer holds it.
|
|
49
|
+
*/
|
|
50
|
+
renew(fence: LeaseFence, holder: string, attemptMs: number, leaseMs: number): Promise<LeaseFence | null>;
|
|
51
|
+
/** Frees the lease and records the run's outcome and duration; `false` when `fence` no longer held it. */
|
|
52
|
+
finish(fence: LeaseFence, outcome: JobOutcome): Promise<boolean>;
|
|
53
|
+
/** Records that a kick arrived: the run that started before it runs the job once more. Never waits on the lease. */
|
|
54
|
+
markPending(job: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Clears the flag `markPending` set; `true` for the one caller that cleared it. A run clears it when it starts (it
|
|
57
|
+
* covers every kick before it) and again after it succeeded, when `true` means one more run.
|
|
58
|
+
*/
|
|
59
|
+
takePending(job: string): Promise<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Claims one report of a job that has not succeeded within twice `periodMs`, at most once per period: only the
|
|
62
|
+
* caller that gets the claim reports.
|
|
63
|
+
*/
|
|
64
|
+
claimOverdue(job: string, periodMs: number): Promise<OverdueClaim | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Refuses every further write of the attempt `fence` names and cancels the statements it still has running; the
|
|
67
|
+
* runner calls it when the attempt ends.
|
|
68
|
+
*/
|
|
69
|
+
cancelInFlight(fence: LeaseFence): Promise<void>;
|
|
70
|
+
state(job: string): Promise<JobLeaseState | null>;
|
|
71
|
+
}
|
|
72
|
+
/** One framework statement in each dialect's text; a session runs the text of its own. */
|
|
73
|
+
export type DialectTexts = Readonly<Record<SqlDialect, string>>;
|
|
74
|
+
/** SQLite's clock, as a stored timestamp: ISO text, so a comparison of two is a text comparison. */
|
|
75
|
+
export declare const SQLITE_NOW = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
76
|
+
/** SQLite's clock moved by `ms`, an SQL expression of milliseconds (negative is earlier). */
|
|
77
|
+
export declare function sqliteNowPlus(ms: string): string;
|
|
78
|
+
/** A stored SQLite timestamp (`SQLITE_NOW_FORMAT`) as a `Date`, or `null`; anything else is refused. */
|
|
79
|
+
export declare function sqliteTimestamp(value: unknown, what: string): Date | null;
|
|
80
|
+
/**
|
|
81
|
+
* The database's clock inside `session`'s transaction: a queue claim compares and moves `due` by it, so no process
|
|
82
|
+
* clock decides when a row is due.
|
|
83
|
+
*/
|
|
84
|
+
export declare function databaseNow(session: Session): Promise<Date>;
|
|
85
|
+
/**
|
|
86
|
+
* Runs `work` in a mutation's transaction under `fence`. Before it: the attempt still holds its lease, the lease row
|
|
87
|
+
* stays locked `for share` until the commit (a takeover or a renewal waits), and every statement is bounded by the
|
|
88
|
+
* attempt's remaining time. After it, just before the commit: the attempt still holds the lease within its deadline.
|
|
89
|
+
* Otherwise the transaction rolls back with `LeaseLostError`; so does a statement the deadline or the runner cancelled.
|
|
90
|
+
* On SQLite only the two checks run: no other event reaches the Durable Object while the transaction is open.
|
|
91
|
+
*/
|
|
92
|
+
export declare function fencedTransaction<T>(session: Session, fence: LeaseFence, work: (transaction: Session) => Promise<T>): Promise<T>;
|
|
93
|
+
export declare function jobLeases(runtime: () => DataLayerRuntime): JobLeases;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OperationNode } from 'kysely';
|
|
2
|
+
import type { RefGrant } from './capability';
|
|
3
|
+
import type { Container } from './policy';
|
|
4
|
+
export interface LinkValue {
|
|
5
|
+
readonly link: Container;
|
|
6
|
+
readonly value: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The column values a Ref addresses over `links`: the first link takes the Ref's id, every other link a value
|
|
10
|
+
* from its chain. A chain entry no link covers cannot be enforced, so it is refused rather than ignored.
|
|
11
|
+
*/
|
|
12
|
+
export declare function linkValues(table: string, links: readonly Container[], grant: RefGrant): readonly LinkValue[];
|
|
13
|
+
export declare function linksPredicate(table: string, values: readonly LinkValue[]): OperationNode;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type FrameworkMigration } from './migrations';
|
|
2
|
+
/** The `strftime` format of a stored timestamp; `strftime(SQLITE_NOW_FORMAT, 'now')` is the database's clock. */
|
|
3
|
+
export declare const SQLITE_NOW_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
4
|
+
export declare const SQLITE_FRAMEWORK_MIGRATIONS: readonly FrameworkMigration[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Migration identity of the framework's tables in every application schema: bundle `fabrika`, same ledger. */
|
|
2
|
+
export declare const FRAMEWORK_MIGRATION_BUNDLE = "fabrika";
|
|
3
|
+
export declare const WEBHOOK_BINDINGS_TABLE = "fabrika_webhook_bindings";
|
|
4
|
+
export declare const WEBHOOK_DELIVERIES_TABLE = "fabrika_webhook_deliveries";
|
|
5
|
+
export declare const JOB_LEASES_TABLE = "fabrika_job_leases";
|
|
6
|
+
export declare const JOB_KICKS_TABLE = "fabrika_job_kicks";
|
|
7
|
+
export declare const FORM_ADMISSIONS_TABLE = "fabrika_form_admissions";
|
|
8
|
+
export declare const FORM_CHALLENGES_TABLE = "fabrika_form_challenges";
|
|
9
|
+
export declare const FORM_FILES_TABLE = "fabrika_form_files";
|
|
10
|
+
export declare const FORM_BYTES_TABLE = "fabrika_form_bytes";
|
|
11
|
+
export declare const FILES_TABLE = "fabrika_files";
|
|
12
|
+
export declare const FILE_DELETIONS_TABLE = "fabrika_file_deletions";
|
|
13
|
+
/** The function an application's last adoption migration calls with the name of a file collection it adopted. */
|
|
14
|
+
export declare const FILES_RELEASE_FUNCTION = "fabrika_files_release";
|
|
15
|
+
/**
|
|
16
|
+
* The function an adoption migration calls right before it reads a collection's table: it copies the rows an older
|
|
17
|
+
* instance wrote since 0008 ran, so each one it adopts has its `fabrika_files` row. Idempotent.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FILES_COPY_FUNCTION = "fabrika_files_copy_legacy";
|
|
20
|
+
export interface FrameworkMigration {
|
|
21
|
+
/** Ordered by name; an applied one is immutable, like an application's. */
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly sql: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const FRAMEWORK_MIGRATIONS: readonly FrameworkMigration[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type DeleteQueryNode, type InsertQueryNode, type OperationNode, ReferenceNode, ReturningNode, type SelectQueryNode, type UpdateQueryNode, WhereNode } from 'kysely';
|
|
2
|
+
import { type NameSet } from './builtins';
|
|
3
|
+
import type { SqlDialect } from './database';
|
|
4
|
+
import type { Condition, Primitive, SelectState } from './description';
|
|
5
|
+
export declare function column(table: string, name: string): ReferenceNode;
|
|
6
|
+
export declare function equals(table: string, name: string, value: Primitive): OperationNode;
|
|
7
|
+
export declare function allOf(nodes: readonly OperationNode[]): OperationNode;
|
|
8
|
+
/** One node, or the OR of several in a parenthesis: inside an AND, `a and b or c` would read as `(a and b) or c`. */
|
|
9
|
+
export declare function anyOf(nodes: readonly OperationNode[]): OperationNode;
|
|
10
|
+
export declare function inList(table: string, name: string, values: readonly string[]): OperationNode;
|
|
11
|
+
/** The scope of a `Within` that is not limited to scope values. */
|
|
12
|
+
export declare function everything(): OperationNode;
|
|
13
|
+
/** LIKE treats `\` as its escape character by default, so the text matches literally. */
|
|
14
|
+
export declare function containsPattern(text: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* The caller's conditions go inside an explicit parenthesis and the scope is the top-level right conjunct, so
|
|
17
|
+
* `x or true` cannot escape it. The guard checks this shape.
|
|
18
|
+
*/
|
|
19
|
+
export declare function scopedWhere(conditions: OperationNode | undefined, scope: OperationNode): WhereNode;
|
|
20
|
+
/** `returning key as id[, version as version]`: a mutation's acknowledgement, never row data. */
|
|
21
|
+
export declare function acknowledgement(table: string, key: string, version: string | undefined): ReturningNode;
|
|
22
|
+
/** `after` is a keyset page's position, ANDed to the caller's conditions inside their parenthesis. */
|
|
23
|
+
export declare function selectNode(state: SelectState, visible: readonly string[], scope: OperationNode, after?: OperationNode, days?: NameSet, dialect?: SqlDialect): SelectQueryNode;
|
|
24
|
+
export interface AggregateColumns {
|
|
25
|
+
readonly groupBy: readonly string[];
|
|
26
|
+
readonly count: boolean;
|
|
27
|
+
readonly sum: readonly string[];
|
|
28
|
+
readonly min: readonly string[];
|
|
29
|
+
readonly max: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export declare function aggregateNode(table: string, columns: AggregateColumns, conditions: readonly Condition[], scope: OperationNode, days?: NameSet): SelectQueryNode;
|
|
32
|
+
/** `conflict` names the columns of a unique index the insert does nothing on: `on conflict (<columns>) do nothing`. */
|
|
33
|
+
export declare function insertNode(table: string, values: ReadonlyMap<string, Primitive | null>, answer: ReturningNode, conflict?: readonly string[]): InsertQueryNode;
|
|
34
|
+
export declare function updateNode(table: string, values: ReadonlyMap<string, Primitive | null>, bump: string | undefined, where: WhereNode, answer: ReturningNode): UpdateQueryNode;
|
|
35
|
+
export declare function deleteNode(table: string, where: WhereNode, key: string): DeleteQueryNode;
|
|
36
|
+
/** `select key from table where (conditions) and scope limit 1`: does the scoped object exist? */
|
|
37
|
+
export declare function existsNode(table: string, key: string, scope: OperationNode, conditions?: OperationNode): SelectQueryNode;
|
|
38
|
+
/** `select key as id, version as version from table where (conditions) and scope limit 1`: an update that sets nothing. */
|
|
39
|
+
export declare function acknowledgementNode(table: string, key: string, version: string | undefined, where: WhereNode): SelectQueryNode;
|
|
40
|
+
/** `select columns from table where scope limit 1`: the containment columns of one object, for the framework's locator. */
|
|
41
|
+
export declare function locateNode(table: string, columns: readonly string[], scope: OperationNode): SelectQueryNode;
|
|
42
|
+
/**
|
|
43
|
+
* The same check, locked: `for share` keeps the row from moving until the transaction that checked it ends, `for
|
|
44
|
+
* update` also keeps every other writer and locker of it waiting.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lockNode(table: string, key: string, scope: OperationNode, lock?: 'share' | 'update'): SelectQueryNode;
|
|
47
|
+
/** The alias a keyset page's position column is selected under; a column name holds no dot, so none collides. */
|
|
48
|
+
export declare function cursorAlias(index: number): string;
|
|
49
|
+
/** `select …, cast(<column> as text) as "keyset.c<n>"`: each order column's exact value, which a Date would round. */
|
|
50
|
+
export declare function withCursorColumns(node: SelectQueryNode, table: string, columns: readonly string[]): SelectQueryNode;
|
|
51
|
+
/**
|
|
52
|
+
* A queue claim's lease: `select <columns> from table where (<conditions> and (<due> is not null and <due> <= $now and
|
|
53
|
+
* <attempts> < $max)) and scope order by <due>, <key> limit $n for update skip locked`. A row another claim holds is
|
|
54
|
+
* skipped, never waited for.
|
|
55
|
+
*/
|
|
56
|
+
export declare function claimNode(state: SelectState, columns: readonly string[], due: {
|
|
57
|
+
readonly due: string;
|
|
58
|
+
readonly attempts: string;
|
|
59
|
+
readonly now: Date;
|
|
60
|
+
readonly maxAttempts: number;
|
|
61
|
+
}, scope: OperationNode, days: NameSet, dialect?: SqlDialect): SelectQueryNode;
|
|
62
|
+
/** `select <columns> from table where scope for update`: a mutation's precondition read of its own target. */
|
|
63
|
+
export declare function lockedSelectNode(table: string, columns: readonly string[], scope: OperationNode, days?: NameSet): SelectQueryNode;
|