@vibeorm/generator 2.2.3 → 3.0.0

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
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ // packages/generator/src/module-schema.ts
3
+ import { prepareDatabaseModuleSchema } from "@vibeorm/schema";
2
4
  // packages/generator/src/emit-module.ts
3
5
  import { createHash } from "crypto";
4
6
  import { VibeError as VibeError2, providerToDialect } from "@vibeorm/schema";
@@ -30,10 +32,13 @@ function maybeParens(params) {
30
32
  return type.includes("|") ? `(${type})` : type;
31
33
  }
32
34
  function stableStringify(params) {
33
- return renderValue({ value: params.value, indent: "" });
35
+ return renderValue({ value: params.value, indent: "", javascript: false });
36
+ }
37
+ function stableJs(params) {
38
+ return renderValue({ value: params.value, indent: "", javascript: true });
34
39
  }
35
40
  function renderValue(params) {
36
- const { value, indent } = params;
41
+ const { value, indent, javascript } = params;
37
42
  if (value === null)
38
43
  return "null";
39
44
  const kind = typeof value;
@@ -43,7 +48,7 @@ function renderValue(params) {
43
48
  if (value.length === 0)
44
49
  return "[]";
45
50
  const inner = `${indent} `;
46
- const items = value.map((item) => inner + renderValue({ value: item, indent: inner }));
51
+ const items = value.map((item) => inner + renderValue({ value: item, indent: inner, javascript }));
47
52
  return `[
48
53
  ${items.join(`,
49
54
  `)}
@@ -55,7 +60,7 @@ ${indent}]`;
55
60
  if (keys.length === 0)
56
61
  return "{}";
57
62
  const inner = `${indent} `;
58
- const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderValue({ value: record[key], indent: inner })}`);
63
+ const items = keys.map((key) => `${inner}${javascript && key === "__proto__" ? `[${JSON.stringify(key)}]` : JSON.stringify(key)}: ${renderValue({ value: record[key], indent: inner, javascript })}`);
59
64
  return `{
60
65
  ${items.join(`,
61
66
  `)}
@@ -117,7 +122,7 @@ export { sql, decode, sqlModels, sqlProjection } from "./index.js";
117
122
  export const databaseModule = defineDatabaseModule({ ...${JSON.stringify(module)}, schema, manifest: ${JSON.stringify(manifest)} });
118
123
  ` },
119
124
  { name: "schema.js", content: `// Generated data-only schema and runtime manifest.
120
- export const schema = ${serialized};
125
+ export const schema = ${stableJs({ value: view.schema })};
121
126
  export const moduleManifest = ${JSON.stringify(manifest)};
122
127
  ` },
123
128
  { name: "schema.d.ts", content: `import type { SchemaIR, DatabaseModuleManifest } from "@vibeorm/schema";
@@ -369,7 +374,7 @@ ${validatorLines}
369
374
  function serializeSchema(params) {
370
375
  const { schema } = params;
371
376
  if (schema.rls === undefined)
372
- return stableStringify({ value: schema });
377
+ return stableJs({ value: schema });
373
378
  return renderIr({ value: schema, indent: "", ordered: false });
374
379
  }
375
380
  function renderIr(params) {
@@ -398,7 +403,7 @@ ${indent}]`;
398
403
  if (keys.length === 0)
399
404
  return "{}";
400
405
  const inner = `${indent} `;
401
- const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderIr({ value: record[key], indent: inner, ordered: ordered || key === "rls" })}`);
406
+ const items = keys.map((key) => `${inner}${key === "__proto__" ? `[${JSON.stringify(key)}]` : JSON.stringify(key)}: ${renderIr({ value: record[key], indent: inner, ordered: ordered || key === "rls" })}`);
402
407
  return `{
403
408
  ${items.join(`,
404
409
  `)}
@@ -439,7 +444,7 @@ ${view.models.map((model) => ` [${tsString({ value: model.clientName })}]: (opt
439
444
  }
440
445
  if (!baked.empty) {
441
446
  parts.push(`/** Extension manifest \u2014 verified against the live extension list (VIBE_STALE_CLIENT). */
442
- ` + `const extensionManifest = ${stableStringify({ value: baked.manifest })};`);
447
+ ` + `const extensionManifest = ${stableJs({ value: baked.manifest })};`);
443
448
  parts.push(`/** Baked extension surfaces (extensions.md D9). */
444
449
  ` + `const extensionMounts = [
445
450
  ${baked.mounts.map((mount) => ` ${mount},`).join(`
@@ -448,7 +453,7 @@ ${baked.mounts.map((mount) => ` ${mount},`).join(`
448
453
  }
449
454
  if (hasComputed) {
450
455
  parts.push(`/** Computed-field manifest \u2014 verified against the live computed config (VIBE_STALE_CLIENT). */
451
- ` + `const computedManifest = ${stableStringify({ value: computed })};`);
456
+ ` + `const computedManifest = ${stableJs({ value: computed })};`);
452
457
  }
453
458
  const destructured = [
454
459
  "adapter",
@@ -534,6 +539,15 @@ function inputBaseType(params) {
534
539
  return "Date | string";
535
540
  return field.enumName ?? INPUT_TYPES[field.scalar];
536
541
  }
542
+ function aggregateOperandType(params) {
543
+ const { field, key } = params;
544
+ if (key === "_count")
545
+ return "number";
546
+ if (key === "_avg")
547
+ return "number";
548
+ const value = field.scalar === "DateTime" ? "Date | string" : inputBaseType({ field });
549
+ return value;
550
+ }
537
551
  function filterName(params) {
538
552
  const { field } = params;
539
553
  const prefix = field.enumName ?? FILTER_PREFIX[field.scalar];
@@ -589,6 +603,12 @@ function orMany(params) {
589
603
  const { type } = params;
590
604
  return `${type} | readonly ${maybeParens({ type })}[]`;
591
605
  }
606
+ function aggregatePredicateType(params) {
607
+ const { value } = params;
608
+ const skip = params.skippable === false ? "" : " | Skip";
609
+ const operators = ["equals", "not", "gt", "gte", "lt", "lte"];
610
+ return `${value} | { ${operators.map((operator) => `readonly ${operator}?: ${value}${skip}`).join("; ")} }`;
611
+ }
592
612
  function payloadFieldType(params) {
593
613
  const { field } = params;
594
614
  const base = payloadBaseType({ field });
@@ -638,16 +658,25 @@ export type RelationStrategy = "query" | "join";
638
658
  * query still applies its own where/scoped/policy/RLS filters.
639
659
  */
640
660
  export type KeysetToken = string;
661
+ /** A bounded page; the continuation is null when no lookahead row exists. */
662
+ export type KeysetPage<T> = { readonly items: T[]; readonly nextCursor: KeysetToken | null };
641
663
 
642
664
  /** Per-query keyset options. */
643
665
  export type KeysetOptions = {
644
666
  /**
645
667
  * Asserts that every DateTime ordering column in this query holds
646
- * millisecond-exact values. Required on postgres and mysql, whose timestamps
647
- * may be finer than a JavaScript Date; NOT safe for \`@default(now())\`
648
- * columns on postgres, which are microsecond-precise.
668
+ * millisecond-exact values. Needed on postgres and mysql only for a column
669
+ * that may be finer than a JavaScript Date \u2014 one without a declared
670
+ * precision of 0-3; a column declared \`@db.Timestamptz(3)\` needs none. NOT
671
+ * safe for a microsecond \`@default(now())\` column.
649
672
  */
650
673
  readonly timestampPrecision?: "milliseconds";
674
+ /**
675
+ * The owner a page token belongs to (a user or tenant id). Mixed into the
676
+ * token's query binding beside the filters, so a token minted for one scope
677
+ * is refused for another. Encoded, not secret.
678
+ */
679
+ readonly scope?: string;
651
680
  };
652
681
 
653
682
  /** Result of createMany/updateMany/deleteMany. */
@@ -675,6 +704,21 @@ export type UpsertManyPayload = { readonly count: number; readonly affectedRows:
675
704
  */
676
705
  type Provided<T> = Exclude<T, Skip | undefined>;
677
706
 
707
+ /** An absent projection member supplies the inactive fallback value. */
708
+ type SelectionMember<S, K> = K extends keyof S ? S[K] : undefined;
709
+
710
+ /** Object-bag keys only; primitive SKIP contributes no Symbol members. */
711
+ type BagKeys<S> = S extends object ? keyof S : never;
712
+
713
+ /** Read only object bags, preserving unions and optional bag omission. */
714
+ type SelectionValue<S, K> = S extends object ? SelectionMember<S, K> : undefined;
715
+
716
+ /** Only an active included relation replaces the selected relation. */
717
+ type IncludedRelation<S, I> = I extends false | undefined | Skip ? S : I;
718
+
719
+ /** Count include overrides even with false; omitted/nullish values fall back. */
720
+ type IncludedCount<S, I> = I extends null | undefined | Skip ? S : I;
721
+
678
722
  /** Keys of an omit object whose value is exactly true \u2014 the scalars the result drops. */
679
723
  type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 };
680
724
 
@@ -935,7 +979,13 @@ function checkedCreateLines(params) {
935
979
  function uncheckedCreateLines(params) {
936
980
  const { model, omitRelation } = params;
937
981
  const omitted = new Set(omitRelation === undefined ? [] : model.relations.find((relation) => relation.name === omitRelation)?.fkFields ?? []);
938
- return model.fields.filter((field) => !omitted.has(field.name)).map((field) => createFieldProp({ field }));
982
+ return [
983
+ ...model.fields.filter((field) => !omitted.has(field.name)).map((field) => createFieldProp({ field })),
984
+ ...model.relations.map((relation) => {
985
+ const value = params.scalarOnly !== true && relation.fkFields.length === 0 && relation.name !== omitRelation ? nestedRefName({ relation, verb: "Create" }) : "never";
986
+ return ` readonly ${propKey({ name: relation.name })}?: ${value};`;
987
+ })
988
+ ];
939
989
  }
940
990
  function checkedUpdateLines(params) {
941
991
  const { model, omitRelation } = params;
@@ -955,7 +1005,13 @@ function checkedUpdateLines(params) {
955
1005
  function uncheckedUpdateLines(params) {
956
1006
  const { model, omitRelation } = params;
957
1007
  const omitted = new Set(omitRelation === undefined ? [] : model.relations.find((relation) => relation.name === omitRelation)?.fkFields ?? []);
958
- return model.fields.filter((field) => !omitted.has(field.name)).map((field) => updateFieldProp({ field }));
1008
+ return [
1009
+ ...model.fields.filter((field) => !omitted.has(field.name)).map((field) => updateFieldProp({ field })),
1010
+ ...model.relations.map((relation) => {
1011
+ const value = params.scalarOnly !== true && relation.fkFields.length === 0 && relation.name !== omitRelation ? nestedRefName({ relation, verb: "Update" }) : "never";
1012
+ return ` readonly ${propKey({ name: relation.name })}?: ${value};`;
1013
+ })
1014
+ ];
959
1015
  }
960
1016
  function register(params) {
961
1017
  const { registry, name, text } = params;
@@ -1032,7 +1088,7 @@ function registerNestedContext(params) {
1032
1088
  lines: [` readonly where: ${t}WhereUniqueInput;`, ` readonly create: ${createUnion};`]
1033
1089
  })
1034
1090
  });
1035
- const createManyData = inverse === null ? `readonly ${t}CreateManyInput[]` : `readonly ${t}UncheckedCreate${suffix}Input[]`;
1091
+ const createManyData = inverse === null ? `readonly ${t}CreateManyInput[]` : `readonly { ${uncheckedCreateLines({ model: target, omitRelation: inverse, scalarOnly: true }).map((line) => skippableMembers({ block: line }).trim()).join(" ")} }[]`;
1036
1092
  if (many) {
1037
1093
  register({
1038
1094
  registry,
@@ -1123,7 +1179,7 @@ function registerNestedContext(params) {
1123
1179
  ` readonly connectOrCreate?: ${connectOrCreateName};`,
1124
1180
  ` readonly update?: ${updateUnion};`,
1125
1181
  ` readonly upsert?: { readonly create: ${createUnion}; readonly update: ${updateUnion} };`,
1126
- ...allowDetach ? [` readonly disconnect?: boolean;`, ` readonly delete?: boolean;`] : []
1182
+ ...allowDetach ? [` readonly disconnect?: boolean | ${t}WhereInput;`, ` readonly delete?: boolean | ${t}WhereInput;`] : []
1127
1183
  ]
1128
1184
  })
1129
1185
  });
@@ -1145,7 +1201,7 @@ ${[
1145
1201
  ].join(`
1146
1202
  `)}
1147
1203
  };`);
1148
- parts.push(`export type ${n}ScalarFieldEnum = ${model.fields.map((field) => tsString({ value: field.name })).join(" | ")};`);
1204
+ parts.push(`export type ${n}ScalarFieldEnum = ${literalUnion({ members: model.fields.map((field) => field.name) }) || "never"};`);
1149
1205
  if (computedEntries.length > 0) {
1150
1206
  parts.push(`export type ${n}SelectableFieldEnum = ${n}ScalarFieldEnum | ${computedEntries.map((entry) => tsString({ value: entry.field })).join(" | ")};`);
1151
1207
  }
@@ -1265,12 +1321,12 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1265
1321
  }));
1266
1322
  parts.push(inputAlias({ name: `${n}CreateInput`, lines: checkedCreateLines({ model }) }));
1267
1323
  parts.push(inputAlias({ name: `${n}UncheckedCreateInput`, lines: uncheckedCreateLines({ model }) }));
1268
- parts.push(`export type ${n}CreateManyInput = ${n}UncheckedCreateInput;`);
1324
+ parts.push(inputAlias({ name: `${n}CreateManyInput`, lines: uncheckedCreateLines({ model, scalarOnly: true }) }));
1269
1325
  parts.push(inputAlias({ name: `${n}UpdateInput`, lines: checkedUpdateLines({ model }) }));
1270
1326
  parts.push(inputAlias({ name: `${n}UncheckedUpdateInput`, lines: uncheckedUpdateLines({ model }) }));
1271
- parts.push(`export type ${n}UpdateManyInput = ${n}UncheckedUpdateInput;`);
1327
+ parts.push(inputAlias({ name: `${n}UpdateManyInput`, lines: uncheckedUpdateLines({ model, scalarOnly: true }) }));
1272
1328
  const numericFields = model.fields.filter((field) => !field.isList && field.scalar !== null && NUMERIC_SCALARS.has(field.scalar));
1273
- const minmaxFields = model.fields.filter((field) => !field.isList && field.scalar !== null && MINMAX_SCALARS.has(field.scalar));
1329
+ const minmaxFields = model.fields.filter((field) => !field.isList && (field.enumName !== null || field.scalar !== null && MINMAX_SCALARS.has(field.scalar)));
1274
1330
  parts.push(inputAlias({
1275
1331
  name: `${n}CountAggregateInput`,
1276
1332
  lines: [
@@ -1297,6 +1353,44 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1297
1353
  aggregateOpProps.push(` readonly _min?: ${n}MinAggregateInput;`);
1298
1354
  aggregateOpProps.push(` readonly _max?: ${n}MaxAggregateInput;`);
1299
1355
  }
1356
+ const groupAggregates = [
1357
+ { key: "_count", prefix: "Count", fields: model.fields },
1358
+ ...numericFields.length > 0 ? [
1359
+ { key: "_avg", prefix: "Avg", fields: numericFields },
1360
+ { key: "_sum", prefix: "Sum", fields: numericFields }
1361
+ ] : [],
1362
+ ...minmaxFields.length > 0 ? [
1363
+ { key: "_min", prefix: "Min", fields: minmaxFields },
1364
+ { key: "_max", prefix: "Max", fields: minmaxFields }
1365
+ ] : []
1366
+ ];
1367
+ const groupAggregateFields = new Map(groupAggregates.map((aggregate) => [aggregate, new Set(aggregate.fields)]));
1368
+ const havingLines = [
1369
+ ` readonly AND?: ${orMany({ type: `${n}HavingInput` })};`,
1370
+ ` readonly OR?: ${orMany({ type: `${n}HavingInput` })};`,
1371
+ ` readonly NOT?: ${orMany({ type: `${n}HavingInput` })};`
1372
+ ];
1373
+ const groupOrderLines = [...scalarOrderLines];
1374
+ for (const aggregate of groupAggregates) {
1375
+ const havingFields = aggregate.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: ${aggregatePredicateType({ value: aggregateOperandType({ field, key: aggregate.key }) })};`);
1376
+ const orderFields = aggregate.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: SortOrder;`);
1377
+ if (aggregate.key === "_count") {
1378
+ havingFields.push(` readonly _all?: ${aggregatePredicateType({ value: "number" })};`);
1379
+ orderFields.push(` readonly _all?: SortOrder;`);
1380
+ }
1381
+ parts.push(inputAlias({ name: `${n}${aggregate.prefix}AggregateHavingInput`, lines: havingFields }));
1382
+ parts.push(inputAlias({ name: `${n}${aggregate.prefix}AggregateOrderByInput`, lines: orderFields }));
1383
+ havingLines.push(` readonly ${aggregate.key}?: ${n}${aggregate.prefix}AggregateHavingInput;`);
1384
+ groupOrderLines.push(` readonly ${aggregate.key}?: ${n}${aggregate.prefix}AggregateOrderByInput;`);
1385
+ }
1386
+ for (const field of model.fields) {
1387
+ const aggregates = groupAggregates.filter((aggregate) => groupAggregateFields.get(aggregate).has(field)).map((aggregate) => `readonly ${aggregate.key}?: ${aggregatePredicateType({ value: aggregateOperandType({ field, key: aggregate.key }), skippable: field.scalar !== "Json" })} | Skip`);
1388
+ const filter = `(${filterName({ field })} & { ${aggregates.join("; ")} })`;
1389
+ const value = field.isList || field.scalar === "Json" ? filter : `${inputBaseType({ field })} | ${filter}${field.isOptional ? " | null" : ""}`;
1390
+ havingLines.push(` readonly ${propKey({ name: field.name })}?: ${value};`);
1391
+ }
1392
+ parts.push(inputAlias({ name: `${n}HavingInput`, lines: havingLines }));
1393
+ parts.push(inputAlias({ name: `${n}GroupByOrderByInput`, lines: groupOrderLines }));
1300
1394
  for (const nested of [false, true]) {
1301
1395
  parts.push(inputAlias({
1302
1396
  name: `${n}${nested ? "RelationFindManyArgs" : "FindManyArgs"}`,
@@ -1319,6 +1413,21 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1319
1413
  ]
1320
1414
  }));
1321
1415
  }
1416
+ parts.push(inputAlias({
1417
+ name: `${n}FindPageArgs`,
1418
+ lines: [
1419
+ ` readonly where?: ${n}WhereInput;`,
1420
+ ` readonly orderBy: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1421
+ ` readonly take: number;`,
1422
+ ` readonly after?: KeysetToken;`,
1423
+ ` readonly keyset?: KeysetOptions;`,
1424
+ ` readonly select?: ${n}Select;`,
1425
+ ` readonly include?: ${n}Include;`,
1426
+ ` readonly omit?: ${n}OmitInput;`,
1427
+ ` readonly relationStrategy?: RelationStrategy;`,
1428
+ ...["skip", "cursor", "distinct", "lock"].map((key) => ` readonly ${key}?: never;`)
1429
+ ]
1430
+ }));
1322
1431
  parts.push(`export type ${n}FindFirstArgs = ${n}FindManyArgs;`);
1323
1432
  parts.push(inputAlias({
1324
1433
  name: `${n}FindUniqueArgs`,
@@ -1334,6 +1443,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1334
1443
  parts.push(inputAlias({
1335
1444
  name: `${n}CreateArgs`,
1336
1445
  lines: [
1446
+ ` readonly relationStrategy?: never;`,
1337
1447
  ` readonly onConflict?: never;`,
1338
1448
  ` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
1339
1449
  ` readonly select?: ${n}Select;`,
@@ -1348,6 +1458,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1348
1458
  parts.push(inputAlias({
1349
1459
  name: `${n}CreateOrSkipArgs`,
1350
1460
  lines: [
1461
+ ` readonly relationStrategy?: never;`,
1351
1462
  ` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
1352
1463
  ` readonly onConflict: ${n}CreateOnConflictInput;`,
1353
1464
  ` readonly select?: ${n}Select;`,
@@ -1358,6 +1469,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1358
1469
  parts.push(inputAlias({
1359
1470
  name: `${n}CreateManyArgs`,
1360
1471
  lines: [
1472
+ ` readonly relationStrategy?: never;`,
1361
1473
  ` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
1362
1474
  ` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`
1363
1475
  ]
@@ -1365,6 +1477,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1365
1477
  parts.push(inputAlias({
1366
1478
  name: `${n}CreateManyAndReturnArgs`,
1367
1479
  lines: [
1480
+ ` readonly relationStrategy?: never;`,
1368
1481
  ` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
1369
1482
  ` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`,
1370
1483
  ` readonly select?: ${n}Select;`,
@@ -1403,13 +1516,19 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1403
1516
  ` readonly skipUpdatedAt?: boolean;`
1404
1517
  ]
1405
1518
  }));
1519
+ const returningSelect = [
1520
+ ...model.fields.map((field) => `readonly ${propKey({ name: field.name })}?: boolean | Skip`),
1521
+ ...computedEntries.map((entry) => `readonly ${propKey({ name: entry.field })}?: boolean | Skip`),
1522
+ ...model.relations.map((relation) => `readonly ${propKey({ name: relation.name })}?: false | Skip`),
1523
+ ...hasCounts ? ["readonly _count?: false | Skip"] : []
1524
+ ].join("; ");
1406
1525
  parts.push(inputAlias({
1407
1526
  name: `${n}UpdateManyAndReturnArgs`,
1408
1527
  lines: [
1409
1528
  ` readonly include?: never;`,
1410
1529
  ` readonly where?: ${n}WhereInput;`,
1411
1530
  ` readonly data: ${n}UpdateManyInput;`,
1412
- ` readonly select?: ${n}Select;`,
1531
+ ` readonly select?: { ${returningSelect} };`,
1413
1532
  ` readonly omit?: ${n}OmitInput;`,
1414
1533
  ` readonly skipUpdatedAt?: boolean;`
1415
1534
  ]
@@ -1461,8 +1580,8 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1461
1580
  lines: [
1462
1581
  ` readonly by: readonly ${n}ScalarFieldEnum[];`,
1463
1582
  ` readonly where?: ${n}WhereInput;`,
1464
- ` readonly orderBy?: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1465
- ` readonly having?: ${n}WhereInput;`,
1583
+ ` readonly orderBy?: ${orMany({ type: `${n}GroupByOrderByInput` })};`,
1584
+ ` readonly having?: ${n}HavingInput;`,
1466
1585
  ` readonly take?: number;`,
1467
1586
  ` readonly skip?: number;`,
1468
1587
  ...aggregateOpProps
@@ -1481,9 +1600,9 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1481
1600
  ` + ` { [K in keyof CS & (${listUnion}) as ${uncertainPart("CS")}]?: number }
1482
1601
  ` + ` : C extends { readonly select: Skip } ? { ${allCounts} }
1483
1602
  ` + ` : "select" extends keyof C ? Partial<{ ${allCounts} }> : { ${allCounts} };`);
1484
- parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> }
1485
- ` + ` : S extends { readonly _count: false | undefined | Skip } ? unknown
1486
- ` + ` : S extends { readonly _count?: infer C extends boolean | object | Skip } ? { _count?: ${n}CountResult<Provided<C>> } : unknown;`);
1603
+ parts.push(`type ${n}CountPayload<C> = [C] extends [true | object] ? { _count: ${n}CountResult<C> }
1604
+ ` + ` : [C] extends [false | undefined | Skip] ? unknown
1605
+ ` + ` : { _count?: ${n}CountResult<Exclude<C, false | undefined | Skip>> };`);
1487
1606
  }
1488
1607
  if (hasRelations) {
1489
1608
  const relationCases = model.relations.map((relation) => {
@@ -1503,20 +1622,23 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1503
1622
  parts.push(`type ${n}RelationPayload<K extends keyof ${n}Relations, A> = {
1504
1623
  ${relationCases}
1505
1624
  }[K];`);
1506
- parts.push(`type ${n}IncludePayload<A> = A extends { readonly include: infer I extends object }
1507
- ` + ` ? { [K in keyof I & keyof ${n}Relations as ${exactPart("I")}]: ${n}RelationPayload<K, I[K]> } &
1508
- ` + ` { [K in keyof I & keyof ${n}Relations as ${uncertainPart("I")}]?: ${n}RelationPayload<K, I[K]> }${hasCounts ? ` &
1509
- ${n}CountPayload<I>` : ""}
1510
- ` + ` : "include" extends keyof A ? Partial<${n}IncludePayload<{ include: Provided<A["include"]> }>> : unknown;`);
1625
+ parts.push(`type ${n}ResolvedSelection<A, S = SelectionValue<A, "select">, I = SelectionValue<A, "include">> = {
1626
+ ` + ` [K in (BagKeys<S> | BagKeys<I>) & (${hasCounts ? `keyof ${n}Relations | "_count"` : `keyof ${n}Relations`})]: ${hasCounts ? `K extends "_count" ? IncludedCount<SelectionValue<S, K>, SelectionValue<I, K>> : ` : ""}IncludedRelation<SelectionValue<S, K>, SelectionValue<I, K>>;
1627
+ ` + `};`);
1628
+ parts.push(`type ${n}IncludePayload<A, I = "include" extends keyof A ? "select" extends keyof A ? ${n}ResolvedSelection<A> : SelectionValue<A, "include"> : SelectionValue<A, "select">> =
1629
+ ` + ` [Exclude<SelectionValue<I, BagKeys<I> & (${hasCounts ? `keyof ${n}Relations | "_count"` : `keyof ${n}Relations`})>, false | undefined | Skip>] extends [never] ? unknown :
1630
+ ` + ` { [K in BagKeys<I> & keyof ${n}Relations as ${exactPart("I").replaceAll("I[K]", "SelectionValue<I, K>")}]: ${n}RelationPayload<K, Exclude<SelectionValue<I, K>, false | undefined | Skip>> } &
1631
+ ` + ` { [K in BagKeys<I> & keyof ${n}Relations as ${uncertainPart("I").replaceAll("I[K]", "SelectionValue<I, K>")}]?: ${n}RelationPayload<K, Exclude<SelectionValue<I, K>, false | undefined | Skip>> }${hasCounts ? ` &
1632
+ ${n}CountPayload<SelectionValue<I, "_count">>` : ""};`);
1511
1633
  parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
1512
1634
  ` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1513
1635
  ` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] } &
1514
- ` + ` { [K in keyof S & keyof ${n}Relations as ${exactPart("S")}]: ${n}RelationPayload<K, S[K]> } &
1515
- ` + ` { [K in keyof S & keyof ${n}Relations as ${uncertainPart("S")}]?: ${n}RelationPayload<K, S[K]> }${hasCounts ? ` &
1516
- ${n}CountPayload<S>` : ""}
1636
+ ` + ` ("include" extends keyof A ? ${n}IncludePayload<A> :
1637
+ ` + ` [Exclude<SelectionValue<S, BagKeys<S> & (${hasCounts ? `keyof ${n}Relations | "_count"` : `keyof ${n}Relations`})>, false | undefined | Skip>] extends [never] ? unknown : ${n}IncludePayload<A>)
1517
1638
  ` + ` : A extends { readonly select: Skip } ? ${n}OmitBase<A> & ${n}IncludePayload<A>
1518
- ` + ` : "select" extends keyof A ? Partial<${n}>
1519
- ` + ` : ${n}OmitBase<A> & ${n}IncludePayload<A>;`);
1639
+ ` + ` : "select" extends keyof A ? Partial<${n}> & ${n}IncludePayload<A>
1640
+ ` + ` : "include" extends keyof A ? ${n}OmitBase<A> & ${n}IncludePayload<A>
1641
+ ` + ` : ${n}OmitBase<A>;`);
1520
1642
  } else {
1521
1643
  parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
1522
1644
  ` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
@@ -1525,7 +1647,7 @@ ${relationCases}
1525
1647
  ` + ` : "select" extends keyof A ? Partial<${n}> : ${n}OmitBase<A>;`);
1526
1648
  }
1527
1649
  const aggregateFields = (fields, value) => `{ ${fields.map((field) => `${propKey({ name: field.name })}: ${value(field)}`).join("; ")} }`;
1528
- const countValues = aggregateFields(model.fields, () => "number").replace(" }", "; _all: number }");
1650
+ const countValues = `{ ${[...model.fields.map((field) => `${propKey({ name: field.name })}: number`), "_all: number"].join("; ")} }`;
1529
1651
  parts.push(`type ${n}AggregateValues = {
1530
1652
  ` + ` _count: ${countValues};
1531
1653
  ` + ` _avg: ${aggregateFields(numericFields, () => "number | null")};
@@ -1546,6 +1668,7 @@ ${relationCases}
1546
1668
  ` + ` (number extends A["by"]["length"] ? Partial<Pick<${n}, A["by"][number]>> : Pick<${n}, A["by"][number]>) & ${n}AggregateResult<A>;`);
1547
1669
  const readMethods = [
1548
1670
  ` findMany<A extends ${n}FindManyArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A>[]>;`,
1671
+ ` findPage<A extends ${n}FindPageArgs>(args: A): VibeQueryPromise<KeysetPage<${n}GetPayload<A>>>;`,
1549
1672
  ` findFirst<A extends ${n}FindFirstArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A> | null>;`,
1550
1673
  ` findFirstOrThrow<A extends ${n}FindFirstArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A>>;`,
1551
1674
  ` findUnique<A extends ${n}FindUniqueArgs>(args: A): VibeQueryPromise<${n}GetPayload<A> | null>;`,
@@ -1656,7 +1779,7 @@ function emitViewTypesForModel(params) {
1656
1779
  const target = relation.target;
1657
1780
  const targetClientKey = view.modelByName.get(target)?.clientName ?? target;
1658
1781
  const guard = `D extends { readonly ${propKey({ name: targetClientKey })}: infer RF extends object }`;
1659
- const masked = `ViewRow<${target}ViewSelectScalars<RF, A> & ${target}ViewReveal<A>>`;
1782
+ const masked = `${target}ViewResult<RF, Exclude<A, false | undefined | Skip>, D>`;
1660
1783
  const unmasked = `(A extends object ? ${target}GetPayload<A> : ${target})`;
1661
1784
  let expression;
1662
1785
  if (relation.isList) {
@@ -1881,8 +2004,9 @@ var CONNECTION_SURFACE_LINES = ` readonly $connect: () => Promise<void>;
1881
2004
  var DEFINE_VIEW_LINE = ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
1882
2005
  ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
1883
2006
  `;
1884
- var READ_ONLY_LINE = ` /** A read-only handle over this one: reads pass through, and every write, raw-SQL and control method is refused with \`VIBE_READ_ONLY\`. Closed under composition \u2014 transactions, views, scoped and policy handles built from it stay read-only. */
1885
- ` + ` readonly $readOnly: () => ReadOnlyClientInstance;
2007
+ var READ_ONLY_DOC = ` /** A read-only handle over this one: reads pass through, and every write, raw-SQL and control method is refused with \`VIBE_READ_ONLY\`. Closed under composition \u2014 transactions, views, scoped and policy handles built from it stay read-only. */
2008
+ `;
2009
+ var READ_ONLY_LINE = READ_ONLY_DOC + ` readonly $readOnly: () => ReadOnlyClientInstance;
1886
2010
  `;
1887
2011
  function instanceSurfaceLines(params) {
1888
2012
  const { view, transactionType, inTransaction, selfLabel } = params;
@@ -2064,7 +2188,7 @@ function emitClient(params) {
2064
2188
  ` + ` // Deliberately ABSENT: model delegates, $models, $transaction, $queryRaw / $executeRaw / $queryRawUnsafe / $executeRawUnsafe, $advisoryLock / $tryAdvisoryLock, $defineView, $readOnly and every extension mount \u2014 an unbound client has no identity, so it cannot read or write a row. Bind one with $withContext.
2065
2189
  ` + `};`);
2066
2190
  } else {
2067
- parts.push(`export type VibeClientInstance = ClientContext<"root"> & {
2191
+ parts.push(`export type VibeClientInstance = ClientContext<${tsString({ value: view.models.some((model) => model.policy !== undefined) ? "restricted" : "root" })}> & {
2068
2192
  ` + instanceSurfaceLines({
2069
2193
  view,
2070
2194
  transactionType: "VibeTransactionClientInstance",
@@ -2072,7 +2196,7 @@ function emitClient(params) {
2072
2196
  selfLabel: "db"
2073
2197
  }) + policyClientLines({ view, selfType: "VibeClientInstance" }) + scopedClientLines({ view, scopedType: "ScopedClientInstance" }) + READ_ONLY_LINE + clientExtensionLines + `};`);
2074
2198
  parts.push(`/** The handle \`$transaction\` gives its callback. A helper that REQUIRES a transaction takes this type, and an ordinary client no longer satisfies it. */
2075
- ` + `export type VibeTransactionClientInstance = ClientContext<"transaction"> & {
2199
+ ` + `export type VibeTransactionClientInstance = ClientContext<${tsString({ value: view.models.some((model) => model.policy !== undefined) ? "restricted" : "transaction" })}> & {
2076
2200
  ` + instanceSurfaceLines({
2077
2201
  view,
2078
2202
  transactionType: "VibeTransactionClientInstance",
@@ -2135,7 +2259,8 @@ ${modelsProps.join(`
2135
2259
  ` + ` };
2136
2260
  ` + (params2.inTransaction ? advisoryLockLines({ view, selfLabel: "tx" }) : CONNECTION_SURFACE_LINES) + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
2137
2261
  ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
2138
- ` + READ_ONLY_LINE;
2262
+ ` + READ_ONLY_DOC + ` readonly $readOnly: () => ReadOnlyClientInstance<"restricted">;
2263
+ `;
2139
2264
  return [
2140
2265
  banner({ title: "Tenant scoping ($scoped)" }),
2141
2266
  `/** 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. */
@@ -2145,11 +2270,11 @@ export type ScopedModelsConfig = {
2145
2270
  ${entries.join(`
2146
2271
  `)}
2147
2272
  };`,
2148
- `export type ScopedClientInstance = {
2273
+ `export type ScopedClientInstance = ClientContext<"restricted"> & {
2149
2274
  ` + scopedSurface({ transactionType: "ScopedTransactionClientInstance", inTransaction: false }) + ` // 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).
2150
2275
  ` + `};`,
2151
2276
  `/** The scoped handle inside \`$transaction\`: same scope, plus the locks that require a transaction. */
2152
- ` + `export type ScopedTransactionClientInstance = {
2277
+ ` + `export type ScopedTransactionClientInstance = ClientContext<"restricted"> & {
2153
2278
  ` + scopedSurface({ transactionType: "ScopedTransactionClientInstance", inTransaction: true }) + ` // Deliberately ABSENT: the raw-SQL doors, $scoped, and $connect / $disconnect (the open transaction owns the connection).
2154
2279
  ` + `};`
2155
2280
  ];
@@ -2186,6 +2311,9 @@ function emitReadOnlySection(params) {
2186
2311
  const extensionLines = baked.readOnlyClientDts.length === 0 ? "" : `${baked.readOnlyClientDts.join(`
2187
2312
  `)}
2188
2313
  `;
2314
+ const scopeLines = scopedClientLines({ view, scopedType: 'ReadOnlyClientInstance<"restricted">' });
2315
+ const scopeSurface = scopeLines.length === 0 ? "" : `(ContextKind extends "readOnly" ? {
2316
+ ${scopeLines}} : {}) & `;
2189
2317
  return [
2190
2318
  banner({ title: "Read-only clients ($readOnly)" }),
2191
2319
  `/**
@@ -2199,24 +2327,66 @@ function emitReadOnlySection(params) {
2199
2327
  ` + ` * await reader.user.findMany(); // fine
2200
2328
  ` + ` * await reader.user.upsertMany({ \u2026 }); // VIBE_READ_ONLY, and a compile error
2201
2329
  ` + ` */
2202
- ` + `export type ReadOnlyClientInstance = ClientContext<"readOnly"> & {
2203
- ` + delegateSurfaceLines({ view, delegateSuffix: "ReadOnlyDelegate" }) + transactionLines({ transactionType: "ReadOnlyClientInstance", reads: true }) + DEFINE_VIEW_LINE + policyClientLines({ view, selfType: "ReadOnlyClientInstance" }) + scopedClientLines({ view, scopedType: "ReadOnlyClientInstance" }) + ` /** Idempotent \u2014 restricting an already-restricted handle returns the same handle. */
2204
- ` + ` readonly $readOnly: () => ReadOnlyClientInstance;
2330
+ ` + `export type ReadOnlyClientInstance<ContextKind extends "readOnly" | "restricted" = ${tsString({ value: hasNativeRls({ view }) || view.models.some((model) => model.policy !== undefined) ? "restricted" : "readOnly" })}> = ClientContext<ContextKind> & ${scopeSurface}{
2331
+ ` + delegateSurfaceLines({ view, delegateSuffix: "ReadOnlyDelegate" }) + transactionLines({ transactionType: "ReadOnlyClientInstance<ContextKind>", reads: true }) + DEFINE_VIEW_LINE + policyClientLines({ view, selfType: "ReadOnlyClientInstance<ContextKind>" }) + ` /** Idempotent: the same read-only restriction and underlying context are retained. */
2332
+ ` + ` readonly $readOnly: () => ReadOnlyClientInstance<ContextKind>;
2205
2333
  ` + extensionLines + ` // Deliberately ABSENT: every write method, $queryRaw / $executeRaw / $queryRawUnsafe / $executeRawUnsafe, $connect / $disconnect, $advisoryLock / $tryAdvisoryLock, $withContext, and every extension surface its author did not classify as a read.
2206
2334
  ` + `};`
2207
2335
  ];
2208
2336
  }
2209
- function emitDts(params) {
2337
+ var UNSUPPORTED_INPUT_MEMBER_NAMES = new Set([
2338
+ "toString",
2339
+ "valueOf",
2340
+ "constructor",
2341
+ "toLocaleString",
2342
+ "hasOwnProperty",
2343
+ "isPrototypeOf",
2344
+ "propertyIsEnumerable"
2345
+ ]);
2346
+ function groupComputed(params) {
2347
+ const byModel = new Map;
2348
+ for (const entry of params.computed) {
2349
+ const model = entry.model;
2350
+ const list = byModel.get(model);
2351
+ if (list === undefined)
2352
+ byModel.set(model, [entry]);
2353
+ else
2354
+ list.push(entry);
2355
+ }
2356
+ return byModel;
2357
+ }
2358
+ function assertGeneratedInputNames(params) {
2359
+ const check = (member) => {
2360
+ if (!UNSUPPORTED_INPUT_MEMBER_NAMES.has(member.name))
2361
+ return;
2362
+ const hint = member.kind === "field" ? "Rename the logical field and preserve its database column with @map or DSL .map()." : member.kind === "relation" ? "Rename the logical relation while keeping its FK fields, references, relation pairing name and constraint mapping. Relation fields do not have a column @map." : "Rename the computed alias in the computed configuration; it has no database column to map.";
2363
+ throw new VibeError4({
2364
+ code: "VIBE_UNSUPPORTED_CAPABILITY",
2365
+ message: `Generated TypeScript inputs cannot represent logical ${member.kind} "${member.model}.${member.name}" safely because it conflicts with an inherited Object member. ${hint} SchemaIR and the dynamic client still accept this name.`,
2366
+ meta: { reason: "generated_input_member", model: member.model, member: member.name, kind: member.kind }
2367
+ });
2368
+ };
2369
+ const computedByModel = groupComputed({ computed: params.computed ?? [] });
2370
+ for (const model of params.view.models) {
2371
+ for (const field of model.fields)
2372
+ check({ model: model.name, name: field.name, kind: "field" });
2373
+ for (const relation of model.relations)
2374
+ check({ model: model.name, name: relation.name, kind: "relation" });
2375
+ for (const entry of computedByModel.get(model.name) ?? []) {
2376
+ check({ model: model.name, name: entry.field, kind: "computed" });
2377
+ }
2378
+ }
2379
+ }
2380
+ function prepareDts(params) {
2381
+ assertGeneratedInputNames({ view: params.view, computed: params.computed ?? [] });
2382
+ return () => emitPreparedDts(params);
2383
+ }
2384
+ function emitPreparedDts(params) {
2210
2385
  const { view } = params;
2211
2386
  const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
2212
2387
  const computed = params.computed ?? [];
2213
2388
  const hasComputed = computed.length > 0;
2214
- const computedByModel = new Map;
2215
- for (const entry of computed) {
2216
- const list = computedByModel.get(entry.model) ?? [];
2217
- list.push(entry);
2218
- computedByModel.set(entry.model, list);
2219
- }
2389
+ const computedByModel = groupComputed({ computed });
2220
2390
  const parts = [];
2221
2391
  parts.push(GENERATED_HEADER);
2222
2392
  const schemaTypeImports = [
@@ -2451,7 +2621,8 @@ var README = `# Generated VibeORM client
2451
2621
  This directory was generated by \`@vibeorm/generator\` from your VibeORM schema \u2014 do not edit it by hand, any change is overwritten on the next generate. After every schema change, regenerate with \`vibeorm generate\` (or programmatically via \`generateClient({ schema, outDir })\` from \`@vibeorm/generator\`).
2452
2622
  `;
2453
2623
  async function generateClient(params) {
2454
- const { schema, outDir } = params;
2624
+ const { outDir } = params;
2625
+ const schema = params.module === undefined ? params.schema : prepareDatabaseModuleSchema({ schema: params.schema, module: params.module });
2455
2626
  const view = buildSchemaView({ schema });
2456
2627
  const baked = bakeExtensions({ schema: view.schema, extensions: params.extensions ?? [] });
2457
2628
  let computed = [];
@@ -2459,11 +2630,12 @@ async function generateClient(params) {
2459
2630
  validateComputedConfig({ schema, computed: params.computed });
2460
2631
  computed = buildComputedManifest({ computed: params.computed });
2461
2632
  }
2633
+ const declarations = prepareDts({ view, baked, computed });
2462
2634
  if (params.module !== undefined && (!baked.empty || computed.length > 0))
2463
2635
  throw new VibeError6({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Package modules do not support computed fields or extension mounts in this protocol", meta: { reason: "module_schema_feature", moduleId: params.module.id } });
2464
2636
  const outputs = [
2465
2637
  { name: "index.js", content: emitJs({ view, baked, computed }) },
2466
- { name: "index.d.ts", content: emitDts({ view, baked, computed }) },
2638
+ { name: "index.d.ts", content: declarations() },
2467
2639
  { name: "enums.js", content: emitEnumsJs({ view }) },
2468
2640
  { name: "enums.d.ts", content: emitEnumsDts({ view }) },
2469
2641
  { name: "README.md", content: README },
@@ -2480,7 +2652,8 @@ async function generateClient(params) {
2480
2652
  }
2481
2653
  export {
2482
2654
  bakeExtensions,
2483
- generateClient
2655
+ generateClient,
2656
+ prepareDatabaseModuleSchema
2484
2657
  };
2485
2658
 
2486
- //# debugId=BC4813FB63493C9C64756E2164756E21
2659
+ //# debugId=0EF8A4E0C9CC959A64756E2164756E21