@ultimat3/entity 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +126 -40
- package/package.json +4 -3
- package/src/column.d.ts +28 -0
- package/src/column.d.ts.map +1 -0
- package/src/column.js +75 -0
- package/src/column.js.map +1 -0
- package/src/column.ts +134 -0
- package/src/columns.d.ts +39 -0
- package/src/columns.d.ts.map +1 -0
- package/src/columns.js +136 -0
- package/src/columns.js.map +1 -0
- package/src/columns.ts +164 -217
- package/src/cursor.ts +187 -0
- package/src/database.d.ts +21 -0
- package/src/database.d.ts.map +1 -0
- package/src/database.js +38 -0
- package/src/database.js.map +1 -0
- package/src/database.ts +62 -0
- package/src/describe.d.ts +16 -0
- package/src/describe.d.ts.map +1 -0
- package/src/describe.js +79 -0
- package/src/describe.js.map +1 -0
- package/src/describe.ts +106 -0
- package/src/entity.d.ts +58 -0
- package/src/entity.d.ts.map +1 -0
- package/src/entity.js +160 -0
- package/src/entity.js.map +1 -0
- package/src/entity.ts +246 -99
- package/src/errors.d.ts +18 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +59 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +27 -6
- package/src/expr.d.ts +41 -0
- package/src/expr.d.ts.map +1 -0
- package/src/expr.js +94 -0
- package/src/expr.js.map +1 -0
- package/src/expr.ts +231 -0
- package/src/index.d.ts +23 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +12 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +36 -20
- package/src/invariants.d.ts +36 -0
- package/src/invariants.d.ts.map +1 -0
- package/src/invariants.js +53 -0
- package/src/invariants.js.map +1 -0
- package/src/invariants.ts +53 -49
- package/src/pg-driver.ts +154 -0
- package/src/pg-row.ts +110 -0
- package/src/pg-sql.ts +162 -0
- package/src/plan.ts +82 -0
- package/src/query.d.ts +30 -0
- package/src/query.d.ts.map +1 -0
- package/src/query.js +74 -0
- package/src/query.js.map +1 -0
- package/src/query.ts +144 -0
- package/src/registry.d.ts +45 -0
- package/src/registry.d.ts.map +1 -0
- package/src/registry.js +26 -0
- package/src/registry.js.map +1 -0
- package/src/registry.ts +8 -5
- package/src/repo.d.ts +54 -0
- package/src/repo.d.ts.map +1 -0
- package/src/repo.js +203 -0
- package/src/repo.js.map +1 -0
- package/src/repo.ts +0 -0
- package/src/seed.d.ts +20 -0
- package/src/seed.d.ts.map +1 -0
- package/src/seed.js +43 -0
- package/src/seed.js.map +1 -0
- package/src/seed.ts +69 -0
- package/src/tenancy.d.ts +41 -0
- package/src/tenancy.d.ts.map +1 -0
- package/src/tenancy.js +57 -0
- package/src/tenancy.js.map +1 -0
- package/src/tenancy.ts +68 -19
- package/src/types.d.ts +99 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +8 -0
- package/src/types.js.map +1 -0
- package/src/types.ts +94 -44
- package/src/view.ts +97 -0
package/src/errors.ts
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
// The entity layer's stable error codes. Each factory produces the exact command
|
|
2
2
|
// that fixes the situation — `X_DB_DRIFT` is the flagship: it names the table, the
|
|
3
3
|
// column and the generator invocation.
|
|
4
|
-
import { UltimateError } from '@ultimat3/core';
|
|
4
|
+
import { registerErrorCodes, UltimateError } from '@ultimat3/core';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/** Codes this package declares and owns. */
|
|
7
|
+
export const ENTITY_OWNED_ERROR_CODES = [
|
|
7
8
|
'X_ENTITY_DUPLICATE',
|
|
8
9
|
'X_INVARIANT_VIOLATED',
|
|
9
10
|
'X_TENANCY_UNSCOPED',
|
|
10
|
-
'X_DB_DRIFT',
|
|
11
11
|
'X_NOT_FOUND',
|
|
12
12
|
] as const;
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* `X_DB_DRIFT` is `@ultimat3/db`'s — drift is a fact about migrations, and this package imports db
|
|
16
|
+
* rather than the other way round. `dbDrift()` below throws it; nothing here titles it, because a
|
|
17
|
+
* second copy of the title is what lets the two packages disagree about what the code means.
|
|
18
|
+
*/
|
|
19
|
+
export const ENTITY_BORROWED_ERROR_CODES = ['X_DB_DRIFT'] as const;
|
|
20
|
+
|
|
21
|
+
/** Every code entity can throw: the ones it owns plus the one it borrows. */
|
|
22
|
+
export const ENTITY_ERROR_CODES = [
|
|
23
|
+
...ENTITY_OWNED_ERROR_CODES,
|
|
24
|
+
...ENTITY_BORROWED_ERROR_CODES,
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type EntityOwnedErrorCode = (typeof ENTITY_OWNED_ERROR_CODES)[number];
|
|
14
28
|
export type EntityErrorCode = (typeof ENTITY_ERROR_CODES)[number];
|
|
15
29
|
|
|
16
|
-
export const ENTITY_ERROR_TITLES: Readonly<Record<
|
|
30
|
+
export const ENTITY_ERROR_TITLES: Readonly<Record<EntityOwnedErrorCode, string>> = {
|
|
17
31
|
X_ENTITY_DUPLICATE: 'two entities claim the same name',
|
|
18
32
|
X_INVARIANT_VIOLATED: 'a domain invariant rejected this row',
|
|
19
33
|
X_TENANCY_UNSCOPED: 'a tenant-scoped query has no org predicate',
|
|
20
|
-
X_DB_DRIFT: 'schema differs from migrations',
|
|
21
34
|
X_NOT_FOUND: 'no row for that id',
|
|
22
35
|
};
|
|
23
36
|
|
|
37
|
+
// Registered at module load, unconditionally, in one call. Without this the registry humanises the
|
|
38
|
+
// code and every surface renders a title this package never wrote; with a presence guard, a second
|
|
39
|
+
// package claiming one of these codes would silently win instead of throwing X_ERROR_CODE_DUPLICATE.
|
|
40
|
+
registerErrorCodes(
|
|
41
|
+
Object.fromEntries(Object.entries(ENTITY_ERROR_TITLES).map(([code, title]) => [code, { title }])),
|
|
42
|
+
);
|
|
43
|
+
|
|
24
44
|
export class EntityError extends UltimateError {
|
|
45
|
+
override readonly name = 'EntityError';
|
|
46
|
+
|
|
25
47
|
constructor(init: { code: EntityErrorCode; cause: string; fix: string }) {
|
|
26
48
|
super({
|
|
27
49
|
code: init.code,
|
|
@@ -29,7 +51,6 @@ export class EntityError extends UltimateError {
|
|
|
29
51
|
fix: init.fix,
|
|
30
52
|
docs: `https://ultimate.dev/errors/${init.code}`,
|
|
31
53
|
});
|
|
32
|
-
this.name = 'EntityError';
|
|
33
54
|
}
|
|
34
55
|
}
|
|
35
56
|
|
package/src/expr.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Row = Readonly<Record<string, unknown>>;
|
|
2
|
+
/** Property path -> physical column name. */
|
|
3
|
+
export type Resolve = (path: readonly string[]) => string;
|
|
4
|
+
export interface Expr {
|
|
5
|
+
readonly kind: 'check' | 'unique';
|
|
6
|
+
readonly paths: readonly (readonly string[])[];
|
|
7
|
+
readonly message: string;
|
|
8
|
+
/** `null` when the rule is a JS predicate the database cannot be told about. */
|
|
9
|
+
toSql(resolve: Resolve): string | null;
|
|
10
|
+
holds(row: Row): boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ColumnExpr {
|
|
13
|
+
/** `btrim(...)` in SQL, `.trim()` in the app — the same rule, both sides. */
|
|
14
|
+
trimmed(): ColumnExpr;
|
|
15
|
+
minLength(length: number): Expr;
|
|
16
|
+
contains(value: string): Expr;
|
|
17
|
+
/** A `RegExp` reaches the database; a function is app-only. */
|
|
18
|
+
matches(pattern: RegExp | ((value: string) => boolean)): Expr;
|
|
19
|
+
atLeast(value: number | bigint): Expr;
|
|
20
|
+
eq(value: string | number | boolean | bigint | ColumnExpr): Expr;
|
|
21
|
+
isTrue(): Expr;
|
|
22
|
+
/** Money is two physical columns; these are how a rule names one of them. */
|
|
23
|
+
readonly minor: ColumnExpr;
|
|
24
|
+
readonly currency: ColumnExpr;
|
|
25
|
+
}
|
|
26
|
+
type RowPredicate = (...values: never[]) => boolean;
|
|
27
|
+
export type InvariantColumns = {
|
|
28
|
+
readonly [column: string]: ColumnExpr;
|
|
29
|
+
} & {
|
|
30
|
+
/** Decided by the database — a single row cannot see a duplicate. */
|
|
31
|
+
unique(columns: readonly string[]): Expr;
|
|
32
|
+
/** Lifts a domain predicate over several columns. App-only by construction. */
|
|
33
|
+
satisfies(predicate: RowPredicate, columns: readonly string[]): Expr;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The `c` an invariant is written against. A Proxy so a typo in a column name is a
|
|
37
|
+
* declaration-time error naming the columns that do exist, not `undefined is not a function`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const invariantColumns: (entity: string, properties: readonly string[]) => InvariantColumns;
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=expr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expr.d.ts","sourceRoot":"","sources":["expr.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEpD,6CAA6C;AAC7C,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAE1D,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC;IACvC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC;CAC1B;AASD,MAAM,WAAW,UAAU;IACzB,6EAA6E;IAC7E,OAAO,IAAI,UAAU,CAAC;IACtB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,+DAA+D;IAC/D,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IACjE,MAAM,IAAI,IAAI,CAAC;IACf,6EAA6E;IAC7E,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;CAC/B;AAED,KAAK,YAAY,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CACvC,GAAG;IACF,qEAAqE;IACrE,MAAM,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACzC,+EAA+E;IAC/E,SAAS,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;CACtE,CAAC;AAqJF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,WACnB,MAAM,cACF,SAAS,MAAM,EAAE,KAC5B,gBAiBF,CAAC"}
|
package/src/expr.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The invariant expression language. One declaration compiles to two enforcement points: a
|
|
2
|
+
// predicate the app runs on every write, and the SQL predicate the migration emits.
|
|
3
|
+
//
|
|
4
|
+
// Expressions are written against property keys (`c.likeCount`) and resolved to physical names
|
|
5
|
+
// (`like_count`) by `entity()`, because the author never writes a physical name.
|
|
6
|
+
//
|
|
7
|
+
// A JS predicate (`matches(isValidSlug)`, `satisfies(fn, [...])`) cannot be translated to SQL.
|
|
8
|
+
// It still runs in the app, and reports `sql: null` so `x verify` can warn that the database
|
|
9
|
+
// does not know this rule — silently pretending it reached Postgres would be worse.
|
|
10
|
+
import { invariantViolated } from './errors';
|
|
11
|
+
const terms = new WeakMap();
|
|
12
|
+
const walk = (row, path) => path.reduce((value, key) => (typeof value === 'object' && value !== null ? value[key] : undefined), row);
|
|
13
|
+
const literal = (value) => typeof value === 'string' ? `'${value.replaceAll("'", "''")}'` : String(value);
|
|
14
|
+
const check = (paths, message, sql, holds) => ({ kind: 'check', paths, message, toSql: sql, holds });
|
|
15
|
+
const isColumnExpr = (value) => typeof value === 'object' && value !== null && terms.has(value);
|
|
16
|
+
const expr = (term) => {
|
|
17
|
+
const one = (message, sql, holds) => check([term.path], message, sql, (row) => holds(term.read(row)));
|
|
18
|
+
const built = {
|
|
19
|
+
trimmed: () => expr({
|
|
20
|
+
path: term.path,
|
|
21
|
+
label: `trimmed ${term.label}`,
|
|
22
|
+
sql: (resolve) => `btrim(${term.sql(resolve)})`,
|
|
23
|
+
read: (row) => {
|
|
24
|
+
const value = term.read(row);
|
|
25
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
minLength: (length) => one(`${term.label} must be at least ${length} character${length === 1 ? '' : 's'}`, (resolve) => `char_length(${term.sql(resolve)}) >= ${length}`, (value) => typeof value === 'string' && value.length >= length),
|
|
29
|
+
contains: (value) => one(`${term.label} must contain ${literal(value)}`, (resolve) => `position(${literal(value)} in ${term.sql(resolve)}) > 0`, (actual) => typeof actual === 'string' && actual.includes(value)),
|
|
30
|
+
matches: (pattern) => one(`${term.label} must match ${pattern instanceof RegExp ? pattern.source : pattern.name || 'the rule'}`, (resolve) => pattern instanceof RegExp ? `${term.sql(resolve)} ~ ${literal(pattern.source)}` : null, (value) => typeof value === 'string' &&
|
|
31
|
+
(pattern instanceof RegExp ? pattern.test(value) : pattern(value))),
|
|
32
|
+
atLeast: (bound) => one(`${term.label} must be at least ${bound}`, (resolve) => `${term.sql(resolve)} >= ${bound}`, (value) => (typeof value === 'number' || typeof value === 'bigint') && value >= bound),
|
|
33
|
+
eq: (other) => isColumnExpr(other)
|
|
34
|
+
? sameAs(term, other)
|
|
35
|
+
: one(`${term.label} must equal ${literal(other)}`, (resolve) => `${term.sql(resolve)} = ${literal(other)}`, (value) => value === other),
|
|
36
|
+
isTrue: () => one(`${term.label} must be true`, (resolve) => term.sql(resolve), (value) => value === true),
|
|
37
|
+
get minor() {
|
|
38
|
+
return part(term, 'minor');
|
|
39
|
+
},
|
|
40
|
+
get currency() {
|
|
41
|
+
return part(term, 'currency');
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
terms.set(built, term);
|
|
45
|
+
return built;
|
|
46
|
+
};
|
|
47
|
+
const part = (term, key) => expr({
|
|
48
|
+
path: [...term.path, key],
|
|
49
|
+
label: `${term.label}.${key}`,
|
|
50
|
+
sql: (resolve) => resolve([...term.path, key]),
|
|
51
|
+
read: (row) => walk(row, [...term.path, key]),
|
|
52
|
+
});
|
|
53
|
+
const sameAs = (left, other) => {
|
|
54
|
+
const right = terms.get(other);
|
|
55
|
+
if (right === undefined)
|
|
56
|
+
throw invariantViolated('invariant', 'eq', 'not a column expression');
|
|
57
|
+
return check([left.path, right.path], `${left.label} must equal ${right.label}`, (resolve) => `${left.sql(resolve)} = ${right.sql(resolve)}`, (row) => left.read(row) === right.read(row));
|
|
58
|
+
};
|
|
59
|
+
const columnTerm = (property) => ({
|
|
60
|
+
path: [property],
|
|
61
|
+
label: property,
|
|
62
|
+
sql: (resolve) => resolve([property]),
|
|
63
|
+
read: (row) => row[property],
|
|
64
|
+
});
|
|
65
|
+
const unique = (columns) => ({
|
|
66
|
+
kind: 'unique',
|
|
67
|
+
paths: columns.map((column) => [column]),
|
|
68
|
+
message: `${columns.join(', ')} must be unique`,
|
|
69
|
+
toSql: (resolve) => columns.map((column) => resolve([column])).join(', '),
|
|
70
|
+
// A single row cannot see a duplicate: the unique index is the authority.
|
|
71
|
+
holds: () => true,
|
|
72
|
+
});
|
|
73
|
+
const satisfies = (predicate, columns) => check(columns.map((column) => [column]), `${columns.join(', ')} must satisfy ${predicate.name || 'the rule'}`, () => null, (row) => Reflect.apply(predicate, undefined, columns.map((column) => row[column])) === true);
|
|
74
|
+
/**
|
|
75
|
+
* The `c` an invariant is written against. A Proxy so a typo in a column name is a
|
|
76
|
+
* declaration-time error naming the columns that do exist, not `undefined is not a function`.
|
|
77
|
+
*/
|
|
78
|
+
export const invariantColumns = (entity, properties) => {
|
|
79
|
+
const known = new Set(properties);
|
|
80
|
+
const helpers = { unique, satisfies };
|
|
81
|
+
return new Proxy(helpers, {
|
|
82
|
+
get(target, property) {
|
|
83
|
+
if (property === 'unique' || property === 'satisfies')
|
|
84
|
+
return target[property];
|
|
85
|
+
if (typeof property !== 'string')
|
|
86
|
+
return undefined;
|
|
87
|
+
if (!known.has(property)) {
|
|
88
|
+
throw invariantViolated(entity, 'invariant', `no column "${property}"; declared columns are ${properties.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
return expr(columnTerm(property));
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=expr.js.map
|
package/src/expr.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expr.js","sourceRoot":"","sources":["expr.ts"],"names":[],"mappings":"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"}
|
package/src/expr.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// The invariant expression language. One declaration compiles to two enforcement points: a
|
|
2
|
+
// predicate the app runs on every write, and the SQL predicate the migration emits.
|
|
3
|
+
//
|
|
4
|
+
// Expressions are written against property keys (`c.likeCount`) and resolved to physical names
|
|
5
|
+
// (`like_count`) by `entity()`, because the author never writes a physical name.
|
|
6
|
+
//
|
|
7
|
+
// A JS predicate (`matches(isValidSlug)`, `satisfies(fn, [...])`) cannot be translated to SQL.
|
|
8
|
+
// It still runs in the app, and reports `sql: null` so `x verify` can warn that the database
|
|
9
|
+
// does not know this rule — silently pretending it reached Postgres would be worse.
|
|
10
|
+
|
|
11
|
+
import { invariantViolated } from './errors';
|
|
12
|
+
|
|
13
|
+
export type Row = Readonly<Record<string, unknown>>;
|
|
14
|
+
|
|
15
|
+
/** Property path -> physical column name. */
|
|
16
|
+
export type Resolve = (path: readonly string[]) => string;
|
|
17
|
+
|
|
18
|
+
export interface Expr {
|
|
19
|
+
readonly kind: 'check' | 'unique';
|
|
20
|
+
readonly paths: readonly (readonly string[])[];
|
|
21
|
+
readonly message: string;
|
|
22
|
+
/** `null` when the rule is a JS predicate the database cannot be told about. */
|
|
23
|
+
toSql(resolve: Resolve): string | null;
|
|
24
|
+
holds(row: Row): boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Term {
|
|
28
|
+
readonly path: readonly string[];
|
|
29
|
+
readonly label: string;
|
|
30
|
+
sql(resolve: Resolve): string;
|
|
31
|
+
read(row: Row): unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ColumnExpr {
|
|
35
|
+
/** `btrim(...)` in SQL, `.trim()` in the app — the same rule, both sides. */
|
|
36
|
+
trimmed(): ColumnExpr;
|
|
37
|
+
minLength(length: number): Expr;
|
|
38
|
+
contains(value: string): Expr;
|
|
39
|
+
/** A `RegExp` reaches the database; a function is app-only. */
|
|
40
|
+
matches(pattern: RegExp | ((value: string) => boolean)): Expr;
|
|
41
|
+
atLeast(value: number | bigint): Expr;
|
|
42
|
+
eq(value: string | number | boolean | bigint | ColumnExpr): Expr;
|
|
43
|
+
isTrue(): Expr;
|
|
44
|
+
/** Money is two physical columns; these are how a rule names one of them. */
|
|
45
|
+
readonly minor: ColumnExpr;
|
|
46
|
+
readonly currency: ColumnExpr;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type RowPredicate = (...values: never[]) => boolean;
|
|
50
|
+
|
|
51
|
+
export type InvariantColumns = {
|
|
52
|
+
readonly [column: string]: ColumnExpr;
|
|
53
|
+
} & {
|
|
54
|
+
/** Decided by the database — a single row cannot see a duplicate. */
|
|
55
|
+
unique(columns: readonly string[]): Expr;
|
|
56
|
+
/** Lifts a domain predicate over several columns. App-only by construction. */
|
|
57
|
+
satisfies(predicate: RowPredicate, columns: readonly string[]): Expr;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const terms = new WeakMap<ColumnExpr, Term>();
|
|
61
|
+
|
|
62
|
+
const walk = (row: Row, path: readonly string[]): unknown =>
|
|
63
|
+
path.reduce<unknown>(
|
|
64
|
+
(value, key) => (typeof value === 'object' && value !== null ? (value as Row)[key] : undefined),
|
|
65
|
+
row,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const literal = (value: unknown): string =>
|
|
69
|
+
typeof value === 'string' ? `'${value.replaceAll("'", "''")}'` : String(value);
|
|
70
|
+
|
|
71
|
+
const check = (
|
|
72
|
+
paths: readonly (readonly string[])[],
|
|
73
|
+
message: string,
|
|
74
|
+
sql: (resolve: Resolve) => string | null,
|
|
75
|
+
holds: (row: Row) => boolean,
|
|
76
|
+
): Expr => ({ kind: 'check', paths, message, toSql: sql, holds });
|
|
77
|
+
|
|
78
|
+
const isColumnExpr = (value: unknown): value is ColumnExpr =>
|
|
79
|
+
typeof value === 'object' && value !== null && terms.has(value as ColumnExpr);
|
|
80
|
+
|
|
81
|
+
const expr = (term: Term): ColumnExpr => {
|
|
82
|
+
const one = (
|
|
83
|
+
message: string,
|
|
84
|
+
sql: (resolve: Resolve) => string | null,
|
|
85
|
+
holds: (value: unknown) => boolean,
|
|
86
|
+
): Expr => check([term.path], message, sql, (row) => holds(term.read(row)));
|
|
87
|
+
|
|
88
|
+
const built: ColumnExpr = {
|
|
89
|
+
trimmed: () =>
|
|
90
|
+
expr({
|
|
91
|
+
path: term.path,
|
|
92
|
+
label: `trimmed ${term.label}`,
|
|
93
|
+
sql: (resolve) => `btrim(${term.sql(resolve)})`,
|
|
94
|
+
read: (row) => {
|
|
95
|
+
const value = term.read(row);
|
|
96
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
minLength: (length) =>
|
|
101
|
+
one(
|
|
102
|
+
`${term.label} must be at least ${length} character${length === 1 ? '' : 's'}`,
|
|
103
|
+
(resolve) => `char_length(${term.sql(resolve)}) >= ${length}`,
|
|
104
|
+
(value) => typeof value === 'string' && value.length >= length,
|
|
105
|
+
),
|
|
106
|
+
|
|
107
|
+
contains: (value) =>
|
|
108
|
+
one(
|
|
109
|
+
`${term.label} must contain ${literal(value)}`,
|
|
110
|
+
(resolve) => `position(${literal(value)} in ${term.sql(resolve)}) > 0`,
|
|
111
|
+
(actual) => typeof actual === 'string' && actual.includes(value),
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
matches: (pattern) =>
|
|
115
|
+
one(
|
|
116
|
+
`${term.label} must match ${pattern instanceof RegExp ? pattern.source : pattern.name || 'the rule'}`,
|
|
117
|
+
(resolve) =>
|
|
118
|
+
pattern instanceof RegExp ? `${term.sql(resolve)} ~ ${literal(pattern.source)}` : null,
|
|
119
|
+
(value) =>
|
|
120
|
+
typeof value === 'string' &&
|
|
121
|
+
(pattern instanceof RegExp ? pattern.test(value) : pattern(value)),
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
atLeast: (bound) =>
|
|
125
|
+
one(
|
|
126
|
+
`${term.label} must be at least ${bound}`,
|
|
127
|
+
(resolve) => `${term.sql(resolve)} >= ${bound}`,
|
|
128
|
+
(value) => (typeof value === 'number' || typeof value === 'bigint') && value >= bound,
|
|
129
|
+
),
|
|
130
|
+
|
|
131
|
+
eq: (other) =>
|
|
132
|
+
isColumnExpr(other)
|
|
133
|
+
? sameAs(term, other)
|
|
134
|
+
: one(
|
|
135
|
+
`${term.label} must equal ${literal(other)}`,
|
|
136
|
+
(resolve) => `${term.sql(resolve)} = ${literal(other)}`,
|
|
137
|
+
(value) => value === other,
|
|
138
|
+
),
|
|
139
|
+
|
|
140
|
+
isTrue: () =>
|
|
141
|
+
one(
|
|
142
|
+
`${term.label} must be true`,
|
|
143
|
+
(resolve) => term.sql(resolve),
|
|
144
|
+
(value) => value === true,
|
|
145
|
+
),
|
|
146
|
+
|
|
147
|
+
get minor() {
|
|
148
|
+
return part(term, 'minor');
|
|
149
|
+
},
|
|
150
|
+
get currency() {
|
|
151
|
+
return part(term, 'currency');
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
terms.set(built, term);
|
|
156
|
+
return built;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const part = (term: Term, key: string): ColumnExpr =>
|
|
160
|
+
expr({
|
|
161
|
+
path: [...term.path, key],
|
|
162
|
+
label: `${term.label}.${key}`,
|
|
163
|
+
sql: (resolve) => resolve([...term.path, key]),
|
|
164
|
+
read: (row) => walk(row, [...term.path, key]),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const sameAs = (left: Term, other: ColumnExpr): Expr => {
|
|
168
|
+
const right = terms.get(other);
|
|
169
|
+
if (right === undefined) throw invariantViolated('invariant', 'eq', 'not a column expression');
|
|
170
|
+
return check(
|
|
171
|
+
[left.path, right.path],
|
|
172
|
+
`${left.label} must equal ${right.label}`,
|
|
173
|
+
(resolve) => `${left.sql(resolve)} = ${right.sql(resolve)}`,
|
|
174
|
+
(row) => left.read(row) === right.read(row),
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const columnTerm = (property: string): Term => ({
|
|
179
|
+
path: [property],
|
|
180
|
+
label: property,
|
|
181
|
+
sql: (resolve) => resolve([property]),
|
|
182
|
+
read: (row) => row[property],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const unique = (columns: readonly string[]): Expr => ({
|
|
186
|
+
kind: 'unique',
|
|
187
|
+
paths: columns.map((column) => [column]),
|
|
188
|
+
message: `${columns.join(', ')} must be unique`,
|
|
189
|
+
toSql: (resolve) => columns.map((column) => resolve([column])).join(', '),
|
|
190
|
+
// A single row cannot see a duplicate: the unique index is the authority.
|
|
191
|
+
holds: () => true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const satisfies = (predicate: RowPredicate, columns: readonly string[]): Expr =>
|
|
195
|
+
check(
|
|
196
|
+
columns.map((column) => [column]),
|
|
197
|
+
`${columns.join(', ')} must satisfy ${predicate.name || 'the rule'}`,
|
|
198
|
+
() => null,
|
|
199
|
+
(row) =>
|
|
200
|
+
Reflect.apply(
|
|
201
|
+
predicate,
|
|
202
|
+
undefined,
|
|
203
|
+
columns.map((column) => row[column]),
|
|
204
|
+
) === true,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The `c` an invariant is written against. A Proxy so a typo in a column name is a
|
|
209
|
+
* declaration-time error naming the columns that do exist, not `undefined is not a function`.
|
|
210
|
+
*/
|
|
211
|
+
export const invariantColumns = (
|
|
212
|
+
entity: string,
|
|
213
|
+
properties: readonly string[],
|
|
214
|
+
): InvariantColumns => {
|
|
215
|
+
const known = new Set(properties);
|
|
216
|
+
const helpers = { unique, satisfies };
|
|
217
|
+
return new Proxy(helpers, {
|
|
218
|
+
get(target, property) {
|
|
219
|
+
if (property === 'unique' || property === 'satisfies') return target[property];
|
|
220
|
+
if (typeof property !== 'string') return undefined;
|
|
221
|
+
if (!known.has(property)) {
|
|
222
|
+
throw invariantViolated(
|
|
223
|
+
entity,
|
|
224
|
+
'invariant',
|
|
225
|
+
`no column "${property}"; declared columns are ${properties.join(', ')}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return expr(columnTerm(property));
|
|
229
|
+
},
|
|
230
|
+
}) as InvariantColumns;
|
|
231
|
+
};
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type { TextOptions } from './columns';
|
|
2
|
+
export { boolean, enumerated, integer, locale, money, newId, text, timestamp, tz, url, uuid, } from './columns';
|
|
3
|
+
export type { Database, DatabaseOptions, Driver, EntitySet } from './database';
|
|
4
|
+
export { database, memoryDriver } from './database';
|
|
5
|
+
export type { Entity, EntityCore, EntityInit, IndexInit } from './entity';
|
|
6
|
+
export { entity, SOFT_DELETE_COLUMN } from './entity';
|
|
7
|
+
export type { EntityErrorCode } from './errors';
|
|
8
|
+
export { dbDrift, ENTITY_ERROR_CODES, ENTITY_ERROR_TITLES, EntityError, entityDuplicate, invariantViolated, notFound, tenancyUnscoped, } from './errors';
|
|
9
|
+
export type { ColumnExpr, Expr, InvariantColumns, Resolve } from './expr';
|
|
10
|
+
export type { Invariant, InvariantDef, InvariantKind } from './invariants';
|
|
11
|
+
export { assertInvariants, constraintName, invariant, invariantsToSql, toSql, } from './invariants';
|
|
12
|
+
export type { ReadBuilder, Table } from './query';
|
|
13
|
+
export { tableFor } from './query';
|
|
14
|
+
export type { ColumnDescription, EntityDescription, InvariantDescription, RegistryEntry, } from './registry';
|
|
15
|
+
export { clearRegistry, describeEntities, entityNames, getEntity, registerEntity, } from './registry';
|
|
16
|
+
export type { FindManyArgs, Page, Repo, RepoOptions, Transactor, Tx } from './repo';
|
|
17
|
+
export { decodeCursor, encodeCursor, memoryRepo, memoryTransactor } from './repo';
|
|
18
|
+
export type { Seed, SeedContext, SeedOptions } from './seed';
|
|
19
|
+
export { defineSeed, seedId } from './seed';
|
|
20
|
+
export type { Operator, Predicate, QueryPlan, SortDirection, SortKey } from './tenancy';
|
|
21
|
+
export { assertScoped, describePlan, emptyPlan, hasOrgPredicate, isOrgScoped, ORG_COLUMN, orgScoped, tenantColumnOf, } from './tenancy';
|
|
22
|
+
export type { AnyColumn, Column, ColumnDefault, ColumnKind, ColumnMap, ColumnMeta, IndexDef, Insertable, MoneyInput, MoneyValue, OnDelete, ReferenceOptions, RowOf, TimestampColumn, TypeOf, UuidColumn, } from './types';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAEA,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,KAAK,EACL,KAAK,EACL,IAAI,EACJ,SAAS,EACT,EAAE,EACF,GAAG,EACH,IAAI,GACL,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACtD,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EACL,OAAO,EACP,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,UAAU,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC1E,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3E,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,eAAe,EACf,KAAK,GACN,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACpF,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAClF,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC5C,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,eAAe,EACf,WAAW,EACX,UAAU,EACV,SAAS,EACT,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,SAAS,EACT,MAAM,EACN,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,KAAK,EACL,eAAe,EACf,MAAM,EACN,UAAU,GACX,MAAM,SAAS,CAAC"}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The public surface of @ultimat3/entity. Explicit, never `export *`.
|
|
2
|
+
export { boolean, enumerated, integer, locale, money, newId, text, timestamp, tz, url, uuid, } from './columns';
|
|
3
|
+
export { database, memoryDriver } from './database';
|
|
4
|
+
export { entity, SOFT_DELETE_COLUMN } from './entity';
|
|
5
|
+
export { dbDrift, ENTITY_ERROR_CODES, ENTITY_ERROR_TITLES, EntityError, entityDuplicate, invariantViolated, notFound, tenancyUnscoped, } from './errors';
|
|
6
|
+
export { assertInvariants, constraintName, invariant, invariantsToSql, toSql, } from './invariants';
|
|
7
|
+
export { tableFor } from './query';
|
|
8
|
+
export { clearRegistry, describeEntities, entityNames, getEntity, registerEntity, } from './registry';
|
|
9
|
+
export { decodeCursor, encodeCursor, memoryRepo, memoryTransactor } from './repo';
|
|
10
|
+
export { defineSeed, seedId } from './seed';
|
|
11
|
+
export { assertScoped, describePlan, emptyPlan, hasOrgPredicate, isOrgScoped, ORG_COLUMN, orgScoped, tenantColumnOf, } from './tenancy';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAGtE,OAAO,EACL,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,KAAK,EACL,KAAK,EACL,IAAI,EACJ,SAAS,EACT,EAAE,EACF,GAAG,EACH,IAAI,GACL,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEtD,OAAO,EACL,OAAO,EACP,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,SAAS,EACT,eAAe,EACf,KAAK,GACN,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOnC,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,WAAW,EACX,SAAS,EACT,cAAc,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAElF,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE5C,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,eAAe,EACf,WAAW,EACX,UAAU,EACV,SAAS,EACT,cAAc,GACf,MAAM,WAAW,CAAC"}
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
// The public surface of @ultimat3/entity. Explicit, never `export *`.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Re-exported so an `entity` file needs one import, not two. Same object as schema's. */
|
|
4
|
+
export type { Infer } from '@ultimat3/schema';
|
|
5
|
+
export { t } from '@ultimat3/schema';
|
|
6
|
+
export type { TextOptions } from './columns';
|
|
4
7
|
export {
|
|
5
8
|
boolean,
|
|
6
|
-
|
|
9
|
+
enumerated,
|
|
7
10
|
integer,
|
|
8
|
-
jsonb,
|
|
9
11
|
locale,
|
|
10
12
|
money,
|
|
11
13
|
newId,
|
|
12
|
-
nullable,
|
|
13
|
-
orgId,
|
|
14
|
-
references,
|
|
15
|
-
slug,
|
|
16
|
-
softDelete,
|
|
17
|
-
table,
|
|
18
14
|
text,
|
|
19
|
-
|
|
15
|
+
timestamp,
|
|
20
16
|
tz,
|
|
17
|
+
url,
|
|
18
|
+
uuid,
|
|
21
19
|
} from './columns';
|
|
22
|
-
export type {
|
|
23
|
-
export {
|
|
20
|
+
export type { Database, DatabaseOptions, Driver, EntitySet } from './database';
|
|
21
|
+
export { database, memoryDriver } from './database';
|
|
22
|
+
export type { Entity, EntityCore, EntityInit, IndexInit } from './entity';
|
|
23
|
+
export { entity, SOFT_DELETE_COLUMN } from './entity';
|
|
24
24
|
export type { EntityErrorCode } from './errors';
|
|
25
25
|
export {
|
|
26
26
|
dbDrift,
|
|
@@ -32,15 +32,19 @@ export {
|
|
|
32
32
|
notFound,
|
|
33
33
|
tenancyUnscoped,
|
|
34
34
|
} from './errors';
|
|
35
|
-
export type {
|
|
35
|
+
export type { ColumnExpr, Expr, InvariantColumns, Resolve } from './expr';
|
|
36
|
+
export type { Invariant, InvariantDef, InvariantKind } from './invariants';
|
|
36
37
|
export {
|
|
37
38
|
assertInvariants,
|
|
38
39
|
constraintName,
|
|
39
40
|
invariant,
|
|
40
41
|
invariantsToSql,
|
|
41
42
|
toSql,
|
|
42
|
-
unique,
|
|
43
43
|
} from './invariants';
|
|
44
|
+
export type { PostgresDriverOptions } from './pg-driver';
|
|
45
|
+
export { postgresDriver, postgresRepo, postgresTransactor } from './pg-driver';
|
|
46
|
+
export type { ReadBuilder, Table } from './query';
|
|
47
|
+
export { tableFor } from './query';
|
|
44
48
|
export type {
|
|
45
49
|
ColumnDescription,
|
|
46
50
|
EntityDescription,
|
|
@@ -55,8 +59,10 @@ export {
|
|
|
55
59
|
registerEntity,
|
|
56
60
|
} from './registry';
|
|
57
61
|
export type { FindManyArgs, Page, Repo, RepoOptions, Transactor, Tx } from './repo';
|
|
58
|
-
export {
|
|
59
|
-
export type {
|
|
62
|
+
export { memoryRepo, memoryTransactor } from './repo';
|
|
63
|
+
export type { Seed, SeedContext, SeedOptions } from './seed';
|
|
64
|
+
export { defineSeed, seedId } from './seed';
|
|
65
|
+
export type { Operator, Predicate, QueryPlan, SortDirection, SortKey } from './tenancy';
|
|
60
66
|
export {
|
|
61
67
|
assertScoped,
|
|
62
68
|
describePlan,
|
|
@@ -65,15 +71,25 @@ export {
|
|
|
65
71
|
isOrgScoped,
|
|
66
72
|
ORG_COLUMN,
|
|
67
73
|
orgScoped,
|
|
74
|
+
tenantColumnOf,
|
|
68
75
|
} from './tenancy';
|
|
69
76
|
export type {
|
|
70
|
-
|
|
77
|
+
AnyColumn,
|
|
78
|
+
Column,
|
|
71
79
|
ColumnDefault,
|
|
72
80
|
ColumnKind,
|
|
73
81
|
ColumnMap,
|
|
82
|
+
ColumnMeta,
|
|
74
83
|
IndexDef,
|
|
75
|
-
|
|
84
|
+
Insertable,
|
|
85
|
+
MoneyInput,
|
|
86
|
+
MoneyValue,
|
|
87
|
+
OnDelete,
|
|
88
|
+
ReferenceOptions,
|
|
76
89
|
RowOf,
|
|
77
|
-
|
|
90
|
+
TimestampColumn,
|
|
91
|
+
TypeOf,
|
|
92
|
+
UuidColumn,
|
|
78
93
|
} from './types';
|
|
79
|
-
|
|
94
|
+
// `viewFor` stays internal: a view is reached through the entity, as `posts.$view([...])`.
|
|
95
|
+
export type { EntityView } from './view';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Expr, InvariantColumns, Resolve } from './expr';
|
|
2
|
+
/** `assert` is a rule only the app can run — a JS predicate with no SQL translation. */
|
|
3
|
+
export type InvariantKind = 'check' | 'unique' | 'assert';
|
|
4
|
+
export interface Invariant<T> {
|
|
5
|
+
/** Becomes the constraint name: `<table>_<name>_check`. Keep it snake_case. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly kind: InvariantKind;
|
|
8
|
+
/** Safe to log and useful to an agent: says what was expected, not what leaked. */
|
|
9
|
+
readonly message: string;
|
|
10
|
+
/** SQL predicate for `check`, the column list for `unique`, `null` for `assert`. */
|
|
11
|
+
readonly sql: string | null;
|
|
12
|
+
/** Physical column names the rule reads. */
|
|
13
|
+
readonly columns: readonly string[];
|
|
14
|
+
/** Partial-constraint predicate, e.g. `deleted_at is null`. */
|
|
15
|
+
readonly where?: string;
|
|
16
|
+
readonly holds: (row: T) => boolean;
|
|
17
|
+
}
|
|
18
|
+
/** What `invariant()` returns: a rule that does not yet know its physical column names. */
|
|
19
|
+
export interface InvariantDef {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly build: (columns: InvariantColumns) => Expr;
|
|
22
|
+
}
|
|
23
|
+
/** `invariant('post_like_count_non_negative', (c) => c.likeCount.atLeast(0))` */
|
|
24
|
+
export declare const invariant: (name: string, build: (columns: InvariantColumns) => Expr) => InvariantDef;
|
|
25
|
+
/** Called by `entity()`: resolves property paths to physical names and freezes the rule. */
|
|
26
|
+
export declare const bindInvariant: <T>(def: InvariantDef, columns: InvariantColumns, resolve: Resolve, partialWhere: string | undefined) => Invariant<T>;
|
|
27
|
+
export declare const constraintName: (table: string, inv: {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly kind: InvariantKind;
|
|
30
|
+
}) => string;
|
|
31
|
+
/** The DDL the migration emits. One statement, terminated, ready to diff. */
|
|
32
|
+
export declare const toSql: <T>(table: string, inv: Invariant<T>) => string | null;
|
|
33
|
+
export declare const invariantsToSql: <T>(table: string, invariants: readonly Invariant<T>[]) => string;
|
|
34
|
+
/** Runs on every write. Reports every violation at once so one round trip fixes all. */
|
|
35
|
+
export declare const assertInvariants: <T>(entityName: string, invariants: readonly Invariant<T>[], row: T) => void;
|
|
36
|
+
//# sourceMappingURL=invariants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invariants.d.ts","sourceRoot":"","sources":["invariants.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAO,MAAM,QAAQ,CAAC;AAEnE,wFAAwF;AACxF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oFAAoF;IACpF,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;CACrC;AAED,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACrD;AAED,iFAAiF;AACjF,eAAO,MAAM,SAAS,SACd,MAAM,SACL,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,KACzC,YAAiC,CAAC;AAKrC,4FAA4F;AAC5F,eAAO,MAAM,aAAa,GAAI,CAAC,OACxB,YAAY,WACR,gBAAgB,WAChB,OAAO,gBACF,MAAM,GAAG,SAAS,KAC/B,SAAS,CAAC,CAAC,CAcb,CAAC;AAEF,eAAO,MAAM,cAAc,UAClB,MAAM,OACR;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,KAC3D,MAA2E,CAAC;AAE/E,6EAA6E;AAC7E,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,OAAO,SAAS,CAAC,CAAC,CAAC,KAAG,MAAM,GAAG,IASpE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,MAAM,cAAc,SAAS,SAAS,CAAC,CAAC,CAAC,EAAE,KAAG,MAIzE,CAAC;AAEhB,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,GAAI,CAAC,cACpB,MAAM,cACN,SAAS,SAAS,CAAC,CAAC,CAAC,EAAE,OAC9B,CAAC,KACL,IAKF,CAAC"}
|