@zmdb/schema 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +30 -0
- package/dist/custom-types/index.d.ts +41 -0
- package/dist/custom-types/index.d.ts.map +1 -0
- package/dist/custom-types/index.js +32 -0
- package/dist/custom-types/index.js.map +1 -0
- package/dist/derive/index.d.ts +122 -0
- package/dist/derive/index.d.ts.map +1 -0
- package/dist/derive/index.js +13 -0
- package/dist/derive/index.js.map +1 -0
- package/dist/derive/query.d.ts +62 -0
- package/dist/derive/query.d.ts.map +1 -0
- package/dist/derive/query.js +18 -0
- package/dist/derive/query.js.map +1 -0
- package/dist/dto/index.d.ts +224 -0
- package/dist/dto/index.d.ts.map +1 -0
- package/dist/dto/index.js +118 -0
- package/dist/dto/index.js.map +1 -0
- package/dist/entity-modeling/index.d.ts +12 -0
- package/dist/entity-modeling/index.d.ts.map +1 -0
- package/dist/entity-modeling/index.js +28 -0
- package/dist/entity-modeling/index.js.map +1 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +374 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +735 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/validation-shape.d.ts +46 -0
- package/dist/ir/validation-shape.d.ts.map +1 -0
- package/dist/ir/validation-shape.js +130 -0
- package/dist/ir/validation-shape.js.map +1 -0
- package/dist/ir/vocabulary.d.ts +54 -0
- package/dist/ir/vocabulary.d.ts.map +1 -0
- package/dist/ir/vocabulary.js +51 -0
- package/dist/ir/vocabulary.js.map +1 -0
- package/dist/naming/index.d.ts +26 -0
- package/dist/naming/index.d.ts.map +1 -0
- package/dist/naming/index.js +147 -0
- package/dist/naming/index.js.map +1 -0
- package/dist/openapi/index.d.ts +57 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +98 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/relations/index.d.ts +23 -0
- package/dist/relations/index.d.ts.map +1 -0
- package/dist/relations/index.js +98 -0
- package/dist/relations/index.js.map +1 -0
- package/dist/tags/index.d.ts +261 -0
- package/dist/tags/index.d.ts.map +1 -0
- package/dist/tags/index.js +64 -0
- package/dist/tags/index.js.map +1 -0
- package/package.json +82 -0
- package/src/custom-types/index.ts +59 -0
- package/src/derive/index.ts +224 -0
- package/src/derive/query.ts +128 -0
- package/src/dto/index.ts +395 -0
- package/src/entity-modeling/index.ts +33 -0
- package/src/index.ts +263 -0
- package/src/ir/index.ts +1085 -0
- package/src/ir/validation-shape.ts +145 -0
- package/src/ir/vocabulary.ts +56 -0
- package/src/naming/index.ts +159 -0
- package/src/openapi/index.ts +133 -0
- package/src/relations/index.ts +134 -0
- package/src/tags/index.ts +284 -0
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zmdb/schema",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Schema DSL + compile-time type derivation (Entity/Create/Update/read DTOs), relations, OpenAPI, and custom types — the single source of truth for a zmdb data layer.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dto",
|
|
7
|
+
"openapi",
|
|
8
|
+
"orm",
|
|
9
|
+
"schema",
|
|
10
|
+
"type-derivation",
|
|
11
|
+
"typescript",
|
|
12
|
+
"zmdb"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/ambasta/zmdb#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ambasta/zmdb/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "GPL-3.0-or-later",
|
|
19
|
+
"author": "zmdb contributors",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ambasta/zmdb.git",
|
|
23
|
+
"directory": "packages/schema"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./tags": {
|
|
33
|
+
"types": "./dist/tags/index.d.ts",
|
|
34
|
+
"import": "./dist/tags/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./ir": {
|
|
37
|
+
"types": "./dist/ir/index.d.ts",
|
|
38
|
+
"import": "./dist/ir/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./derive": {
|
|
41
|
+
"types": "./dist/derive/index.d.ts",
|
|
42
|
+
"import": "./dist/derive/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./dto": {
|
|
45
|
+
"types": "./dist/dto/index.d.ts",
|
|
46
|
+
"import": "./dist/dto/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./naming": {
|
|
49
|
+
"types": "./dist/naming/index.d.ts",
|
|
50
|
+
"import": "./dist/naming/index.js"
|
|
51
|
+
},
|
|
52
|
+
"./relations": {
|
|
53
|
+
"types": "./dist/relations/index.d.ts",
|
|
54
|
+
"import": "./dist/relations/index.js"
|
|
55
|
+
},
|
|
56
|
+
"./entity-modeling": {
|
|
57
|
+
"types": "./dist/entity-modeling/index.d.ts",
|
|
58
|
+
"import": "./dist/entity-modeling/index.js"
|
|
59
|
+
},
|
|
60
|
+
"./openapi": {
|
|
61
|
+
"types": "./dist/openapi/index.d.ts",
|
|
62
|
+
"import": "./dist/openapi/index.js"
|
|
63
|
+
},
|
|
64
|
+
"./custom-types": {
|
|
65
|
+
"types": "./dist/custom-types/index.d.ts",
|
|
66
|
+
"import": "./dist/custom-types/index.js"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public",
|
|
71
|
+
"tag": "beta"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
75
|
+
"test": "vitest run"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=26"
|
|
79
|
+
},
|
|
80
|
+
"main": "./dist/index.js",
|
|
81
|
+
"types": "./dist/index.d.ts"
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Custom types & codecs — see ./SPEC.md.
|
|
2
|
+
|
|
3
|
+
import { type Codec } from '../ir/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A column type the library does not know, described by its owner.
|
|
7
|
+
*
|
|
8
|
+
* Three types, because a column has three (plan D3): `Wire` is what JSON carries,
|
|
9
|
+
* `TS` is what handler code holds, and `DB` is what the driver binds. A codec that
|
|
10
|
+
* named only two of them left the third to be guessed, and the guess was "the same
|
|
11
|
+
* as the app type" — which is how a `Money` instance ended up being handed to
|
|
12
|
+
* `JSON.stringify` and to a query parameter unchanged.
|
|
13
|
+
*
|
|
14
|
+
* All four functions are required. A codec whose `toWire` was optional would be a
|
|
15
|
+
* codec that sometimes converts, and the caller cannot tell which kind it has.
|
|
16
|
+
*/
|
|
17
|
+
export interface CustomType<Wire, TS, DB = unknown> {
|
|
18
|
+
/** DDL type, e.g. `'jsonb'`. Dialect spelling is the emitter's business. */
|
|
19
|
+
readonly sqlType: string;
|
|
20
|
+
/** Serialise for the driver. */
|
|
21
|
+
readonly toDb: (value: TS) => DB;
|
|
22
|
+
/** Parse a driver row value. */
|
|
23
|
+
readonly fromDb: (raw: DB) => TS;
|
|
24
|
+
/** Serialise for a JSON response. */
|
|
25
|
+
readonly toWire: (value: TS) => Wire;
|
|
26
|
+
/** Parse a JSON request body value. */
|
|
27
|
+
readonly fromWire: (raw: Wire) => TS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function defineType<Wire, TS, DB>(def: CustomType<Wire, TS, DB>): CustomType<Wire, TS, DB> {
|
|
31
|
+
return Object.freeze({ ...def });
|
|
32
|
+
}
|
|
33
|
+
export function encodeValue<Wire, TS, DB>(type: CustomType<Wire, TS, DB>, value: TS): DB {
|
|
34
|
+
return type.toDb(value);
|
|
35
|
+
}
|
|
36
|
+
export function decodeValue<Wire, TS, DB>(type: CustomType<Wire, TS, DB>, raw: DB): TS {
|
|
37
|
+
return type.fromDb(raw);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Adapt a `CustomType` to the `Codec` the IR's wire crossing asks for.
|
|
42
|
+
*
|
|
43
|
+
* The IR speaks `unknown` on both sides because it is data, not generics; the
|
|
44
|
+
* conversion is one cast at this boundary rather than one at every registry literal.
|
|
45
|
+
* A `Codec<'Money'>` tag names the key this goes under:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* wireDecoder(Schema, 'create', { Money: wireCodec(MoneyType) })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function wireCodec<Wire, TS, DB>(type: CustomType<Wire, TS, DB>): Codec {
|
|
52
|
+
// boundary: a registry is keyed by name, so the value arriving at either direction is only
|
|
53
|
+
// as typed as the declaration that named this codec. The validator is what proves it, and
|
|
54
|
+
// it runs before `decode` and after `encode`.
|
|
55
|
+
return {
|
|
56
|
+
decode: (wire: unknown) => type.fromWire(wire as Wire),
|
|
57
|
+
encode: (app: unknown) => type.toWire(app as TS),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// @zmdb/schema/derive: the DTO suite, derived from a tagged type.
|
|
2
|
+
//
|
|
3
|
+
// There is one `Entity`, one `CreateDTO`, one `UpdateDTO`, one `PrimaryKeyOf`, and they
|
|
4
|
+
// are these. `../index.ts` re-exports them under the same names it used to define
|
|
5
|
+
// schema-value twins under; the twins are deleted, not deprecated.
|
|
6
|
+
//
|
|
7
|
+
// Every derivation takes the declared type and nothing else. Nothing here tests
|
|
8
|
+
// `T extends { columns: ... }`, nothing dispatches on whether it was handed a value, and
|
|
9
|
+
// so nothing pays for that test per use. A caller who holds a schema value crosses back
|
|
10
|
+
// at a boundary — `TaggedSchema<T>` in a parameter position, `T` by inference — and the
|
|
11
|
+
// derivations never see the value at all.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
type AnyRelation,
|
|
15
|
+
type HasDefault,
|
|
16
|
+
type PrimaryKey,
|
|
17
|
+
type Sensitive,
|
|
18
|
+
type Serial,
|
|
19
|
+
type SoftDelete,
|
|
20
|
+
type Sql,
|
|
21
|
+
type Table,
|
|
22
|
+
type Unique,
|
|
23
|
+
type WireAs,
|
|
24
|
+
} from '../tags/index.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What every derivation takes: a type that could have been declared as a table.
|
|
28
|
+
*
|
|
29
|
+
* An alias for `Table<string>`, and the constraint is the point. `Table` is an
|
|
30
|
+
* all-optional weak type, so TypeScript's weak-type rule rejects anything with no property
|
|
31
|
+
* in common with it — which a generated schema *value* is. `Entity<typeof UserSchema>`, the
|
|
32
|
+
* spelling this design replaced, is therefore a compile error rather than the schema's own
|
|
33
|
+
* five properties dressed up as a row. Worth constraining for precisely because the wrong
|
|
34
|
+
* answer was structurally plausible: it had keys and it had types, so nothing downstream
|
|
35
|
+
* would have complained.
|
|
36
|
+
*
|
|
37
|
+
* An index-signature row still passes, because a string index can hold the slot — see
|
|
38
|
+
* `dto/index.ts`'s `UnknownRow`, the one corner of the query surface keyed by a table
|
|
39
|
+
* *name* instead of by a declaration.
|
|
40
|
+
*/
|
|
41
|
+
export type DeclaredTable = Table<string>;
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Key filters.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
//
|
|
47
|
+
// `NonNullable<T[K]>` rather than a bare `T[K]` is load-bearing. A nullable
|
|
48
|
+
// column with a default is declared `(string & HasDefault) | null`, and `null` is
|
|
49
|
+
// not assignable to a weak object type, so the union as a whole does not match
|
|
50
|
+
// `HasDefault`. Testing the non-nullable arm is what makes a nullable defaulted
|
|
51
|
+
// column optional on insert instead of required. `tagged-dto.type-test.ts` pins
|
|
52
|
+
// this down: it is exactly the sort of thing that silently returns `never`.
|
|
53
|
+
//
|
|
54
|
+
// `-?` strips optionality from the probe so an already-optional property is still
|
|
55
|
+
// examined under `exactOptionalPropertyTypes`.
|
|
56
|
+
//
|
|
57
|
+
// Symbol keys are filtered out everywhere below: entity-level tags (`Table`,
|
|
58
|
+
// `Fts`, `SoftDelete`, `ForeignKey`) arrive through `extends` and would
|
|
59
|
+
// otherwise show up in `keyof`.
|
|
60
|
+
|
|
61
|
+
export type KeysCarrying<T, Tag> = {
|
|
62
|
+
[K in keyof T]-?: NonNullable<T[K]> extends Tag ? (K extends string ? K : never) : never;
|
|
63
|
+
}[keyof T];
|
|
64
|
+
|
|
65
|
+
/** Columns the database generates. Omitted from `CreateDTO` outright. */
|
|
66
|
+
export type SerialKeys<T> = KeysCarrying<T, Serial>;
|
|
67
|
+
/** Columns with a database default. Optional on insert, not absent. */
|
|
68
|
+
export type DefaultKeys<T> = KeysCarrying<T, HasDefault>;
|
|
69
|
+
export type PrimaryKeyKeys<T> = KeysCarrying<T, PrimaryKey>;
|
|
70
|
+
export type SensitiveKeys<T> = KeysCarrying<T, Sensitive>;
|
|
71
|
+
export type UniqueKeys<T> = KeysCarrying<T, Unique>;
|
|
72
|
+
/** The entity-level soft-delete tag names one managed column. */
|
|
73
|
+
export type SoftDeleteKeys<T> = T extends SoftDelete<infer Column> ? Extract<Column, ColumnKeys<T>> : never;
|
|
74
|
+
/** Columns whose declared type admits `null`. Native, not a tag. */
|
|
75
|
+
export type NullableKeys<T> = { [K in keyof T]-?: null extends T[K] ? (K extends string ? K : never) : never }[keyof T];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Properties declared with a relation tag: a join target, not a column.
|
|
79
|
+
*
|
|
80
|
+
* These have to come out of `Entity<T>`, and therefore out of everything derived
|
|
81
|
+
* from it. A relation left in would be a column to `INSERT`, a column to `SELECT`
|
|
82
|
+
* and a JSON Schema property, none of which it is. `./query.ts` puts them back where
|
|
83
|
+
* they belong, in `Populated<T, K>`.
|
|
84
|
+
*/
|
|
85
|
+
export type RelationKeys<T> = KeysCarrying<T, AnyRelation>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Everything that is a column: a data key that is not a relation.
|
|
89
|
+
*
|
|
90
|
+
* Spelled as its own projection rather than as a subtraction. `Exclude<AllKeys<T>,
|
|
91
|
+
* RelationKeys<T>>` does not compile: for an unresolved `T` both operands normalise to
|
|
92
|
+
* the same deferred expression, so the subtraction yields `never`, and then
|
|
93
|
+
* `Pick<Entity<T>, DefaultKeys<T>>` is an error because nothing is a key of an entity
|
|
94
|
+
* with no keys.
|
|
95
|
+
*/
|
|
96
|
+
export type ColumnKeys<T> = {
|
|
97
|
+
[K in keyof T]-?: NonNullable<T[K]> extends AnyRelation ? never : K extends string ? K : never;
|
|
98
|
+
}[keyof T];
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// The DTO suite.
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The selectable row: every column, required, sensitive columns included, tags
|
|
106
|
+
* preserved. Tags must survive here or the constraints would not survive `Omit`
|
|
107
|
+
* and `Partial` downstream.
|
|
108
|
+
*
|
|
109
|
+
* Relations are not columns and are not here. See `RelationKeys`.
|
|
110
|
+
*/
|
|
111
|
+
export type Entity<T extends DeclaredTable> = { -readonly [K in ColumnKeys<T>]-?: T[K] };
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* A tag filter's keys, narrowed to the keys `Entity<T>` actually has.
|
|
115
|
+
*
|
|
116
|
+
* `SerialKeys<T>` and friends are subsets of `ColumnKeys<T>` by construction — a
|
|
117
|
+
* relation cannot carry `Serial` — but TypeScript will not take it on trust. Relating
|
|
118
|
+
* two of these projections for an unresolved `T` works only while the *target*'s
|
|
119
|
+
* template is unconditional, which `DataKeys<T>` was and `ColumnKeys<T>` is not. So the
|
|
120
|
+
* subset is stated as an intersection, which is assignable to either side by
|
|
121
|
+
* definition, rather than proved. Nothing changes for a concrete type.
|
|
122
|
+
*/
|
|
123
|
+
type AsColumns<T extends DeclaredTable, K> = K & keyof Entity<T>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Insert shape. `Serial` columns are **absent** — naming one is a compile error,
|
|
127
|
+
* because the database generates the value. `HasDefault` columns are **present
|
|
128
|
+
* and optional**, because supplying one is legitimate. That distinction is the
|
|
129
|
+
* whole reason the two tags are separate.
|
|
130
|
+
*
|
|
131
|
+
* A nullable column is optional for the same reason a defaulted one is: omitting it
|
|
132
|
+
* inserts `NULL`, which is exactly what passing `null` does, so demanding the key adds
|
|
133
|
+
* ceremony and no information. The published document has always said this — a nullable
|
|
134
|
+
* column has never appeared in a `create` document's `required` — and so did the
|
|
135
|
+
* repository's runtime check. Requiring `bio: null` in the type while the contract said
|
|
136
|
+
* it was optional meant a client that followed the contract wrote a payload the type
|
|
137
|
+
* rejected, which is the disagreement this phase exists to remove. It stays *present* and
|
|
138
|
+
* optional rather than absent, because passing `null` explicitly is legitimate.
|
|
139
|
+
*/
|
|
140
|
+
type OptionalCreateKeys<T extends DeclaredTable> = Exclude<DefaultKeys<T> | NullableKeys<T>, SoftDeleteKeys<T>>;
|
|
141
|
+
|
|
142
|
+
export type CreateDTO<T extends DeclaredTable> = Omit<
|
|
143
|
+
Entity<T>,
|
|
144
|
+
SerialKeys<T> | DefaultKeys<T> | NullableKeys<T> | SoftDeleteKeys<T>
|
|
145
|
+
> &
|
|
146
|
+
Partial<Pick<Entity<T>, AsColumns<T, OptionalCreateKeys<T>>>>;
|
|
147
|
+
|
|
148
|
+
/** Patch shape: identity and framework-managed columns dropped, everything else optional. */
|
|
149
|
+
export type UpdateDTO<T extends DeclaredTable> = Partial<
|
|
150
|
+
Omit<Entity<T>, SerialKeys<T> | PrimaryKeyKeys<T> | SoftDeleteKeys<T>>
|
|
151
|
+
>;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* What a read endpoint may return. `Sensitive` columns are removed from the
|
|
155
|
+
* type, so a leak is a compile error rather than a serializer's responsibility.
|
|
156
|
+
*/
|
|
157
|
+
export type ReadDTO<T extends DeclaredTable> = Omit<Entity<T>, SensitiveKeys<T>>;
|
|
158
|
+
|
|
159
|
+
type IsUnion<T, U = T> = T extends unknown ? ([U] extends [T] ? false : true) : never;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The key value: a scalar for a single-column key, an object map for a composite
|
|
163
|
+
* one, `unknown` when the type declares no primary key.
|
|
164
|
+
*
|
|
165
|
+
* Named `PrimaryKeyOf` so the tag can be `PrimaryKey`, which is the name typed at
|
|
166
|
+
* every declaration site (plan D1).
|
|
167
|
+
*/
|
|
168
|
+
export type PrimaryKeyOf<T extends DeclaredTable> = [PrimaryKeyKeys<T>] extends [never]
|
|
169
|
+
? unknown
|
|
170
|
+
: IsUnion<PrimaryKeyKeys<T>> extends true
|
|
171
|
+
? { [K in PrimaryKeyKeys<T>]: Entity<T>[AsColumns<T, K>] }
|
|
172
|
+
: Entity<T>[AsColumns<T, PrimaryKeyKeys<T>>];
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// The wire shape (plan D3).
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
//
|
|
178
|
+
// A column has three types: wire (what arrives over HTTP), app (what handler code
|
|
179
|
+
// sees) and db (what the dialect declares). `Entity<T>` is the app type. `Wire<T>`
|
|
180
|
+
// is what a JSON body actually contains, which for a timestamp is an ISO-8601
|
|
181
|
+
// string, never a `Date` — a `Date` cannot survive JSON. The web pipeline decodes
|
|
182
|
+
// wire → app once at the boundary so handlers keep seeing `Date`.
|
|
183
|
+
//
|
|
184
|
+
// A column whose type the library does not know says its own wire form with
|
|
185
|
+
// `WireAs<W>`, and that beats the SQL-type rules: a codec can put anything it likes on
|
|
186
|
+
// the wire, and only the declaration knows what.
|
|
187
|
+
|
|
188
|
+
// A column's nullability belongs to the column, not to the layer: a `timestamp | null`
|
|
189
|
+
// is a `string | null` on the wire. Factored out so each layer rule below is one line
|
|
190
|
+
// and the three cannot drift apart.
|
|
191
|
+
type OrNull<V, W> = null extends V ? W | null : W;
|
|
192
|
+
|
|
193
|
+
type WireValue<V> =
|
|
194
|
+
NonNullable<V> extends WireAs<infer W>
|
|
195
|
+
? OrNull<V, W>
|
|
196
|
+
: NonNullable<V> extends Sql<'timestamp'>
|
|
197
|
+
? OrNull<V, string>
|
|
198
|
+
: NonNullable<V> extends Sql<'bigint'>
|
|
199
|
+
? OrNull<V, string>
|
|
200
|
+
: V;
|
|
201
|
+
|
|
202
|
+
/** The over-the-wire shape of an entity: JSON-representable throughout. */
|
|
203
|
+
export type Wire<T> = { -readonly [K in ColumnKeys<T>]-?: WireValue<T[K]> };
|
|
204
|
+
|
|
205
|
+
/** The over-the-wire shape of an insert payload. */
|
|
206
|
+
export type WireCreateDTO<T extends DeclaredTable> = { [K in keyof CreateDTO<T>]: WireValue<CreateDTO<T>[K]> };
|
|
207
|
+
|
|
208
|
+
// The read/query surface — `WhereDTO`, `OrderByDTO`, `PaginationDTO`, `Projection`,
|
|
209
|
+
// `GetDTO`, `ListDTO`, `Populated`, `JoinRow` — is in `./query.ts`, re-exported here
|
|
210
|
+
// so `@zmdb/schema/derive` is one import.
|
|
211
|
+
export type {
|
|
212
|
+
GetDTO,
|
|
213
|
+
GetOptions,
|
|
214
|
+
JoinRow,
|
|
215
|
+
ListDTO,
|
|
216
|
+
ListResult,
|
|
217
|
+
OrderByDTO,
|
|
218
|
+
PaginationDTO,
|
|
219
|
+
Populated,
|
|
220
|
+
PopulatedEntity,
|
|
221
|
+
Projection,
|
|
222
|
+
RelationPath,
|
|
223
|
+
WhereDTO,
|
|
224
|
+
} from './query.js';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @zmdb/schema/derive/query — the relation-aware half of the read surface.
|
|
2
|
+
//
|
|
3
|
+
// This file used to restate the whole query DTO family, because there were two of them:
|
|
4
|
+
// `../dto/index.ts` was keyed by the schema value and these were keyed by the declared
|
|
5
|
+
// type. There is one family now — `WhereDTO`, `OrderByDTO`, `PaginationDTO`, `Projection`,
|
|
6
|
+
// `GetOptions`, `GetDTO` and `ListDTO` all take the declared type, and they live in
|
|
7
|
+
// `../dto/index.ts` alongside the operator vocabulary and the runtime folders that read
|
|
8
|
+
// them. Two copies of `WhereDTO` were two operator sets to keep in step; the copy is gone
|
|
9
|
+
// and the names below are re-exports, so importing from either path is the same type.
|
|
10
|
+
//
|
|
11
|
+
// What is genuinely *here* is the three shapes that need a relation: a populated row and
|
|
12
|
+
// the two spellings of a join row. Those read `RelationKeys<T>` and the relation tags off
|
|
13
|
+
// the declaration, which is something only a declared type can answer — a schema value
|
|
14
|
+
// carries no relations at all, which is why the version of `Populated` this replaced had
|
|
15
|
+
// to rebuild the cardinality out of a `RelationMeta` through six nested conditionals.
|
|
16
|
+
|
|
17
|
+
import { type DeclaredTable, type Entity, type RelationKeys } from './index.js';
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
type GetDTO,
|
|
21
|
+
type GetOptions,
|
|
22
|
+
type ListDTO,
|
|
23
|
+
type ListResult,
|
|
24
|
+
type OrderByDTO,
|
|
25
|
+
type PaginationDTO,
|
|
26
|
+
type Projection,
|
|
27
|
+
type WhereDTO,
|
|
28
|
+
} from '../dto/index.js';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Relations.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
//
|
|
34
|
+
// `K & keyof T` throughout: `RelationKeys<T>` *is* a subset of `keyof T` by
|
|
35
|
+
// construction, but it is produced by a mapped-type projection and TypeScript does not
|
|
36
|
+
// carry that fact forward, so an unqualified `T[K]` does not compile.
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What a populated relation holds: fetched rows, and what happens when nothing matched.
|
|
40
|
+
*
|
|
41
|
+
* Cardinality comes from the declaration and nothing else, which is the point: `comments?:
|
|
42
|
+
* Comment[] & OneToMany<…>` is an array because it was written as one, and `author?: User &
|
|
43
|
+
* ManyToOne<…>` is not. The schema-value version had to read the cardinality out of a
|
|
44
|
+
* `RelationMeta` and rebuild the array, through six nested conditional types named
|
|
45
|
+
* `RelationEntityFromDef` and `RelationCardinalityFromDef`, because a relation *value* does
|
|
46
|
+
* not carry the target's type. Both are deleted.
|
|
47
|
+
*
|
|
48
|
+
* Without a nested path, the target becomes `Entity<>`: its own relations are absent.
|
|
49
|
+
* A nested path adds only its requested descendants, using this same derivation.
|
|
50
|
+
* And a to-one gains `| null`, because a foreign key that matches nothing is a row the
|
|
51
|
+
* database can hold — `null` is what the repository attaches for it, and a type that said
|
|
52
|
+
* `User` there would be wrong for the case the query cannot rule out. A to-many needs no
|
|
53
|
+
* such arm: no match is the empty array.
|
|
54
|
+
*/
|
|
55
|
+
type PopulatedValue<V, Paths extends string> =
|
|
56
|
+
NonNullable<V> extends readonly (infer E)[]
|
|
57
|
+
? readonly ([Paths] extends [never] ? Entity<E & DeclaredTable> : PopulatedRow<E & DeclaredTable, Paths>)[]
|
|
58
|
+
:
|
|
59
|
+
| ([Paths] extends [never]
|
|
60
|
+
? Entity<NonNullable<V> & DeclaredTable>
|
|
61
|
+
: PopulatedRow<NonNullable<V> & DeclaredTable, Paths>)
|
|
62
|
+
| null;
|
|
63
|
+
|
|
64
|
+
/** Validate a supplied path without expanding every path of a recursive entity graph. */
|
|
65
|
+
export type RelationPath<T extends DeclaredTable, Path extends string> = Path extends `${infer Head}.${infer Tail}`
|
|
66
|
+
? Head extends RelationKeys<T>
|
|
67
|
+
? `${Head}.${RelationPath<RelationTargetOf<T[Head & keyof T]>, Tail>}`
|
|
68
|
+
: never
|
|
69
|
+
: Path extends RelationKeys<T>
|
|
70
|
+
? Path
|
|
71
|
+
: never;
|
|
72
|
+
|
|
73
|
+
type PathHead<Path extends string> = Path extends `${infer Head}.${string}` ? Head : Path;
|
|
74
|
+
type PathTail<Path extends string, Head extends string> = Path extends `${Head}.${infer Tail}` ? Tail : never;
|
|
75
|
+
type RootRelationPath<T> = (RelationKeys<T> & string) | `${RelationKeys<T> & string}.${string}`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The row plus the relation paths named by `K`, populated.
|
|
79
|
+
*
|
|
80
|
+
* Dotted paths contribute their first segment to this row and pass their remaining
|
|
81
|
+
* segments to the child. Keeping direct keys in the mapped key set also lets a generic
|
|
82
|
+
* one-level relation loader index the result with its declared relation key.
|
|
83
|
+
*/
|
|
84
|
+
type PopulatedRow<T extends DeclaredTable, K extends string> = Entity<T> & {
|
|
85
|
+
-readonly [P in (K | PathHead<K>) & keyof T]: PopulatedValue<T[P], PathTail<K, P & string>>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type PopulatedEntity<
|
|
89
|
+
T extends DeclaredTable,
|
|
90
|
+
K extends RootRelationPath<T> = RelationKeys<T> & string,
|
|
91
|
+
> = PopulatedRow<T, K> & ([Exclude<K, RelationPath<T, K>>] extends [never] ? unknown : never);
|
|
92
|
+
|
|
93
|
+
/** Alias kept because both names are in use across the repository and the docs. */
|
|
94
|
+
export type Populated<
|
|
95
|
+
T extends DeclaredTable,
|
|
96
|
+
K extends RootRelationPath<T> = RelationKeys<T> & string,
|
|
97
|
+
> = PopulatedEntity<T, K>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The target of a relation: the element type for a to-many, the type itself for a to-one.
|
|
101
|
+
*
|
|
102
|
+
* `& DeclaredTable` on the way out rather than a constraint on the way in. A relation is
|
|
103
|
+
* declared as `Comment[] & OneToMany<…>`, so the element type is whatever the author wrote
|
|
104
|
+
* there and TypeScript cannot be told in advance that it is a table — but `Entity<>` wants
|
|
105
|
+
* to know, and intersecting is assignable by definition where proving is not.
|
|
106
|
+
*/
|
|
107
|
+
type RelationTargetOf<V> = (NonNullable<V> extends readonly (infer E)[] ? E : NonNullable<V>) & DeclaredTable;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One row of a join: the base row's columns and the target's, flat.
|
|
111
|
+
*
|
|
112
|
+
* A `LEFT JOIN` can produce a row with no match, so the joined half is `Partial`. An
|
|
113
|
+
* `INNER JOIN` cannot, so it is not — and that asymmetry is the whole reason `Kind` is
|
|
114
|
+
* a parameter rather than always-partial.
|
|
115
|
+
*
|
|
116
|
+
* `../relations/index.ts` exports the same asymmetry as `JoinRow<Base, Joined, Kind>`, for
|
|
117
|
+
* the join that names its target directly instead of by a relation. Two shapes, because they
|
|
118
|
+
* take different arguments: that one is given both tables, this one is given a base table and
|
|
119
|
+
* the name of one of its relations. The conditional itself is two lines and duplicating it is
|
|
120
|
+
* cheaper than a shared helper neither would read more clearly.
|
|
121
|
+
*/
|
|
122
|
+
export type JoinRow<
|
|
123
|
+
T extends DeclaredTable,
|
|
124
|
+
K extends RelationKeys<T>,
|
|
125
|
+
Kind extends 'inner' | 'left' = 'left',
|
|
126
|
+
> = Kind extends 'inner'
|
|
127
|
+
? Entity<T> & Entity<RelationTargetOf<T[K & keyof T]>>
|
|
128
|
+
: Entity<T> & Partial<Entity<RelationTargetOf<T[K & keyof T]>>>;
|