@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,395 @@
1
+ import { type DeclaredTable, type RelationKeys } from '../derive/index.js';
2
+ import { type Entity } from '../index.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // WhereDTO + operator set
6
+ // ---------------------------------------------------------------------------
7
+ /**
8
+ * A row of a table the caller named with a string.
9
+ *
10
+ * A subquery target is `{ table: 'orders' }` — a table *name*, not a declared type — so there
11
+ * is nothing for its filter to be keyed by. This says exactly that much and no more: every
12
+ * property is a column of some SQL type, and none of them is a relation, which is what keeps
13
+ * `WhereDTO` willing to derive from it. It is a named type rather than an inline
14
+ * `Record<string, unknown>` so that it stays the one corner of the query surface that is
15
+ * keyed by string; everything else is keyed by the interface the table was declared as.
16
+ */
17
+ export interface UnknownRow {
18
+ readonly [column: string]: string | number | boolean | bigint | Date | null;
19
+ }
20
+ export type SubqueryTarget<V = unknown> =
21
+ | {
22
+ compile(): {
23
+ readonly text: string;
24
+ readonly parameters: readonly unknown[];
25
+ readonly telemetry?: {
26
+ readonly system: string;
27
+ readonly operation: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';
28
+ readonly collection: string;
29
+ };
30
+ };
31
+ readonly _type?: V;
32
+ }
33
+ | { table: string; select?: readonly string[]; where?: WhereDTO<UnknownRow>; readonly _type?: V };
34
+
35
+ type VectorOperand<V> =
36
+ NonNullable<V> extends {
37
+ readonly __zmdbExt?: readonly [extension: string, name: 'vector', args: readonly (string | number)[]];
38
+ }
39
+ ? readonly number[]
40
+ : never;
41
+
42
+ export interface FieldOps<V> {
43
+ eq?: V | SubqueryTarget<V>;
44
+ ne?: V | SubqueryTarget<V>;
45
+ lt?: V | SubqueryTarget<V>;
46
+ lte?: V | SubqueryTarget<V>;
47
+ gt?: V | SubqueryTarget<V>;
48
+ gte?: V | SubqueryTarget<V>;
49
+ in?: readonly V[] | SubqueryTarget<V>;
50
+ nin?: readonly V[] | SubqueryTarget<V>;
51
+ like?: V extends string ? string | SubqueryTarget<string> : never;
52
+ ilike?: V extends string ? string | SubqueryTarget<string> : never;
53
+ l2?: VectorOperand<V>;
54
+ cosine?: VectorOperand<V>;
55
+ ip?: VectorOperand<V>;
56
+ isNull?: boolean;
57
+ notNull?: boolean;
58
+ }
59
+
60
+ export type WhereDTO<T extends DeclaredTable> = {
61
+ [K in keyof Entity<T>]?: Entity<T>[K] | FieldOps<Entity<T>[K]>;
62
+ } & {
63
+ and?: readonly WhereDTO<T>[];
64
+ or?: readonly WhereDTO<T>[];
65
+ exists?: SubqueryTarget<unknown> | readonly SubqueryTarget<unknown>[];
66
+ notExists?: SubqueryTarget<unknown> | readonly SubqueryTarget<unknown>[];
67
+ };
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // §2 OrderBy + Pagination (implemented in #183)
71
+ // ---------------------------------------------------------------------------
72
+ export type OrderDir = 'asc' | 'desc';
73
+
74
+ export type OrderByDTO<T extends DeclaredTable> = ReadonlyArray<{
75
+ column: keyof Entity<T>;
76
+ dir?: OrderDir;
77
+ }>;
78
+
79
+ export type OffsetPage = { limit: number; offset?: number | undefined };
80
+
81
+ export type PaginationDTO<T extends DeclaredTable> =
82
+ | OffsetPage
83
+ | {
84
+ limit: number;
85
+ after?: Partial<Entity<T>> | string | undefined;
86
+ before?: Partial<Entity<T>> | string | undefined;
87
+ };
88
+
89
+ /**
90
+ * Schema-agnostic views of the order/page DTOs — exactly the fields the folders
91
+ * read. `OrderByDTO<T>`/`PaginationDTO<T>` are structurally assignable to these
92
+ * for *any* `T`, so callers pass their own typed DTO with no
93
+ * `as OrderByDTO<CoreSchema<string>>` widening cast (which is what leaked into
94
+ * consumer code, cf. COOKBOOK "sorting" example).
95
+ */
96
+ export type OrderBySpec = ReadonlyArray<{
97
+ column: PropertyKey;
98
+ dir?: OrderDir;
99
+ }>;
100
+
101
+ // `offset?: number | undefined` (not `offset?: number`) so callers under
102
+ // `exactOptionalPropertyTypes` can forward a possibly-absent offset positionally.
103
+ export type PaginationSpec = {
104
+ limit: number;
105
+ offset?: number | undefined;
106
+ after?: Record<string, unknown> | string | undefined;
107
+ before?: Record<string, unknown> | string | undefined;
108
+ };
109
+
110
+ function base64Encode(str: string): string {
111
+ if (globalThis.Buffer) {
112
+ return globalThis.Buffer.from(str, 'utf-8').toString('base64url');
113
+ }
114
+ if (globalThis.btoa) {
115
+ return globalThis.btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
116
+ }
117
+ throw new Error('No base64 encoder available');
118
+ }
119
+
120
+ function base64Decode(str: string): string {
121
+ if (globalThis.Buffer) {
122
+ return globalThis.Buffer.from(str, 'base64url').toString('utf-8');
123
+ }
124
+ if (globalThis.atob) {
125
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
126
+ while (base64.length % 4) base64 += '=';
127
+ return globalThis.atob(base64);
128
+ }
129
+ throw new Error('No base64 decoder available');
130
+ }
131
+
132
+ export function encodeCursor(payload: Record<string, unknown>): string {
133
+ return base64Encode(JSON.stringify(payload));
134
+ }
135
+
136
+ export function decodeCursor(cursor: string): Record<string, unknown> {
137
+ if (typeof cursor !== 'string' || !cursor.trim()) {
138
+ throw new Error('Invalid cursor: must be a non-empty string');
139
+ }
140
+ try {
141
+ const json = base64Decode(cursor);
142
+ const parsed = JSON.parse(json);
143
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
144
+ throw new Error('Invalid cursor payload');
145
+ }
146
+ // boundary: JSON.parse returns unknown (untrusted client payload); runtime check above proves parsed is a non-null, non-array object.
147
+ return parsed as Record<string, unknown>;
148
+ } catch (err) {
149
+ if (err instanceof Error && err.message.startsWith('Invalid cursor')) {
150
+ throw err;
151
+ }
152
+ throw new Error(`Invalid cursor format: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
153
+ }
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // §3 Projection (types only; narrowing wired in #186)
158
+ // ---------------------------------------------------------------------------
159
+ export type Projection<T extends DeclaredTable, K extends keyof Entity<T>> = Pick<Entity<T>, K>;
160
+
161
+ /** Narrow a row to `cols` (new object, stable order); passthrough when undefined. */
162
+ export function project<Row extends Record<string, unknown>>(row: Row, cols: undefined): Row;
163
+
164
+ export function project<Row extends Record<string, unknown>, K extends keyof Row>(
165
+ row: Row,
166
+ cols: readonly K[],
167
+ ): Pick<Row, K>;
168
+
169
+ export function project<Row extends Record<string, unknown>, K extends keyof Row>(
170
+ row: Row,
171
+ cols: readonly K[] | undefined,
172
+ ): Row | Pick<Row, K>;
173
+
174
+ export function project<Row extends Record<string, unknown>, K extends keyof Row>(
175
+ row: Row,
176
+ cols: readonly K[] | undefined,
177
+ ): Row | Pick<Row, K> {
178
+ if (!cols) return row;
179
+ // boundary: a `Pick` is built key-by-key, so it is only complete once the loop
180
+ // ends — there is no expression form that types a partially-filled mapped
181
+ // type. The loop below writes exactly `cols`, which is what `Pick<Row, K>`
182
+ // claims; `noUncheckedIndexedAccess` keeps the reads honest.
183
+ const out = {} as Pick<Row, K>;
184
+ for (const c of cols) out[c] = row[c];
185
+ return out;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // §4 GetDTO
190
+ // ---------------------------------------------------------------------------
191
+ export interface GetOptions<T extends DeclaredTable> {
192
+ select?: readonly (keyof Entity<T>)[];
193
+ /**
194
+ * The relations to fetch alongside the row.
195
+ *
196
+ * `RelationKeys<T>` rather than `readonly string[]`: a declared type names its relations,
197
+ * so a misspelled one is a compile error rather than a relation that silently does not
198
+ * arrive. It was a bare `string[]` while this family was keyed by the schema value, which
199
+ * carries no relations to check a name against.
200
+ */
201
+ populate?: readonly RelationKeys<T>[];
202
+ }
203
+
204
+ export type GetDTO<
205
+ T extends DeclaredTable,
206
+ O extends GetOptions<T> = {},
207
+ > = O['select'] extends readonly (infer K extends keyof Entity<T>)[] ? Projection<T, K> : Entity<T>;
208
+
209
+ /** Apply a Get's select projection to a fetched row. */
210
+ export function getResult<Row extends Record<string, unknown>>(
211
+ row: Row,
212
+ opts?: { select?: readonly (keyof Row)[] },
213
+ ): Row | Partial<Row> {
214
+ return project(row, opts?.select);
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // §4–6 Get/List/Search DTOs (types; result assembly in #166/#169/#172)
219
+ // ---------------------------------------------------------------------------
220
+ export interface ListDTO<T extends DeclaredTable> {
221
+ where?: WhereDTO<T>;
222
+ orderBy?: OrderByDTO<T>;
223
+ page?: PaginationDTO<T>;
224
+ select?: readonly (keyof Entity<T>)[];
225
+ }
226
+
227
+ export interface ListResult<Row> {
228
+ readonly items: readonly Row[];
229
+ readonly total?: number;
230
+ readonly hasMore: boolean;
231
+ readonly cursor?: string;
232
+ }
233
+
234
+ /** Everything `buildListResult` accepts except `select`, which is what its overloads differ on. */
235
+ interface ListOptions {
236
+ limit?: number;
237
+ total?: number;
238
+ cursor?: string;
239
+ orderBy?: OrderBySpec;
240
+ pkColumn?: string;
241
+ }
242
+
243
+ /**
244
+ * Assemble a ListResult: limit+1 trim ⇒ hasMore, per-item projection, opt-in total, opaque cursor.
245
+ *
246
+ * Overloaded on `select` so the no-projection call keeps `ListResult<Row>` instead
247
+ * of widening to `ListResult<Row | Partial<Row>>` — the widening is what forced
248
+ * `as ListResult<Entity<T>>` in `@zmdb/orm`'s `list()`.
249
+ */
250
+ export function buildListResult<Row extends Record<string, unknown>>(
251
+ rows: readonly Row[],
252
+ opts?: ListOptions,
253
+ ): ListResult<Row>;
254
+
255
+ export function buildListResult<Row extends Record<string, unknown>, K extends keyof Row>(
256
+ rows: readonly Row[],
257
+ opts: ListOptions & { select: readonly K[] },
258
+ ): ListResult<Pick<Row, K>>;
259
+
260
+ export function buildListResult<Row extends Record<string, unknown>>(
261
+ rows: readonly Row[],
262
+ opts?: ListOptions & { select?: readonly (keyof Row)[] },
263
+ ): ListResult<Row | Partial<Row>>;
264
+
265
+ export function buildListResult<Row extends Record<string, unknown>>(
266
+ rows: readonly Row[],
267
+ opts?: ListOptions & { select?: readonly (keyof Row)[] },
268
+ ): ListResult<Row | Partial<Row>> {
269
+ const limit = opts?.limit;
270
+ const hasMore = typeof limit === 'number' && rows.length > limit;
271
+ const kept = hasMore ? rows.slice(0, limit) : rows;
272
+ const select = opts?.select;
273
+ const items = select ? kept.map(r => project(r, select)) : kept;
274
+
275
+ let computedCursor: string | undefined = opts?.cursor;
276
+ if (!computedCursor && hasMore && kept.length > 0) {
277
+ const lastRow = kept[kept.length - 1];
278
+ if (lastRow) {
279
+ const cursorObj: Record<string, unknown> = {};
280
+ const cols: { column: PropertyKey; dir?: OrderDir }[] = opts?.orderBy ? [...opts.orderBy] : [];
281
+ if (opts?.pkColumn && !cols.some(c => String(c.column) === opts.pkColumn)) {
282
+ cols.push({ column: opts.pkColumn, dir: 'asc' });
283
+ }
284
+ for (const item of cols) {
285
+ if (!item) continue;
286
+ const colStr = String(item.column);
287
+ if (colStr in lastRow) {
288
+ cursorObj[colStr] = lastRow[colStr];
289
+ }
290
+ }
291
+ if (Object.keys(cursorObj).length > 0) {
292
+ computedCursor = encodeCursor(cursorObj);
293
+ }
294
+ }
295
+ }
296
+ const result: ListResult<Row | Partial<Row>> = {
297
+ items,
298
+ hasMore,
299
+ ...(computedCursor !== undefined ? { cursor: computedCursor } : {}),
300
+ };
301
+ return opts?.total !== undefined ? { ...result, total: opts.total } : result;
302
+ }
303
+
304
+ export interface SearchDTO<T extends DeclaredTable> {
305
+ query: string;
306
+ columns: readonly (keyof Entity<T>)[];
307
+ where?: WhereDTO<T>;
308
+ page?: PaginationDTO<T>;
309
+ rank?: boolean;
310
+ }
311
+
312
+ export type SearchHit<Row> = Row & { readonly _score?: number };
313
+
314
+ export type SearchResult<Row> = ListResult<SearchHit<Row>>;
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // §8 AggregateResult
318
+ // ---------------------------------------------------------------------------
319
+ export type AggFn = 'count' | 'sum' | 'avg' | 'min' | 'max';
320
+
321
+ /**
322
+ * `"relation.column"` for every relation the table declares.
323
+ *
324
+ * There was a second type parameter for this — the repository's relations map — and each of
325
+ * its entries named the target either as a declared type or as a schema value, so the arm
326
+ * that could see the target's columns was the one where an author had happened to write
327
+ * `entity: Order`; everything else fell back to `${Rel}.${string}`. `RelationKeys<T>` reads
328
+ * the target off the declaration, which every relation has, so every relation gets its
329
+ * columns listed.
330
+ */
331
+ type RelationTargetOf<V> = (NonNullable<V> extends readonly (infer E)[] ? E : NonNullable<V>) & DeclaredTable;
332
+
333
+ type RelatedColumns<T extends DeclaredTable> = {
334
+ [Rel in RelationKeys<T> & string]: `${Rel}.${keyof Entity<RelationTargetOf<T[Rel & keyof T]>> & string}`;
335
+ }[RelationKeys<T> & string];
336
+
337
+ export type AggregateColumn<T extends DeclaredTable> = (keyof Entity<T> & string) | RelatedColumns<T> | (string & {});
338
+
339
+ export interface ComputedSpec<T extends DeclaredTable = DeclaredTable> {
340
+ fn: AggFn;
341
+ column?: AggregateColumn<T>;
342
+ raw?: string;
343
+ }
344
+
345
+ export interface AggregateSpec<T extends DeclaredTable> {
346
+ joins?:
347
+ | readonly (RelationKeys<T> & string)[]
348
+ | readonly { relation: RelationKeys<T> & string; kind?: 'inner' | 'left' | 'right' }[];
349
+ where?: WhereDTO<T> | Record<string, unknown>;
350
+ groupBy?: readonly AggregateColumn<T>[];
351
+ computed: Record<string, ComputedSpec<T>>;
352
+ having?: Readonly<{ column: AggregateColumn<T>; op: string; value: unknown }>;
353
+ orderBy?: ReadonlyArray<{ column: AggregateColumn<T>; dir?: OrderDir }>;
354
+ limit?: number;
355
+ offset?: number;
356
+ }
357
+
358
+ type AggComputedType<T extends DeclaredTable, C> = C extends { fn: 'count' }
359
+ ? number
360
+ : C extends { fn: 'sum' | 'avg' }
361
+ ? number | null
362
+ : C extends { fn: 'min' | 'max'; column: infer Col extends keyof Entity<T> }
363
+ ? Entity<T>[Col] | null
364
+ : number | null;
365
+
366
+ export type AggregateResult<T extends DeclaredTable, Spec extends AggregateSpec<T>> = {
367
+ [K in Spec['groupBy'] extends readonly (infer G extends keyof Entity<T>)[] ? G : never]: Entity<T>[K];
368
+ } & {
369
+ [K in keyof Spec['computed']]: AggComputedType<T, Spec['computed'][K]>;
370
+ };
371
+
372
+ /** Ordered field list for an aggregate spec: group-key cols then computed keys. */
373
+ export function describeAggregate<T extends DeclaredTable>(spec: AggregateSpec<T>): readonly string[] {
374
+ const keys = (spec.groupBy ?? []).map(k => String(k));
375
+ return [...keys, ...Object.keys(spec.computed)];
376
+ }
377
+
378
+ /** Assemble a SearchResult (reuses buildListResult; preserves _score on hits). */
379
+ export function buildSearchResult<Row extends Record<string, unknown>>(
380
+ rows: readonly SearchHit<Row>[],
381
+ opts?: { limit?: number; select?: readonly (keyof Row)[]; total?: number },
382
+ ): SearchResult<Row | Partial<Row>> {
383
+ const limit = opts?.limit;
384
+ const hasMore = typeof limit === 'number' && rows.length > limit;
385
+ const kept = hasMore ? rows.slice(0, limit) : rows;
386
+ const items = kept.map(hit => {
387
+ // `SearchHit<Row>` is `Row & {_score?}`, so it *is* a `Row` for projection
388
+ // purposes — no `hit as Row` needed once `project` is keyed on the argument.
389
+ const base = opts?.select ? project(hit, opts.select) : hit;
390
+ // preserve the ranking score on the projected hit
391
+ return hit._score !== undefined ? { ...base, _score: hit._score } : base;
392
+ });
393
+ const result: SearchResult<Row | Partial<Row>> = { items, hasMore };
394
+ return opts?.total !== undefined ? { ...result, total: opts.total } : result;
395
+ }
@@ -0,0 +1,33 @@
1
+ // §2 embeddables
2
+ export function flattenEmbeddable(prefix: string, value: Record<string, unknown>): Record<string, unknown> {
3
+ const out: Record<string, unknown> = {};
4
+ for (const [k, v] of Object.entries(value)) out[`${prefix}_${k}`] = v;
5
+ return out;
6
+ }
7
+ export function liftEmbeddable(prefix: string, row: Record<string, unknown>): Record<string, unknown> {
8
+ const p = `${prefix}_`;
9
+ const out: Record<string, unknown> = {};
10
+ for (const [k, v] of Object.entries(row)) {
11
+ if (k.startsWith(p)) out[k.slice(p.length)] = v;
12
+ }
13
+ return out;
14
+ }
15
+
16
+ // §3 inheritance
17
+ export interface SingleTableInheritance {
18
+ discriminator: string;
19
+ map: Record<string, readonly string[]>;
20
+ }
21
+ export function discriminatorFor(_sti: SingleTableInheritance, type: string): string {
22
+ return type;
23
+ }
24
+ export function rowToSubtype(
25
+ sti: SingleTableInheritance,
26
+ row: Record<string, unknown>,
27
+ ): { type: string; data: Record<string, unknown> } {
28
+ const type = String(row[sti.discriminator]);
29
+ const cols = sti.map[type] ?? [];
30
+ const data: Record<string, unknown> = {};
31
+ for (const c of cols) data[c] = row[c];
32
+ return { type, data };
33
+ }