@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/src/index.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// @zmdb/schema — the vocabulary a table is described in, and the derivations
|
|
2
|
+
// every other package reads off it.
|
|
3
|
+
//
|
|
4
|
+
// There is no builder DSL any more — `defineSchema`, the ten column builders and the
|
|
5
|
+
// eight function-style modifiers were deleted with the last of their callers, and
|
|
6
|
+
// `schemaOf<T>()` is what produces a schema value now. A table is declared once, as a
|
|
7
|
+
// type, in the tags of `./tags`; `@zmdb/compiler` reflects that declaration into a
|
|
8
|
+
// `SchemaIR` and `schemaFromIR` turns the IR into the value the query compiler reads. So
|
|
9
|
+
// what is left in this file is the *data model* — `SqlType`, `ColumnFlags`, `ColumnMeta`,
|
|
10
|
+
// `CoreSchema` — plus the derived-type family and the type-level assertion helpers.
|
|
11
|
+
//
|
|
12
|
+
// The derived types have one spelling each, and it takes the declared type. See the
|
|
13
|
+
// DTO suite below.
|
|
14
|
+
|
|
15
|
+
// The IR bindings below are types. With verbatimModuleSyntax, the named import
|
|
16
|
+
// clause remains an empty runtime import of the IR module.
|
|
17
|
+
import { type ExtensionType, type SchemaIR } from './ir/index.js';
|
|
18
|
+
|
|
19
|
+
export type SqlType =
|
|
20
|
+
| 'serial'
|
|
21
|
+
| 'integer'
|
|
22
|
+
| 'bigint'
|
|
23
|
+
| 'numeric'
|
|
24
|
+
| 'text'
|
|
25
|
+
| 'varchar'
|
|
26
|
+
| 'boolean'
|
|
27
|
+
| 'timestamp'
|
|
28
|
+
| 'json'
|
|
29
|
+
| 'jsonEnum';
|
|
30
|
+
|
|
31
|
+
export interface ValidationRule {
|
|
32
|
+
readonly kind: string;
|
|
33
|
+
readonly value?: unknown;
|
|
34
|
+
readonly message?: string;
|
|
35
|
+
/**
|
|
36
|
+
* The argument list, for a rule that came from `@zmdb/validator`'s runtime `tags`.
|
|
37
|
+
*
|
|
38
|
+
* Declared rather than tolerated: `ir/index.ts`'s `ruleArgument` has always read it,
|
|
39
|
+
* and `openapi.spec.ts` has always passed one, so leaving it off the type meant the
|
|
40
|
+
* only two writers of this field disagreed with its declaration.
|
|
41
|
+
*/
|
|
42
|
+
readonly args?: readonly unknown[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Optional members admit `undefined` explicitly: under `exactOptionalPropertyTypes` a
|
|
46
|
+
// flag map rebuilt by a mapped type over a type *parameter* resolves each member to
|
|
47
|
+
// `F[K]` (i.e. `boolean | undefined`), which a bare `?: boolean` rejects. The builder
|
|
48
|
+
// chain that first forced this is gone, but `columnMetaFromIR` builds a flag map the same
|
|
49
|
+
// way — it sets only the flags the IR asserts — so the looser member type is still what
|
|
50
|
+
// makes the result assignable.
|
|
51
|
+
//
|
|
52
|
+
// `nullable` is required rather than optional, and that asymmetry is deliberate: every
|
|
53
|
+
// other flag is a fact a declaration opts into, while nullability is a fact every column
|
|
54
|
+
// has one way or the other. A missing `nullable` would read as "not stated", and there is
|
|
55
|
+
// nothing for that to mean.
|
|
56
|
+
export interface ColumnFlags {
|
|
57
|
+
readonly nullable: boolean;
|
|
58
|
+
readonly primaryKey?: boolean | undefined;
|
|
59
|
+
readonly unique?: boolean | undefined;
|
|
60
|
+
readonly autoIncrement?: boolean | undefined;
|
|
61
|
+
readonly hasDefault?: boolean | undefined;
|
|
62
|
+
readonly length?: number | undefined;
|
|
63
|
+
readonly enum?: readonly string[] | undefined;
|
|
64
|
+
readonly sensitive?: boolean | undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ColumnMeta {
|
|
68
|
+
readonly type: SqlType | ExtensionType;
|
|
69
|
+
readonly flags: ColumnFlags;
|
|
70
|
+
readonly default?: unknown;
|
|
71
|
+
readonly references?: { readonly target: string };
|
|
72
|
+
readonly validation?: readonly ValidationRule[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type ColumnsMap = Readonly<Record<string, ColumnMeta>>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A schema value: a table described in physical SQL names, for the code that runs.
|
|
79
|
+
*
|
|
80
|
+
* There is no type parameter for the column map, and its absence is the point. It used to
|
|
81
|
+
* carry the *literal* map so that `Entity<S>` could read property types out of it; every
|
|
82
|
+
* derivation now takes the declared type instead, so a literal map would be a parameter
|
|
83
|
+
* nothing reads. `table`, `columns`, `primaryKey` and the local side of `references` are
|
|
84
|
+
* in database vocabulary. The carried IR keeps declared names for derived types,
|
|
85
|
+
* validation, payloads and result aliases.
|
|
86
|
+
*/
|
|
87
|
+
export interface CoreSchema<T extends string = string> {
|
|
88
|
+
readonly table: T;
|
|
89
|
+
readonly columns: ColumnsMap;
|
|
90
|
+
readonly primaryKey: readonly string[];
|
|
91
|
+
readonly references: readonly { readonly column: string; readonly target: string }[];
|
|
92
|
+
readonly ftsTable?: string | boolean | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* The IR this value was built from, carried rather than recomputed.
|
|
95
|
+
*
|
|
96
|
+
* Required, and that is the point. `columns` is a lossy projection: a `ColumnMeta` has
|
|
97
|
+
* a `SqlType` and a flag map, and it has nowhere to put a json payload's shape, a
|
|
98
|
+
* constraint's arguments, or the difference between a column that is `integer + Serial`
|
|
99
|
+
* and one that is merely `integer`. There used to be an `irFromSchema` that guessed
|
|
100
|
+
* those back — four walkers over column metadata, each reconstructing what the
|
|
101
|
+
* declaration had already said, each with its own idea of the answer. Every one of them
|
|
102
|
+
* now reads this field, so the DDL, the validator, the JSON Schema and the seeder are
|
|
103
|
+
* looking at the same bytes.
|
|
104
|
+
*
|
|
105
|
+
* `schemaFromIR(schema.ir)` is `schema` (see `ir.spec.ts`), which is what makes the
|
|
106
|
+
* field safe to depend on: it holds everything the value does, so nothing has to choose
|
|
107
|
+
* between them.
|
|
108
|
+
*/
|
|
109
|
+
readonly ir: SchemaIR;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// The generated schema value (REQ-TF-10)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
declare const zmdbEntity: unique symbol;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A schema value that remembers the type it was generated from.
|
|
120
|
+
*
|
|
121
|
+
* The query compiler wants the table and the column types as data, so a tagged
|
|
122
|
+
* declaration still has to become a `CoreSchema` — but a `CoreSchema` has erased which
|
|
123
|
+
* type it came from, and a column map cannot be read back into one: it has a `SqlType`
|
|
124
|
+
* and a flag bag, and nowhere to put a json payload's shape. This phantom keeps the
|
|
125
|
+
* answer instead of reconstructing it. `schemaOf<User>()` is a `TaggedSchema<User>`, so
|
|
126
|
+
* anything holding the value can recover `User` and derive from the declaration.
|
|
127
|
+
*
|
|
128
|
+
* The slot is a `unique symbol` like every tag in `./tags`, and for the same reason:
|
|
129
|
+
* un-forgeable, and it erases, so no generated literal carries it at runtime. It is
|
|
130
|
+
* *required* rather than optional, which is what makes it inferrable: a function that
|
|
131
|
+
* wants the declared type asks for a `TaggedSchema<T>` and gets `T` from the argument,
|
|
132
|
+
* which is how `defineRepository`, `defineEntityStateMachine` and `findJoined` are
|
|
133
|
+
* parameterised on a declaration while still being handed a value.
|
|
134
|
+
*/
|
|
135
|
+
export interface TaggedSchema<T> extends CoreSchema<string> {
|
|
136
|
+
readonly [zmdbEntity]: T;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The schema value for a tagged type, generated at build time.
|
|
141
|
+
*
|
|
142
|
+
* The only way to get one, and it declares nothing: `User` already says the table, the
|
|
143
|
+
* columns, the keys and the constraints, so this asks for that declaration as data.
|
|
144
|
+
* `@zmdb/compiler` replaces the call with a frozen literal — `schemaFromIR` applied
|
|
145
|
+
* to the IR it read off `T` — so the schema is written exactly once, in the type.
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* const users = defineRepository(schemaOf<User>(), driver);
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* There is no runtime implementation and cannot be one, for the same reason
|
|
152
|
+
* `toJsonSchema<T>()` has none: the answer is a function of a type argument, and type
|
|
153
|
+
* arguments do not survive to runtime. A build that did not run the transform gets an
|
|
154
|
+
* error saying so rather than a plausible-looking empty schema.
|
|
155
|
+
*/
|
|
156
|
+
export function schemaOf<T>(): TaggedSchema<T> {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'schemaOf<T>() was not replaced at build time. It is compiled away by @zmdb/compiler ' +
|
|
159
|
+
'(the unplugin, Metro adapter, or project compiler), which did not run over this file — a type argument cannot ' +
|
|
160
|
+
'be read at runtime, so there is nothing to fall back to.',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// The DTO suite (REQ-TF-4)
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
//
|
|
168
|
+
// Re-exported, not defined. `./derive` reads the declared type, and there is no second
|
|
169
|
+
// spelling any more: each of these used to have a column-map twin here, with every
|
|
170
|
+
// derivation choosing between them by asking `S extends TaggedSchema<infer T>`.
|
|
171
|
+
//
|
|
172
|
+
// Both halves of that had to go. The column-map walk could not answer some of the
|
|
173
|
+
// questions — a `json` column came out `unknown`, because a payload's shape is a type
|
|
174
|
+
// and a `ColumnMeta` has nowhere to put one — so the two branches did not merely differ
|
|
175
|
+
// in spelling, they differed in what they knew. And the dispatch meant every derivation
|
|
176
|
+
// asked a question about its argument before it could begin, which is the inversion this
|
|
177
|
+
// design exists to remove: the declaration is the source, and a value generated from it
|
|
178
|
+
// is downstream.
|
|
179
|
+
//
|
|
180
|
+
// What takes the place of the dispatch is inference, once, at the boundary where a value
|
|
181
|
+
// actually arrives: a function that is handed a generated schema declares the parameter
|
|
182
|
+
// `TaggedSchema<T>` and gets `T` from it (`defineRepository`, `findJoined`,
|
|
183
|
+
// `defineEntityStateMachine`, `repositoryToken`). Everything after that point is
|
|
184
|
+
// parameterised on the declared type. `schema-of.type-test.ts` pins the crossing.
|
|
185
|
+
export {
|
|
186
|
+
type CreateDTO,
|
|
187
|
+
type DeclaredTable,
|
|
188
|
+
type Entity,
|
|
189
|
+
type PrimaryKeyOf,
|
|
190
|
+
type ReadDTO,
|
|
191
|
+
type UpdateDTO,
|
|
192
|
+
} from './derive/index.js';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* True for a non-null, non-array object.
|
|
196
|
+
*
|
|
197
|
+
* Lives here (the DAG root) because every downstream package needs the same
|
|
198
|
+
* proof before a keyed read: without it, reading `value[key]` off an `unknown`
|
|
199
|
+
* requires `as Record<string, unknown>`, and those casts are exactly what
|
|
200
|
+
* ARCHITECTURE §2.1 forbids on the public surface.
|
|
201
|
+
*/
|
|
202
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
203
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Type-level assertion utilities
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Used by the `*.type-test.ts` files next to each module. Those files contain no
|
|
210
|
+
// runtime code and are never executed: they are *compiled*, and a broken derived
|
|
211
|
+
// type is a typecheck failure like any other.
|
|
212
|
+
//
|
|
213
|
+
// Why not `expectTypeOf` from vitest? Because vitest only ever *runs* those files
|
|
214
|
+
// — `expectTypeOf(...)` is a no-op at runtime and `@ts-expect-error` is inert, so
|
|
215
|
+
// every such assertion in a `.spec.ts` was decoration, not a gate — doubly so
|
|
216
|
+
// while the package tsconfigs still excluded `**/*.spec.ts`, which also made the
|
|
217
|
+
// `@ts-expect-error` directives in them unchecked. Specs are inside the program
|
|
218
|
+
// now, and assertions written with `Expect`/`Equal` are enforced by
|
|
219
|
+
// `yarn typecheck`, which is what CI runs.
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Invariant type equality. Stricter than mutual assignability: it distinguishes
|
|
223
|
+
* `any`/`unknown`/`never` and does not collapse optional vs `| undefined`.
|
|
224
|
+
*/
|
|
225
|
+
export type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
|
|
226
|
+
|
|
227
|
+
/** One-way assignability, for "at least this shape" assertions. */
|
|
228
|
+
export type Extends<A, B> = [A] extends [B] ? true : false;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Two-way assignability: each side accepts the other, differences in intersection
|
|
232
|
+
* spelling and union ordering included.
|
|
233
|
+
*
|
|
234
|
+
* This is the right tool for asserting what a derived type *means* when the columns
|
|
235
|
+
* are tagged. `Entity<User>['email']` is `string & Sql<'text'>`, not `string`, because
|
|
236
|
+
* a tag survives every derivation on purpose (that is how a projection or an aggregate
|
|
237
|
+
* still knows the column's SQL type). `Equal` sees two different types there and is
|
|
238
|
+
* correct to; `Mutual` sees that either can be used where the other is expected, which
|
|
239
|
+
* is the claim such a test is usually making. Pair it with `Equal<keyof A, keyof B>`
|
|
240
|
+
* when the key set and optionality matter too — assignability alone does not pin those.
|
|
241
|
+
*/
|
|
242
|
+
export type Mutual<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
|
|
243
|
+
|
|
244
|
+
/** Assert a type-level predicate. `Expect<Equal<X, Y>>` fails to compile if X ≠ Y. */
|
|
245
|
+
export type Expect<T extends true> = T;
|
|
246
|
+
|
|
247
|
+
/** Negative form: `ExpectNot<Equal<X, Y>>` fails to compile if X = Y. */
|
|
248
|
+
export type ExpectNot<T extends false> = T;
|
|
249
|
+
|
|
250
|
+
// Relations: resolution and the two row helpers. `Populated`/`PopulatedEntity` come from
|
|
251
|
+
// `./derive`, which reads the relation off the declared type — there is no relations map to
|
|
252
|
+
// derive them from any more, and no `manyToOne`/`oneToMany`/`oneToOne`/`manyToMany` builder
|
|
253
|
+
// to write one with.
|
|
254
|
+
export { resolveRelation } from './relations/index.js';
|
|
255
|
+
export { type ResolvedRelation } from './relations/index.js';
|
|
256
|
+
export { type Populated, type PopulatedEntity } from './derive/index.js';
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Entity State Machine & State Transition Helpers
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
export { type WhereDTO, type ListDTO, type ListResult, type OrderByDTO, type PaginationDTO } from './dto/index.js';
|
|
263
|
+
export { buildListResult } from './dto/index.js';
|