@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
@@ -0,0 +1,284 @@
1
+ // @zmdb/schema/tags — the type-first declaration vocabulary.
2
+ //
3
+ // A domain type is declared as a plain interface plus these tags; everything else,
4
+ // the DTOs, the validators, the JSON Schema, the SQL — is derived from it.
5
+ //
6
+ // interface User extends Table<'users'> {
7
+ // id: number & Sql<'integer'> & Serial & PrimaryKey;
8
+ // email: string & Sql<'varchar'> & Length<255> & Unique;
9
+ // nickname: (string & MinLength<3>) | null;
10
+ // }
11
+ //
12
+ // Every established tag is an OPTIONAL unique-symbol slot, and all three parts carry
13
+ // weight:
14
+ //
15
+ // - `unique symbol` is un-forgeable and cannot collide with a real data
16
+ // property of the same name. A consumer cannot accidentally (or deliberately)
17
+ // synthesise `Serial` by typing a property.
18
+ // - `?` means no runtime value is ever required, so the tag erases completely.
19
+ // Zero bytes reach the output — asserted by `erasure.spec.ts`, which compiles
20
+ // a tagged declaration and its untagged twin and compares the emitted bytes.
21
+ // - an all-optional (weak) object type is not assignable from an unrelated
22
+ // type, which is what lets `T[K] extends Serial ? K : never` give an exact
23
+ // answer rather than a false positive.
24
+ //
25
+ // A tag only has to NAME a constraint, never prove it. There are no conditional
26
+ // types, no recursion and no template-literal arithmetic in this file, which is
27
+ // what makes "zero type-level computation" true by construction instead of by
28
+ // discipline.
29
+ //
30
+ // There is deliberately NO tag for nullability, optionality, enums, arrays,
31
+ // readonly-ness or nested JSON shape: TypeScript already expresses those as
32
+ // `| null`, `?`, a literal union, `T[]`, `readonly` and a nested interface, and
33
+ // the reflection reads them off the type directly.
34
+ //
35
+ // A note on duplicate installs (plan D5).
36
+ //
37
+ // `unique symbol` identity is nominal, so two *copies* of this module produce two
38
+ // non-matching tags even though their source text is identical. A key filter then
39
+ // collapses to `never`, and `never` is assignable to anything, so `CreateDTO`
40
+ // silently stops omitting a serial column and starts requiring it. Reflection is
41
+ // name-based and therefore immune, which would make the emitted validator disagree
42
+ // with the derived type — that asymmetry is the real hazard, not the duplicate
43
+ // itself.
44
+ //
45
+ // Nothing in this file can guard against it: a runtime check here would give the
46
+ // tags a runtime cost, which is the one thing they must not have. The guard belongs
47
+ // in the reflection, which can see the escaped symbol ids (`__@zmdbSerial@1` vs
48
+ // `__@zmdbSerial@12`) that the type system distinguishes, and refuses the build
49
+ // when one tag name resolves to two declarations. Until that lands,
50
+ // `duplicate-install.type-test.ts` pins the failure mode exactly — using `Equal`
51
+ // throughout, never assignability, for the reason given there.
52
+
53
+ import { type SqlType } from '../index.js';
54
+ import { type ProtoScalar, type ReferentialAction, type RelationKind } from '../ir/index.js';
55
+
56
+ declare const zmdbTable: unique symbol;
57
+ declare const zmdbPhysical: unique symbol;
58
+ declare const zmdbFts: unique symbol;
59
+ declare const zmdbShardKey: unique symbol;
60
+ declare const zmdbSortKey: unique symbol;
61
+ declare const zmdbRowstore: unique symbol;
62
+ declare const zmdbSoftDelete: unique symbol;
63
+ declare const zmdbSqlType: unique symbol;
64
+ declare const zmdbPrimaryKey: unique symbol;
65
+ declare const zmdbSerial: unique symbol;
66
+ declare const zmdbUnique: unique symbol;
67
+ declare const zmdbDefault: unique symbol;
68
+ declare const zmdbSensitive: unique symbol;
69
+ declare const zmdbReferences: unique symbol;
70
+ declare const zmdbOnDelete: unique symbol;
71
+ declare const zmdbOnUpdate: unique symbol;
72
+ declare const zmdbForeignKey: unique symbol;
73
+ declare const zmdbLength: unique symbol;
74
+ declare const zmdbNumeric: unique symbol;
75
+ declare const zmdbCodec: unique symbol;
76
+ declare const zmdbWire: unique symbol;
77
+ declare const zmdbRelation: unique symbol;
78
+ declare const zmdbMin: unique symbol;
79
+ declare const zmdbMax: unique symbol;
80
+ declare const zmdbMinLength: unique symbol;
81
+ declare const zmdbMaxLength: unique symbol;
82
+ declare const zmdbPattern: unique symbol;
83
+ declare const zmdbRule: unique symbol;
84
+ declare const zmdbProtoField: unique symbol;
85
+ declare const zmdbProtoScalar: unique symbol;
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Entity-level tags — applied to the interface itself, via `extends`.
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /** The table an entity maps to. `interface User extends Table<'users'>`. */
92
+ export type Table<Name extends string> = { readonly [zmdbTable]?: Name };
93
+
94
+ /**
95
+ * The physical SQL identifier when it must override the configured naming strategy.
96
+ *
97
+ * In interface position it names the table; in a property intersection it names the
98
+ * column. The reflector decides which from the position, so one zero-runtime tag covers
99
+ * both without changing the declaration's TypeScript-facing name.
100
+ */
101
+ export type Physical<Name extends string> = { readonly [zmdbPhysical]?: Name };
102
+
103
+ /** The full-text-search table backing this entity (`CoreSchema.ftsTable`). */
104
+ export type Fts<Name extends string | true> = { readonly [zmdbFts]?: Name };
105
+
106
+ /** SingleStore distribution columns, in declared order. */
107
+ export type ShardKey<Columns extends readonly string[]> = { readonly [zmdbShardKey]?: Columns };
108
+
109
+ /** SingleStore columnstore sort columns, in declared order. */
110
+ export type SortKey<Columns extends readonly string[]> = { readonly [zmdbSortKey]?: Columns };
111
+
112
+ /** Select SingleStore's row-oriented table storage instead of its columnstore default. */
113
+ export type Rowstore = { readonly [zmdbRowstore]?: true };
114
+
115
+ /** The nullable timestamp column managed by repository soft delete. */
116
+ export type SoftDelete<Column extends string> = { readonly [zmdbSoftDelete]?: Column };
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Column-level structural tags — facts the SQL layer needs that TypeScript
120
+ // cannot express. `Sql<T>` is the important one: `integer`, `bigint` and
121
+ // `numeric` are all `number` in TypeScript, so the column type has to be said.
122
+ // ---------------------------------------------------------------------------
123
+
124
+ /**
125
+ * The SQL column types a declaration may name: every `SqlType` except `serial`.
126
+ *
127
+ * `serial` is not a column type. Postgres documents it as notational convenience for
128
+ * an `integer` with a sequence default, and that is how it is spelled here —
129
+ * `number & Sql<'integer'> & Serial` — which makes `Serial` the single place the fact
130
+ * lives, the way `| null` is the single place nullability lives (REQ-TF-2).
131
+ *
132
+ * Saying it twice was not merely redundant, it was wrong, and the way it was wrong is
133
+ * worth recording because nothing about the tags predicts it. Tag payloads are
134
+ * invariant, so the old `Sql<'serial'>` spelling was not assignable to `Sql<'integer'>` and
135
+ * a value read out of a serial primary key could not be written into an `integer` foreign key.
136
+ * `orders.create({ userId: user.id })` — the most ordinary line in a relational model —
137
+ * did not compile. With `serial` off the vocabulary the tags on `id` are
138
+ * `Sql<'integer'> & Serial & PrimaryKey`, which drops to `Sql<'integer'>` on the way in,
139
+ * and it does.
140
+ *
141
+ * The residue is honest and much smaller: two columns whose SQL types genuinely differ
142
+ * still do not interchange, so assigning a `Sql<'varchar'>` value into a `Sql<'text'>`
143
+ * column needs a conversion at the boundary. That is a rarer thing to write than a
144
+ * foreign key, and unlike the serial case the two columns really are different types.
145
+ */
146
+ export type ColumnSqlType = Exclude<SqlType, 'serial'>;
147
+
148
+ /** The abstract SQL column type. Dialects render the spelling — see `../ir`. */
149
+ export type Sql<T extends ColumnSqlType> = { readonly [zmdbSqlType]?: T };
150
+
151
+ /**
152
+ * A column type supplied by a database extension.
153
+ *
154
+ * The installable extension and the SQL type are separate because PostGIS installs
155
+ * `postgis` and provides `geometry`. Arguments are type parameters rather than a SQL
156
+ * fragment, so reflection can validate and render each one without parsing SQL. This is
157
+ * the one structural marker in the vocabulary: the frozen IR contract names
158
+ * `__zmdbExt` directly, so it needs neither a `declare const` nor a runtime symbol.
159
+ */
160
+ export type Ext<E extends string, N extends string, A extends readonly (string | number)[] = []> = {
161
+ readonly __zmdbExt?: [E, N, A];
162
+ };
163
+
164
+ export type PrimaryKey = { readonly [zmdbPrimaryKey]?: true };
165
+ /** Database-generated. Omitted from `CreateDTO` entirely, not made optional. */
166
+ export type Serial = { readonly [zmdbSerial]?: true };
167
+ export type Unique = { readonly [zmdbUnique]?: true };
168
+ /** Has a database default, so it is *optional* on insert rather than absent. */
169
+ export type HasDefault = { readonly [zmdbDefault]?: true };
170
+ /** Never serialised. `ReadDTO<T>` cannot name it, so a leak is a type error. */
171
+ export type Sensitive = { readonly [zmdbSensitive]?: true };
172
+ export type References<Target extends string> = { readonly [zmdbReferences]?: Target };
173
+ export { type ReferentialAction } from '../ir/index.js';
174
+ export type OnDelete<Action extends ReferentialAction> = { readonly [zmdbOnDelete]?: Action };
175
+ export type OnUpdate<Action extends ReferentialAction> = { readonly [zmdbOnUpdate]?: Action };
176
+ export type ForeignKey<LocalColumns extends string, TargetTable extends string, TargetColumns extends string> = {
177
+ readonly [zmdbForeignKey]?: {
178
+ readonly columns: LocalColumns;
179
+ readonly targetTable: TargetTable;
180
+ readonly targetColumns: TargetColumns;
181
+ };
182
+ };
183
+ /** `varchar(N)`. Also emits `maxLength: N` into JSON Schema. */
184
+ export type Length<N extends number> = { readonly [zmdbLength]?: N };
185
+ /** `numeric(P, S)` precision and scale. */
186
+ export type Numeric<P extends number, S extends number> = { readonly [zmdbNumeric]?: readonly [P, S] };
187
+ /** Names a `CustomType` codec that converts between the wire, app and db types. */
188
+ export type Codec<Name extends string> = { readonly [zmdbCodec]?: Name };
189
+
190
+ /**
191
+ * What this column looks like over the wire, when that is not what it looks like in
192
+ * the app.
193
+ *
194
+ * The only tag whose payload is a *type* rather than a literal, and it has to be: a
195
+ * codec's wire type is arbitrary — cents as a decimal string, a UUID as a string, a
196
+ * point as `[number, number]` — so there is nothing to name it with but the type
197
+ * itself. `Wire<T>` reads it; a column without it is its own wire type, which is
198
+ * right for everything JSON can carry natively.
199
+ *
200
+ * amount: Money & Sql<'integer'> & Codec<'Money'> & WireAs<string>;
201
+ *
202
+ * `Sql<'timestamp'>` and `Sql<'bigint'>` do not need it. Their wire form follows from
203
+ * the SQL type and is built into `Wire<T>`, so writing it out would be a second place
204
+ * for the same fact to be wrong.
205
+ */
206
+ export type WireAs<W> = { readonly [zmdbWire]?: W };
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Relation tags — cardinality plus the column that carries the join.
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * The four cardinalities. Listed once so `AnyRelation` cannot fall behind the tags — and
214
+ * listed in `../ir`, where the same four exist as data for the reflection to check against.
215
+ * Re-exported here because this is where a reader looking at the tags expects to find it.
216
+ */
217
+ export { type RelationKind } from '../ir/index.js';
218
+
219
+ /**
220
+ * Matches a property carrying any relation tag, whatever its cardinality.
221
+ *
222
+ * `derive`'s `RelationKeys<T>` needs this because a relation is **not** a column:
223
+ * `Entity<T>` has to exclude `author` and `comments` or a join target would show up
224
+ * as something to `INSERT`. Written as `{ kind: RelationKind }` rather than
225
+ * `unknown` on purpose — an optional slot typed `unknown` is satisfied by a tag
226
+ * payload of any shape, including a future non-relation tag that happens to reuse
227
+ * the symbol, and matching on `kind` keeps the four cardinalities the whole set.
228
+ *
229
+ * Cardinality itself is deliberately *not* read back out of the tag. The declared
230
+ * type already says it: `author?: User & ManyToOne<…>` is to-one and
231
+ * `comments?: Comment[] & OneToMany<…>` is to-many, natively.
232
+ */
233
+ export type AnyRelation = { readonly [zmdbRelation]?: { readonly kind: RelationKind } };
234
+
235
+ export type ManyToOne<Target extends string, Fk extends string> = {
236
+ readonly [zmdbRelation]?: { readonly kind: 'manyToOne'; readonly target: Target; readonly fk: Fk };
237
+ };
238
+ export type OneToMany<Target extends string, Fk extends string> = {
239
+ readonly [zmdbRelation]?: { readonly kind: 'oneToMany'; readonly target: Target; readonly fk: Fk };
240
+ };
241
+ export type OneToOne<Target extends string, Fk extends string> = {
242
+ readonly [zmdbRelation]?: { readonly kind: 'oneToOne'; readonly target: Target; readonly fk: Fk };
243
+ };
244
+ export type ManyToMany<Target extends string, Through extends string> = {
245
+ readonly [zmdbRelation]?: { readonly kind: 'manyToMany'; readonly target: Target; readonly through: Through };
246
+ };
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Validation constraints. These are the whole reason the AOT can emit a real
250
+ // runtime check from a type: each one puts a literal in a type position.
251
+ // ---------------------------------------------------------------------------
252
+
253
+ export type Min<N extends number> = { readonly [zmdbMin]?: N };
254
+ export type Max<N extends number> = { readonly [zmdbMax]?: N };
255
+ export type MinLength<N extends number> = { readonly [zmdbMinLength]?: N };
256
+ export type MaxLength<N extends number> = { readonly [zmdbMaxLength]?: N };
257
+ export type Pattern<S extends string> = { readonly [zmdbPattern]?: S };
258
+
259
+ /**
260
+ * The named escape hatch. `ValidationRule.kind` is an open `string`, so a
261
+ * consumer can register a check the vocabulary does not model. The reflection
262
+ * records the name and emits a call to the registered predicate — it does not
263
+ * invent a check, and an unregistered name is a build error (plan D4).
264
+ */
265
+ export type Rule<Name extends string> = { readonly [zmdbRule]?: Name };
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Protobuf tags — facts the wire contract needs that TypeScript cannot state.
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /** Protobuf field number. Required on every property of a protobuf message. */
272
+ export type ProtoField<N extends number> = { readonly [zmdbProtoField]?: N };
273
+
274
+ /** Protobuf scalar spelling where TypeScript does not determine width or signedness. */
275
+ export type Proto<K extends ProtoScalar> = { readonly [zmdbProtoScalar]?: K };
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Readability aliases. Not tags — these expand to native TypeScript.
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /** `Nullable<string>` is exactly `string | null`. No tag involved. */
282
+ export type Nullable<T> = T | null;
283
+ /** Non-null is the default; this exists for symmetry at the declaration site. */
284
+ export type NonNull<T> = Exclude<T, null | undefined>;