@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.
Files changed (68) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +30 -0
  3. package/dist/custom-types/index.d.ts +41 -0
  4. package/dist/custom-types/index.d.ts.map +1 -0
  5. package/dist/custom-types/index.js +32 -0
  6. package/dist/custom-types/index.js.map +1 -0
  7. package/dist/derive/index.d.ts +122 -0
  8. package/dist/derive/index.d.ts.map +1 -0
  9. package/dist/derive/index.js +13 -0
  10. package/dist/derive/index.js.map +1 -0
  11. package/dist/derive/query.d.ts +62 -0
  12. package/dist/derive/query.d.ts.map +1 -0
  13. package/dist/derive/query.js +18 -0
  14. package/dist/derive/query.js.map +1 -0
  15. package/dist/dto/index.d.ts +224 -0
  16. package/dist/dto/index.d.ts.map +1 -0
  17. package/dist/dto/index.js +118 -0
  18. package/dist/dto/index.js.map +1 -0
  19. package/dist/entity-modeling/index.d.ts +12 -0
  20. package/dist/entity-modeling/index.d.ts.map +1 -0
  21. package/dist/entity-modeling/index.js +28 -0
  22. package/dist/entity-modeling/index.js.map +1 -0
  23. package/dist/index.d.ts +151 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +84 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/ir/index.d.ts +374 -0
  28. package/dist/ir/index.d.ts.map +1 -0
  29. package/dist/ir/index.js +735 -0
  30. package/dist/ir/index.js.map +1 -0
  31. package/dist/ir/validation-shape.d.ts +46 -0
  32. package/dist/ir/validation-shape.d.ts.map +1 -0
  33. package/dist/ir/validation-shape.js +130 -0
  34. package/dist/ir/validation-shape.js.map +1 -0
  35. package/dist/ir/vocabulary.d.ts +54 -0
  36. package/dist/ir/vocabulary.d.ts.map +1 -0
  37. package/dist/ir/vocabulary.js +51 -0
  38. package/dist/ir/vocabulary.js.map +1 -0
  39. package/dist/naming/index.d.ts +26 -0
  40. package/dist/naming/index.d.ts.map +1 -0
  41. package/dist/naming/index.js +147 -0
  42. package/dist/naming/index.js.map +1 -0
  43. package/dist/openapi/index.d.ts +57 -0
  44. package/dist/openapi/index.d.ts.map +1 -0
  45. package/dist/openapi/index.js +98 -0
  46. package/dist/openapi/index.js.map +1 -0
  47. package/dist/relations/index.d.ts +23 -0
  48. package/dist/relations/index.d.ts.map +1 -0
  49. package/dist/relations/index.js +98 -0
  50. package/dist/relations/index.js.map +1 -0
  51. package/dist/tags/index.d.ts +261 -0
  52. package/dist/tags/index.d.ts.map +1 -0
  53. package/dist/tags/index.js +64 -0
  54. package/dist/tags/index.js.map +1 -0
  55. package/package.json +82 -0
  56. package/src/custom-types/index.ts +59 -0
  57. package/src/derive/index.ts +224 -0
  58. package/src/derive/query.ts +128 -0
  59. package/src/dto/index.ts +395 -0
  60. package/src/entity-modeling/index.ts +33 -0
  61. package/src/index.ts +263 -0
  62. package/src/ir/index.ts +1085 -0
  63. package/src/ir/validation-shape.ts +145 -0
  64. package/src/ir/vocabulary.ts +56 -0
  65. package/src/naming/index.ts +159 -0
  66. package/src/openapi/index.ts +133 -0
  67. package/src/relations/index.ts +134 -0
  68. 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';