@vibeorm/generator 2.0.0-alpha.1 → 2.0.0-alpha.10

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/dist/index.js CHANGED
@@ -2,10 +2,14 @@
2
2
  // packages/generator/src/generate.ts
3
3
  import { mkdir, writeFile } from "fs/promises";
4
4
  import { join } from "path";
5
+ import { buildComputedManifest, validateComputedConfig } from "@vibeorm/schema";
5
6
 
6
7
  // packages/generator/src/emit-dts.ts
7
8
  import { VibeError as VibeError3 } from "@vibeorm/schema";
8
9
 
10
+ // packages/generator/src/emit-js.ts
11
+ import { internalError } from "@vibeorm/schema";
12
+
9
13
  // packages/generator/src/extension-bake.ts
10
14
  import {
11
15
  VibeError,
@@ -127,72 +131,6 @@ function bakeExtensions(params) {
127
131
  };
128
132
  }
129
133
 
130
- // packages/generator/src/type-names.ts
131
- var PAYLOAD_TYPES = {
132
- String: "string",
133
- Boolean: "boolean",
134
- Int: "number",
135
- BigInt: "bigint",
136
- Float: "number",
137
- Decimal: "string",
138
- DateTime: "Date",
139
- Json: "JsonValue",
140
- Bytes: "Uint8Array"
141
- };
142
- var INPUT_TYPES = {
143
- String: "string",
144
- Boolean: "boolean",
145
- Int: "number",
146
- BigInt: "bigint | number",
147
- Float: "number",
148
- Decimal: "string | number",
149
- DateTime: "Date | string",
150
- Json: "InputJsonValue",
151
- Bytes: "Uint8Array"
152
- };
153
- var FILTER_PREFIX = {
154
- String: "String",
155
- Boolean: "Bool",
156
- Int: "Int",
157
- BigInt: "BigInt",
158
- Float: "Float",
159
- Decimal: "Decimal",
160
- DateTime: "DateTime",
161
- Json: "Json",
162
- Bytes: "Bytes"
163
- };
164
- var NUMERIC_SCALARS = new Set(["Int", "BigInt", "Float", "Decimal"]);
165
- var MINMAX_SCALARS = new Set([
166
- "String",
167
- "Int",
168
- "BigInt",
169
- "Float",
170
- "Decimal",
171
- "DateTime"
172
- ]);
173
- function payloadBaseType(params) {
174
- const { field } = params;
175
- return field.enumName ?? PAYLOAD_TYPES[field.scalar];
176
- }
177
- function inputBaseType(params) {
178
- const { field } = params;
179
- return field.enumName ?? INPUT_TYPES[field.scalar];
180
- }
181
- function filterName(params) {
182
- const { field } = params;
183
- const prefix = field.enumName ?? FILTER_PREFIX[field.scalar];
184
- if (field.isList)
185
- return `${prefix}ListFilter`;
186
- return field.isOptional ? `${prefix}NullableFilter` : `${prefix}Filter`;
187
- }
188
- function updateOpsName(params) {
189
- const { field } = params;
190
- if (field.isList || field.scalar === null || !NUMERIC_SCALARS.has(field.scalar))
191
- return null;
192
- const prefix = FILTER_PREFIX[field.scalar];
193
- return field.isOptional ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
194
- }
195
-
196
134
  // packages/generator/src/util.ts
197
135
  import { VibeError as VibeError2 } from "@vibeorm/schema";
198
136
  function pascal(params) {
@@ -258,6 +196,240 @@ ${indent2}}`;
258
196
  });
259
197
  }
260
198
 
199
+ // packages/generator/src/emit-js.ts
200
+ var HEADER = `// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
201
+ // Regenerate with \`vibeorm generate\` after every schema change.`;
202
+ function enumEntryExports(params) {
203
+ return [...params.view.enums.map((declared) => declared.name), "enumValidators"];
204
+ }
205
+ function emitEnumsJs(params) {
206
+ const { view } = params;
207
+ const parts = [
208
+ `${HEADER}
209
+ // Lightweight entry: enum constants + boundary parsers. No runtime client, no schema module.`
210
+ ];
211
+ if (view.enums.length === 0) {
212
+ parts.push(`/** Boundary parsers grouped by enum name \u2014 this schema declares no enums. */
213
+ ` + `export const enumValidators = Object.freeze({});`);
214
+ return `${parts.join(`
215
+
216
+ `)}
217
+ `;
218
+ }
219
+ parts.push(`import { VibeError } from "@vibeorm/schema";`);
220
+ for (const declared of view.enums) {
221
+ const entries = declared.values.map((value) => ` ${value === "__proto__" ? `[${tsString({ value })}]` : tsString({ value })}: ${tsString({ value })},`).join(`
222
+ `);
223
+ parts.push(`export const ${declared.name} = Object.freeze({
224
+ ${entries}
225
+ });`);
226
+ }
227
+ parts.push(`/** Membership validator for one enum: \`values\`, \`is\`, \`parse\` (VIBE_VALIDATION on a non-member). */
228
+ ` + `function enumValidator(params) {
229
+ ` + ` const { name, values } = params;
230
+ ` + ` const members = new Set(values);
231
+ ` + ` const is = (value) => typeof value === "string" && members.has(value);
232
+ ` + ` return Object.freeze({
233
+ ` + ` values: Object.freeze([...values]),
234
+ ` + ` is,
235
+ ` + ` parse(input) {
236
+ ` + ` const { value } = input;
237
+ ` + ` if (is(value)) return value;
238
+ ` + ` const received = typeof value === "string" ? value.slice(0, 80) : value === null ? "null" : typeof value;
239
+ ` + ` throw new VibeError({
240
+ ` + ` code: "VIBE_VALIDATION",
241
+ ` + ` message: \`Invalid \${name} value \${JSON.stringify(received)} \u2014 expected one of \${values.join(", ")}\`,
242
+ ` + ` meta: { enum: name, received },
243
+ ` + ` });
244
+ ` + ` },
245
+ ` + ` });
246
+ ` + `}`);
247
+ const validatorLines = view.enums.map((declared) => ` ${declared.name === "__proto__" ? `[${tsString({ value: declared.name })}]` : declared.name}: enumValidator({ name: ${tsString({ value: declared.name })}, values: [${declared.values.map((value) => tsString({ value })).join(", ")}] }),`).join(`
248
+ `);
249
+ parts.push(`/** Boundary parsers grouped by enum name, so an enum member named \`parse\` cannot collide. */
250
+ ` + `export const enumValidators = Object.freeze({
251
+ ${validatorLines}
252
+ });`);
253
+ return `${parts.join(`
254
+
255
+ `)}
256
+ `;
257
+ }
258
+ function serializeSchema(params) {
259
+ const { schema } = params;
260
+ if (schema.rls === undefined)
261
+ return stableStringify({ value: schema });
262
+ return renderIr({ value: schema, indent: "", ordered: false });
263
+ }
264
+ function renderIr(params) {
265
+ const { value, indent: indent2, ordered } = params;
266
+ if (value === null)
267
+ return "null";
268
+ const kind = typeof value;
269
+ if (kind === "string" || kind === "number" || kind === "boolean")
270
+ return JSON.stringify(value);
271
+ if (Array.isArray(value)) {
272
+ if (value.length === 0)
273
+ return "[]";
274
+ const inner2 = `${indent2} `;
275
+ const items2 = value.map((item) => inner2 + renderIr({ value: item, indent: inner2, ordered }));
276
+ return `[
277
+ ${items2.join(`,
278
+ `)}
279
+ ${indent2}]`;
280
+ }
281
+ if (kind !== "object") {
282
+ throw internalError({ message: `internal: cannot serialize a ${kind} value into the generated client`, meta: { kind } });
283
+ }
284
+ const record = value;
285
+ const defined = Object.keys(record).filter((key) => record[key] !== undefined);
286
+ const keys = ordered ? defined : defined.sort();
287
+ if (keys.length === 0)
288
+ return "{}";
289
+ const inner = `${indent2} `;
290
+ const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderIr({ value: record[key], indent: inner, ordered: ordered || key === "rls" })}`);
291
+ return `{
292
+ ${items.join(`,
293
+ `)}
294
+ ${indent2}}`;
295
+ }
296
+ function emitJs(params) {
297
+ const { view } = params;
298
+ const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
299
+ const computed = params.computed ?? [];
300
+ const hasComputed = computed.length > 0;
301
+ const parts = [];
302
+ parts.push(HEADER);
303
+ parts.push(`import { createClient } from "@vibeorm/runtime";`);
304
+ if (baked.jsImports.length > 0)
305
+ parts.push(baked.jsImports.join(`
306
+ `));
307
+ parts.push(`export { VibeError } from "@vibeorm/schema";`);
308
+ parts.push(`export { SKIP } from "@vibeorm/runtime";`);
309
+ parts.push(`export { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`);
310
+ parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
311
+ export const schema = ${serializeSchema({ schema: view.schema })};`);
312
+ if (baked.empty && !hasComputed) {
313
+ parts.push(`/** Build a typed client over a database adapter. */
314
+ ` + `export function VibeClient(options) {
315
+ ` + ` const { adapter, ...clientOptions } = options;
316
+ ` + ` return createClient({ schema, adapter, options: clientOptions });
317
+ ` + `}`);
318
+ return `${parts.join(`
319
+
320
+ `)}
321
+ `;
322
+ }
323
+ if (!baked.empty) {
324
+ parts.push(`/** Extension manifest \u2014 verified against the live extension list (VIBE_STALE_CLIENT). */
325
+ ` + `const extensionManifest = ${stableStringify({ value: baked.manifest })};`);
326
+ parts.push(`/** Baked extension surfaces (extensions.md D9). */
327
+ ` + `const extensionMounts = [
328
+ ${baked.mounts.map((mount) => ` ${mount},`).join(`
329
+ `)}
330
+ ];`);
331
+ }
332
+ if (hasComputed) {
333
+ parts.push(`/** Computed-field manifest \u2014 verified against the live computed config (VIBE_STALE_CLIENT). */
334
+ ` + `const computedManifest = ${stableStringify({ value: computed })};`);
335
+ }
336
+ const destructured = [
337
+ "adapter",
338
+ ...baked.empty ? [] : ["extensions"],
339
+ ...hasComputed ? ["computed"] : [],
340
+ "...clientOptions"
341
+ ].join(", ");
342
+ const createLines = [
343
+ ` schema,`,
344
+ ` adapter,`,
345
+ ` options: clientOptions,`,
346
+ ...baked.empty ? [] : [` extensions: { manifest: extensionManifest, mounts: extensionMounts, instances: extensions ?? [] },`],
347
+ ...hasComputed ? [` computed: { manifest: computedManifest, specs: computed },`] : []
348
+ ];
349
+ parts.push(`/** Build a typed client over a database adapter. */
350
+ ` + `export function VibeClient(options) {
351
+ ` + ` const { ${destructured} } = options;
352
+ ` + ` return createClient({
353
+ ${createLines.join(`
354
+ `)}
355
+ });
356
+ ` + `}`);
357
+ return `${parts.join(`
358
+
359
+ `)}
360
+ `;
361
+ }
362
+
363
+ // packages/generator/src/type-names.ts
364
+ var PAYLOAD_TYPES = {
365
+ String: "string",
366
+ Boolean: "boolean",
367
+ Int: "number",
368
+ BigInt: "bigint",
369
+ Float: "number",
370
+ Decimal: "string",
371
+ DateTime: "Date",
372
+ Json: "JsonValue",
373
+ Bytes: "Uint8Array"
374
+ };
375
+ var INPUT_TYPES = {
376
+ String: "string",
377
+ Boolean: "boolean",
378
+ Int: "number",
379
+ BigInt: "bigint | number",
380
+ Float: "number",
381
+ Decimal: "string | number",
382
+ DateTime: "Date | string | DbNow",
383
+ Json: "InputJsonValue",
384
+ Bytes: "Uint8Array"
385
+ };
386
+ var FILTER_PREFIX = {
387
+ String: "String",
388
+ Boolean: "Bool",
389
+ Int: "Int",
390
+ BigInt: "BigInt",
391
+ Float: "Float",
392
+ Decimal: "Decimal",
393
+ DateTime: "DateTime",
394
+ Json: "Json",
395
+ Bytes: "Bytes"
396
+ };
397
+ var NUMERIC_SCALARS = new Set(["Int", "BigInt", "Float", "Decimal"]);
398
+ var MINMAX_SCALARS = new Set([
399
+ "String",
400
+ "Int",
401
+ "BigInt",
402
+ "Float",
403
+ "Decimal",
404
+ "DateTime"
405
+ ]);
406
+ function payloadBaseType(params) {
407
+ const { field } = params;
408
+ if (field.jsonType !== undefined)
409
+ return field.jsonType.name;
410
+ return field.enumName ?? PAYLOAD_TYPES[field.scalar];
411
+ }
412
+ function inputBaseType(params) {
413
+ const { field } = params;
414
+ if (field.jsonType !== undefined)
415
+ return field.jsonType.name;
416
+ return field.enumName ?? INPUT_TYPES[field.scalar];
417
+ }
418
+ function filterName(params) {
419
+ const { field } = params;
420
+ const prefix = field.enumName ?? FILTER_PREFIX[field.scalar];
421
+ if (field.isList)
422
+ return `${prefix}ListFilter`;
423
+ return field.isOptional ? `${prefix}NullableFilter` : `${prefix}Filter`;
424
+ }
425
+ function updateOpsName(params) {
426
+ const { field } = params;
427
+ if (field.isList || field.scalar === null || !NUMERIC_SCALARS.has(field.scalar))
428
+ return null;
429
+ const prefix = FILTER_PREFIX[field.scalar];
430
+ return field.isOptional ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
431
+ }
432
+
261
433
  // packages/generator/src/emit-dts.ts
