@fabrika/db 0.8.2 → 0.9.0-cloudflare.1
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 +691 -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 +193 -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,193 @@
|
|
|
1
|
+
import type { Database } from './database';
|
|
2
|
+
import type { Primitive } from './description';
|
|
3
|
+
import { type Tables } from './policy';
|
|
4
|
+
import type { TableProbe } from './store';
|
|
5
|
+
/** A value a test row holds in a column; `null` for a nullable column the fixture left empty. */
|
|
6
|
+
export type SampleValue = Primitive | null;
|
|
7
|
+
/** One seeded row and where it lies. */
|
|
8
|
+
export interface WorldRow {
|
|
9
|
+
/** The row's key: the dimension value of a dimension table's row, a child's own key. */
|
|
10
|
+
readonly key: string;
|
|
11
|
+
/** A child row's parent key; `null` for a dimension table. */
|
|
12
|
+
readonly parent: string | null;
|
|
13
|
+
/** The value of the grant dimension the row lies in (`IsolationWorlds.dimension`). */
|
|
14
|
+
readonly scope: string;
|
|
15
|
+
/**
|
|
16
|
+
* Every dimension value the row lies in: a dimension table's own first, then each container, innermost first. A
|
|
17
|
+
* share link's chain is this list.
|
|
18
|
+
*/
|
|
19
|
+
readonly chain: readonly {
|
|
20
|
+
readonly type: string;
|
|
21
|
+
readonly value: string;
|
|
22
|
+
}[];
|
|
23
|
+
/** The row's values of the policy's update columns, as seeded. */
|
|
24
|
+
readonly updates: Readonly<Record<string, SampleValue>>;
|
|
25
|
+
/**
|
|
26
|
+
* The distinct value written into every text column the fixture filled itself, in this row and in its containers.
|
|
27
|
+
* Each occurs in no other fixture, so one found in an answer to another scope is a leaked row.
|
|
28
|
+
*/
|
|
29
|
+
readonly markers: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
/** One seeded row of an application table, which lies in no dimension. */
|
|
32
|
+
export interface ApplicationRow {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
/** The row's values of the policy's update columns, as seeded. */
|
|
35
|
+
readonly updates: Readonly<Record<string, SampleValue>>;
|
|
36
|
+
/** The distinct value written into every text column the fixture filled itself. */
|
|
37
|
+
readonly markers: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
/** Rows of application tables seeded together for one case, and their removal once the case is over. */
|
|
40
|
+
export interface SeededApplicationRows {
|
|
41
|
+
/** Each table's row key, by table. */
|
|
42
|
+
readonly keys: Readonly<Record<string, string>>;
|
|
43
|
+
/**
|
|
44
|
+
* Deletes the rows, the rows their required foreign keys and file columns made the fixture insert, and every row
|
|
45
|
+
* that refers to one of them, however deep (what the case's handler wrote about them), in one transaction, so rows
|
|
46
|
+
* that refer to each other go together. A row already gone is skipped.
|
|
47
|
+
*/
|
|
48
|
+
remove(): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
/** A row seeded under another: its key, and the distinct value written into every text column the fixture filled. */
|
|
51
|
+
export interface SeededRow {
|
|
52
|
+
readonly key: string;
|
|
53
|
+
readonly markers: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What the isolation fixture cannot seed for a schema: a CHECK its values fail, a required foreign key to a scoped table
|
|
57
|
+
* outside the containment chain. It is a limit of the generator, named with the constraint or the column, never a
|
|
58
|
+
* failure of the handler or the policy a case is about.
|
|
59
|
+
*/
|
|
60
|
+
export declare class SeedLimitError extends Error {
|
|
61
|
+
readonly name = "SeedLimitError";
|
|
62
|
+
}
|
|
63
|
+
/** One row of a table in each of two containment chains that share no value. */
|
|
64
|
+
export interface IsolationWorlds {
|
|
65
|
+
/**
|
|
66
|
+
* The dimension a caller's grants are scoped by: a dimension table's container, or its own dimension when it has
|
|
67
|
+
* none; a child table's parent's.
|
|
68
|
+
*/
|
|
69
|
+
readonly dimension: string;
|
|
70
|
+
readonly own: WorldRow;
|
|
71
|
+
readonly other: WorldRow;
|
|
72
|
+
}
|
|
73
|
+
/** What a test kit seeds through, over its migrated schema. */
|
|
74
|
+
export interface TestSeeding {
|
|
75
|
+
/**
|
|
76
|
+
* Seeds one row of `table` in each of two fresh chains, with every container row a table owns. `values` (column →
|
|
77
|
+
* value) are written into both rows of `table`: the state an endpoint's precondition needs. A text column keeps its
|
|
78
|
+
* marker, so a seed of one, or of a column that places the row, throws.
|
|
79
|
+
*/
|
|
80
|
+
seedWorlds<DB, P>(tables: Tables<DB, P>, table: string, values?: Readonly<Record<string, SampleValue>>): Promise<IsolationWorlds>;
|
|
81
|
+
/**
|
|
82
|
+
* Seeds one row of the application table `table`: its key, version and creator stamped, `values` (column → value)
|
|
83
|
+
* as given, a text column too, every other column sampled. A value for a stamped column or a file column throws.
|
|
84
|
+
*/
|
|
85
|
+
seedRow<DB, P>(tables: Tables<DB, P>, table: string, values?: Readonly<Record<string, SampleValue>>): Promise<ApplicationRow>;
|
|
86
|
+
/**
|
|
87
|
+
* Seeds one row of each application table `rows` names (table → column → value), as `seedRow` does, in foreign-key
|
|
88
|
+
* order: a foreign key from one of them to another holds that row's key unless the values give it. Every table's
|
|
89
|
+
* values are checked before the first insert, and a table that fails removes the rows seeded before it. `remove`
|
|
90
|
+
* deletes them, the rows a required foreign key of theirs made it insert, and every row that refers to any of them,
|
|
91
|
+
* so the next case starts without them.
|
|
92
|
+
*/
|
|
93
|
+
seedApplicationRows<DB, P>(tables: Tables<DB, P>, rows: Readonly<Record<string, Readonly<Record<string, SampleValue>>>>): Promise<SeededApplicationRows>;
|
|
94
|
+
/**
|
|
95
|
+
* Seeds one row of `table`, a child or dimension table that lies in the object of `under` (a row `seedWorlds`
|
|
96
|
+
* seeded), placed in that row's chain: the row a fixture names by id, such as a note's comment. Answers its key and
|
|
97
|
+
* the markers it holds.
|
|
98
|
+
*/
|
|
99
|
+
seedUnder<DB, P>(tables: Tables<DB, P>, table: string, under: WorldRow): Promise<SeededRow>;
|
|
100
|
+
/** Values for the policy's insert columns of `table` that its schema accepts: every required one, sampled; a file column a seeded file. */
|
|
101
|
+
sampleInsert<DB, P>(tables: Tables<DB, P>, table: string): Promise<Readonly<Record<string, SampleValue>>>;
|
|
102
|
+
/**
|
|
103
|
+
* The object key of every file the fixture seeded into a file column, oldest first. Each stands for an empty object,
|
|
104
|
+
* which a test's object store holds when a handler serves or opens the file.
|
|
105
|
+
*/
|
|
106
|
+
seededObjects(): readonly string[];
|
|
107
|
+
}
|
|
108
|
+
export interface TestDatabase extends TestSeeding {
|
|
109
|
+
/** The connection `boot` binds the data layer to: on Postgres the runtime role on the migrated schema; on SQLite the database itself. */
|
|
110
|
+
readonly database: Database;
|
|
111
|
+
/** The migrated schema: `app_test_<hex>` on Postgres, `main` on SQLite. */
|
|
112
|
+
readonly schema: string;
|
|
113
|
+
/** Closes the connections and drops the schema and its roles (on SQLite: closes the in-memory database). */
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
/** A column of the migrated schema, as the seeding reads it. */
|
|
117
|
+
export interface SeedColumn {
|
|
118
|
+
readonly table: string;
|
|
119
|
+
readonly name: string;
|
|
120
|
+
/** The type as the database names it, for a message. */
|
|
121
|
+
readonly type: string;
|
|
122
|
+
readonly nullable: boolean;
|
|
123
|
+
/** The database fills it when an insert leaves it out: a default, an identity, a generated column. */
|
|
124
|
+
readonly defaulted: boolean;
|
|
125
|
+
/** A text type that is no enum: where nothing else fills it, the fixture writes a marker into it. */
|
|
126
|
+
readonly text: boolean;
|
|
127
|
+
/** A uuid type: a key or containment column of it holds a random uuid as its scope value. */
|
|
128
|
+
readonly uuid: boolean;
|
|
129
|
+
/** A sample of the column's type for a required column the test does not care about; a `SeedLimitError` for a type it cannot fill. */
|
|
130
|
+
readonly sample: () => Primitive;
|
|
131
|
+
}
|
|
132
|
+
/** A CHECK constraint of the schema, by the columns it names. */
|
|
133
|
+
export interface CheckConstraint {
|
|
134
|
+
readonly name: string;
|
|
135
|
+
readonly table: string;
|
|
136
|
+
readonly columns: readonly string[];
|
|
137
|
+
/** The values of a one-column `IN (…)` list the fixture writes in its place, first one first; empty for any other CHECK. */
|
|
138
|
+
readonly values: readonly string[];
|
|
139
|
+
}
|
|
140
|
+
/** A live, empty file of `kind`, as the framework's put leaves one once a reference claims it. */
|
|
141
|
+
export interface SeededFile {
|
|
142
|
+
readonly id: string;
|
|
143
|
+
readonly kind: string;
|
|
144
|
+
readonly objectKey: string;
|
|
145
|
+
readonly sha256: string;
|
|
146
|
+
readonly objectVersion: string;
|
|
147
|
+
}
|
|
148
|
+
/** A row the fixture inserted, found again by `match` (column → value): what a removal deletes. */
|
|
149
|
+
export interface InsertedRow {
|
|
150
|
+
readonly table: string;
|
|
151
|
+
readonly match: ReadonlyMap<string, SampleValue>;
|
|
152
|
+
}
|
|
153
|
+
/** How many rows one removal deletes, referring rows included, before it gives up. */
|
|
154
|
+
export declare const MAX_REMOVED_ROWS = 10000;
|
|
155
|
+
/** The seed limit of a removal that reaches more than `MAX_REMOVED_ROWS` rows. */
|
|
156
|
+
export declare function removalLimit(): SeedLimitError;
|
|
157
|
+
/** What a kit gives the seeding: its schema's columns and CHECK constraints, and the statements it runs. */
|
|
158
|
+
export interface SeedingBackend {
|
|
159
|
+
readonly columns: readonly SeedColumn[];
|
|
160
|
+
/** Every CHECK constraint of the schema's tables; read once, on the first column that needs them. */
|
|
161
|
+
checks(): Promise<readonly CheckConstraint[]>;
|
|
162
|
+
/** Inserts one row and answers it as stored; a CHECK or foreign key it fails is a `SeedLimitError` (`checkLimit`, `foreignKeyLimit`). */
|
|
163
|
+
insert(table: string, values: ReadonlyMap<string, SampleValue>): Promise<Readonly<Record<string, unknown>>>;
|
|
164
|
+
/** One row of `table`, of `columns`, or `undefined` when it has none. */
|
|
165
|
+
firstRow(table: string, columns: readonly string[]): Promise<Readonly<Record<string, unknown>> | undefined>;
|
|
166
|
+
insertFile(file: SeededFile): Promise<void>;
|
|
167
|
+
/**
|
|
168
|
+
* Deletes `rows` (each found by its `match`; one already gone is skipped) and every row that refers to one of them,
|
|
169
|
+
* however deep, in one transaction whose foreign keys are checked once all are gone, so rows that refer to each other
|
|
170
|
+
* go together. No write moves or adds a referring row between finding the rows and deleting them.
|
|
171
|
+
*/
|
|
172
|
+
remove(rows: readonly InsertedRow[]): Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
/** The seed limit of a CHECK a seeded row fails. */
|
|
175
|
+
export declare function checkLimit(constraint: string, table: string): SeedLimitError;
|
|
176
|
+
/** The seed limit of a foreign key a seeded row breaks. */
|
|
177
|
+
export declare function foreignKeyLimit(constraint: string, table: string): SeedLimitError;
|
|
178
|
+
/** The seed limit of a required column whose type the fixture has no sample of. */
|
|
179
|
+
export declare function typeLimit(table: string, column: string, type: string): SeedLimitError;
|
|
180
|
+
export declare function randomHex(): string;
|
|
181
|
+
/**
|
|
182
|
+
* The object version a seeded file was scanned at: whoever stocks its empty object (`seededObjects`) stores it under
|
|
183
|
+
* this version, so a file of a scanning kind is served as a live one is, by its scanned version.
|
|
184
|
+
*/
|
|
185
|
+
export declare const SEEDED_OBJECT_VERSION = "\"seeded\"";
|
|
186
|
+
/** The isolation fixtures over the schema `backend` describes. */
|
|
187
|
+
export declare function testSeeding(backend: SeedingBackend): TestSeeding;
|
|
188
|
+
/**
|
|
189
|
+
* The untyped handle over one table of the data layer `scoped(tables)` created; it reaches the database once `boot`
|
|
190
|
+
* has bound that layer. Every runtime check of a store applies: kind, dimension, action and the request's authority.
|
|
191
|
+
* `module` is the kit's, which refuses a production process.
|
|
192
|
+
*/
|
|
193
|
+
export declare function probeTable(module: string, layer: unknown, table: string): TableProbe;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TableProbe } from './store';
|
|
2
|
+
import { type TestDatabase } from './testing-shared';
|
|
3
|
+
export { ChildTable, defineChildStore, defineStore, DimensionTable, type TableProbe } from './store';
|
|
4
|
+
export { type ApplicationRow, type IsolationWorlds, type SampleValue, SEEDED_OBJECT_VERSION, type SeededApplicationRows, type SeededRow, SeedLimitError, type TestDatabase, type WorldRow, } from './testing-shared';
|
|
5
|
+
export interface TestDatabaseOptions {
|
|
6
|
+
/** The application's `db/migrations/` directory. */
|
|
7
|
+
readonly migrations: string | URL;
|
|
8
|
+
/** The test file. `@fabrika/db/testing` names it when it skips; this kit never skips. */
|
|
9
|
+
readonly label: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A fresh in-memory database with the framework's and the application's migrations applied by the runner a Durable
|
|
13
|
+
* Object boots with (`@fabrika/db/sqlite-migrate`), opened with its engine (`@fabrika/db/sqlite`). Never `null`: it
|
|
14
|
+
* needs no backend, so it never skips; its type is still assignable to `@fabrika/db/testing`'s, so a generated test
|
|
15
|
+
* that checks for `null` compiles against both kits.
|
|
16
|
+
*/
|
|
17
|
+
export declare function testDatabase(options: TestDatabaseOptions): Promise<TestDatabase>;
|
|
18
|
+
/**
|
|
19
|
+
* The untyped handle over one table of the data layer `scoped(tables)` created; it reaches the database once `boot`
|
|
20
|
+
* has bound that layer. Every runtime check of a store applies: kind, dimension, action and the request's authority.
|
|
21
|
+
*/
|
|
22
|
+
export declare function tableProbe(layer: unknown, table: string): TableProbe;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { TableProbe } from './store';
|
|
2
|
+
import { type TestDatabase } from './testing-shared';
|
|
3
|
+
export { ChildTable, defineChildStore, defineStore, DimensionTable, type TableProbe } from './store';
|
|
4
|
+
export { type ApplicationRow, type IsolationWorlds, type SampleValue, SEEDED_OBJECT_VERSION, type SeededApplicationRows, type SeededRow, SeedLimitError, type TestDatabase, type WorldRow, } from './testing-shared';
|
|
5
|
+
export declare const TEST_POSTGRES_URL = "FABRIKA_TEST_POSTGRES_URL";
|
|
6
|
+
/** Set to `1` by the platform build: a missing test database is then a failure, not a skip. */
|
|
7
|
+
export declare const TEST_POSTGRES_REQUIRED = "FABRIKA_TEST_POSTGRES_REQUIRED";
|
|
8
|
+
export interface TestDatabaseOptions {
|
|
9
|
+
/** The application's `db/migrations/` directory. */
|
|
10
|
+
readonly migrations: string | URL;
|
|
11
|
+
/** The test file, for the skip message. */
|
|
12
|
+
readonly label: string;
|
|
13
|
+
/**
|
|
14
|
+
* How long to wait for free connection slots, in ms, before the file fails saying so; `DEFAULT_CONNECTION_WAIT_MS` by
|
|
15
|
+
* default. Once one file of a process gave up, every later one fails at once.
|
|
16
|
+
*/
|
|
17
|
+
readonly connectionWaitMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A fresh schema with the framework's and the application's migrations applied by the real runner (`@fabrika/platform-node`) as its
|
|
21
|
+
* migration role, connected and seeded as its runtime role, or `null` after the standard skip message when
|
|
22
|
+
* `FABRIKA_TEST_POSTGRES_URL` is unset. That URL is the provisioning credential, which must be able to create roles.
|
|
23
|
+
* With `FABRIKA_TEST_POSTGRES_REQUIRED=1` a missing URL throws instead. The URL is a credential: no message names it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function testDatabase(options: TestDatabaseOptions): Promise<TestDatabase | null>;
|
|
26
|
+
/**
|
|
27
|
+
* The connections a test database holds once open: the provisioning connection, the seeder and the runtime role's.
|
|
28
|
+
* Migrating and reading the catalog each borrow one more for a moment, and so does the drop once the others ended.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TEST_DATABASE_CONNECTIONS: number;
|
|
31
|
+
/**
|
|
32
|
+
* The untyped handle over one table of the data layer `scoped(tables)` created; it reaches the database once `boot`
|
|
33
|
+
* has bound that layer. Every runtime check of a store applies: kind, dimension, action and the request's authority.
|
|
34
|
+
*/
|
|
35
|
+
export declare function tableProbe(layer: unknown, table: string): TableProbe;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { ColumnList, ColumnPolicies, FilePolicies, RecordActions, ReferencePolicy, TableActions, Tables, VersionColumn } from './policy';
|
|
2
|
+
declare const databaseType: unique symbol;
|
|
3
|
+
/**
|
|
4
|
+
* The generated column list (`src/db/schema.generated.ts`), which also carries the schema's row types, so that
|
|
5
|
+
* `defineTables(vocabulary, columns)` takes no type argument. The brand is type-only: no value has the property.
|
|
6
|
+
*/
|
|
7
|
+
export type TypedColumnList<DB> = ColumnList<DB> & {
|
|
8
|
+
readonly [databaseType]?: DB;
|
|
9
|
+
};
|
|
10
|
+
/** The row types a typed column list carries; `never` for a list without them. */
|
|
11
|
+
export type ColumnTypes<C> = typeof databaseType extends keyof C ? C extends {
|
|
12
|
+
readonly [databaseType]?: infer DB;
|
|
13
|
+
} ? Exclude<DB, undefined> : never : never;
|
|
14
|
+
/**
|
|
15
|
+
* What `defineTables(vocabulary, columns)` reads of `@fabrika/app/contract`'s `defineVocabulary` result: the action
|
|
16
|
+
* and dimension names, and each dimension's container. `@fabrika/db` does not import `@fabrika/app`.
|
|
17
|
+
*/
|
|
18
|
+
export interface TableVocabulary {
|
|
19
|
+
readonly actions: {
|
|
20
|
+
readonly [action: string]: string;
|
|
21
|
+
};
|
|
22
|
+
readonly dimensions: {
|
|
23
|
+
readonly [dimension: string]: {
|
|
24
|
+
readonly in?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export type VocabularyAction<V> = V extends {
|
|
29
|
+
readonly actions: infer A;
|
|
30
|
+
} ? keyof A & string : never;
|
|
31
|
+
export type VocabularyDimension<V> = V extends {
|
|
32
|
+
readonly dimensions: infer D;
|
|
33
|
+
} ? keyof D & string : never;
|
|
34
|
+
/** The dimension `D` lies in, by the vocabulary; `never` for a top-level one. */
|
|
35
|
+
export type VocabularyContainerOf<V, D> = V extends {
|
|
36
|
+
readonly dimensions: infer Ds;
|
|
37
|
+
} ? D extends keyof Ds ? Ds[D] extends {
|
|
38
|
+
readonly in: infer C extends string;
|
|
39
|
+
} ? C : never : never : never;
|
|
40
|
+
/** A table without an `id` column names its key. */
|
|
41
|
+
interface NamesItsKey<Col extends string> {
|
|
42
|
+
readonly key: Col;
|
|
43
|
+
}
|
|
44
|
+
/** `key` defaults to `id`, so it is required only where the table has no `id` column. */
|
|
45
|
+
type KeyField<Col extends string> = 'id' extends Col ? {
|
|
46
|
+
readonly key?: Col;
|
|
47
|
+
} : NamesItsKey<Col>;
|
|
48
|
+
/** A contained dimension table's container column; `dimension`, when stated, must be the vocabulary's. */
|
|
49
|
+
export interface VocabularyContainer<C extends string, Col extends string> {
|
|
50
|
+
readonly column: Col;
|
|
51
|
+
readonly dimension?: C;
|
|
52
|
+
}
|
|
53
|
+
/** A contained table off the `<container>_id` convention names its container column. */
|
|
54
|
+
interface NamesItsContainerColumn<C extends string, Col extends string> {
|
|
55
|
+
readonly in: VocabularyContainer<C, Col>;
|
|
56
|
+
}
|
|
57
|
+
/** The container column defaults to `<container>_id`, so `in` is required only off that convention. */
|
|
58
|
+
type ContainerField<C extends string, Col extends string> = [C] extends [never] ? {
|
|
59
|
+
readonly in?: never;
|
|
60
|
+
} : `${C}_id` extends Col ? {
|
|
61
|
+
readonly in?: VocabularyContainer<C, Col>;
|
|
62
|
+
} : NamesItsContainerColumn<C, Col>;
|
|
63
|
+
/** The scope of a table that owns dimension `D`, which the vocabulary puts in `C` (`never`: in no container). */
|
|
64
|
+
type DimensionTableScope<D extends string, C extends string, Col extends string> = {
|
|
65
|
+
readonly dimension: D;
|
|
66
|
+
} & KeyField<Col> & ContainerField<C, Col>;
|
|
67
|
+
/** A dimension table's scope: the dimension it owns; its container is the vocabulary's. */
|
|
68
|
+
export type VocabularyDimensionScope<V, D extends string, Col extends string> = DimensionTableScope<D, VocabularyContainerOf<V, D>, Col>;
|
|
69
|
+
/** A child table's scope: `column` defaults to `<parent dimension>_id`, and the inherited chain is the parent's. */
|
|
70
|
+
export type VocabularyChildScope<Table extends string, Col extends string> = {
|
|
71
|
+
readonly child: Table;
|
|
72
|
+
readonly column?: Col;
|
|
73
|
+
readonly inherit?: readonly Col[];
|
|
74
|
+
} & KeyField<Col>;
|
|
75
|
+
interface WritableFields<Col extends string, A extends string, Ver extends Col, Acts = TableActions<A>> {
|
|
76
|
+
readonly actions: Acts;
|
|
77
|
+
readonly insert?: readonly Col[];
|
|
78
|
+
readonly update?: readonly Col[];
|
|
79
|
+
readonly version?: Ver;
|
|
80
|
+
readonly createdBy?: Col;
|
|
81
|
+
readonly columns?: ColumnPolicies<Col, A>;
|
|
82
|
+
/** The file columns, each with the file kind it holds (`c.files`): a `uuid` foreign key to `fabrika_files`. */
|
|
83
|
+
readonly files?: FilePolicies<Col>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The scope of the dimension the policy states (`S`, its scope as written); for any other name, the dimension alone,
|
|
87
|
+
* so that a misspelt one is reported against the vocabulary's dimension names rather than against one of them. With
|
|
88
|
+
* no scope as written (an annotated map), every dimension's scope.
|
|
89
|
+
*/
|
|
90
|
+
type DimensionScopes<V, Col extends string, S> = unknown extends S ? {
|
|
91
|
+
[D in VocabularyDimension<V>]: VocabularyDimensionScope<V, D, Col>;
|
|
92
|
+
}[VocabularyDimension<V>] : S extends {
|
|
93
|
+
readonly dimension: infer D extends VocabularyDimension<V>;
|
|
94
|
+
} ? DimensionTableScope<D, VocabularyContainerOf<V, D>, Col> : {
|
|
95
|
+
readonly dimension: VocabularyDimension<V>;
|
|
96
|
+
};
|
|
97
|
+
type StatedDimensionOf<P, T> = T extends keyof P ? P[T] extends {
|
|
98
|
+
readonly scope: {
|
|
99
|
+
readonly dimension: infer D extends string;
|
|
100
|
+
};
|
|
101
|
+
} ? D : never : never;
|
|
102
|
+
/** A child off the `<parent dimension>_id` convention names its column for the parent. */
|
|
103
|
+
interface NamesItsParentColumn<Col extends string> {
|
|
104
|
+
readonly column: Col;
|
|
105
|
+
}
|
|
106
|
+
/** The column for the parent defaults to `<ParentDimension>_id`, so `column` is required only off that convention. */
|
|
107
|
+
type ParentColumnField<ParentDimension extends string, Col extends string> = `${ParentDimension}_id` extends Col ? {
|
|
108
|
+
readonly column?: Col;
|
|
109
|
+
} : NamesItsParentColumn<Col>;
|
|
110
|
+
/** The scope of a child of `Parent`, a table that owns `ParentDimension`. */
|
|
111
|
+
type ChildTableScope<Parent extends string, ParentDimension extends string, Col extends string> = {
|
|
112
|
+
readonly child: Parent;
|
|
113
|
+
readonly inherit?: readonly Col[];
|
|
114
|
+
} & KeyField<Col> & ParentColumnField<ParentDimension, Col>;
|
|
115
|
+
/**
|
|
116
|
+
* The scope of the child as written, by the parent it states (`P`, the policies as written): its parent is a table
|
|
117
|
+
* that states a dimension scope, and its column defaults to that table's dimension. With no policies as written, any
|
|
118
|
+
* table.
|
|
119
|
+
*/
|
|
120
|
+
type ChildScopes<Table extends string, Col extends string, S, P> = unknown extends P ? VocabularyChildScope<Table, Col> : S extends {
|
|
121
|
+
readonly child: infer Parent extends string;
|
|
122
|
+
} ? [StatedDimensionOf<P, Parent>] extends [never] ? {
|
|
123
|
+
readonly child: {
|
|
124
|
+
[T in keyof P]: P[T] extends {
|
|
125
|
+
readonly scope: {
|
|
126
|
+
readonly dimension: string;
|
|
127
|
+
};
|
|
128
|
+
} ? T : never;
|
|
129
|
+
}[keyof P] & string;
|
|
130
|
+
} : ChildTableScope<Parent, StatedDimensionOf<P, Parent>, Col> : VocabularyChildScope<Table, Col>;
|
|
131
|
+
type AuthoredDimensionPolicy<Col extends string, A extends string, V, Ver extends Col, S> = WritableFields<Col, A, Ver, RecordActions<A>> & {
|
|
132
|
+
readonly scope: DimensionScopes<V, Col, S>;
|
|
133
|
+
};
|
|
134
|
+
type AuthoredChildPolicy<Table extends string, Col extends string, A extends string, Ver extends Col, S, P> = WritableFields<Col, A, Ver, RecordActions<A>> & {
|
|
135
|
+
readonly scope: ChildScopes<Table, Col, S, P>;
|
|
136
|
+
};
|
|
137
|
+
/** An application table's scope: why its rows belong to no dimension; `key` defaults to `id`. */
|
|
138
|
+
type AuthoredApplicationPolicy<Col extends string, A extends string, Ver extends Col> = WritableFields<Col, A, Ver> & {
|
|
139
|
+
readonly scope: {
|
|
140
|
+
readonly app: string;
|
|
141
|
+
} & KeyField<Col>;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* One table's policy as an application writes it under a vocabulary. `S` is its scope as written: its mode picks the
|
|
145
|
+
* one policy form a mistake is reported against, rather than a union of all four. `P` is every policy as written.
|
|
146
|
+
*/
|
|
147
|
+
export type VocabularyTablePolicy<Table extends string, Col extends string, A extends string, V, Ver extends Col, S = unknown, P = unknown> = S extends {
|
|
148
|
+
readonly dimension: unknown;
|
|
149
|
+
} ? AuthoredDimensionPolicy<Col, A, V, Ver, S> : S extends {
|
|
150
|
+
readonly child: unknown;
|
|
151
|
+
} ? AuthoredChildPolicy<Table, Col, A, Ver, S, P> : S extends {
|
|
152
|
+
readonly app: unknown;
|
|
153
|
+
} ? AuthoredApplicationPolicy<Col, A, Ver> : S extends {
|
|
154
|
+
readonly reference: unknown;
|
|
155
|
+
} ? ReferencePolicy<Col, A> : AuthoredDimensionPolicy<Col, A, V, Ver, S> | AuthoredChildPolicy<Table, Col, A, Ver, S, P> | AuthoredApplicationPolicy<Col, A, Ver> | ReferencePolicy<Col, A>;
|
|
156
|
+
type StatedScope<P, T> = T extends keyof P ? P[T] extends {
|
|
157
|
+
readonly scope: infer S;
|
|
158
|
+
} ? S : unknown : unknown;
|
|
159
|
+
/** The policy of table `T` with nothing stated: every form, every dimension's scope, any parent. */
|
|
160
|
+
type AnyTablePolicy<DB, V, T extends keyof DB & string> = VocabularyTablePolicy<keyof DB & string, keyof DB[T] & string, VocabularyAction<V>, V, VersionColumn<DB[T]>>;
|
|
161
|
+
/**
|
|
162
|
+
* Every table of the schema, and no other, maps to a policy; a misspelt action or dimension fails at its own line.
|
|
163
|
+
* `P` is the policies as written, which picks each table's form by the scope it states; a policy written under this
|
|
164
|
+
* map's own annotation states nothing narrower, so it is checked against every form.
|
|
165
|
+
*/
|
|
166
|
+
export type VocabularyPolicyMap<DB, V, P = unknown> = {
|
|
167
|
+
readonly [T in keyof DB & string]: AnyTablePolicy<DB, V, T> extends (T extends keyof P ? P[T] : unknown) ? AnyTablePolicy<DB, V, T> : VocabularyTablePolicy<keyof DB & string, keyof DB[T] & string, VocabularyAction<V>, V, VersionColumn<DB[T]>, StatedScope<P, T>, P>;
|
|
168
|
+
};
|
|
169
|
+
type Flat<T> = {
|
|
170
|
+
[K in keyof T]: T[K];
|
|
171
|
+
};
|
|
172
|
+
type KeyOf<S> = S extends {
|
|
173
|
+
readonly key: infer K extends string;
|
|
174
|
+
} ? K : 'id';
|
|
175
|
+
type ContainerColumn<S, C extends string> = S extends {
|
|
176
|
+
readonly in: {
|
|
177
|
+
readonly column: infer Col extends string;
|
|
178
|
+
};
|
|
179
|
+
} ? Col : `${C}_id`;
|
|
180
|
+
type ParentDimension<P, T> = T extends keyof P ? P[T] extends {
|
|
181
|
+
readonly scope: {
|
|
182
|
+
readonly dimension: infer D extends string;
|
|
183
|
+
};
|
|
184
|
+
} ? D : never : never;
|
|
185
|
+
type NormalScope<S, V, P> = S extends {
|
|
186
|
+
readonly dimension: infer D extends string;
|
|
187
|
+
} ? Flat<{
|
|
188
|
+
readonly dimension: D;
|
|
189
|
+
readonly key: KeyOf<S>;
|
|
190
|
+
} & ([VocabularyContainerOf<V, D>] extends [never] ? unknown : {
|
|
191
|
+
readonly in: {
|
|
192
|
+
readonly dimension: VocabularyContainerOf<V, D>;
|
|
193
|
+
readonly column: ContainerColumn<S, VocabularyContainerOf<V, D>>;
|
|
194
|
+
};
|
|
195
|
+
})> : S extends {
|
|
196
|
+
readonly child: infer T extends string;
|
|
197
|
+
} ? {
|
|
198
|
+
readonly child: T;
|
|
199
|
+
readonly key: KeyOf<S>;
|
|
200
|
+
readonly column: S extends {
|
|
201
|
+
readonly column: infer C extends string;
|
|
202
|
+
} ? C : `${ParentDimension<P, T>}_id`;
|
|
203
|
+
readonly inherit: readonly string[];
|
|
204
|
+
} : S extends {
|
|
205
|
+
readonly app: infer R extends string;
|
|
206
|
+
} ? {
|
|
207
|
+
readonly app: R;
|
|
208
|
+
readonly key: KeyOf<S>;
|
|
209
|
+
} : S;
|
|
210
|
+
/**
|
|
211
|
+
* The policies as the layer models them: every dimension table's key and container, and every child's key and
|
|
212
|
+
* column, stated as the vocabulary and the conventions derive them.
|
|
213
|
+
*/
|
|
214
|
+
export type NormalizedPolicies<P, V> = {
|
|
215
|
+
readonly [T in keyof P]: P[T] extends {
|
|
216
|
+
readonly scope: infer S;
|
|
217
|
+
} ? Flat<Omit<P[T], 'scope'> & {
|
|
218
|
+
readonly scope: NormalScope<S, V, P>;
|
|
219
|
+
}> : P[T];
|
|
220
|
+
};
|
|
221
|
+
/** Takes the policies of exactly the tables of the schema, written in the vocabulary form. */
|
|
222
|
+
export type DefineVocabularyPolicies<DB, V> = <const P extends VocabularyPolicyMap<DB, V, P>>(policies: P & {
|
|
223
|
+
readonly [K in Exclude<keyof P, keyof DB>]: never;
|
|
224
|
+
}) => Tables<DB, NormalizedPolicies<P, V>>;
|
|
225
|
+
export {};
|