@fabrika/db 0.8.1 → 0.9.0-dev.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/db",
3
- "version": "0.8.1",
3
+ "version": "0.9.0-dev.2",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,47 +12,48 @@
12
12
  },
13
13
  "files": [
14
14
  "src",
15
- "!src/**/__tests__/**"
15
+ "!src/**/__tests__/**",
16
+ "types"
16
17
  ],
17
18
  "exports": {
18
19
  ".": {
19
20
  "bun": "./src/index.ts",
20
- "types": "./src/index.ts",
21
+ "types": "./types/src/index.d.ts",
21
22
  "default": "./src/index.ts"
22
23
  },
23
24
  "./capability": {
24
25
  "bun": "./src/capability.ts",
25
- "types": "./src/capability.ts",
26
+ "types": "./types/src/capability.d.ts",
26
27
  "default": "./src/capability.ts"
27
28
  },
28
29
  "./tables": {
29
30
  "bun": "./src/tables.ts",
30
- "types": "./src/tables.ts",
31
+ "types": "./types/src/tables.d.ts",
31
32
  "default": "./src/tables.ts"
32
33
  },
33
34
  "./framework": {
34
35
  "bun": "./src/framework.ts",
35
- "types": "./src/framework.ts",
36
+ "types": "./types/src/framework.d.ts",
36
37
  "default": "./src/framework.ts"
37
38
  },
38
39
  "./migrations": {
39
40
  "bun": "./src/migrations.ts",
40
- "types": "./src/migrations.ts",
41
+ "types": "./types/src/migrations.d.ts",
41
42
  "default": "./src/migrations.ts"
42
43
  },
43
44
  "./production": {
44
45
  "bun": "./src/production.ts",
45
- "types": "./src/production.ts",
46
+ "types": "./types/src/production.d.ts",
46
47
  "default": "./src/production.ts"
47
48
  },
48
49
  "./codegen": {
49
50
  "bun": "./src/codegen.ts",
50
- "types": "./src/codegen.ts",
51
+ "types": "./types/src/codegen.d.ts",
51
52
  "default": "./src/codegen.ts"
52
53
  },
53
54
  "./testing": {
54
55
  "bun": "./src/testing.ts",
55
- "types": "./src/testing.ts",
56
+ "types": "./types/src/testing.d.ts",
56
57
  "default": "./src/testing.ts"
57
58
  }
58
59
  },
@@ -61,8 +62,8 @@
61
62
  "test": "bun test"
62
63
  },
63
64
  "dependencies": {
64
- "@fabrika/auth-core": "0.8.1",
65
- "@fabrika/platform-node": "0.8.1",
65
+ "@fabrika/auth-core": "0.9.0-dev.2",
66
+ "@fabrika/platform-node": "0.9.0-dev.2",
66
67
  "kysely": "0.29.6",
67
68
  "kysely-postgres-js": "5.0.1",
68
69
  "zod": "^3.25.76"
package/src/testing.ts CHANGED
@@ -371,6 +371,7 @@ export async function testDatabase(options: TestDatabaseOptions): Promise<TestDa
371
371
  if (process.env[TEST_POSTGRES_REQUIRED] === '1') {
372
372
  throw new Error(`${options.label}: ${TEST_POSTGRES_REQUIRED}=1, but ${TEST_POSTGRES_URL} is unset`)
373
373
  }
374
+ // The Zerops build counts this line's text in every framework release's output: never reword it.
374
375
  console.warn(`${options.label} skipped: set ${TEST_POSTGRES_URL} to run the Postgres-backed tests`)
375
376
  return null
376
377
  }