262
434
  function banner(params) {
263
435
  return `// \u2500\u2500\u2500 ${params.title} \u2500\u2500\u2500`;
@@ -271,6 +443,23 @@ ${lines.join(`
271
443
  `)}
272
444
  };`;
273
445
  }
446
+ var OPTIONAL_MEMBER = /^(\s*(?:readonly\s+)?(?:[A-Za-z_$][\w$]*|"(?:[^"\\]|\\.)*")\?:\s*)(.+);$/;
447
+ function skippableMembers(params) {
448
+ return params.block.split(`
449
+ `).map((line) => {
450
+ const match = OPTIONAL_MEMBER.exec(line);
451
+ const head = match?.[1];
452
+ const type = match?.[2];
453
+ if (head === undefined || type === undefined)
454
+ return line;
455
+ return `${head}${type.includes("=>") ? `(${type})` : type} | Skip;`;
456
+ }).join(`
457
+ `);
458
+ }
459
+ function inputAlias(params) {
460
+ const { name, lines } = params;
461
+ return objectAlias({ name, lines: lines.map((block) => skippableMembers({ block })) });
462
+ }
274
463
  function orMany(params) {
275
464
  const { type } = params;
276
465
  return `${type} | readonly ${maybeParens({ type })}[]`;
@@ -316,18 +505,73 @@ export type SortOrderWithNulls = SortOrder | { readonly sort: SortOrder; readonl
316
505
  /** How relations are loaded: batched WHERE-IN queries, or a single JOIN (pg/pglite). */
317
506
  export type RelationStrategy = "query" | "join";
318
507
 
508
+ /**
509
+ * An opaque, versioned keyset position (\`after\`) \u2014 ordering values plus a
510
+ * unique tie-breaker, minted by a previous page. It is ENCODED, not encrypted:
511
+ * whoever holds it can read the ordering values inside. It is also NOT an
512
+ * authorization credential \u2014 it selects a position, never a row, and every
513
+ * query still applies its own where/scoped/policy/RLS filters.
514
+ */
515
+ export type KeysetToken = string;
516
+
517
+ /** Per-query keyset options. */
518
+ export type KeysetOptions = {
519
+ /**
520
+ * Asserts that every DateTime ordering column in this query holds
521
+ * millisecond-exact values. Required on postgres and mysql, whose timestamps
522
+ * may be finer than a JavaScript Date; NOT safe for \`@default(now())\`
523
+ * columns on postgres, which are microsecond-precise.
524
+ */
525
+ readonly timestampPrecision?: "milliseconds";
526
+ };
527
+
319
528
  /** Result of createMany/updateMany/deleteMany. */
320
529
  export type BatchPayload = { readonly count: number };
321
530
 
531
+ /**
532
+ * What upsertMany reports. \`count\` is the rows submitted after duplicate-key
533
+ * validation \u2014 every one of them was inserted or updated, since the statement
534
+ * has no skip arm and the whole operation is atomic. \`affectedRows\` is the
535
+ * driver's own number, and only where that number IS a row count: it is null on
536
+ * mysql, whose ON DUPLICATE KEY scores an insert 1, a changed update 2 and an
537
+ * unchanged update 0.
538
+ */
539
+ export type UpsertManyPayload = { readonly count: number; readonly affectedRows: number | null };
540
+
541
+ /**
542
+ * An argument member as the RESULT machinery must read it. Absent and the
543
+ * \`SKIP\` sentinel both mean "not provided", so neither may narrow a payload:
544
+ * without this, \`{ omit?: UserOmitInput | Skip }\` would collapse the key
545
+ * helpers below to \`never\` and promise fields the query may still have dropped.
546
+ */
547
+ type Provided<T> = Exclude<T, Skip | undefined>;
548
+
322
549
  /** Keys of an omit object whose value is exactly true \u2014 the scalars the result drops. */
323
- type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 };`;
550
+ type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 };
551
+
552
+ /** Keys a selection has not RULED OUT \u2014 everything except a literal false/undefined. */
553
+ type PossibleKeys<S> = keyof { [K in keyof S as S[K] extends false | undefined ? never : K]: 0 };
554
+
555
+ /**
556
+ * Keys whose flag can STILL be false or missing when the query runs \u2014 a widened
557
+ * \`boolean\`, or an optional nested selection. They are emitted OPTIONAL: the
558
+ * result type must not promise a field a run-time flag can switch off.
559
+ */
560
+ type UncertainKeys<S> = PossibleKeys<S> &
561
+ keyof { [K in keyof S as [false] extends [S[K]] ? K : [undefined] extends [S[K]] ? K : never]: 0 };
562
+
563
+ /** Keys the selection definitely turns on: a literal \`true\`, or a nested selection object. */
564
+ type ExactKeys<S> = Exclude<PossibleKeys<S>, UncertainKeys<S>>;
565
+
566
+ /** An aggregate object is absent when none of its selectors is enabled. */
567
+ type HasSelectedField<S> = [S] extends [true] ? true : [S] extends [object] ? ExactKeys<S> extends never ? false : true : false;`;
324
568
  var SCALAR_FILTER_CONFIGS = [
325
569
  { prefix: "String", value: "string", comparable: true, text: true },
326
570
  { prefix: "Int", value: "number", comparable: true, text: false },
327
571
  { prefix: "Float", value: "number", comparable: true, text: false },
328
572
  { prefix: "BigInt", value: "bigint | number", comparable: true, text: false },
329
573
  { prefix: "Decimal", value: "string | number", comparable: true, text: false },
330
- { prefix: "DateTime", value: "Date | string", comparable: true, text: false },
574
+ { prefix: "DateTime", value: "Date | string", comparable: true, text: false, clock: true },
331
575
  { prefix: "Bool", value: "boolean", comparable: false, text: false },
332
576
  { prefix: "Bytes", value: "Uint8Array", comparable: false, text: false }
333
577
  ];
@@ -335,17 +579,18 @@ function scalarFilterAlias(params) {
335
579
  const { config, nullable } = params;
336
580
  const name = nullable ? `${config.prefix}NullableFilter` : `${config.prefix}Filter`;
337
581
  const value = config.value;
338
- const valueOrNull = nullable ? `${value} | null` : value;
582
+ const point = config.clock === true ? `${value} | DbNow` : value;
583
+ const valueOrNull = nullable ? `${point} | null` : point;
339
584
  const array = `readonly ${maybeParens({ type: value })}[]`;
340
585
  const lines = [];
341
586
  lines.push(` readonly equals?: ${valueOrNull};`);
342
587
  lines.push(` readonly in?: ${array};`);
343
588
  lines.push(` readonly notIn?: ${array};`);
344
589
  if (config.comparable) {
345
- lines.push(` readonly lt?: ${value};`);
346
- lines.push(` readonly lte?: ${value};`);
347
- lines.push(` readonly gt?: ${value};`);
348
- lines.push(` readonly gte?: ${value};`);
590
+ lines.push(` readonly lt?: ${point};`);
591
+ lines.push(` readonly lte?: ${point};`);
592
+ lines.push(` readonly gt?: ${point};`);
593
+ lines.push(` readonly gte?: ${point};`);
349
594
  }
350
595
  if (config.text) {
351
596
  lines.push(` readonly contains?: ${value};`);
@@ -354,13 +599,13 @@ function scalarFilterAlias(params) {
354
599
  lines.push(` readonly mode?: QueryMode;`);
355
600
  }
356
601
  lines.push(` readonly not?: ${value} | ${name}${nullable ? " | null" : ""};`);
357
- return objectAlias({ name, lines });
602
+ return inputAlias({ name, lines });
358
603
  }
359
604
  function scalarListFilterAlias(params) {
360
605
  const { config } = params;
361
606
  const value = config.value;
362
607
  const array = `readonly ${maybeParens({ type: value })}[]`;
363
- return objectAlias({
608
+ return inputAlias({
364
609
  name: `${config.prefix}ListFilter`,
365
610
  lines: [
366
611
  ` readonly equals?: ${array} | null;`,
@@ -380,7 +625,7 @@ var UPDATE_OPS_CONFIGS = [
380
625
  function updateOpsAlias(params) {
381
626
  const { prefix, value, nullable } = params;
382
627
  const name = nullable ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
383
- return objectAlias({
628
+ return inputAlias({
384
629
  name,
385
630
  lines: [
386
631
  ` readonly set?: ${nullable ? `${value} | null` : value};`,
@@ -391,6 +636,25 @@ function updateOpsAlias(params) {
391
636
  ]
392
637
  });
393
638
  }
639
+ function jsonFilterAlias(params) {
640
+ const { nullable } = params;
641
+ const document = nullable ? "InputJsonValue | null" : "InputJsonValue";
642
+ return inputAlias({
643
+ name: nullable ? "JsonNullableFilter" : "JsonFilter",
644
+ lines: [
645
+ ` readonly path?: readonly (string | number)[];`,
646
+ ` readonly mode?: "default" | "insensitive";`,
647
+ ` readonly equals?: ${document};`,
648
+ ` readonly not?: ${document};`,
649
+ ` readonly string_contains?: string;`,
650
+ ` readonly string_starts_with?: string;`,
651
+ ` readonly string_ends_with?: string;`,
652
+ ` readonly array_contains?: InputJsonValue;`,
653
+ ` readonly array_starts_with?: InputJsonValue;`,
654
+ ` readonly array_ends_with?: InputJsonValue;`
655
+ ]
656
+ });
657
+ }
394
658
  function filterSection() {
395
659
  const parts = [banner({ title: "Scalar filters" })];
396
660
  for (const config of SCALAR_FILTER_CONFIGS) {
@@ -398,15 +662,9 @@ function filterSection() {
398
662
  parts.push(scalarFilterAlias({ config, nullable: true }));
399
663
  parts.push(scalarListFilterAlias({ config }));
400
664
  }
401
- parts.push(objectAlias({
402
- name: "JsonFilter",
403
- lines: [` readonly equals?: InputJsonValue;`, ` readonly not?: InputJsonValue;`]
404
- }));
405
- parts.push(objectAlias({
406
- name: "JsonNullableFilter",
407
- lines: [` readonly equals?: InputJsonValue | null;`, ` readonly not?: InputJsonValue | null;`]
408
- }));
409
- parts.push(objectAlias({
665
+ parts.push(jsonFilterAlias({ nullable: false }));
666
+ parts.push(jsonFilterAlias({ nullable: true }));
667
+ parts.push(inputAlias({
410
668
  name: "JsonListFilter",
411
669
  lines: [
412
670
  ` readonly equals?: readonly InputJsonValue[] | null;`,
@@ -423,18 +681,55 @@ function filterSection() {
423
681
  }
424
682
  return parts;
425
683
  }
426
- function emitEnum(params) {
684
+ var GENERATED_HEADER = `// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
685
+ // Regenerate with \`vibeorm generate\` after every schema change.`;
686
+ function emitEnumDeclaration(params) {
427
687
  const { declared } = params;
428
688
  const name = declared.name;
429
- const parts = [banner({ title: `Enum ${name}` })];
430
689
  const valueLines = declared.values.map((value) => ` readonly ${propKey({ name: value })}: ${tsString({ value })};`);
431
- parts.push(`export declare const ${name}: {
690
+ return [
691
+ banner({ title: `Enum ${name}` }),
692
+ `export declare const ${name}: {
432
693
  ${valueLines.join(`
433
694
  `)}
434
- };`);
435
- parts.push(`export type ${name} = ${declared.values.map((value) => tsString({ value })).join(" | ")};`);
695
+ };`,
696
+ `export type ${name} = ${declared.values.map((value) => tsString({ value })).join(" | ")};`,
697
+ `/** Boundary validator for ${name}: \`values\`, \`is\`, \`parse\` (throws VIBE_VALIDATION on a non-member). */
698
+ ` + `export type ${name}Validator = {
699
+ ` + ` readonly values: readonly ${name}[];
700
+ ` + ` is(value: unknown): value is ${name};
701
+ ` + ` parse(params: { readonly value: unknown }): ${name};
702
+ ` + `};`
703
+ ].join(`
704
+
705
+ `);
706
+ }
707
+ function emitEnumsDts(params) {
708
+ const { view } = params;
709
+ const parts = [
710
+ `${GENERATED_HEADER}
711
+ // Lightweight entry: enum constants + boundary parsers. No runtime client, no schema module.`
712
+ ];
713
+ for (const declared of view.enums)
714
+ parts.push(emitEnumDeclaration({ declared }));
715
+ parts.push(banner({ title: "Enum validators" }));
716
+ const validatorLines = view.enums.map((declared) => ` readonly ${declared.name}: ${declared.name}Validator;`);
717
+ parts.push(`/** Boundary parsers grouped by enum name, so an enum member named \`parse\` cannot collide. */
718
+ ` + (validatorLines.length === 0 ? `export declare const enumValidators: Record<string, never>;` : `export declare const enumValidators: {
719
+ ${validatorLines.join(`
720
+ `)}
721
+ };`));
722
+ return `${parts.join(`
723
+
724
+ `)}
725
+ `;
726
+ }
727
+ function emitEnum(params) {
728
+ const { declared } = params;
729
+ const name = declared.name;
730
+ const parts = [banner({ title: `Enum ${name} filters` })];
436
731
  const array = `readonly ${name}[]`;
437
- parts.push(objectAlias({
732
+ parts.push(inputAlias({
438
733
  name: `${name}Filter`,
439
734
  lines: [
440
735
  ` readonly equals?: ${name};`,
@@ -443,7 +738,7 @@ ${valueLines.join(`
443
738
  ` readonly not?: ${name} | ${name}Filter;`
444
739
  ]
445
740
  }));
446
- parts.push(objectAlias({
741
+ parts.push(inputAlias({
447
742
  name: `${name}NullableFilter`,
448
743
  lines: [
449
744
  ` readonly equals?: ${name} | null;`,
@@ -452,7 +747,7 @@ ${valueLines.join(`
452
747
  ` readonly not?: ${name} | ${name}NullableFilter | null;`
453
748
  ]
454
749
  }));
455
- parts.push(objectAlias({
750
+ parts.push(inputAlias({
456
751
  name: `${name}ListFilter`,
457
752
  lines: [
458
753
  ` readonly equals?: ${array} | null;`,
@@ -569,7 +864,7 @@ function registerNestedContext(params) {
569
864
  register({
570
865
  registry,
571
866
  name: `${t}Create${suffix}Input`,
572
- text: objectAlias({
867
+ text: inputAlias({
573
868
  name: `${t}Create${suffix}Input`,
574
869
  lines: checkedCreateLines({ model: target, omitRelation: inverse })
575
870
  })
@@ -577,7 +872,7 @@ function registerNestedContext(params) {
577
872
  register({
578
873
  registry,
579
874
  name: `${t}UncheckedCreate${suffix}Input`,
580
- text: objectAlias({
875
+ text: inputAlias({
581
876
  name: `${t}UncheckedCreate${suffix}Input`,
582
877
  lines: uncheckedCreateLines({ model: target, omitRelation: inverse })
583
878
  })
@@ -585,7 +880,7 @@ function registerNestedContext(params) {
585
880
  register({
586
881
  registry,
587
882
  name: `${t}Update${suffix}Input`,
588
- text: objectAlias({
883
+ text: inputAlias({
589
884
  name: `${t}Update${suffix}Input`,
590
885
  lines: checkedUpdateLines({ model: target, omitRelation: inverse })
591
886
  })
@@ -593,7 +888,7 @@ function registerNestedContext(params) {
593
888
  register({
594
889
  registry,
595
890
  name: `${t}UncheckedUpdate${suffix}Input`,
596
- text: objectAlias({
891
+ text: inputAlias({
597
892
  name: `${t}UncheckedUpdate${suffix}Input`,
598
893
  lines: uncheckedUpdateLines({ model: target, omitRelation: inverse })
599
894
  })
@@ -603,7 +898,7 @@ function registerNestedContext(params) {
603
898
  register({
604
899
  registry,
605
900
  name: connectOrCreateName,
606
- text: objectAlias({
901
+ text: inputAlias({
607
902
  name: connectOrCreateName,
608
903
  lines: [` readonly where: ${t}WhereUniqueInput;`, ` readonly create: ${createUnion};`]
609
904
  })
@@ -613,7 +908,7 @@ function registerNestedContext(params) {
613
908
  register({
614
909
  registry,
615
910
  name: `${t}CreateNestedMany${suffix}Input`,
616
- text: objectAlias({
911
+ text: inputAlias({
617
912
  name: `${t}CreateNestedMany${suffix}Input`,
618
913
  lines: [
619
914
  ` readonly create?: ${orMany({ type: createUnion })};`,
@@ -627,7 +922,7 @@ function registerNestedContext(params) {
627
922
  register({
628
923
  registry,
629
924
  name: updateWithWhere,
630
- text: objectAlias({
925
+ text: inputAlias({
631
926
  name: updateWithWhere,
632
927
  lines: [` readonly where: ${t}WhereUniqueInput;`, ` readonly data: ${updateUnion};`]
633
928
  })
@@ -636,7 +931,7 @@ function registerNestedContext(params) {
636
931
  register({
637
932
  registry,
638
933
  name: updateManyWithWhere,
639
- text: objectAlias({
934
+ text: inputAlias({
640
935
  name: updateManyWithWhere,
641
936
  lines: [` readonly where: ${t}WhereInput;`, ` readonly data: ${t}UpdateManyInput;`]
642
937
  })
@@ -645,7 +940,7 @@ function registerNestedContext(params) {
645
940
  register({
646
941
  registry,
647
942
  name: upsertWithWhere,
648
- text: objectAlias({
943
+ text: inputAlias({
649
944
  name: upsertWithWhere,
650
945
  lines: [
651
946
  ` readonly where: ${t}WhereUniqueInput;`,
@@ -657,7 +952,7 @@ function registerNestedContext(params) {
657
952
  register({
658
953
  registry,
659
954
  name: `${t}UpdateNestedMany${suffix}Input`,
660
- text: objectAlias({
955
+ text: inputAlias({
661
956
  name: `${t}UpdateNestedMany${suffix}Input`,
662
957
  lines: [
663
958
  ` readonly create?: ${orMany({ type: createUnion })};`,
@@ -679,7 +974,7 @@ function registerNestedContext(params) {
679
974
  register({
680
975
  registry,
681
976
  name: `${t}CreateNestedOne${suffix}Input`,
682
- text: objectAlias({
977
+ text: inputAlias({
683
978
  name: `${t}CreateNestedOne${suffix}Input`,
684
979
  lines: [
685
980
  ` readonly create?: ${createUnion};`,
@@ -691,7 +986,7 @@ function registerNestedContext(params) {
691
986
  register({
692
987
  registry,
693
988
  name: `${t}UpdateNestedOne${suffix}Input`,
694
- text: objectAlias({
989
+ text: inputAlias({
695
990
  name: `${t}UpdateNestedOne${suffix}Input`,
696
991
  lines: [
697
992
  ` readonly create?: ${createUnion};`,
@@ -705,9 +1000,9 @@ function registerNestedContext(params) {
705
1000
  });
706
1001
  }
707
1002
  }
708
- var AGGREGATE_OPS_UNION = `"_count" | "_avg" | "_sum" | "_min" | "_max"`;
709
1003
  function emitModel(params) {
710
1004
  const { model, view, registry } = params;
1005
+ const computedEntries = params.computed ?? [];
711
1006
  const n = model.name;
712
1007
  const hasRelations = model.relations.length > 0;
713
1008
  const parts = [banner({ title: `Model ${n}` })];
@@ -715,10 +1010,17 @@ function emitModel(params) {
715
1010
  registerNestedContext({ registry, view, relation });
716
1011
  }
717
1012
  parts.push(`export type ${n} = {
718
- ${model.fields.map((field) => ` ${propKey({ name: field.name })}: ${payloadFieldType({ field })};`).join(`
1013
+ ${[
1014
+ ...model.fields.map((field) => ` ${propKey({ name: field.name })}: ${payloadFieldType({ field })};`),
1015
+ ...computedEntries.map((entry) => ` ${propKey({ name: entry.field })}: ${entry.type};`)
1016
+ ].join(`
719
1017
  `)}
720
1018
  };`);
721
1019
  parts.push(`export type ${n}ScalarFieldEnum = ${model.fields.map((field) => tsString({ value: field.name })).join(" | ")};`);
1020
+ if (computedEntries.length > 0) {
1021
+ parts.push(`export type ${n}SelectableFieldEnum = ${n}ScalarFieldEnum | ${computedEntries.map((entry) => tsString({ value: entry.field })).join(" | ")};`);
1022
+ }
1023
+ const selectableEnum = computedEntries.length > 0 ? `${n}SelectableFieldEnum` : `${n}ScalarFieldEnum`;
722
1024
  if (hasRelations) {
723
1025
  parts.push(`export type ${n}Relations = {
724
1026
  ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${relationPayloadType({ relation })};`).join(`
@@ -730,31 +1032,35 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
730
1032
  const countLine = ` readonly _count?: boolean | { readonly select: ${n}CountSelect };`;
731
1033
  const relationSelectValue = (relation) => `boolean | ${relation.target}${relation.isList ? "FindManyArgs" : "Args"}`;
732
1034
  if (hasCounts) {
733
- parts.push(objectAlias({
1035
+ parts.push(inputAlias({
734
1036
  name: `${n}CountSelect`,
735
- lines: listRelations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: boolean;`)
1037
+ lines: listRelations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: boolean | { readonly where?: ${relation.target}WhereInput };`)
736
1038
  }));
737
1039
  }
738
- parts.push(objectAlias({
1040
+ parts.push(inputAlias({
739
1041
  name: `${n}Select`,
740
1042
  lines: [
741
1043
  ...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: boolean;`),
1044
+ ...computedEntries.map((entry) => ` readonly ${propKey({ name: entry.field })}?: boolean;`),
742
1045
  ...model.relations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: ${relationSelectValue(relation)};`),
743
1046
  ...hasCounts ? [countLine] : []
744
1047
  ]
745
1048
  }));
746
- parts.push(objectAlias({
1049
+ parts.push(inputAlias({
747
1050
  name: `${n}Include`,
748
1051
  lines: [
749
1052
  ...model.relations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: ${relationSelectValue(relation)};`),
750
1053
  ...hasCounts ? [countLine] : []
751
1054
  ]
752
1055
  }));
753
- parts.push(objectAlias({
1056
+ parts.push(inputAlias({
754
1057
  name: `${n}OmitInput`,
755
- lines: model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: boolean;`)
1058
+ lines: [
1059
+ ...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: boolean;`),
1060
+ ...computedEntries.map((entry) => ` readonly ${propKey({ name: entry.field })}?: boolean;`)
1061
+ ]
756
1062
  }));
757
- parts.push(objectAlias({
1063
+ parts.push(inputAlias({
758
1064
  name: `${n}Args`,
759
1065
  lines: [
760
1066
  ` readonly select?: ${n}Select;`,
@@ -763,7 +1069,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
763
1069
  ` readonly relationStrategy?: RelationStrategy;`
764
1070
  ]
765
1071
  }));
766
- parts.push(objectAlias({
1072
+ parts.push(inputAlias({
767
1073
  name: `${n}ListRelationFilter`,
768
1074
  lines: [
769
1075
  ` readonly some?: ${n}WhereInput;`,
@@ -771,7 +1077,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
771
1077
  ` readonly none?: ${n}WhereInput;`
772
1078
  ]
773
1079
  }));
774
- parts.push(objectAlias({
1080
+ parts.push(inputAlias({
775
1081
  name: `${n}RelationFilter`,
776
1082
  lines: [` readonly is?: ${n}WhereInput | null;`, ` readonly isNot?: ${n}WhereInput | null;`]
777
1083
  }));
@@ -797,35 +1103,35 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
797
1103
  whereLines.push(` readonly ${key}?: ${relation.target}RelationFilter | ${relation.target}WhereInput${relation.isOptional ? " | null" : ""};`);
798
1104
  }
799
1105
  }
800
- parts.push(objectAlias({ name: `${n}WhereInput`, lines: whereLines }));
1106
+ parts.push(inputAlias({ name: `${n}WhereInput`, lines: whereLines }));
801
1107
  if (model.uniques.length === 0) {
802
1108
  parts.push(`export type ${n}WhereUniqueInput = never;`);
803
1109
  } else {
804
1110
  const members = model.uniques.map((member) => {
805
1111
  const first = member.fields[0];
806
1112
  if (member.fields.length === 1 && first !== undefined) {
807
- return `{ readonly ${propKey({ name: first.name })}: ${inputBaseType({ field: first })} }`;
1113
+ return `{ readonly ${propKey({ name: first.name })}: ${inputBaseType({ field: first })}${member.nullsNotDistinct && first.isOptional ? " | null" : ""} }`;
808
1114
  }
809
- const inner = member.fields.map((field) => `readonly ${propKey({ name: field.name })}: ${inputBaseType({ field })}`).join("; ");
1115
+ const inner = member.fields.map((field) => `readonly ${propKey({ name: field.name })}: ${inputBaseType({ field })}${member.nullsNotDistinct && field.isOptional ? " | null" : ""}`).join("; ");
810
1116
  return `{ readonly ${propKey({ name: member.key })}: { ${inner} } }`;
811
1117
  });
812
1118
  parts.push(`export type ${n}WhereUniqueInput =
813
1119
  | ${members.join(`
814
1120
  | `)};`);
815
1121
  }
816
- parts.push(objectAlias({
1122
+ parts.push(inputAlias({
817
1123
  name: `${n}OrderByInput`,
818
1124
  lines: model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: ${field.isOptional ? "SortOrderWithNulls" : "SortOrder"};`)
819
1125
  }));
820
- parts.push(objectAlias({ name: `${n}CreateInput`, lines: checkedCreateLines({ model }) }));
821
- parts.push(objectAlias({ name: `${n}UncheckedCreateInput`, lines: uncheckedCreateLines({ model }) }));
1126
+ parts.push(inputAlias({ name: `${n}CreateInput`, lines: checkedCreateLines({ model }) }));
1127
+ parts.push(inputAlias({ name: `${n}UncheckedCreateInput`, lines: uncheckedCreateLines({ model }) }));
822
1128
  parts.push(`export type ${n}CreateManyInput = ${n}UncheckedCreateInput;`);
823
- parts.push(objectAlias({ name: `${n}UpdateInput`, lines: checkedUpdateLines({ model }) }));
824
- parts.push(objectAlias({ name: `${n}UncheckedUpdateInput`, lines: uncheckedUpdateLines({ model }) }));
1129
+ parts.push(inputAlias({ name: `${n}UpdateInput`, lines: checkedUpdateLines({ model }) }));
1130
+ parts.push(inputAlias({ name: `${n}UncheckedUpdateInput`, lines: uncheckedUpdateLines({ model }) }));
825
1131
  parts.push(`export type ${n}UpdateManyInput = ${n}UncheckedUpdateInput;`);
826
1132
  const numericFields = model.fields.filter((field) => !field.isList && field.scalar !== null && NUMERIC_SCALARS.has(field.scalar));
827
1133
  const minmaxFields = model.fields.filter((field) => !field.isList && field.scalar !== null && MINMAX_SCALARS.has(field.scalar));
828
- parts.push(objectAlias({
1134
+ parts.push(inputAlias({
829
1135
  name: `${n}CountAggregateInput`,
830
1136
  lines: [
831
1137
  ...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`),
@@ -834,7 +1140,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
834
1140
  }));
835
1141
  const aggregateOpProps = [` readonly _count?: true | ${n}CountAggregateInput;`];
836
1142
  if (numericFields.length > 0) {
837
- parts.push(objectAlias({
1143
+ parts.push(inputAlias({
838
1144
  name: `${n}AvgAggregateInput`,
839
1145
  lines: numericFields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`)
840
1146
  }));
@@ -843,7 +1149,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
843
1149
  aggregateOpProps.push(` readonly _sum?: ${n}SumAggregateInput;`);
844
1150
  }
845
1151
  if (minmaxFields.length > 0) {
846
- parts.push(objectAlias({
1152
+ parts.push(inputAlias({
847
1153
  name: `${n}MinAggregateInput`,
848
1154
  lines: minmaxFields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`)
849
1155
  }));
@@ -851,7 +1157,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
851
1157
  aggregateOpProps.push(` readonly _min?: ${n}MinAggregateInput;`);
852
1158
  aggregateOpProps.push(` readonly _max?: ${n}MaxAggregateInput;`);
853
1159
  }
854
- parts.push(objectAlias({
1160
+ parts.push(inputAlias({
855
1161
  name: `${n}FindManyArgs`,
856
1162
  lines: [
857
1163
  ` readonly where?: ${n}WhereInput;`,
@@ -860,24 +1166,28 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
860
1166
  ` readonly include?: ${n}Include;`,
861
1167
  ` readonly omit?: ${n}OmitInput;`,
862
1168
  ` readonly cursor?: ${n}WhereUniqueInput;`,
1169
+ ` readonly after?: KeysetToken;`,
1170
+ ` readonly keyset?: KeysetOptions;`,
863
1171
  ` readonly take?: number;`,
864
1172
  ` readonly skip?: number;`,
865
1173
  ` readonly distinct?: ${orMany({ type: `${n}ScalarFieldEnum` })};`,
866
- ` readonly relationStrategy?: RelationStrategy;`
1174
+ ` readonly relationStrategy?: RelationStrategy;`,
1175
+ ` readonly lock?: "update";`
867
1176
  ]
868
1177
  }));
869
1178
  parts.push(`export type ${n}FindFirstArgs = ${n}FindManyArgs;`);
870
- parts.push(objectAlias({
1179
+ parts.push(inputAlias({
871
1180
  name: `${n}FindUniqueArgs`,
872
1181
  lines: [
873
1182
  ` readonly where: ${n}WhereUniqueInput;`,
874
1183
  ` readonly select?: ${n}Select;`,
875
1184
  ` readonly include?: ${n}Include;`,
876
1185
  ` readonly omit?: ${n}OmitInput;`,
877
- ` readonly relationStrategy?: RelationStrategy;`
1186
+ ` readonly relationStrategy?: RelationStrategy;`,
1187
+ ` readonly lock?: "update";`
878
1188
  ]
879
1189
  }));
880
- parts.push(objectAlias({
1190
+ parts.push(inputAlias({
881
1191
  name: `${n}CreateArgs`,
882
1192
  lines: [
883
1193
  ` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
@@ -886,48 +1196,97 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
886
1196
  ` readonly omit?: ${n}OmitInput;`
887
1197
  ]
888
1198
  }));
889
- parts.push(objectAlias({
1199
+ parts.push(`export type ${n}ConflictTarget = readonly ${n}ScalarFieldEnum[] | { readonly constraint: string };`);
1200
+ parts.push(inputAlias({
1201
+ name: `${n}CreateOnConflictInput`,
1202
+ lines: [
1203
+ ` readonly action: "nothing";`,
1204
+ ` readonly target?: ${n}ConflictTarget;`,
1205
+ ` readonly where?: string;`
1206
+ ]
1207
+ }));
1208
+ parts.push(inputAlias({
1209
+ name: `${n}CreateOrSkipArgs`,
1210
+ lines: [
1211
+ ` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
1212
+ ` readonly onConflict: ${n}CreateOnConflictInput;`,
1213
+ ` readonly select?: ${n}Select;`,
1214
+ ` readonly include?: ${n}Include;`,
1215
+ ` readonly omit?: ${n}OmitInput;`
1216
+ ]
1217
+ }));
1218
+ parts.push(inputAlias({
890
1219
  name: `${n}CreateManyArgs`,
891
1220
  lines: [
892
1221
  ` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
893
- ` readonly skipDuplicates?: boolean;`
1222
+ ` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`
894
1223
  ]
895
1224
  }));
896
- parts.push(objectAlias({
1225
+ parts.push(inputAlias({
897
1226
  name: `${n}CreateManyAndReturnArgs`,
898
1227
  lines: [
899
1228
  ` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
900
- ` readonly skipDuplicates?: boolean;`,
1229
+ ` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`,
901
1230
  ` readonly select?: ${n}Select;`,
902
1231
  ` readonly omit?: ${n}OmitInput;`
903
1232
  ]
904
1233
  }));
905
- parts.push(objectAlias({
1234
+ parts.push(inputAlias({
1235
+ name: `${n}UpsertManyArgs`,
1236
+ lines: [
1237
+ ` readonly data: readonly ${n}CreateManyInput[];`,
1238
+ ` readonly conflictTarget: ${n}ConflictTarget;`,
1239
+ ` readonly conflictWhere?: string;`,
1240
+ ` readonly update: readonly ${n}ScalarFieldEnum[];`,
1241
+ ` readonly skipUpdatedAt?: boolean;`,
1242
+ ` readonly allowAnyUniqueConflict?: boolean;`
1243
+ ]
1244
+ }));
1245
+ parts.push(inputAlias({
906
1246
  name: `${n}UpdateArgs`,
907
1247
  lines: [
908
- ` readonly where: ${n}WhereUniqueInput;`,
1248
+ ` readonly where: ${n}UpdateWhereInput;`,
909
1249
  ` readonly data: ${n}UpdateInput | ${n}UncheckedUpdateInput;`,
910
1250
  ` readonly select?: ${n}Select;`,
911
1251
  ` readonly include?: ${n}Include;`,
912
- ` readonly omit?: ${n}OmitInput;`
1252
+ ` readonly omit?: ${n}OmitInput;`,
1253
+ ` readonly skipUpdatedAt?: boolean;`
913
1254
  ]
914
1255
  }));
915
- parts.push(objectAlias({
1256
+ parts.push(`/** update()'s where: a concrete unique selector AND any additional predicates (optimistic-concurrency guards such as \`{ id, version }\`). */
1257
+ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1258
+ parts.push(inputAlias({
916
1259
  name: `${n}UpdateManyArgs`,
917
- lines: [` readonly where?: ${n}WhereInput;`, ` readonly data: ${n}UpdateManyInput;`]
1260
+ lines: [
1261
+ ` readonly where?: ${n}WhereInput;`,
1262
+ ` readonly data: ${n}UpdateManyInput;`,
1263
+ ` readonly skipUpdatedAt?: boolean;`
1264
+ ]
918
1265
  }));
919
- parts.push(objectAlias({
1266
+ parts.push(inputAlias({
1267
+ name: `${n}UpdateManyAndReturnArgs`,
1268
+ lines: [
1269
+ ` readonly include?: never;`,
1270
+ ` readonly where?: ${n}WhereInput;`,
1271
+ ` readonly data: ${n}UpdateManyInput;`,
1272
+ ` readonly select?: ${n}Select;`,
1273
+ ` readonly omit?: ${n}OmitInput;`,
1274
+ ` readonly skipUpdatedAt?: boolean;`
1275
+ ]
1276
+ }));
1277
+ parts.push(inputAlias({
920
1278
  name: `${n}UpsertArgs`,
921
1279
  lines: [
922
1280
  ` readonly where: ${n}WhereUniqueInput;`,
923
1281
  ` readonly create: ${n}CreateInput | ${n}UncheckedCreateInput;`,
924
1282
  ` readonly update: ${n}UpdateInput | ${n}UncheckedUpdateInput;`,
1283
+ ` readonly onConflict?: { readonly constraint: string };`,
925
1284
  ` readonly select?: ${n}Select;`,
926
1285
  ` readonly include?: ${n}Include;`,
927
1286
  ` readonly omit?: ${n}OmitInput;`
928
1287
  ]
929
1288
  }));
930
- parts.push(objectAlias({
1289
+ parts.push(inputAlias({
931
1290
  name: `${n}DeleteArgs`,
932
1291
  lines: [
933
1292
  ` readonly where: ${n}WhereUniqueInput;`,
@@ -936,10 +1295,11 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
936
1295
  ` readonly omit?: ${n}OmitInput;`
937
1296
  ]
938
1297
  }));
939
- parts.push(objectAlias({ name: `${n}DeleteManyArgs`, lines: [` readonly where?: ${n}WhereInput;`] }));
940
- parts.push(objectAlias({
1298
+ parts.push(inputAlias({ name: `${n}DeleteManyArgs`, lines: [` readonly where?: ${n}WhereInput;`] }));
1299
+ parts.push(inputAlias({
941
1300
  name: `${n}CountArgs`,
942
1301
  lines: [
1302
+ ` readonly select?: ${n}CountAggregateInput;`,
943
1303
  ` readonly where?: ${n}WhereInput;`,
944
1304
  ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
945
1305
  ` readonly cursor?: ${n}WhereUniqueInput;`,
@@ -947,7 +1307,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
947
1307
  ` readonly skip?: number;`
948
1308
  ]
949
1309
  }));
950
- parts.push(objectAlias({
1310
+ parts.push(inputAlias({
951
1311
  name: `${n}AggregateArgs`,
952
1312
  lines: [
953
1313
  ` readonly where?: ${n}WhereInput;`,
@@ -958,7 +1318,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
958
1318
  ...aggregateOpProps
959
1319
  ]
960
1320
  }));
961
- parts.push(objectAlias({
1321
+ parts.push(inputAlias({
962
1322
  name: `${n}GroupByArgs`,
963
1323
  lines: [
964
1324
  ` readonly by: readonly ${n}ScalarFieldEnum[];`,
@@ -970,20 +1330,26 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
970
1330
  ...aggregateOpProps
971
1331
  ]
972
1332
  }));
973
- const falseFilter = (source) => `${source}[K] extends false | undefined ? never : K`;
974
- parts.push(`type ${n}OmitBase<A> = A extends { readonly omit: infer O extends object } ? Omit<${n}, OmittedKeys<O>> : ${n};`);
1333
+ const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
1334
+ const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
1335
+ parts.push(`type ${n}OmitBase<A> = A extends { readonly omit: infer O extends object }
1336
+ ` + ` ? Omit<${n}, PossibleKeys<O> & keyof ${n}> & { [K in UncertainKeys<O> & keyof ${n}]?: ${n}[K] }
1337
+ ` + ` : "omit" extends keyof A ? Omit<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}> & Partial<Pick<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}>> : ${n};`);
975
1338
  if (hasCounts) {
976
1339
  const listUnion = listRelations.map((relation) => tsString({ value: relation.name })).join(" | ");
977
1340
  const allCounts = listRelations.map((relation) => `${propKey({ name: relation.name })}: number`).join("; ");
978
1341
  parts.push(`type ${n}CountResult<C> = C extends { readonly select: infer CS extends object }
979
- ` + ` ? { [K in keyof CS & (${listUnion}) as ${falseFilter("CS")}]: number }
1342
+ ` + ` ? { [K in keyof CS & (${listUnion}) as ${exactPart("CS")}]: number } &
1343
+ ` + ` { [K in keyof CS & (${listUnion}) as ${uncertainPart("CS")}]?: number }
980
1344
  ` + ` : { ${allCounts} };`);
981
- parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> } : unknown;`);
1345
+ parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> }
1346
+ ` + ` : S extends { readonly _count: false | undefined } ? unknown
1347
+ ` + ` : S extends { readonly _count?: infer C extends boolean | object } ? { _count?: ${n}CountResult<C> } : unknown;`);
982
1348
  }
983
1349
  if (hasRelations) {
984
1350
  const relationCases = model.relations.map((relation) => {
985
1351
  const nested = `${relation.target}GetPayload<A>`;
986
- const guard = `A extends { readonly select: unknown } | { readonly include: unknown } | { readonly omit: unknown }`;
1352
+ const guard = `A extends object`;
987
1353
  let expression;
988
1354
  if (relation.isList) {
989
1355
  expression = `${guard} ? ${nested}[] : ${relation.target}[]`;
@@ -999,47 +1365,68 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
999
1365
  ${relationCases}
1000
1366
  }[K];`);
1001
1367
  parts.push(`type ${n}IncludePayload<A> = A extends { readonly include: infer I extends object }
1002
- ` + ` ? { [K in keyof I & keyof ${n}Relations as ${falseFilter("I")}]: ${n}RelationPayload<K, I[K]> }${hasCounts ? ` & ${n}CountPayload<I>` : ""}
1003
- ` + ` : unknown;`);
1368
+ ` + ` ? { [K in keyof I & keyof ${n}Relations as ${exactPart("I")}]: ${n}RelationPayload<K, I[K]> } &
1369
+ ` + ` { [K in keyof I & keyof ${n}Relations as ${uncertainPart("I")}]?: ${n}RelationPayload<K, I[K]> }${hasCounts ? ` &
1370
+ ${n}CountPayload<I>` : ""}
1371
+ ` + ` : "include" extends keyof A ? Partial<${n}IncludePayload<{ include: Provided<A["include"]> }>> : unknown;`);
1004
1372
  parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
1005
- ` + ` ? { [K in keyof S & ${n}ScalarFieldEnum as ${falseFilter("S")}]: ${n}[K & keyof ${n}] } &
1006
- ` + ` { [K in keyof S & keyof ${n}Relations as ${falseFilter("S")}]: ${n}RelationPayload<K, S[K]> }${hasCounts ? ` &
1373
+ ` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1374
+ ` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] } &
1375
+ ` + ` { [K in keyof S & keyof ${n}Relations as ${exactPart("S")}]: ${n}RelationPayload<K, S[K]> } &
1376
+ ` + ` { [K in keyof S & keyof ${n}Relations as ${uncertainPart("S")}]?: ${n}RelationPayload<K, S[K]> }${hasCounts ? ` &
1007
1377
  ${n}CountPayload<S>` : ""}
1378
+ ` + ` : "select" extends keyof A ? Partial<${n}>
1008
1379
  ` + ` : ${n}OmitBase<A> & ${n}IncludePayload<A>;`);
1009
1380
  } else {
1010
1381
  parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
1011
- ` + ` ? { [K in keyof S & ${n}ScalarFieldEnum as ${falseFilter("S")}]: ${n}[K & keyof ${n}] }
1012
- ` + ` : ${n}OmitBase<A>;`);
1382
+ ` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1383
+ ` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
1384
+ ` + ` : "select" extends keyof A ? Partial<${n}> : ${n}OmitBase<A>;`);
1013
1385
  }
1014
- parts.push(`export type ${n}AggregateResult<A> = {
1015
- ` + ` [K in keyof A & (${AGGREGATE_OPS_UNION})]:
1016
- ` + ` K extends "_count" ? (A[K] extends true ? number : { [F in keyof A[K]]: number })
1017
- ` + ` : K extends "_avg" ? { [F in keyof A[K]]: number | null }
1018
- ` + ` : { [F in keyof A[K] & keyof ${n}]: ${n}[F & keyof ${n}] | null };
1019
- ` + `};`);
1386
+ const aggregateFields = (fields, value) => `{ ${fields.map((field) => `${propKey({ name: field.name })}: ${value(field)}`).join("; ")} }`;
1387
+ const countValues = aggregateFields(model.fields, () => "number").replace(" }", "; _all: number }");
1388
+ parts.push(`type ${n}AggregateValues = {
1389
+ ` + ` _count: ${countValues};
1390
+ ` + ` _avg: ${aggregateFields(numericFields, () => "number | null")};
1391
+ ` + ` _sum: ${aggregateFields(numericFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
1392
+ ` + ` _min: ${aggregateFields(minmaxFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
1393
+ ` + ` _max: ${aggregateFields(minmaxFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
1394
+ };`);
1395
+ parts.push(`type ${n}AggregateSelection<V, S> = S extends true ? number : S extends object
1396
+ ` + ` ? { [K in keyof S & keyof V as ${exactPart("S")}]: V[K] } &
1397
+ ` + ` { [K in keyof S & keyof V as ${uncertainPart("S")}]?: V[K] } : never;`);
1398
+ parts.push(`type ${n}CountOutput<A> = A extends { readonly select: infer S extends object }
1399
+ ` + ` ? ${n}AggregateSelection<${n}AggregateValues["_count"], S>
1400
+ ` + ` : "select" extends keyof A ? number | ${n}AggregateSelection<${n}AggregateValues["_count"], Provided<A["select"]>> : number;`);
1401
+ parts.push(`export type ${n}AggregateResult<A> =
1402
+ ` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? K : never]: ${n}AggregateSelection<${n}AggregateValues[K], A[K]> } &
1403
+ ` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? never : K]?: ${n}AggregateSelection<${n}AggregateValues[K], Provided<A[K]>> };`);
1020
1404
  parts.push(`export type ${n}GroupByResult<A extends ${n}GroupByArgs> =
1021
- ` + ` { [K in A["by"][number]]: ${n}[K & keyof ${n}] } & ${n}AggregateResult<A>;`);
1405
+ ` + ` (number extends A["by"]["length"] ? Partial<Pick<${n}, A["by"][number]>> : Pick<${n}, A["by"][number]>) & ${n}AggregateResult<A>;`);
1022
1406
  const readMethods = [
1023
- ` findMany<A extends ${n}FindManyArgs = ${n}FindManyArgs>(args?: A): Promise<${n}GetPayload<A>[]>;`,
1024
- ` findFirst<A extends ${n}FindFirstArgs = ${n}FindFirstArgs>(args?: A): Promise<${n}GetPayload<A> | null>;`,
1025
- ` findFirstOrThrow<A extends ${n}FindFirstArgs = ${n}FindFirstArgs>(args?: A): Promise<${n}GetPayload<A>>;`,
1407
+ ` findMany<A extends ${n}FindManyArgs = {}>(args?: A): Promise<${n}GetPayload<A>[]>;`,
1408
+ ` findFirst<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A> | null>;`,
1409
+ ` findFirstOrThrow<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A>>;`,
1026
1410
  ` findUnique<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
1027
1411
  ` findUniqueOrThrow<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A>>;`
1028
1412
  ];
1029
1413
  const writeMethods = [
1414
+ ` create<A extends ${n}CreateOrSkipArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
1030
1415
  ` create<A extends ${n}CreateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1031
1416
  ` createMany(args: ${n}CreateManyArgs): Promise<BatchPayload>;`,
1032
1417
  ` createManyAndReturn<A extends ${n}CreateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
1033
1418
  ` update<A extends ${n}UpdateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1034
1419
  ` updateMany(args: ${n}UpdateManyArgs): Promise<BatchPayload>;`,
1420
+ ` updateManyAndReturn<A extends ${n}UpdateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
1035
1421
  ` upsert<A extends ${n}UpsertArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1422
+ ` upsertMany(args: ${n}UpsertManyArgs): Promise<UpsertManyPayload>;`,
1036
1423
  ` delete<A extends ${n}DeleteArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1037
1424
  ` deleteMany(args?: ${n}DeleteManyArgs): Promise<BatchPayload>;`
1038
1425
  ];
1039
1426
  const aggregateMethods = [
1040
- ` count(args?: ${n}CountArgs): Promise<number>;`,
1427
+ ` count<A extends ${n}CountArgs = {}>(args?: A): Promise<${n}CountOutput<A>>;`,
1041
1428
  ` aggregate<A extends ${n}AggregateArgs>(args: A): Promise<${n}AggregateResult<A>>;`,
1042
- ` groupBy<A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;`
1429
+ ` groupBy<const A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;`
1043
1430
  ];
1044
1431
  const delegateMethods = model.isView ? [...readMethods, ...aggregateMethods] : [...readMethods, ...writeMethods, ...aggregateMethods];
1045
1432
  delegateMethods.push(...params.extensionLines ?? []);
@@ -1108,49 +1495,54 @@ function emitViewTypesForModel(params) {
1108
1495
  ` readonly relationStrategy?: RelationStrategy;`
1109
1496
  ]
1110
1497
  }));
1111
- const falseFilter = (source) => `${source}[K] extends false | undefined ? never : K`;
1498
+ const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
1499
+ const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
1112
1500
  parts.push(`type ${n}ViewSelectScalars<F, A> = A extends { readonly select: infer S extends object }
1113
- ` + ` ? { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${falseFilter("S")}]: ${n}[K & keyof ${n}] }
1114
- ` + ` : ${n}ViewScalars<F>;`);
1501
+ ` + ` ? { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1502
+ ` + ` { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
1503
+ ` + ` : "select" extends keyof A ? Partial<${n}ViewScalars<F>> : ${n}ViewScalars<F>;`);
1115
1504
  parts.push(`type ${n}ViewReveal<A> = A extends { readonly reveal: infer R extends object }
1116
- ` + ` ? { [K in keyof R & ${n}ScalarFieldEnum as ${falseFilter("R")}]: ${n}[K & keyof ${n}] }
1117
- ` + ` : unknown;`);
1505
+ ` + ` ? { [K in keyof R & ${n}ScalarFieldEnum as ${exactPart("R")}]: ${n}[K & keyof ${n}] } &
1506
+ ` + ` { [K in keyof R & ${n}ScalarFieldEnum as ${uncertainPart("R")}]?: ${n}[K & keyof ${n}] }
1507
+ ` + ` : "reveal" extends keyof A ? Partial<${n}ViewReveal<{ reveal: NonNullable<A["reveal"]> }>> : unknown;`);
1118
1508
  if (hasRelations) {
1119
1509
  const relationCases = model.relations.map((relation) => {
1120
1510
  const target = relation.target;
1121
1511
  const targetClientKey = view.modelByName.get(target)?.clientName ?? target;
1122
1512
  const guard = `D extends { readonly ${propKey({ name: targetClientKey })}: infer RF extends object }`;
1123
- const masked = `ViewRow<${target}ViewScalars<RF>>`;
1513
+ const masked = `ViewRow<${target}ViewSelectScalars<RF, A> & ${target}ViewReveal<A>>`;
1514
+ const unmasked = `(A extends object ? ${target}GetPayload<A> : ${target})`;
1124
1515
  let expression;
1125
1516
  if (relation.isList) {
1126
- expression = `${guard} ? ${masked}[] : ${target}[]`;
1517
+ expression = `${guard} ? ${masked}[] : ${unmasked}[]`;
1127
1518
  } else if (relation.isOptional) {
1128
- expression = `(${guard} ? ${masked} : ${target}) | null`;
1519
+ expression = `(${guard} ? ${masked} : ${unmasked}) | null`;
1129
1520
  } else {
1130
- expression = `${guard} ? ${masked} : ${target}`;
1521
+ expression = `${guard} ? ${masked} : ${unmasked}`;
1131
1522
  }
1132
1523
  return ` ${propKey({ name: relation.name })}: ${expression};`;
1133
1524
  }).join(`
1134
1525
  `);
1135
- parts.push(`type ${n}ViewRelationResult<K extends keyof ${n}Relations, D> = {
1526
+ parts.push(`type ${n}ViewRelationResult<K extends keyof ${n}Relations, D, A> = {
1136
1527
  ${relationCases}
1137
1528
  }[K];`);
1138
1529
  parts.push(`type ${n}ViewIncludeResult<A, D> = A extends { readonly include: infer I extends object }
1139
- ` + ` ? { [K in keyof I & keyof ${n}Relations as ${falseFilter("I")}]: ${n}ViewRelationResult<K, D> }
1140
- ` + ` : unknown;`);
1530
+ ` + ` ? { [K in keyof I & keyof ${n}Relations as ${exactPart("I")}]: ${n}ViewRelationResult<K, D, I[K]> } &
1531
+ ` + ` { [K in keyof I & keyof ${n}Relations as ${uncertainPart("I")}]?: ${n}ViewRelationResult<K, D, I[K]> }
1532
+ ` + ` : "include" extends keyof A ? Partial<${n}ViewIncludeResult<{ include: NonNullable<A["include"]> }, D>> : unknown;`);
1141
1533
  parts.push(`export type ${n}ViewResult<F, A, D> = ViewRow<${n}ViewSelectScalars<F, A> & ${n}ViewReveal<A> & ${n}ViewIncludeResult<A, D>>;`);
1142
1534
  } else {
1143
1535
  parts.push(`export type ${n}ViewResult<F, A, D> = ViewRow<${n}ViewSelectScalars<F, A> & ${n}ViewReveal<A>>;`);
1144
1536
  }
1145
1537
  parts.push(`export type ${n}ViewDelegate<F, D extends ViewDefinitionInput = ViewDefinitionInput> = {
1146
- ` + ` findMany<A extends ${n}ViewFindManyArgs = ${n}ViewFindManyArgs>(args?: A): Promise<${n}ViewResult<F, A, D>[]>;
1147
- ` + ` findFirst<A extends ${n}ViewFindManyArgs = ${n}ViewFindManyArgs>(args?: A): Promise<${n}ViewResult<F, A, D> | null>;
1148
- ` + ` findFirstOrThrow<A extends ${n}ViewFindManyArgs = ${n}ViewFindManyArgs>(args?: A): Promise<${n}ViewResult<F, A, D>>;
1538
+ ` + ` findMany<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D>[]>;
1539
+ ` + ` findFirst<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D> | null>;
1540
+ ` + ` findFirstOrThrow<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D>>;
1149
1541
  ` + ` findUnique<A extends ${n}ViewFindUniqueArgs>(args: A): Promise<${n}ViewResult<F, A, D> | null>;
1150
1542
  ` + ` findUniqueOrThrow<A extends ${n}ViewFindUniqueArgs>(args: A): Promise<${n}ViewResult<F, A, D>>;
1151
- ` + ` count(args?: ${n}CountArgs): Promise<number>;
1543
+ ` + ` count<A extends ${n}CountArgs = {}>(args?: A): Promise<${n}CountOutput<A>>;
1152
1544
  ` + ` aggregate<A extends ${n}AggregateArgs>(args: A): Promise<${n}AggregateResult<A>>;
1153
- ` + ` groupBy<A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;
1545
+ ` + ` groupBy<const A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;
1154
1546
  ` + `};`);
1155
1547
  return parts.join(`
1156
1548
 
@@ -1184,38 +1576,236 @@ ${view.models.map((model) => ` ${propKey({ name: model.clientName })}: ${model.
1184
1576
  }
1185
1577
  return parts;
1186
1578
  }
1579
+ function jsonTypeImportLines(params) {
1580
+ const bySpecifier = new Map;
1581
+ for (const model of params.view.models) {
1582
+ for (const field of model.fields) {
1583
+ if (field.jsonType === undefined)
1584
+ continue;
1585
+ const names = bySpecifier.get(field.jsonType.from) ?? new Set;
1586
+ names.add(field.jsonType.name);
1587
+ bySpecifier.set(field.jsonType.from, names);
1588
+ }
1589
+ }
1590
+ return [...bySpecifier.keys()].sort().map((from) => `import type { ${[...bySpecifier.get(from)].sort().join(", ")} } from ${tsString({ value: from })};`);
1591
+ }
1592
+ function hasNativeRls(params) {
1593
+ return params.view.schema.rls !== undefined;
1594
+ }
1595
+ var RLS_CONTEXT_TYPES = {
1596
+ String: "string",
1597
+ Boolean: "boolean",
1598
+ Int: "number",
1599
+ BigInt: "bigint",
1600
+ Float: "number",
1601
+ Decimal: "string",
1602
+ DateTime: "Date"
1603
+ };
1604
+ function emitRlsSection(params) {
1605
+ const rls = params.view.schema.rls;
1606
+ if (rls === undefined)
1607
+ return [];
1608
+ const lines = Object.entries(rls.context).map(([key, slot]) => ` readonly ${propKey({ name: key })}: ${RLS_CONTEXT_TYPES[slot.type]}${slot.nullable ? " | null" : ""};`);
1609
+ const doc = `/** The identity context every query on this schema must be bound to \u2014 all slots required, an explicit null included. */`;
1610
+ return [
1611
+ banner({ title: "Native row-level security" }),
1612
+ lines.length === 0 ? `${doc}
1613
+ export type RlsContext = Record<string, never>;` : `${doc}
1614
+ export type RlsContext = {
1615
+ ${lines.join(`
1616
+ `)}
1617
+ };`
1618
+ ];
1619
+ }
1620
+ function instanceSurfaceLines(params) {
1621
+ const { view, selfType } = params;
1622
+ const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`);
1623
+ const modelsProps = view.models.flatMap((model) => {
1624
+ const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`;
1625
+ return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${model.name}Delegate;`];
1626
+ });
1627
+ return `${delegateProps.join(`
1628
+ `)}
1629
+ ` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
1630
+ ` + ` readonly $models: {
1631
+ ${modelsProps.join(`
1632
+ `)}
1633
+ };
1634
+ ` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
1635
+ ` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
1636
+ ` + ` /** ONE transaction. Callback form: everything on \`tx\` rides the same BEGIN/COMMIT; a throw rolls back. Array form (Prisma parity): UN-AWAITED delegate calls run sequentially in one transaction, results returned positionally. */
1637
+ ` + ` readonly $transaction: {
1638
+ ` + ` <T>(fn: (tx: ${selfType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
1639
+ ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1640
+ ` + ` };
1641
+ ` + ` readonly $queryRaw: <T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T[]>;
1642
+ ` + ` readonly $executeRaw: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
1643
+ ` + ` readonly $queryRawUnsafe: <T = Record<string, unknown>>(text: string, ...values: unknown[]) => Promise<T[]>;
1644
+ ` + ` readonly $executeRawUnsafe: (text: string, ...values: unknown[]) => Promise<number>;
1645
+ ` + ` readonly $connect: () => Promise<void>;
1646
+ ` + ` readonly $disconnect: () => Promise<void>;
1647
+ ` + ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
1648
+ ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
1649
+ `;
1650
+ }
1651
+ function telemetryDeclarations() {
1652
+ return [
1653
+ `/** One SQL execution inside an operation. Parameter values are redacted; \`sql\` and \`parameters\` appear only with the matching capture option. */`,
1654
+ `export type VibeTelemetryStatementEvent = {`,
1655
+ ` readonly operationId: string;`,
1656
+ ` readonly index: number;`,
1657
+ ` readonly kind: "rows" | "affected" | "raw" | "extension";`,
1658
+ ` readonly model: string | null;`,
1659
+ ` readonly method: string;`,
1660
+ ` readonly dialect: string;`,
1661
+ ` readonly provider: string;`,
1662
+ ` /** Stable normalized grouping key \u2014 bounded and literal-free, safe as a metric label. */`,
1663
+ ` readonly fingerprint: string;`,
1664
+ ` readonly outcome: "success" | "failure";`,
1665
+ ` /** Adapter-call elapsed time: networking and possibly pool wait included \u2014 not pure server execution time. */`,
1666
+ ` readonly durationMs: number;`,
1667
+ ` readonly parameterCount: number;`,
1668
+ ` readonly inTransaction: boolean;`,
1669
+ ` readonly rowCount?: number;`,
1670
+ ` readonly errorCode?: string;`,
1671
+ ` readonly slow?: boolean;`,
1672
+ ` readonly sql?: string;`,
1673
+ ` readonly parameters?: readonly unknown[];`,
1674
+ `};`,
1675
+ ``,
1676
+ `/** One public ORM call, start to terminal outcome. Emitted exactly once. Unknown values are ABSENT, never zero. */`,
1677
+ `export type VibeTelemetryOperationEvent = {`,
1678
+ ` readonly operationId: string;`,
1679
+ ` readonly parentOperationId?: string;`,
1680
+ ` readonly context?: unknown;`,
1681
+ ` readonly kind: "model" | "raw" | "transaction" | "extension";`,
1682
+ ` readonly model: string | null;`,
1683
+ ` readonly method: string;`,
1684
+ ` readonly dialect: string;`,
1685
+ ` readonly provider: string;`,
1686
+ ` readonly outcome: "success" | "failure";`,
1687
+ ` readonly stage: "refused" | "validation" | "statement" | "decode" | "transaction" | "completed" | "unknown";`,
1688
+ ` readonly dispatched: boolean;`,
1689
+ ` readonly startedAt?: number;`,
1690
+ ` readonly durationMs?: number;`,
1691
+ ` /** Creation to dispatch \u2014 labelled separately, never folded into durationMs. */`,
1692
+ ` readonly queueWaitMs?: number;`,
1693
+ ` readonly statementCount: number;`,
1694
+ ` readonly statementDurationMs?: number;`,
1695
+ ` readonly rowCount?: number;`,
1696
+ ` readonly errorCode?: string;`,
1697
+ ` readonly detail?: { readonly renderMs?: number; readonly adapterMs?: number; readonly processingMs?: number };`,
1698
+ ` readonly statements: readonly VibeTelemetryStatementEvent[];`,
1699
+ ` readonly droppedStatements?: number;`,
1700
+ ` readonly slow?: boolean;`,
1701
+ ` readonly sampled: true;`,
1702
+ `};`,
1703
+ ``,
1704
+ `/** Where events go. A throw from any callback is contained: it can never change a database outcome. */`,
1705
+ `export type VibeTelemetryExporter = {`,
1706
+ ` readonly onOperation?: (event: VibeTelemetryOperationEvent) => void;`,
1707
+ ` readonly onStatement?: (event: VibeTelemetryStatementEvent) => void;`,
1708
+ ` readonly onDrop?: (event: { readonly reason: "buffer_full" | "statement_limit"; readonly count: number }) => void;`,
1709
+ ` readonly flush?: () => void | Promise<void>;`,
1710
+ ` /** NEVER called by the client \u2014 \`$disconnect()\` flushes only, because the provider is yours. */`,
1711
+ ` readonly shutdown?: () => void | Promise<void>;`,
1712
+ `};`,
1713
+ ``,
1714
+ `/** Complete counters (spans may be sampled, these are not) and explicit lifecycle. */`,
1715
+ `export type VibeTelemetryHandle = {`,
1716
+ ` readonly metrics: () => {`,
1717
+ ` readonly countsRepresent: "all";`,
1718
+ ` readonly sampleRate: number;`,
1719
+ ` readonly sampledOperations: number;`,
1720
+ ` readonly operations: { readonly total: number; readonly failed: number; readonly refused: number };`,
1721
+ ` readonly statements: { readonly total: number; readonly failed: number };`,
1722
+ ` readonly rows: { readonly known: number; readonly total: number };`,
1723
+ ` readonly operationLatencyMs: { readonly bounds: readonly number[]; readonly counts: readonly number[]; readonly sum: number; readonly count: number };`,
1724
+ ` readonly statementLatencyMs: { readonly bounds: readonly number[]; readonly counts: readonly number[]; readonly sum: number; readonly count: number };`,
1725
+ ` readonly dropped: { readonly operations: number; readonly statements: number };`,
1726
+ ` };`,
1727
+ ` readonly flush: () => Promise<void>;`,
1728
+ ` readonly shutdown: () => Promise<void>;`,
1729
+ `};`,
1730
+ ``,
1731
+ `/** Opt-in operation telemetry. Defaults are conservative: no SQL text, no parameter values, no sampling. */`,
1732
+ `export type VibeTelemetryOptions = {`,
1733
+ ` readonly exporter: VibeTelemetryExporter;`,
1734
+ ` /** Attach rendered SQL text (default false \u2014 raw text can embed secrets). */`,
1735
+ ` readonly captureSql?: boolean;`,
1736
+ ` /** Attach parameter VALUES (default false \u2014 only the count is reported). */`,
1737
+ ` readonly captureParameterValues?: boolean;`,
1738
+ ` /** Split render / adapter / processing time on the operation event (default false). */`,
1739
+ ` readonly detailedTimings?: boolean;`,
1740
+ ` /** Span sampling probability, 0..1 (default 1). Counters still count every execution. */`,
1741
+ ` readonly sampleRate?: number;`,
1742
+ ` readonly sampler?: (input: { readonly kind: "model" | "raw" | "transaction" | "extension"; readonly model: string | null; readonly method: string; readonly context?: unknown }) => boolean;`,
1743
+ ` /** Read your current parent context SYNCHRONOUSLY at call time, so an application span survives the async boundary. */`,
1744
+ ` readonly context?: () => unknown;`,
1745
+ ` readonly delivery?: "immediate" | "buffered";`,
1746
+ ` readonly maxBufferedOperations?: number;`,
1747
+ ` readonly maxStatementsPerOperation?: number;`,
1748
+ ` readonly slowStatementMs?: number;`,
1749
+ ` readonly slowOperationMs?: number;`,
1750
+ ` readonly onExporterError?: (params: { readonly error: unknown; readonly phase: "operation" | "statement" | "drop" | "flush" | "shutdown" | "sampler" | "context" }) => void;`,
1751
+ ` /** Called once while the client is built, with the handle that owns metrics and lifecycle. */`,
1752
+ ` readonly onReady?: (handle: VibeTelemetryHandle) => void;`,
1753
+ `};`
1754
+ ].join(`
1755
+ `);
1756
+ }
1187
1757
  function emitClient(params) {
1188
- const { view, baked } = params;
1758
+ const { view, baked, hasComputed } = params;
1759
+ const nativeRls = hasNativeRls({ view });
1189
1760
  const parts = [banner({ title: "Client" })];
1761
+ parts.push(telemetryDeclarations());
1190
1762
  parts.push(objectAlias({
1191
1763
  name: "VibeClientOptions",
1192
1764
  lines: [
1193
1765
  ` readonly adapter: DatabaseAdapter;`,
1194
1766
  ` readonly onQuery?: (event: QueryEvent) => void;`,
1195
1767
  ` readonly relationStrategy?: RelationStrategy;`,
1768
+ ` /** Append the primary key to ORDER BY when a find has no explicit orderBy (default false) \u2014 deterministic row order. */`,
1769
+ ` readonly defaultOrderByPk?: boolean;`,
1770
+ ` /** Append the primary key as a tie-breaker to \`distinct\` ordering (default true) \u2014 pins WHICH row represents each distinct key. */`,
1771
+ ` readonly distinctOrderByPk?: boolean;`,
1772
+ ` /** Opt-in write-boundary validators, per model \xD7 field: \`{ User: { email: zodSchema } }\`. Any Standard Schema validator (zod, valibot, arktype) works; they run on every write path before encoding. */`,
1773
+ ` readonly validate?: WriteValidators;`,
1774
+ ` /** Opt-in operation telemetry: one event per public ORM call, with the statements it ran. Parameter values are redacted by default and the exporter can never change a database outcome. */`,
1775
+ ` readonly telemetry?: VibeTelemetryOptions;`,
1196
1776
  ...baked.empty ? [] : [
1197
1777
  ` /** The live extension instances \u2014 verified against the baked manifest. */`,
1198
1778
  ` readonly extensions?: readonly VibeExtension[];`
1199
- ]
1779
+ ],
1780
+ ...hasComputed ? [
1781
+ ` /** The live computed-field config \u2014 verified against the baked manifest (VIBE_STALE_CLIENT). */`,
1782
+ ` readonly computed: ComputedConfig;`
1783
+ ] : [],
1784
+ ...nativeRls ? [
1785
+ ` /** Native row-level security: the non-bypassing database role the runtime binds each transaction to (required on PGlite; server connections log in as it). */`,
1786
+ ` readonly nativeRls?: { readonly runtimeRole?: string };`
1787
+ ] : []
1200
1788
  ]
1201
1789
  }));
1202
- const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`);
1203
1790
  const clientExtensionLines = baked.clientDts.length === 0 ? "" : `${baked.clientDts.join(`
1204
1791
  `)}
1205
1792
  `;
1206
- parts.push(`export type VibeClientInstance = {
1207
- ` + `${delegateProps.join(`
1208
- `)}
1209
- ` + ` readonly $transaction: <T>(fn: (tx: VibeClientInstance) => Promise<T>, options?: TransactionOptions) => Promise<T>;
1210
- ` + ` readonly $queryRaw: <T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T[]>;
1211
- ` + ` readonly $executeRaw: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
1212
- ` + ` readonly $queryRawUnsafe: <T = Record<string, unknown>>(text: string, ...values: unknown[]) => Promise<T[]>;
1213
- ` + ` readonly $executeRawUnsafe: (text: string, ...values: unknown[]) => Promise<number>;
1793
+ if (nativeRls) {
1794
+ parts.push(`export type BoundVibeClientInstance = {
1795
+ ` + instanceSurfaceLines({ view, selfType: "BoundVibeClientInstance" }) + clientExtensionLines + `};`);
1796
+ parts.push(`export type VibeClientInstance = {
1797
+ ` + ` /** Bind the request's identity context \u2014 ALL of it, every declared slot \u2014 and get the client that can actually query. */
1798
+ ` + ` readonly $withContext: (context: RlsContext) => BoundVibeClientInstance;
1799
+ ` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
1800
+ ` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
1214
1801
  ` + ` readonly $connect: () => Promise<void>;
1215
1802
  ` + ` readonly $disconnect: () => Promise<void>;
1216
- ` + ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
1217
- ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
1218
- ` + clientExtensionLines + `};`);
1803
+ ` + ` // Deliberately ABSENT: model delegates, $models, $transaction, $queryRaw / $executeRaw / $queryRawUnsafe / $executeRawUnsafe, $defineView and every extension mount \u2014 an unbound client has no identity, so it cannot read or write a row. Bind one with $withContext.
1804
+ ` + `};`);
1805
+ } else {
1806
+ parts.push(`export type VibeClientInstance = {
1807
+ ` + instanceSurfaceLines({ view, selfType: "VibeClientInstance" }) + policyClientLines({ view }) + scopedClientLines({ view }) + clientExtensionLines + `};`);
1808
+ }
1219
1809
  parts.push(`/** Build a typed client over a database adapter. */
1220
1810
  export declare function VibeClient(options: VibeClientOptions): VibeClientInstance;`);
1221
1811
  parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
@@ -1224,22 +1814,127 @@ export declare const schema: SchemaIR;`);
1224
1814
 
1225
1815
  `);
1226
1816
  }
1817
+ function scopedColumnCandidates(params) {
1818
+ return params.model.fields.filter((field) => !field.isList && (field.scalar === "String" || field.scalar === "Int" || field.scalar === "BigInt")).map((field) => field.name);
1819
+ }
1820
+ function emitScopedSection(params) {
1821
+ const { view } = params;
1822
+ if (hasNativeRls({ view }))
1823
+ return [];
1824
+ if (view.models.some((model) => model.policy !== undefined))
1825
+ return [];
1826
+ const entries = view.models.map((model) => {
1827
+ const arms = [`"none"`];
1828
+ const columns = scopedColumnCandidates({ model });
1829
+ if (columns.length > 0) {
1830
+ const union = columns.map((name) => `"${name}"`).join(" | ");
1831
+ arms.push(`{ readonly column: ${union}; readonly nested?: ScopedNestedMode }`);
1832
+ }
1833
+ const throughs = model.relations.filter((relation) => !relation.isList).map((relation) => relation.name);
1834
+ if (throughs.length > 0) {
1835
+ const union = throughs.map((name) => `"${name}"`).join(" | ");
1836
+ arms.push(`{ readonly through: ${union}; readonly nested?: ScopedNestedMode }`);
1837
+ }
1838
+ return ` readonly ${propKey({ name: model.name })}: ${arms.join(" | ")};`;
1839
+ });
1840
+ const scopedDelegateType = (model) => `${model.name}Delegate`;
1841
+ const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`);
1842
+ const modelsProps = view.models.flatMap((model) => {
1843
+ const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`;
1844
+ return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${scopedDelegateType(model)};`];
1845
+ });
1846
+ return [
1847
+ banner({ title: "Tenant scoping ($scoped)" }),
1848
+ `/** What a scoped handle does when this model appears INSIDE another model's nested write: "verify" (default) rewrites reference verbs into tenant-verified lookups; "refuse" rejects them. */
1849
+ export type ScopedNestedMode = "verify" | "refuse";`,
1850
+ `/** Per-model tenancy classification \u2014 TOTAL: every model must be named ("none" = global, passes through untouched). */
1851
+ export type ScopedModelsConfig = {
1852
+ ${entries.join(`
1853
+ `)}
1854
+ };`,
1855
+ `export type ScopedClientInstance = {
1856
+ ` + `${delegateProps.join(`
1857
+ `)}
1858
+ ` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
1859
+ ` + ` readonly $models: {
1860
+ ${modelsProps.join(`
1861
+ `)}
1862
+ };
1863
+ ` + ` /** Subscribe to client events \u2014 shared with the base client. */
1864
+ ` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
1865
+ ` + ` /** ONE transaction; the callback receives a handle that INHERITS the scope. The array form takes un-awaited scoped delegate calls. */
1866
+ ` + ` readonly $transaction: {
1867
+ ` + ` <T>(fn: (tx: ScopedClientInstance) => Promise<T>, options?: TransactionOptions): Promise<T>;
1868
+ ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1869
+ ` + ` };
1870
+ ` + ` readonly $connect: () => Promise<void>;
1871
+ ` + ` readonly $disconnect: () => Promise<void>;
1872
+ ` + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
1873
+ ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
1874
+ ` + ` // Deliberately ABSENT: $queryRaw / $executeRaw / $queryRawUnsafe / $executeRawUnsafe (raw SQL cannot be tenant-verified \u2014 use the base client) and $scoped (scope once, from the base client).
1875
+ ` + `};`
1876
+ ];
1877
+ }
1878
+ function scopedClientLines(params) {
1879
+ if (hasNativeRls(params))
1880
+ return "";
1881
+ if (params.view.models.some((model) => model.policy !== undefined))
1882
+ return "";
1883
+ return ` /** Tenant scoping: a total per-model tenancy map + a scope value \u2192 a NEW handle whose every call is confined to that tenant. scope: null builds the handle but refuses calls on scoped models. */
1884
+ ` + ` readonly $scoped: (config: { readonly scope: string | number | bigint | null; readonly models: ScopedModelsConfig }) => ScopedClientInstance;
1885
+ `;
1886
+ }
1887
+ function policyClientLines(params) {
1888
+ const policied = params.view.models.filter((model) => model.policy !== undefined);
1889
+ if (policied.length === 0)
1890
+ return "";
1891
+ const CONTEXT_SCALAR_TYPES = { String: "string", Int: "number", BigInt: "bigint" };
1892
+ const contextKeys = new Map;
1893
+ for (const model of policied) {
1894
+ const policy = model.policy;
1895
+ const field = model.fields.find((entry) => entry.name === policy.field);
1896
+ contextKeys.set(policy.context, field?.scalar == null ? "string" : CONTEXT_SCALAR_TYPES[field.scalar] ?? "string");
1897
+ }
1898
+ const contextType = `{ ${[...contextKeys.entries()].map(([key, type]) => `readonly ${propKey({ name: key })}: ${type};`).join(" ")} }`;
1899
+ return ` /** Row scoping (@@policy): bind a per-request context; policied models fail closed without one. */
1900
+ ` + ` readonly $withPolicy: (context: ${contextType}) => VibeClientInstance;
1901
+ ` + ` /** Deliberate, greppable full-access escape from policy scoping. */
1902
+ ` + ` readonly $bypassPolicy: () => VibeClientInstance;
1903
+ `;
1904
+ }
1227
1905
  function emitDts(params) {
1228
1906
  const { view } = params;
1229
1907
  const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
1908
+ const computed = params.computed ?? [];
1909
+ const hasComputed = computed.length > 0;
1910
+ const computedByModel = new Map;
1911
+ for (const entry of computed) {
1912
+ const list = computedByModel.get(entry.model) ?? [];
1913
+ list.push(entry);
1914
+ computedByModel.set(entry.model, list);
1915
+ }
1230
1916
  const parts = [];
1231
- parts.push(`// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
1232
- // Regenerate with \`vibeorm generate\` after every schema change.`);
1917
+ parts.push(GENERATED_HEADER);
1918
+ const schemaTypeImports = [
1919
+ "SchemaIR",
1920
+ ...baked.empty ? [] : ["VibeExtension"],
1921
+ ...hasComputed ? ["ComputedConfig"] : []
1922
+ ].sort();
1233
1923
  const importLines = [
1234
- `import type { DatabaseAdapter, QueryEvent, TransactionOptions } from "@vibeorm/runtime";`,
1235
- `import type { SchemaIR${baked.empty ? "" : ", VibeExtension"} } from "@vibeorm/schema";`,
1236
- ...baked.dtsImports
1924
+ `import type { DatabaseAdapter, DbNow, QueryEvent, Skip, TransactionOptions, WriteValidators } from "@vibeorm/runtime";`,
1925
+ `import type { ${schemaTypeImports.join(", ")} } from "@vibeorm/schema";`,
1926
+ ...jsonTypeImportLines({ view }),
1927
+ ...baked.dtsImports,
1928
+ `import { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`
1237
1929
  ];
1238
1930
  parts.push(`${importLines.join(`
1239
1931
  `)}
1240
1932
 
1241
1933
  ` + `export { VibeError } from "@vibeorm/schema";
1242
- export type { VibeErrorCode } from "@vibeorm/schema";`);
1934
+ export type { VibeErrorCode } from "@vibeorm/schema";
1935
+ ` + `export { SKIP } from "@vibeorm/runtime";
1936
+ export type { Skip } from "@vibeorm/runtime";
1937
+ ` + `export { ${enumEntryExports({ view }).join(", ")} };`);
1243
1938
  parts.push(PREAMBLE);
1244
1939
  parts.push(...filterSection());
1245
1940
  for (const declared of view.enums)
@@ -1248,11 +1943,13 @@ export type { VibeErrorCode } from "@vibeorm/schema";`);
1248
1943
  const modelParts = [];
1249
1944
  for (const model of view.models) {
1250
1945
  const extensionLines = baked.delegateDts.get(model.name);
1946
+ const modelComputed = computedByModel.get(model.name);
1251
1947
  modelParts.push(emitModel({
1252
1948
  model,
1253
1949
  view,
1254
1950
  registry,
1255
- ...extensionLines === undefined ? {} : { extensionLines }
1951
+ ...extensionLines === undefined ? {} : { extensionLines },
1952
+ ...modelComputed === undefined ? {} : { computed: modelComputed }
1256
1953
  }));
1257
1954
  }
1258
1955
  parts.push(...modelParts);
@@ -1267,62 +1964,9 @@ export type { VibeErrorCode } from "@vibeorm/schema";`);
1267
1964
  parts.push(...baked.moduleDts);
1268
1965
  }
1269
1966
  parts.push(...emitViewSection({ view }));
1270
- parts.push(emitClient({ view, baked }));
1271
- return `${parts.join(`
1272
-
1273
- `)}
1274
- `;
1275
- }
1276
-
1277
- // packages/generator/src/emit-js.ts
1278
- function emitJs(params) {
1279
- const { view } = params;
1280
- const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
1281
- const parts = [];
1282
- parts.push(`// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
1283
- // Regenerate with \`vibeorm generate\` after every schema change.`);
1284
- parts.push(`import { createClient } from "@vibeorm/runtime";`);
1285
- if (baked.jsImports.length > 0)
1286
- parts.push(baked.jsImports.join(`
1287
- `));
1288
- parts.push(`export { VibeError } from "@vibeorm/schema";`);
1289
- parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
1290
- export const schema = ${stableStringify({ value: view.schema })};`);
1291
- for (const declared of view.enums) {
1292
- const entries = declared.values.map((value) => ` ${tsString({ value })}: ${tsString({ value })},`).join(`
1293
- `);
1294
- parts.push(`export const ${declared.name} = Object.freeze({
1295
- ${entries}
1296
- });`);
1297
- }
1298
- if (baked.empty) {
1299
- parts.push(`/** Build a typed client over a database adapter. */
1300
- ` + `export function VibeClient(options) {
1301
- ` + ` const { adapter, ...clientOptions } = options;
1302
- ` + ` return createClient({ schema, adapter, options: clientOptions });
1303
- ` + `}`);
1304
- return `${parts.join(`
1305
-
1306
- `)}
1307
- `;
1308
- }
1309
- parts.push(`/** Extension manifest \u2014 verified against the live extension list (VIBE_STALE_CLIENT). */
1310
- ` + `const extensionManifest = ${stableStringify({ value: baked.manifest })};`);
1311
- parts.push(`/** Baked extension surfaces (extensions.md D9). */
1312
- ` + `const extensionMounts = [
1313
- ${baked.mounts.map((mount) => ` ${mount},`).join(`
1314
- `)}
1315
- ];`);
1316
- parts.push(`/** Build a typed client over a database adapter. */
1317
- ` + `export function VibeClient(options) {
1318
- ` + ` const { adapter, extensions, ...clientOptions } = options;
1319
- ` + ` return createClient({
1320
- ` + ` schema,
1321
- ` + ` adapter,
1322
- ` + ` options: clientOptions,
1323
- ` + ` extensions: { manifest: extensionManifest, mounts: extensionMounts, instances: extensions ?? [] },
1324
- ` + ` });
1325
- ` + `}`);
1967
+ parts.push(...emitScopedSection({ view }));
1968
+ parts.push(...emitRlsSection({ view }));
1969
+ parts.push(emitClient({ view, baked, hasComputed }));
1326
1970
  return `${parts.join(`
1327
1971
 
1328
1972
  `)}
@@ -1330,13 +1974,31 @@ ${baked.mounts.map((mount) => ` ${mount},`).join(`
1330
1974
  }
1331
1975
 
1332
1976
  // packages/generator/src/schema-view.ts
1333
- import { assertValidSchemaIR, isEnumType } from "@vibeorm/schema";
1977
+ import { VibeError as VibeError4, assertValidSchemaIR, enumStorageOf, isEnumType } from "@vibeorm/schema";
1978
+ import { postgresDialect, renderRlsPolicy } from "@vibeorm/sql";
1979
+ var JSON_TYPE_RE = /@type\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+"([^"]+)"\s*\)/;
1980
+ function parseJsonTypeAnnotation(params) {
1981
+ if (params.comment === undefined)
1982
+ return null;
1983
+ const match = JSON_TYPE_RE.exec(params.comment);
1984
+ if (match === null)
1985
+ return null;
1986
+ return { name: match[1], from: match[2] };
1987
+ }
1334
1988
  function byName(left, right) {
1335
1989
  return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
1336
1990
  }
1337
1991
  function buildFieldView(params) {
1338
- const { field } = params;
1992
+ const { field, model } = params;
1339
1993
  const enumRef = isEnumType(field.type);
1994
+ const annotation = parseJsonTypeAnnotation({ comment: field.documentation });
1995
+ if (annotation !== null && (enumRef || field.type !== "Json")) {
1996
+ throw new VibeError4({
1997
+ code: "VIBE_SCHEMA_INVALID",
1998
+ message: `@type(...) annotation on ${model}.${field.name} \u2014 typed JSON is only valid on Json fields`,
1999
+ meta: { model, field: field.name }
2000
+ });
2001
+ }
1340
2002
  return {
1341
2003
  name: field.name,
1342
2004
  scalar: enumRef ? null : field.type,
@@ -1346,7 +2008,8 @@ function buildFieldView(params) {
1346
2008
  isId: field.isId,
1347
2009
  isUnique: field.isUnique,
1348
2010
  hasDefault: field.default !== undefined,
1349
- isUpdatedAt: field.isUpdatedAt
2011
+ isUpdatedAt: field.isUpdatedAt,
2012
+ ...annotation === null ? {} : { jsonType: annotation }
1350
2013
  };
1351
2014
  }
1352
2015
  function resolveInverse(params) {
@@ -1360,8 +2023,8 @@ function buildUniques(params) {
1360
2023
  const fieldByName = new Map(fields.map((field) => [field.name, field]));
1361
2024
  const members = [];
1362
2025
  const seen = new Set;
1363
- const add = (names) => {
1364
- const key = names.join("_");
2026
+ const add = (names, nullsNotDistinct, clientName) => {
2027
+ const key = clientName ?? names.join("_");
1365
2028
  if (seen.has(key))
1366
2029
  return;
1367
2030
  const resolved = [];
@@ -1372,24 +2035,28 @@ function buildUniques(params) {
1372
2035
  resolved.push(field);
1373
2036
  }
1374
2037
  seen.add(key);
1375
- members.push({ key, fields: resolved });
2038
+ members.push({ key, fields: resolved, nullsNotDistinct });
1376
2039
  };
1377
2040
  for (const field of fields) {
1378
2041
  if (field.isId || field.isUnique)
1379
- add([field.name]);
2042
+ add([field.name], false);
1380
2043
  }
1381
- if (model.primaryKey !== undefined && model.primaryKey.fields.length > 1) {
1382
- add(model.primaryKey.fields);
2044
+ if (model.primaryKey !== undefined) {
2045
+ if (model.primaryKey.fields.length === 1)
2046
+ add(model.primaryKey.fields, false);
2047
+ else
2048
+ add(model.primaryKey.fields, false, model.primaryKey.clientName);
1383
2049
  }
1384
2050
  for (const index of model.indexes) {
1385
- if (index.kind === "unique")
1386
- add(index.fields.map((field) => field.name));
2051
+ if (index.kind === "unique" && index.where === undefined) {
2052
+ add(index.fields.map((field) => field.name), index.nullsNotDistinct === true, index.clientName);
2053
+ }
1387
2054
  }
1388
2055
  return members;
1389
2056
  }
1390
2057
  function buildModelView(params) {
1391
2058
  const { model, modelIRByName } = params;
1392
- const fields = model.fields.filter((field) => field.isUnsupported !== true).map((field) => buildFieldView({ field }));
2059
+ const fields = model.fields.filter((field) => field.isUnsupported !== true).map((field) => buildFieldView({ field, model: model.name }));
1393
2060
  const fkFieldNames = new Set;
1394
2061
  const relations = model.relations.map((relation) => {
1395
2062
  for (const fk of relation.fields ?? [])
@@ -1412,23 +2079,38 @@ function buildModelView(params) {
1412
2079
  relations,
1413
2080
  uniques: buildUniques({ model, fields }),
1414
2081
  fkFieldNames,
1415
- isView: model.isView
2082
+ isView: model.isView,
2083
+ ...model.policy === undefined ? {} : { policy: model.policy }
1416
2084
  };
1417
2085
  }
2086
+ function assertRlsRenderable(params) {
2087
+ for (const model of params.schema.models) {
2088
+ if (model.rls === undefined || model.rls === "unprotected")
2089
+ continue;
2090
+ renderRlsPolicy({ dialect: postgresDialect, schema: params.schema, model: model.name });
2091
+ }
2092
+ }
1418
2093
  function buildSchemaView(params) {
1419
2094
  const { schema } = params;
1420
2095
  assertValidSchemaIR({ schema });
2096
+ if (schema.rls !== undefined)
2097
+ assertRlsRenderable({ schema });
1421
2098
  const normalized = {
1422
2099
  datasource: schema.datasource,
1423
2100
  models: [...schema.models].sort(byName),
1424
2101
  enums: [...schema.enums].sort(byName),
1425
- extensions: [...schema.extensions].sort(byName)
2102
+ extensions: [...schema.extensions].sort(byName),
2103
+ ...schema.rls === undefined ? {} : { rls: schema.rls }
1426
2104
  };
1427
2105
  const modelIRByName = new Map(normalized.models.map((model) => [model.name, model]));
1428
2106
  const models = normalized.models.map((model) => buildModelView({ model, modelIRByName }));
1429
2107
  return {
1430
2108
  schema: normalized,
1431
- enums: normalized.enums.map((declared) => ({ name: declared.name, values: declared.values })),
2109
+ enums: normalized.enums.map((declared) => ({
2110
+ name: declared.name,
2111
+ values: declared.values,
2112
+ storage: enumStorageOf({ enumDef: declared })
2113
+ })),
1432
2114
  models,
1433
2115
  modelByName: new Map(models.map((model) => [model.name, model]))
1434
2116
  };
@@ -1443,9 +2125,16 @@ async function generateClient(params) {
1443
2125
  const { schema, outDir } = params;
1444
2126
  const view = buildSchemaView({ schema });
1445
2127
  const baked = bakeExtensions({ schema: view.schema, extensions: params.extensions ?? [] });
2128
+ let computed = [];
2129
+ if (params.computed !== undefined && Object.keys(params.computed).length > 0) {
2130
+ validateComputedConfig({ schema, computed: params.computed });
2131
+ computed = buildComputedManifest({ computed: params.computed });
2132
+ }
1446
2133
  const outputs = [
1447
- { name: "index.js", content: emitJs({ view, baked }) },
1448
- { name: "index.d.ts", content: emitDts({ view, baked }) },
2134
+ { name: "index.js", content: emitJs({ view, baked, computed }) },
2135
+ { name: "index.d.ts", content: emitDts({ view, baked, computed }) },
2136
+ { name: "enums.js", content: emitEnumsJs({ view }) },
2137
+ { name: "enums.d.ts", content: emitEnumsDts({ view }) },
1449
2138
  { name: "README.md", content: README }
1450
2139
  ];
1451
2140
  await mkdir(outDir, { recursive: true });
@@ -1462,4 +2151,4 @@ export {
1462
2151
  bakeExtensions
1463
2152
  };
1464
2153
 
1465
- //# debugId=8080967CFB1AC77464756E2164756E21
2154
+ //# debugId=AC114FAAB4DBE66364756E2164756E21