@@ -0,0 +1,21 @@
1
+ import type { DataLayerRuntime } from './store';
2
+ export interface Admissions {
3
+ /**
4
+ * Takes one from `bucket` of `endpoint` in the database's current minute; `false` when `limit` were already taken
5
+ * there. The counter and the check are one statement.
6
+ */
7
+ take(endpoint: string, bucket: string, limit: number): Promise<boolean>;
8
+ /**
9
+ * Records the digest of a challenge token `endpoint` accepted, kept for `retainMs`; `false` when it was already
10
+ * recorded and has not expired, which makes the token's second use a replay.
11
+ */
12
+ recordChallenge(endpoint: string, tokenDigest: string, retainMs: number): Promise<boolean>;
13
+ /**
14
+ * Reserves `bytes` of `endpoint`'s hourly byte budget `limit`, in the database's current hour; `false`, with nothing
15
+ * taken, when they no longer fit. The check and the counter are one statement.
16
+ */
17
+ reserveBytes(endpoint: string, bytes: number, limit: number): Promise<boolean>;
18
+ /** Gives back `bytes` of a reservation of the current hour that the request did not need. */
19
+ releaseBytes(endpoint: string, bytes: number): Promise<void>;
20
+ }
21
+ export declare function admissions(runtime: () => DataLayerRuntime): Admissions;
@@ -0,0 +1,42 @@
1
+ export declare const ownKeys: {
2
+ (o: object): string[];
3
+ (o: {}): string[];
4
+ };
5
+ export declare const ownDescriptor: (o: any, p: PropertyKey) => PropertyDescriptor | undefined;
6
+ export declare const hasOwn: (o: object, v: PropertyKey) => boolean;
7
+ export declare const isArray: (arg: any) => arg is any[];
8
+ export declare const freeze: {
9
+ <T extends Function>(f: T): T;
10
+ <T extends {
11
+ [idx: string]: U | null | undefined | object;
12
+ }, U extends string | bigint | number | boolean | symbol>(o: T): Readonly<T>;
13
+ <T>(o: T): Readonly<T>;
14
+ };
15
+ export declare const prototypeOf: (o: any) => any;
16
+ export declare const isSafeInteger: (number: unknown) => boolean;
17
+ export declare const OBJECT_PROTOTYPE: unknown;
18
+ export declare const ownNames: (o: any) => string[];
19
+ export declare const ownSymbols: (o: any) => symbol[];
20
+ /**
21
+ * A frozen set of names, read through the captured `Object.hasOwn` only. Entries are defined, not assigned, so
22
+ * `__proto__` or an inherited setter cannot drop one.
23
+ */
24
+ export type NameSet = Readonly<Record<string, true>>;
25
+ export declare function nameSet(names: readonly string[]): NameSet;
26
+ export declare function inSet(set: NameSet, name: unknown): name is string;
27
+ /** The names in the order they were added. */
28
+ export declare function namesOf(set: NameSet): readonly string[];
29
+ /**
30
+ * The elements of a plain array, each passed through `parse` before it is copied; undefined for anything else: a
31
+ * subclass, an array with an own property besides its indexes and `length` (an own `map` or `Symbol.iterator`),
32
+ * a hole, or an accessor element. The input's own methods never run.
33
+ */
34
+ export declare function parseList<T>(value: unknown, parse: (item: unknown) => T): T[] | undefined;
35
+ /** A frozen copy of a string map, read through the captured `Map.prototype.forEach`. */
36
+ export declare function recordOf(map: ReadonlyMap<string, string>): Readonly<Record<string, string>>;
37
+ /** Defines an own property: unlike an assignment, it cannot hit `__proto__` or an inherited setter. */
38
+ export declare function setOwn<V>(record: Record<string, V>, key: string, value: V): void;
39
+ /** Redefines an own data property of any object, as `setOwn` does for a record. */
40
+ export declare function defineValue(target: object, key: string, value: unknown): void;
41
+ /** An own property of a record, or undefined: an inherited one is never read. */
42
+ export declare function ownEntry<V>(record: Readonly<Record<string, V>>, key: string): V | undefined;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * One authorized object, for one action. The framework mints the class that implements it; this package never
3
+ * constructs one. The `~capability` member is type-only: the implementing class declares it so that
4
+ * `Ref<'note', 'notes.read'>` and `Ref<'note', 'notes.delete'>` stay distinct types, and nothing reads it at runtime.
5
+ */
6
+ export interface Ref<Dim extends string, Action extends string> {
7
+ readonly '~capability': {
8
+ readonly kind: 'ref';
9
+ readonly dimension: Dim;
10
+ readonly action: Action;
11
+ };
12
+ }
13
+ /** The scope values of one dimension in which the caller holds one action. */
14
+ export interface Within<Dim extends string, Action extends string> {
15
+ readonly '~capability': {
16
+ readonly kind: 'within';
17
+ readonly dimension: Dim;
18
+ readonly action: Action;
19
+ };
20
+ }
21
+ /**
22
+ * One action held as a global grant: the capability of an `app` endpoint, for data of the whole application. Like a
23
+ * `Ref`, the member is type-only.
24
+ */
25
+ export interface AppCapability<Action extends string> {
26
+ readonly '~capability': {
27
+ readonly kind: 'app';
28
+ readonly action: Action;
29
+ };
30
+ }
31
+ /**
32
+ * One claimed row of one queue table, for one action: what a claim answers as `row.ref`, and what `tx.db.on(row.ref)`
33
+ * finishes the row through. Like a `Ref`, the member is type-only.
34
+ */
35
+ export interface RowRef<Table extends string, Action extends string> {
36
+ readonly '~capability': {
37
+ readonly kind: 'row';
38
+ readonly table: Table;
39
+ readonly action: Action;
40
+ };
41
+ }
42
+ export interface RefGrant {
43
+ readonly kind: 'ref';
44
+ readonly dimension: string;
45
+ readonly action: string;
46
+ readonly principal: string;
47
+ readonly id: string;
48
+ /** The values of the containers this object lies in, by dimension (`workspace → w1` for a note). */
49
+ readonly chain: ReadonlyMap<string, string>;
50
+ }
51
+ /** The values of one container dimension in which the caller holds the action: `organization → [o1]`. */
52
+ export interface ContainerGrant {
53
+ readonly dimension: string;
54
+ readonly values: readonly string[];
55
+ }
56
+ /**
57
+ * The most scope values one `Within` carries, its own and its containers' together. Each becomes one bound
58
+ * parameter of every statement the `Within` scopes; Postgres binds at most 65535.
59
+ */
60
+ export declare const MAX_WITHIN_VALUES = 1000;
61
+ export interface WithinGrant {
62
+ readonly kind: 'within';
63
+ readonly dimension: string;
64
+ readonly action: string;
65
+ readonly principal: string;
66
+ /** The values of `dimension` itself; `all` comes from a grant that is not limited to scope values. */
67
+ readonly values: readonly string[] | 'all';
68
+ /**
69
+ * The containers of `dimension` whose grants also authorise this `Within`. They stay separate from `values`, so
70
+ * every statement judges a row by the chain columns it carries now: a scope moved out of a container is out.
71
+ */
72
+ readonly containers: readonly ContainerGrant[];
73
+ }
74
+ /** An `app` endpoint's grant: the action everywhere in the application, which the proxy checked was held globally. */
75
+ export interface AppGrant {
76
+ readonly kind: 'app';
77
+ readonly action: string;
78
+ readonly principal: string;
79
+ }
80
+ /** A row a claim leased: the table, its key, and the containment it had then, column by column. */
81
+ export interface RowGrant {
82
+ readonly kind: 'row';
83
+ readonly table: string;
84
+ readonly action: string;
85
+ readonly principal: string;
86
+ readonly id: string;
87
+ /** The row's containment columns and their values when it was claimed: a row moved since is out of reach. */
88
+ readonly chain: ReadonlyMap<string, string>;
89
+ }
90
+ export type CapabilityGrant = RefGrant | WithinGrant | AppGrant | RowGrant;
91
+ /** How a refusal names a capability: `a Ref of note`, `a Within of workspace`, `an app capability`, `a row of outbox`. */
92
+ export declare function describeGrant(grant: CapabilityGrant): string;
93
+ /** One generation of a job's lease, held by one attempt. A write of the attempt commits only while it still holds it. */
94
+ export interface LeaseFence {
95
+ readonly job: string;
96
+ /** The attempt holding the lease. */
97
+ readonly holder: string;
98
+ readonly generation: number;
99
+ }
100
+ /**
101
+ * The framework side of a capability. `resolve` returns what an authentic capability of the current request grants,
102
+ * or throws when the value is not one, is forged, or belongs to a request that has ended or is not the current one.
103
+ * The data layer checks the kind, the dimension and the action itself, on every operation.
104
+ */
105
+ export interface CapabilityAuthority {
106
+ resolve(capability: unknown): CapabilityGrant;
107
+ /** Mints the capability of one claimed row, bound to the work running now as every capability is. */
108
+ row(grant: Omit<RowGrant, 'kind'>): unknown;
109
+ /**
110
+ * The lease the work running now must still hold for a write to commit: the job attempt's, or `null` in a
111
+ * request. Every mutation checks it inside its own transaction.
112
+ */
113
+ fence(): LeaseFence | null;
114
+ }
@@ -0,0 +1,52 @@
1
+ import { type DatabaseObject } from '@fabrika/platform-node';
2
+ import { z } from 'zod/v4';
3
+ /** Where an application keeps the generated types and column list; committed, never hand-edited. */
4
+ export declare const SCHEMA_FILE = "src/db/schema.generated.ts";
5
+ export declare class CodegenError extends Error {
6
+ name: string;
7
+ }
8
+ declare const catalogColumnSchema: z.ZodObject<{
9
+ table_name: z.ZodString;
10
+ column_name: z.ZodString;
11
+ type_name: z.ZodString;
12
+ type_type: z.ZodString;
13
+ type_category: z.ZodString;
14
+ nullable: z.ZodBoolean;
15
+ generated: z.ZodEnum<{
16
+ default: "default";
17
+ no: "no";
18
+ always: "always";
19
+ }>;
20
+ enum_labels: z.ZodArray<z.ZodString>;
21
+ }, z.core.$strip>;
22
+ export type CatalogColumn = z.infer<typeof catalogColumnSchema>;
23
+ declare const catalogForeignKeySchema: z.ZodObject<{
24
+ table_name: z.ZodString;
25
+ constraint_name: z.ZodString;
26
+ columns: z.ZodArray<z.ZodString>;
27
+ referenced_table: z.ZodString;
28
+ referenced_columns: z.ZodArray<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ export type CatalogForeignKey = z.infer<typeof catalogForeignKeySchema>;
31
+ /** What codegen reads of the schema: every column, and every foreign key between its tables. */
32
+ export interface Catalog {
33
+ readonly columns: readonly CatalogColumn[];
34
+ readonly foreignKeys: readonly CatalogForeignKey[];
35
+ }
36
+ /**
37
+ * Reads the columns of the application's schema, refusing one that escapes it or holds a database object that
38
+ * `objects`, the migrations' manifest (`PostgresApplicationMigrations.objects`), does not name. The URL is a
39
+ * credential: no message here contains it.
40
+ */
41
+ export declare function introspectSchema(url: string, schema: string, objects: readonly DatabaseObject[]): Promise<readonly CatalogColumn[]>;
42
+ /** `introspectSchema`, with the foreign keys between the schema's tables. */
43
+ export declare function introspectCatalog(url: string, schema: string, objects: readonly DatabaseObject[]): Promise<Catalog>;
44
+ /**
45
+ * The generated module: one interface per application table, the `DB` map Kysely and `defineTables` take, and the
46
+ * runtime column list with each table's foreign keys to application tables. Deterministic for a given catalog, so
47
+ * `fabrika db codegen --verify` can compare text.
48
+ */
49
+ export declare function renderSchema(catalog: readonly CatalogColumn[], foreignKeys?: readonly CatalogForeignKey[]): string;
50
+ /** Introspects the migrated schema, checked against the migrations' manifest, and renders the module for `SCHEMA_FILE`. */
51
+ export declare function generateSchema(url: string, schema: string, objects: readonly DatabaseObject[]): Promise<string>;
52
+ export {};
@@ -0,0 +1,124 @@
1
+ import { type Dialect, Kysely, type RootOperationNode } from 'kysely';
2
+ import { type GuardContext } from './guard';
3
+ type LooseDB = Record<string, Record<string, unknown>>;
4
+ type Executor = Pick<Kysely<LooseDB>, 'executeQuery'>;
5
+ /**
6
+ * `app_<id>`, the schema an application's migrations create (ADR-0017). `@fabrika/platform-node`'s
7
+ * `applicationDatabaseSchema` derives the same name for the migration runner; the two must agree.
8
+ */
9
+ export declare function applicationSchema(appId: string): string;
10
+ export interface PostgresOptions {
11
+ readonly url: string;
12
+ /** The application's schema (`app_<id>`), set as the connection's `search_path`. */
13
+ readonly schema: string;
14
+ readonly max?: number;
15
+ }
16
+ /** The connection the data layer runs on. It has no public query method: everything goes through a store. */
17
+ export declare class Database {
18
+ #private;
19
+ private constructor();
20
+ /**
21
+ * Over `Bun.SQL` with `prepare: false`: Bun otherwise keeps one prepared statement per distinct SQL text per
22
+ * connection, and `in` lists of different lengths make that grow without bound.
23
+ */
24
+ static postgres(options: PostgresOptions): Database;
25
+ /** Any Kysely Postgres dialect; tests use one that records statements instead of sending them. */
26
+ static fromDialect(dialect: Dialect): Database;
27
+ /** The application's schema, the connection's `search_path`; `undefined` over a bare dialect. */
28
+ get schema(): string | undefined;
29
+ close(): Promise<void>;
30
+ }
31
+ /**
32
+ * Opens the pool's first connection, which stays open. A pool with one open connection makes a statement wait for it
33
+ * when the database refuses another (Bun 1.3.14), so only a pool with none fails on a database at its limit: the test
34
+ * kit opens it before a file's first case, where a refusal is waited out instead of failing a case.
35
+ */
36
+ export declare function openFirstConnection(database: Database): Promise<void>;
37
+ /** The connection a session runs on, and whether the request running now may only read. */
38
+ export interface SessionSource {
39
+ readonly database: Database;
40
+ readonly readOnly?: () => boolean;
41
+ }
42
+ /**
43
+ * The one way to a session: every statement of a request — a store's, a locator's, a framework table's — asks the
44
+ * runtime whether the request is read-only. A read-only session refuses every write and lock before it reaches the
45
+ * database, and reads in `READ ONLY` transactions.
46
+ */
47
+ export declare function sessionOf(source: SessionSource): Session;
48
+ /** What a session reports to, and asks, the transaction it runs in. */
49
+ interface SessionHooks {
50
+ /** Hears of every statement the database refused: in a transaction, Postgres aborts the rest of it. */
51
+ readonly failed?: () => void;
52
+ /** Whether the transaction is still the work's: a session kept past it runs nothing on a connection given back. */
53
+ readonly open?: () => boolean;
54
+ }
55
+ /**
56
+ * Runs guarded operation trees; the only path from the layer to the connection. A driver failure leaves it as a
57
+ * `DatabaseError`, which carries the SQLSTATE and nothing the driver said about the row.
58
+ */
59
+ export declare class Session {
60
+ #private;
61
+ constructor(executor: Executor, kysely: Kysely<LooseDB> | undefined, readOnly: boolean, hooks?: SessionHooks);
62
+ get readOnly(): boolean;
63
+ /** Fails the transaction this session runs in, so it commits nothing: a statement refused before it was sent. */
64
+ fail(): void;
65
+ run<O>(node: RootOperationNode, context: GuardContext): Promise<O[]>;
66
+ /**
67
+ * Runs `work` in one transaction (`READ ONLY` in a read-only session); inside one, it runs in the transaction already
68
+ * open. When `signal` aborts, the transaction rolls back and its connection goes back to the pool at once, whether
69
+ * `work` returned or not; its session then runs nothing more.
70
+ */
71
+ transaction<T>(work: (session: Session) => Promise<T>, signal?: AbortSignal): Promise<T>;
72
+ }
73
+ /** How long an invocation's transaction may sit idle before Postgres ends it, and its connection with it. */
74
+ export declare const INVOCATION_IDLE_TIMEOUT_MS = 30000;
75
+ /**
76
+ * The one transaction of an invocation (a request, a webhook delivery): opened by its first statement, then ended
77
+ * once by the framework — committed after the handler returned and its output passed validation, rolled back
78
+ * otherwise. Every handle of the invocation runs in `session()`. Ending it and giving its connection back are two
79
+ * states: once ended no statement runs in it, and the connection is back in the pool whether the COMMIT succeeded,
80
+ * failed, or the connection died.
81
+ */
82
+ export declare class InvocationTransaction {
83
+ #private;
84
+ constructor(database: Database, readOnly: boolean);
85
+ /** A `READ ONLY` transaction: a query's or an export's. */
86
+ get readOnly(): boolean;
87
+ /** Whether the transaction was opened: an invocation that reached no data commits nothing. */
88
+ get opened(): boolean;
89
+ /** Marks it failed, so it commits nothing: a statement refused before it was sent, even when the handler caught it. */
90
+ fail(): void;
91
+ /** No statement runs in it any more. */
92
+ get ended(): boolean;
93
+ /** Its connection is back in the pool (or it never took one). */
94
+ get released(): boolean;
95
+ /** The session the invocation's statements run in; the first call opens the transaction. */
96
+ session(): Promise<Session>;
97
+ /**
98
+ * Commits what the invocation wrote. A statement the database refused aborted the transaction, so it rolls back
99
+ * and throws instead: Postgres would answer the COMMIT with a silent ROLLBACK. A failed COMMIT throws a
100
+ * `DatabaseError`, after the connection is back in the pool.
101
+ */
102
+ commit(): Promise<void>;
103
+ /** Discards what the invocation wrote; idempotent, never throws, and returns once the connection is back. */
104
+ rollback(): Promise<void>;
105
+ }
106
+ /**
107
+ * A scalar parameter of a framework statement; Bun.SQL mis-binds arrays and JSON strings. No Date: these texts skip
108
+ * the compiler that binds one, and a lease time is the database's clock, never the process's.
109
+ */
110
+ export type FixedParameter = string | number | null;
111
+ /**
112
+ * One of the framework's own fixed statements (`job-leases.ts`): a constant text with scalar parameters. They need
113
+ * what the guard refuses on purpose (`now()`, `set_config`, `pg_backend_pid()`, `on conflict`), so they bypass it;
114
+ * no subpath exports this, and no text is ever built from a caller's value.
115
+ */
116
+ export declare function runFixedStatement(session: Session, text: string, parameters: readonly FixedParameter[]): Promise<readonly unknown[]>;
117
+ /** A bound parameter of an application's own statement (`sql.ts`), after its value was encoded for the driver. */
118
+ export type SqlParameter = string | number | boolean | bigint | null;
119
+ /**
120
+ * One `ctx.sql` statement, whose text `sql.ts` classified and whose values are bound parameters only. It runs through the
121
+ * session's one path to the connection, so a statement the database refuses fails the invocation it runs in.
122
+ */
123
+ export declare function runApplicationStatement(session: Session, text: string, parameters: readonly SqlParameter[]): Promise<readonly unknown[]>;
124
+ export {};
@@ -0,0 +1,109 @@
1
+ import { type NameSet } from './builtins';
2
+ export type Primitive = string | number | bigint | boolean | Date;
3
+ export type ComparisonOperator = '=' | '<>' | '<' | '<=' | '>' | '>=';
4
+ export type PatternOperator = 'like' | 'ilike' | 'not like' | 'not ilike';
5
+ export type ListOperator = 'in' | 'not in';
6
+ export type NullOperator = 'is' | 'is not';
7
+ export type AllowedOperator = ComparisonOperator | PatternOperator | ListOperator | NullOperator;
8
+ export interface Compare {
9
+ readonly kind: 'compare';
10
+ readonly column: string;
11
+ readonly operator: AllowedOperator;
12
+ readonly value: Primitive | readonly Primitive[] | null;
13
+ }
14
+ /** A case-insensitive substring match; the text's `%`, `_` and `\` match literally. */
15
+ export interface Contains {
16
+ readonly kind: 'contains';
17
+ readonly column: string;
18
+ readonly text: string;
19
+ }
20
+ export interface AnyOf {
21
+ readonly kind: 'any';
22
+ readonly conditions: readonly (Compare | Contains)[];
23
+ }
24
+ export type Condition = Compare | Contains | AnyOf;
25
+ /** What a select may name: the visible columns of its table and of the children it may join. */
26
+ export interface TableShape {
27
+ readonly table: string;
28
+ readonly visible: NameSet;
29
+ readonly joinable: Readonly<Record<string, JoinShape>>;
30
+ /** The `date` columns, which a read answers as `YYYY-MM-DD` text. */
31
+ readonly dates: NameSet;
32
+ }
33
+ export interface JoinShape {
34
+ readonly shape: TableShape;
35
+ /** The child's read actions: a select of the parent joins it only with one of them. */
36
+ readonly readActions: readonly string[];
37
+ /** The composite foreign key: child column = parent column, for each pair. */
38
+ readonly on: readonly {
39
+ readonly childColumn: string;
40
+ readonly parentColumn: string;
41
+ }[];
42
+ }
43
+ export interface JoinState {
44
+ readonly join: JoinShape;
45
+ readonly select: SelectState;
46
+ }
47
+ export interface SelectState {
48
+ /** Identifies the description a callback received, so it cannot return one from elsewhere. */
49
+ readonly origin: object;
50
+ readonly table: string;
51
+ readonly columns: readonly string[] | undefined;
52
+ readonly conditions: readonly Condition[];
53
+ readonly orderBy: readonly {
54
+ readonly column: string;
55
+ readonly direction: 'asc' | 'desc';
56
+ }[];
57
+ readonly limit: number | undefined;
58
+ readonly offset: number | undefined;
59
+ readonly joins: readonly JoinState[];
60
+ }
61
+ type Comparable<V> = Exclude<V, null | undefined>;
62
+ type Columns<Row> = keyof Row & string;
63
+ type StringColumn<Row> = {
64
+ [K in Columns<Row>]: Comparable<Row[K]> extends string ? K : never;
65
+ }[Columns<Row>];
66
+ type NullableColumn<Row> = {
67
+ [K in Columns<Row>]: null extends Row[K] ? K : never;
68
+ }[Columns<Row>];
69
+ export type ConditionTuple<Row> = {
70
+ [K in Columns<Row>]: readonly [K, ComparisonOperator, Comparable<Row[K]>] | readonly [K, ListOperator, readonly Comparable<Row[K]>[]];
71
+ }[Columns<Row>] | {
72
+ [K in StringColumn<Row>]: readonly [K, PatternOperator | 'contains', string];
73
+ }[StringColumn<Row>];
74
+ type Prefixed<C extends string, J> = {
75
+ [K in keyof J & string as `${C}.${K}`]: J[K];
76
+ };
77
+ export type NoColumns = Record<never, never>;
78
+ /**
79
+ * The only query surface a store callback sees: a frozen description, not a Kysely builder. There is nothing to
80
+ * execute, no raw SQL entry point, and no way to name a table other than this one and its declared children.
81
+ */
82
+ export declare class ScopedSelect<Row, Selected, Children> {
83
+ #private;
84
+ constructor(token: symbol, state: SelectState, shape: TableShape);
85
+ /** The columns to return; without it, every visible column. */
86
+ columns<const C extends Columns<Row>>(columns: readonly C[]): ScopedSelect<Row, Pick<Row, C>, Children>;
87
+ where<C extends Columns<Row>>(column: C, operator: ComparisonOperator, value: Comparable<Row[C]>): ScopedSelect<Row, Selected, Children>;
88
+ where<C extends Columns<Row>>(column: C, operator: ListOperator, values: readonly Comparable<Row[C]>[]): ScopedSelect<Row, Selected, Children>;
89
+ where<C extends StringColumn<Row>>(column: C, operator: PatternOperator, pattern: string): ScopedSelect<Row, Selected, Children>;
90
+ where<C extends NullableColumn<Row>>(column: C, operator: NullOperator, value: null): ScopedSelect<Row, Selected, Children>;
91
+ /** One OR group of conditions on this table's columns. */
92
+ whereAny(conditions: readonly ConditionTuple<Row>[]): ScopedSelect<Row, Selected, Children>;
93
+ /** A case-insensitive substring match in which `%` and `_` in the text match themselves. */
94
+ contains(column: StringColumn<Row>, text: string): ScopedSelect<Row, Selected, Children>;
95
+ orderBy(column: Columns<Row>, direction?: 'asc' | 'desc'): ScopedSelect<Row, Selected, Children>;
96
+ limit(limit: number): ScopedSelect<Row, Selected, Children>;
97
+ offset(offset: number): ScopedSelect<Row, Selected, Children>;
98
+ /** An inner join to a declared child; its columns come back prefixed (`note_comments.body`). */
99
+ join<C extends keyof Children & string, J>(child: C, build: (q: ScopedSelect<Children[C], Children[C], NoColumns>) => ScopedSelect<Children[C], J, NoColumns>): ScopedSelect<Row, Selected & Prefixed<C, J>, Children>;
100
+ }
101
+ /** A column the shape lets a query name; a hidden one is refused like a missing one. */
102
+ export declare function columnOf(shape: TableShape, name: unknown): string;
103
+ /** One `[column, operator, value]` condition over a caller's values, checked before anything reads it further. */
104
+ export declare function parseCondition(shape: TableShape, column: unknown, operator: unknown, value: unknown): Compare | Contains;
105
+ export declare function startSelect<Row, Children>(shape: TableShape): ScopedSelect<Row, Row, Children>;
106
+ /** The description a callback returned; it must derive from the one it received. */
107
+ export declare function finishSelect<Row, Children>(built: unknown, start: ScopedSelect<Row, Row, Children>): SelectState;
108
+ export declare function isPrimitive(value: unknown): value is Primitive;
109
+ export {};
@@ -0,0 +1,97 @@
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 file_scan: "a scan verdict outside the scan job's lease";
52
+ 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";
53
+ readonly on_conflict: "an insert's onConflict names no column of the table, a column the insert leaves empty, or another action";
54
+ readonly public_reach: "a public procedure updates, removes, locks or claims no row, and an insert's conflict writes nothing";
55
+ 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";
56
+ readonly file_scope: "a file is referenced from another top-level scope than the one its first reference set";
57
+ };
58
+ export type QueryRefusal = keyof typeof QUERY_REFUSALS;
59
+ /** A query description or an operation tree outside what the layer allows. */
60
+ export declare class QueryRefusedError extends Error {
61
+ name: string;
62
+ /** What a log may say; the message is for the author's own run and may quote a value. */
63
+ readonly reason: QueryRefusal;
64
+ constructor(reason: QueryRefusal, message: string);
65
+ }
66
+ /** A `ctx.sql` row its row schema refuses: the handler's bug. The message names the column, never its value. */
67
+ export declare class SqlRowError extends Error {
68
+ name: string;
69
+ }
70
+ /** A table policy that cannot be enforced as written. */
71
+ export declare class PolicyError extends Error {
72
+ name: string;
73
+ }
74
+ /** The addressed object does not exist in the capability's scope (HTTP 404). */
75
+ export declare class NotFoundError extends Error {
76
+ name: string;
77
+ }
78
+ /** The object exists, but its version is not the one the caller expected (HTTP 409). */
79
+ export declare class ConflictError extends Error {
80
+ name: string;
81
+ }
82
+ /** A job attempt wrote after it lost its lease (timed out, taken over or expired); nothing was written. */
83
+ export declare class LeaseLostError extends Error {
84
+ name: string;
85
+ }
86
+ /**
87
+ * A statement the database refused or could not run, as the layer reports it: its SQLSTATE only. The driver's message,
88
+ * detail and hint can quote the whole failing row, columns the capability may not read included, so none is kept.
89
+ */
90
+ export declare class DatabaseError extends Error {
91
+ name: string;
92
+ /** The five-character SQLSTATE, or `null` when the statement never reached the database (a lost connection). */
93
+ readonly sqlState: string | null;
94
+ constructor(sqlState: string | null);
95
+ }
96
+ /** The driver's failure as a `DatabaseError`; the driver's error object goes no further. */
97
+ export declare function databaseError(error: unknown): DatabaseError;