@vibeorm/generator 2.1.0 → 2.2.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,22 +1,151 @@
1
1
  // @bun
2
+ // packages/generator/src/emit-module.ts
3
+ import { createHash } from "crypto";
4
+ import { VibeError as VibeError2, providerToDialect } from "@vibeorm/schema";
5
+
6
+ // packages/generator/src/util.ts
7
+ import { VibeError } from "@vibeorm/schema";
8
+ function pascal(params) {
9
+ const { name } = params;
10
+ if (name.length === 0)
11
+ return name;
12
+ return name.charAt(0).toUpperCase() + name.slice(1);
13
+ }
14
+ function clientNameOf(params) {
15
+ const { name } = params;
16
+ if (name.length === 0)
17
+ return name;
18
+ return name.charAt(0).toLowerCase() + name.slice(1);
19
+ }
20
+ var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
21
+ function propKey(params) {
22
+ const { name } = params;
23
+ return IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
24
+ }
25
+ function tsString(params) {
26
+ return JSON.stringify(params.value);
27
+ }
28
+ function maybeParens(params) {
29
+ const { type } = params;
30
+ return type.includes("|") ? `(${type})` : type;
31
+ }
32
+ function stableStringify(params) {
33
+ return renderValue({ value: params.value, indent: "" });
34
+ }
35
+ function renderValue(params) {
36
+ const { value, indent } = params;
37
+ if (value === null)
38
+ return "null";
39
+ const kind = typeof value;
40
+ if (kind === "string" || kind === "number" || kind === "boolean")
41
+ return JSON.stringify(value);
42
+ if (Array.isArray(value)) {
43
+ if (value.length === 0)
44
+ return "[]";
45
+ const inner = `${indent} `;
46
+ const items = value.map((item) => inner + renderValue({ value: item, indent: inner }));
47
+ return `[
48
+ ${items.join(`,
49
+ `)}
50
+ ${indent}]`;
51
+ }
52
+ if (kind === "object") {
53
+ const record = value;
54
+ const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort();
55
+ if (keys.length === 0)
56
+ return "{}";
57
+ const inner = `${indent} `;
58
+ const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderValue({ value: record[key], indent: inner })}`);
59
+ return `{
60
+ ${items.join(`,
61
+ `)}
62
+ ${indent}}`;
63
+ }
64
+ throw new VibeError({
65
+ code: "VIBE_INTERNAL",
66
+ message: `internal: cannot serialize a ${kind} value into the generated client`,
67
+ meta: { kind }
68
+ });
69
+ }
70
+
71
+ // packages/generator/src/emit-module.ts
72
+ var MODULE_DECLARATIONS = `import type { ClientContext, DatabaseAdapter, QueryOperation, TransactionOptions } from "@vibeorm/runtime";
73
+ import type { DatabaseModuleManifest, SchemaIR } from "@vibeorm/schema";
74
+ import type { VibeClientInstance, VibeTransactionClientInstance, ReadOnlyClientInstance } from "./index.js";
75
+ export { sql, decode, sqlModels, sqlProjection } from "./index.js";
76
+ export type ModuleClient = Omit<VibeClientInstance, "$connect" | "$disconnect" | "$transaction" | "$scoped" | "$withPolicy" | "$bypassPolicy" | "$readOnly"> & {
77
+ readonly $transaction: ModuleTransactionClient["$transaction"];
78
+ readonly $readOnly: () => ReadOnlyClientInstance;
79
+ };
80
+ export type ModuleTransactionClient = Omit<VibeTransactionClientInstance, "$transaction" | "$scoped" | "$withPolicy" | "$bypassPolicy"> & {
81
+ readonly $transaction: {
82
+ <T>(run: (tx: ModuleTransactionClient) => Promise<T>, options?: TransactionOptions): Promise<T>;
83
+ <T extends readonly QueryOperation<unknown, "read" | "write">[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
84
+ };
85
+ };
86
+ export declare const databaseModule: {
87
+ readonly manifest: DatabaseModuleManifest;
88
+ readonly schema: SchemaIR;
89
+ readonly create: (params: { adapter: DatabaseAdapter }) => ModuleClient;
90
+ readonly bind: {
91
+ (params: { db: ClientContext<"root"> }): ModuleClient;
92
+ (params: { db: ClientContext<"transaction"> }): ModuleTransactionClient;
93
+ (params: { db: ClientContext<"readOnly"> }): ReadOnlyClientInstance;
94
+ };
95
+ };
96
+ `;
97
+ function emitDatabaseModule(params) {
98
+ const { view, module } = params;
99
+ for (const key of ["id", "contract", "revision"]) {
100
+ if (typeof module[key] !== "string" || module[key].trim().length === 0 || module[key].includes("\x00"))
101
+ throw new VibeError2({ code: "VIBE_CONFIG", message: `Database module ${key} must be a non-empty identifier`, meta: { reason: "module_manifest", key } });
102
+ }
103
+ if (view.schema.rls !== undefined || view.schema.extensions.length > 0 || view.schema.models.some((model) => model.policy !== undefined))
104
+ throw new VibeError2({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: `Module ${module.id} cannot declare extensions, policy or native RLS in this module protocol`, meta: { reason: "module_schema_feature", moduleId: module.id } });
105
+ const serialized = stableStringify({ value: view.schema });
106
+ const manifest = {
107
+ ...module,
108
+ protocol: 1,
109
+ dialect: providerToDialect({ provider: view.schema.datasource.provider }),
110
+ schemaChecksum: createHash("sha256").update(JSON.stringify(JSON.parse(serialized))).digest("hex")
111
+ };
112
+ return [
113
+ { name: "module.js", content: `// Generated package module. No connection or DDL on import.
114
+ import { defineDatabaseModule } from "@vibeorm/runtime";
115
+ import { schema } from "./schema.js";
116
+ export { sql, decode, sqlModels, sqlProjection } from "./index.js";
117
+ export const databaseModule = defineDatabaseModule({ ...${JSON.stringify(module)}, schema, manifest: ${JSON.stringify(manifest)} });
118
+ ` },
119
+ { name: "schema.js", content: `// Generated data-only schema and runtime manifest.
120
+ export const schema = ${serialized};
121
+ export const moduleManifest = ${JSON.stringify(manifest)};
122
+ ` },
123
+ { name: "schema.d.ts", content: `import type { SchemaIR, DatabaseModuleManifest } from "@vibeorm/schema";
124
+ export declare const schema: SchemaIR;
125
+ export declare const moduleManifest: DatabaseModuleManifest;
126
+ ` },
127
+ { name: "module.d.ts", content: MODULE_DECLARATIONS }
128
+ ];
129
+ }
130
+
2
131
  // packages/generator/src/generate.ts
3
132
  import { mkdir, writeFile } from "fs/promises";
4
133
  import { join } from "path";
5
- import { buildComputedManifest, validateComputedConfig } from "@vibeorm/schema";
134
+ import { VibeError as VibeError6, buildComputedManifest, validateComputedConfig } from "@vibeorm/schema";
6
135
 
7
136
  // packages/generator/src/emit-dts.ts
8
- import { DIALECT_CAPABILITIES, VibeError as VibeError3, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
137
+ import { DIALECT_CAPABILITIES, VibeError as VibeError4, providerToDialect as providerToDialect3 } from "@vibeorm/schema";
9
138
 
10
139
  // packages/generator/src/emit-js.ts
11
140
  import { internalError } from "@vibeorm/schema";
12
141
 
13
142
  // packages/generator/src/extension-bake.ts
14
143
  import {
15
- VibeError,
144
+ VibeError as VibeError3,
16
145
  buildExtensionRegistry,
17
146
  extensionDeclarations,
18
147
  extensionManifestEntry,
19
- providerToDialect
148
+ providerToDialect as providerToDialect2
20
149
  } from "@vibeorm/schema";
21
150
  var EMPTY_BAKED_EXTENSIONS = {
22
151
  jsImports: [],
@@ -54,7 +183,7 @@ function readOnlyClassification(params) {
54
183
  if (readOnly === undefined)
55
184
  return;
56
185
  const refuse = (problem) => {
57
- throw new VibeError({
186
+ throw new VibeError3({
58
187
  code: "VIBE_CONFIG",
59
188
  message: `extension "${extension.name}": readOnly ${problem}`,
60
189
  meta: { extension: extension.name, kind: contribution.kind }
@@ -84,7 +213,7 @@ function bakeExtensions(params) {
84
213
  if (params.extensions.length === 0)
85
214
  return EMPTY_BAKED_EXTENSIONS;
86
215
  buildExtensionRegistry({ extensions: params.extensions });
87
- const dialect = providerToDialect({ provider: schema.datasource.provider });
216
+ const dialect = providerToDialect2({ provider: schema.datasource.provider });
88
217
  const provider = schema.datasource.provider;
89
218
  const sorted = [...params.extensions].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
90
219
  const jsImports = new ImportCollector;
@@ -115,7 +244,7 @@ function bakeExtensions(params) {
115
244
  }
116
245
  } else {
117
246
  clientDts.push(...indent(contribution.dts));
118
- const classified = readOnlyLine({ owner: "VibeClientInstance", property, readOnly });
247
+ const classified = readOnlyLine({ owner: schema.rls === undefined ? "VibeClientInstance" : "BoundVibeClientInstance", property, readOnly });
119
248
  if (classified !== undefined)
120
249
  readOnlyClientDts.push(classified);
121
250
  }
@@ -141,7 +270,7 @@ function bakeExtensions(params) {
141
270
  continue;
142
271
  const clash = model.fields.some((field) => field.name === extension.name) || model.relations.some((relation) => relation.name === extension.name);
143
272
  if (clash) {
144
- throw new VibeError({
273
+ throw new VibeError3({
145
274
  code: "VIBE_CONFIG",
146
275
  message: `extension "${extension.name}" collides with a field/relation named "${extension.name}" on model "${model.name}" \u2014 rename one of them`,
147
276
  meta: { extension: extension.name, model: model.name }
@@ -178,71 +307,6 @@ function bakeExtensions(params) {
178
307
  };
179
308
  }
180
309
 
181
- // packages/generator/src/util.ts
182
- import { VibeError as VibeError2 } from "@vibeorm/schema";
183
- function pascal(params) {
184
- const { name } = params;
185
- if (name.length === 0)
186
- return name;
187
- return name.charAt(0).toUpperCase() + name.slice(1);
188
- }
189
- function clientNameOf(params) {
190
- const { name } = params;
191
- if (name.length === 0)
192
- return name;
193
- return name.charAt(0).toLowerCase() + name.slice(1);
194
- }
195
- var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
196
- function propKey(params) {
197
- const { name } = params;
198
- return IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
199
- }
200
- function tsString(params) {
201
- return JSON.stringify(params.value);
202
- }
203
- function maybeParens(params) {
204
- const { type } = params;
205
- return type.includes("|") ? `(${type})` : type;
206
- }
207
- function stableStringify(params) {
208
- return renderValue({ value: params.value, indent: "" });
209
- }
210
- function renderValue(params) {
211
- const { value, indent: indent2 } = params;
212
- if (value === null)
213
- return "null";
214
- const kind = typeof value;
215
- if (kind === "string" || kind === "number" || kind === "boolean")
216
- return JSON.stringify(value);
217
- if (Array.isArray(value)) {
218
- if (value.length === 0)
219
- return "[]";
220
- const inner = `${indent2} `;
221
- const items = value.map((item) => inner + renderValue({ value: item, indent: inner }));
222
- return `[
223
- ${items.join(`,
224
- `)}
225
- ${indent2}]`;
226
- }
227
- if (kind === "object") {
228
- const record = value;
229
- const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort();
230
- if (keys.length === 0)
231
- return "{}";
232
- const inner = `${indent2} `;
233
- const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderValue({ value: record[key], indent: inner })}`);
234
- return `{
235
- ${items.join(`,
236
- `)}
237
- ${indent2}}`;
238
- }
239
- throw new VibeError2({
240
- code: "VIBE_INTERNAL",
241
- message: `internal: cannot serialize a ${kind} value into the generated client`,
242
- meta: { kind }
243
- });
244
- }
245
-
246
310
  // packages/generator/src/emit-js.ts
247
311
  var HEADER = `// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
248
312
  // Regenerate with \`vibeorm generate\` after every schema change.`;
@@ -459,13 +523,15 @@ var MINMAX_SCALARS = new Set([
459
523
  function payloadBaseType(params) {
460
524
  const { field } = params;
461
525
  if (field.jsonType !== undefined)
462
- return field.jsonType.name;
526
+ return `import(${tsString({ value: field.jsonType.from })}).${field.jsonType.name}`;
463
527
  return field.enumName ?? PAYLOAD_TYPES[field.scalar];
464
528
  }
465
529
  function inputBaseType(params) {
466
530
  const { field } = params;
467
531
  if (field.jsonType !== undefined)
468
- return field.jsonType.name;
532
+ return `import(${tsString({ value: field.jsonType.from })}).${field.jsonType.name}`;
533
+ if (field.isList && field.scalar === "DateTime")
534
+ return "Date | string";
469
535
  return field.enumName ?? INPUT_TYPES[field.scalar];
470
536
  }
471
537
  function filterName(params) {
@@ -488,7 +554,7 @@ function banner(params) {
488
554
  return `// \u2500\u2500\u2500 ${params.title} \u2500\u2500\u2500`;
489
555
  }
490
556
  function capabilitiesOf(params) {
491
- return DIALECT_CAPABILITIES[providerToDialect2({ provider: params.view.schema.datasource.provider })];
557
+ return DIALECT_CAPABILITIES[providerToDialect3({ provider: params.view.schema.datasource.provider })];
492
558
  }
493
559
  function literalUnion(params) {
494
560
  return params.members.map((member) => tsString({ value: member })).join(" | ");
@@ -587,6 +653,10 @@ export type KeysetOptions = {
587
653
  /** Result of createMany/updateMany/deleteMany. */
588
654
  export type BatchPayload = { readonly count: number };
589
655
 
656
+ /** Static operation classification only; runtime verifies instance ownership and active lifetime. */
657
+ declare const vibeTransaction: unique symbol;
658
+ export type VibeQueryPromise<T, Access extends "read" | "write" = "read"> = QueryOperation<T, Access>;
659
+
590
660
  /**
591
661
  * What upsertMany reports. \`count\` is the rows submitted after duplicate-key
592
662
  * validation \u2014 every one of them was inserted or updated, since the statement
@@ -609,7 +679,7 @@ type Provided<T> = Exclude<T, Skip | undefined>;
609
679
  type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 };
610
680
 
611
681
  /** Keys a selection has not RULED OUT \u2014 everything except a literal false/undefined. */
612
- type PossibleKeys<S> = keyof { [K in keyof S as S[K] extends false | undefined ? never : K]: 0 };
682
+ type PossibleKeys<S> = keyof { [K in keyof S as S[K] extends false | undefined | Skip ? never : K]: 0 };
613
683
 
614
684
  /**
615
685
  * Keys whose flag can STILL be false or missing when the query runs \u2014 a widened
@@ -617,7 +687,7 @@ type PossibleKeys<S> = keyof { [K in keyof S as S[K] extends false | undefined ?
617
687
  * result type must not promise a field a run-time flag can switch off.
618
688
  */
619
689
  type UncertainKeys<S> = PossibleKeys<S> &
620
- keyof { [K in keyof S as [false] extends [S[K]] ? K : [undefined] extends [S[K]] ? K : never]: 0 };
690
+ keyof { [K in keyof S as [false] extends [S[K]] ? K : [undefined] extends [S[K]] ? K : [Skip] extends [S[K]] ? K : never]: 0 };
621
691
 
622
692
  /** Keys the selection definitely turns on: a literal \`true\`, or a nested selection object. */
623
693
  type ExactKeys<S> = Exclude<PossibleKeys<S>, UncertainKeys<S>>;
@@ -892,7 +962,7 @@ function register(params) {
892
962
  const existing = registry.get(name);
893
963
  if (existing !== undefined) {
894
964
  if (existing !== text) {
895
- throw new VibeError3({
965
+ throw new VibeError4({
896
966
  code: "VIBE_INTERNAL",
897
967
  message: `internal: nested input type "${name}" generated with two different bodies`,
898
968
  meta: { name }
@@ -1400,8 +1470,8 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1400
1470
  ...aggregateOpProps
1401
1471
  ]
1402
1472
  }));
1403
- const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
1404
- const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
1473
+ const exactPart = (source) => `${source}[K] extends false | undefined | Skip ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : Skip extends ${source}[K] ? never : K`;
1474
+ const uncertainPart = (source) => `${source}[K] extends false | undefined | Skip ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : Skip extends ${source}[K] ? K : never`;
1405
1475
  parts.push(`type ${n}OmitBase<A> = A extends { readonly omit: infer O extends object }
1406
1476
  ` + ` ? Omit<${n}, PossibleKeys<O> & keyof ${n}> & { [K in UncertainKeys<O> & keyof ${n}]?: ${n}[K] }
1407
1477
  ` + ` : "omit" extends keyof A ? Omit<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}> & Partial<Pick<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}>> : ${n};`);
@@ -1411,10 +1481,11 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1411
1481
  parts.push(`type ${n}CountResult<C> = C extends { readonly select: infer CS extends object }
1412
1482
  ` + ` ? { [K in keyof CS & (${listUnion}) as ${exactPart("CS")}]: number } &
1413
1483
  ` + ` { [K in keyof CS & (${listUnion}) as ${uncertainPart("CS")}]?: number }
1414
- ` + ` : { ${allCounts} };`);
1484
+ ` + ` : C extends { readonly select: Skip } ? { ${allCounts} }
1485
+ ` + ` : "select" extends keyof C ? Partial<{ ${allCounts} }> : { ${allCounts} };`);
1415
1486
  parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> }
1416
- ` + ` : S extends { readonly _count: false | undefined } ? unknown
1417
- ` + ` : S extends { readonly _count?: infer C extends boolean | object } ? { _count?: ${n}CountResult<C> } : unknown;`);
1487
+ ` + ` : S extends { readonly _count: false | undefined | Skip } ? unknown
1488
+ ` + ` : S extends { readonly _count?: infer C extends boolean | object | Skip } ? { _count?: ${n}CountResult<Provided<C>> } : unknown;`);
1418
1489
  }
1419
1490
  if (hasRelations) {
1420
1491
  const relationCases = model.relations.map((relation) => {
@@ -1445,12 +1516,14 @@ ${relationCases}
1445
1516
  ` + ` { [K in keyof S & keyof ${n}Relations as ${exactPart("S")}]: ${n}RelationPayload<K, S[K]> } &
1446
1517
  ` + ` { [K in keyof S & keyof ${n}Relations as ${uncertainPart("S")}]?: ${n}RelationPayload<K, S[K]> }${hasCounts ? ` &
1447
1518
  ${n}CountPayload<S>` : ""}
1519
+ ` + ` : A extends { readonly select: Skip } ? ${n}OmitBase<A> & ${n}IncludePayload<A>
1448
1520
  ` + ` : "select" extends keyof A ? Partial<${n}>
1449
1521
  ` + ` : ${n}OmitBase<A> & ${n}IncludePayload<A>;`);
1450
1522
  } else {
1451
1523
  parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
1452
1524
  ` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1453
1525
  ` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
1526
+ ` + ` : A extends { readonly select: Skip } ? ${n}OmitBase<A>
1454
1527
  ` + ` : "select" extends keyof A ? Partial<${n}> : ${n}OmitBase<A>;`);
1455
1528
  }
1456
1529
  const aggregateFields = (fields, value) => `{ ${fields.map((field) => `${propKey({ name: field.name })}: ${value(field)}`).join("; ")} }`;
@@ -1470,33 +1543,33 @@ ${relationCases}
1470
1543
  ` + ` : "select" extends keyof A ? number | ${n}AggregateSelection<${n}AggregateValues["_count"], Provided<A["select"]>> : number;`);
1471
1544
  parts.push(`export type ${n}AggregateResult<A> =
1472
1545
  ` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? K : never]: ${n}AggregateSelection<${n}AggregateValues[K], A[K]> } &
1473
- ` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? never : K]?: ${n}AggregateSelection<${n}AggregateValues[K], Provided<A[K]>> };`);
1546
+ ` + ` { [K in keyof A & keyof ${n}AggregateValues as A[K] extends false | undefined | Skip ? never : HasSelectedField<A[K]> extends true ? never : K]?: ${n}AggregateSelection<${n}AggregateValues[K], Provided<A[K]>> };`);
1474
1547
  parts.push(`export type ${n}GroupByResult<A extends ${n}GroupByArgs> =
1475
1548
  ` + ` (number extends A["by"]["length"] ? Partial<Pick<${n}, A["by"][number]>> : Pick<${n}, A["by"][number]>) & ${n}AggregateResult<A>;`);
1476
1549
  const readMethods = [
1477
- ` findMany<A extends ${n}FindManyArgs = {}>(args?: A): Promise<${n}GetPayload<A>[]>;`,
1478
- ` findFirst<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A> | null>;`,
1479
- ` findFirstOrThrow<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A>>;`,
1480
- ` findUnique<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
1481
- ` findUniqueOrThrow<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A>>;`
1550
+ ` findMany<A extends ${n}FindManyArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A>[]>;`,
1551
+ ` findFirst<A extends ${n}FindFirstArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A> | null>;`,
1552
+ ` findFirstOrThrow<A extends ${n}FindFirstArgs = {}>(args?: A): VibeQueryPromise<${n}GetPayload<A>>;`,
1553
+ ` findUnique<A extends ${n}FindUniqueArgs>(args: A): VibeQueryPromise<${n}GetPayload<A> | null>;`,
1554
+ ` findUniqueOrThrow<A extends ${n}FindUniqueArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>>;`
1482
1555
  ];
1483
1556
  const writeMethods = [
1484
- ` create<A extends ${n}CreateOrSkipArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
1485
- ` create<A extends ${n}CreateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1486
- ` createMany(args: ${n}CreateManyArgs): Promise<BatchPayload>;`,
1487
- ` createManyAndReturn<A extends ${n}CreateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
1488
- ` update<A extends ${n}UpdateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1489
- ` updateMany(args: ${n}UpdateManyArgs): Promise<BatchPayload>;`,
1490
- ` updateManyAndReturn<A extends ${n}UpdateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
1491
- ` upsert<A extends ${n}UpsertArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1492
- ` upsertMany(args: ${n}UpsertManyArgs): Promise<UpsertManyPayload>;`,
1493
- ` delete<A extends ${n}DeleteArgs>(args: A): Promise<${n}GetPayload<A>>;`,
1494
- ` deleteMany(args?: ${n}DeleteManyArgs): Promise<BatchPayload>;`
1557
+ ` create<A extends ${n}CreateOrSkipArgs>(args: A): VibeQueryPromise<${n}GetPayload<A> | null, "write">;`,
1558
+ ` create<A extends ${n}CreateArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>, "write">;`,
1559
+ ` createMany(args: ${n}CreateManyArgs): VibeQueryPromise<BatchPayload, "write">;`,
1560
+ ` createManyAndReturn<A extends ${n}CreateManyAndReturnArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>[], "write">;`,
1561
+ ` update<A extends ${n}UpdateArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>, "write">;`,
1562
+ ` updateMany(args: ${n}UpdateManyArgs): VibeQueryPromise<BatchPayload, "write">;`,
1563
+ ` updateManyAndReturn<A extends ${n}UpdateManyAndReturnArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>[], "write">;`,
1564
+ ` upsert<A extends ${n}UpsertArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>, "write">;`,
1565
+ ` upsertMany(args: ${n}UpsertManyArgs): VibeQueryPromise<UpsertManyPayload, "write">;`,
1566
+ ` delete<A extends ${n}DeleteArgs>(args: A): VibeQueryPromise<${n}GetPayload<A>, "write">;`,
1567
+ ` deleteMany(args?: ${n}DeleteManyArgs): VibeQueryPromise<BatchPayload, "write">;`
1495
1568
  ];
1496
1569
  const aggregateMethods = [
1497
- ` count<A extends ${n}CountArgs = {}>(args?: A): Promise<${n}CountOutput<A>>;`,
1498
- ` aggregate<A extends ${n}AggregateArgs>(args: A): Promise<${n}AggregateResult<A>>;`,
1499
- ` groupBy<const A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;`
1570
+ ` count<A extends ${n}CountArgs = {}>(args?: A): VibeQueryPromise<${n}CountOutput<A>>;`,
1571
+ ` aggregate<A extends ${n}AggregateArgs>(args: A): VibeQueryPromise<${n}AggregateResult<A>>;`,
1572
+ ` groupBy<const A extends ${n}GroupByArgs>(args: A): VibeQueryPromise<${n}GroupByResult<A>[]>;`
1500
1573
  ];
1501
1574
  const delegateMethods = model.isView ? [...readMethods, ...aggregateMethods] : [...readMethods, ...writeMethods, ...aggregateMethods];
1502
1575
  delegateMethods.push(...params.extensionLines ?? []);
@@ -1570,8 +1643,8 @@ function emitViewTypesForModel(params) {
1570
1643
  ` readonly relationStrategy?: RelationStrategy;`
1571
1644
  ]
1572
1645
  }));
1573
- const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
1574
- const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
1646
+ const exactPart = (source) => `${source}[K] extends false | undefined | Skip ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : Skip extends ${source}[K] ? never : K`;
1647
+ const uncertainPart = (source) => `${source}[K] extends false | undefined | Skip ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : Skip extends ${source}[K] ? K : never`;
1575
1648
  parts.push(`type ${n}ViewSelectScalars<F, A> = A extends { readonly select: infer S extends object }
1576
1649
  ` + ` ? { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
1577
1650
  ` + ` { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
@@ -1651,19 +1724,6 @@ ${view.models.map((model) => ` ${propKey({ name: model.clientName })}: ${model.
1651
1724
  }
1652
1725
  return parts;
1653
1726
  }
1654
- function jsonTypeImportLines(params) {
1655
- const bySpecifier = new Map;
1656
- for (const model of params.view.models) {
1657
- for (const field of model.fields) {
1658
- if (field.jsonType === undefined)
1659
- continue;
1660
- const names = bySpecifier.get(field.jsonType.from) ?? new Set;
1661
- names.add(field.jsonType.name);
1662
- bySpecifier.set(field.jsonType.from, names);
1663
- }
1664
- }
1665
- return [...bySpecifier.keys()].sort().map((from) => `import type { ${[...bySpecifier.get(from)].sort().join(", ")} } from ${tsString({ value: from })};`);
1666
- }
1667
1727
  function hasNativeRls(params) {
1668
1728
  return params.view.schema.rls !== undefined;
1669
1729
  }
@@ -1806,7 +1866,7 @@ function transactionLines(params) {
1806
1866
  return `${doc}
1807
1867
  ` + ` readonly $transaction: {
1808
1868
  ` + ` <T>(fn: (tx: ${transactionType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
1809
- ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1869
+ ` + ` <T extends readonly VibeQueryPromise<unknown, ${reads ? '"read"' : '"read" | "write"'}>[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1810
1870
  ` + ` };
1811
1871
  `;
1812
1872
  }
@@ -1828,7 +1888,8 @@ var READ_ONLY_LINE = ` /** A read-only handle over this one: reads pass through
1828
1888
  `;
1829
1889
  function instanceSurfaceLines(params) {
1830
1890
  const { view, transactionType, inTransaction, selfLabel } = params;
1831
- return delegateSurfaceLines({ view, delegateSuffix: "Delegate" }) + transactionLines({ transactionType, reads: false }) + RAW_SURFACE_LINES + (inTransaction ? advisoryLockLines({ view, selfLabel }) : "") + (inTransaction ? "" : CONNECTION_SURFACE_LINES) + DEFINE_VIEW_LINE;
1891
+ return (inTransaction ? ` readonly [vibeTransaction]: true;
1892
+ ` : "") + delegateSurfaceLines({ view, delegateSuffix: "Delegate" }) + transactionLines({ transactionType, reads: false }) + RAW_SURFACE_LINES + (inTransaction ? advisoryLockLines({ view, selfLabel }) : "") + (inTransaction ? "" : CONNECTION_SURFACE_LINES) + DEFINE_VIEW_LINE;
1832
1893
  }
1833
1894
  function telemetryDeclarations() {
1834
1895
  return [
@@ -1945,8 +2006,12 @@ function emitClient(params) {
1945
2006
  name: "VibeClientOptions",
1946
2007
  lines: [
1947
2008
  ` readonly adapter: DatabaseAdapter;`,
2009
+ ` /** Strict argument validation defaults to true; false restores legacy omission of undefined values. */`,
2010
+ ` readonly strictArguments?: boolean;`,
1948
2011
  ` readonly onQuery?: (event: QueryEvent) => void;`,
1949
2012
  ` readonly relationStrategy?: RelationStrategy;`,
2013
+ ` /** Opt-in concurrent relation loading: how many of ONE query's relation statements (siblings, \`_count\` entries, key chunks) may be in flight at once. Default 1; ignored inside \`$transaction\`. */`,
2014
+ ` readonly relationLoading?: { readonly concurrency?: number };`,
1950
2015
  ` /** Append the primary key to ORDER BY when a find has no explicit orderBy (default false) \u2014 deterministic row order. */`,
1951
2016
  ` readonly defaultOrderByPk?: boolean;`,
1952
2017
  ` /** Append the primary key as a tie-breaker to \`distinct\` ordering (default true) \u2014 pins WHICH row represents each distinct key. */`,
@@ -1955,6 +2020,8 @@ function emitClient(params) {
1955
2020
  ` readonly validate?: WriteValidators;`,
1956
2021
  ` /** 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. */`,
1957
2022
  ` readonly telemetry?: VibeTelemetryOptions;`,
2023
+ ` /** Statement diagnostics: every statement over \`slowerThanMs\` is reported with its duration and, with \`explain\`, an automatic EXPLAIN summary on PostgreSQL/PGlite (\`"analyze"\` re-executes read statements). \`VIBEORM_DIAGNOSTICS=analyze:5\` switches it on for a run; local runs only. */`,
2024
+ ` readonly diagnostics?: StatementDiagnosticsOptions;`,
1958
2025
  ...baked.empty ? [] : [
1959
2026
  ` /** The live extension instances \u2014 verified against the baked manifest. */`,
1960
2027
  ` readonly extensions?: readonly VibeExtension[];`
@@ -1973,7 +2040,7 @@ function emitClient(params) {
1973
2040
  `)}
1974
2041
  `;
1975
2042
  if (nativeRls) {
1976
- parts.push(`export type BoundVibeClientInstance = {
2043
+ parts.push(`export type BoundVibeClientInstance = ClientContext<"restricted"> & {
1977
2044
  ` + instanceSurfaceLines({
1978
2045
  view,
1979
2046
  transactionType: "BoundVibeTransactionClientInstance",
@@ -1981,7 +2048,7 @@ function emitClient(params) {
1981
2048
  selfLabel: "db"
1982
2049
  }) + READ_ONLY_LINE + clientExtensionLines + `};`);
1983
2050
  parts.push(`/** The bound handle inside \`$transaction\`: same identity, plus the locks that require a transaction, minus the connection lifecycle the transaction owns. */
1984
- ` + `export type BoundVibeTransactionClientInstance = {
2051
+ ` + `export type BoundVibeTransactionClientInstance = ClientContext<"restricted"> & {
1985
2052
  ` + instanceSurfaceLines({
1986
2053
  view,
1987
2054
  transactionType: "BoundVibeTransactionClientInstance",
@@ -1989,7 +2056,7 @@ function emitClient(params) {
1989
2056
  selfLabel: "tx"
1990
2057
  }) + READ_ONLY_LINE + clientExtensionLines + ` // Deliberately ABSENT: $connect / $disconnect (the open transaction owns the connection) and $withContext (identity is bound once, from the root).
1991
2058
  ` + `};`);
1992
- parts.push(`export type VibeClientInstance = {
2059
+ parts.push(`export type VibeClientInstance = ClientContext<"restricted"> & {
1993
2060
  ` + ` /** Bind the request's identity context \u2014 ALL of it, every declared slot \u2014 and get the client that can actually query. */
1994
2061
  ` + ` readonly $withContext: (context: RlsContext) => BoundVibeClientInstance;
1995
2062
  ` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
@@ -1999,7 +2066,7 @@ function emitClient(params) {
1999
2066
  ` + ` // 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.
2000
2067
  ` + `};`);
2001
2068
  } else {
2002
- parts.push(`export type VibeClientInstance = {
2069
+ parts.push(`export type VibeClientInstance = ClientContext<"root"> & {
2003
2070
  ` + instanceSurfaceLines({
2004
2071
  view,
2005
2072
  transactionType: "VibeTransactionClientInstance",
@@ -2007,7 +2074,7 @@ function emitClient(params) {
2007
2074
  selfLabel: "db"
2008
2075
  }) + policyClientLines({ view, selfType: "VibeClientInstance" }) + scopedClientLines({ view, scopedType: "ScopedClientInstance" }) + READ_ONLY_LINE + clientExtensionLines + `};`);
2009
2076
  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. */
2010
- ` + `export type VibeTransactionClientInstance = {
2077
+ ` + `export type VibeTransactionClientInstance = ClientContext<"transaction"> & {
2011
2078
  ` + instanceSurfaceLines({
2012
2079
  view,
2013
2080
  transactionType: "VibeTransactionClientInstance",
@@ -2053,7 +2120,8 @@ function emitScopedSection(params) {
2053
2120
  const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`;
2054
2121
  return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${scopedDelegateType(model)};`];
2055
2122
  });
2056
- const scopedSurface = (params2) => `${delegateProps.join(`
2123
+ const scopedSurface = (params2) => (params2.inTransaction ? ` readonly [vibeTransaction]: true;
2124
+ ` : "") + `${delegateProps.join(`
2057
2125
  `)}
2058
2126
  ` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
2059
2127
  ` + ` readonly $models: {
@@ -2065,7 +2133,7 @@ ${modelsProps.join(`
2065
2133
  ` + ` /** ONE transaction; the callback receives a handle that INHERITS the scope. The array form takes un-awaited scoped delegate calls. */
2066
2134
  ` + ` readonly $transaction: {
2067
2135
  ` + ` <T>(fn: (tx: ${params2.transactionType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
2068
- ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
2136
+ ` + ` <T extends readonly VibeQueryPromise<unknown, "read" | "write">[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
2069
2137
  ` + ` };
2070
2138
  ` + (params2.inTransaction ? advisoryLockLines({ view, selfLabel: "tx" }) : CONNECTION_SURFACE_LINES) + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
2071
2139
  ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
@@ -2133,7 +2201,7 @@ function emitReadOnlySection(params) {
2133
2201
  ` + ` * await reader.user.findMany(); // fine
2134
2202
  ` + ` * await reader.user.upsertMany({ \u2026 }); // VIBE_READ_ONLY, and a compile error
2135
2203
  ` + ` */
2136
- ` + `export type ReadOnlyClientInstance = {
2204
+ ` + `export type ReadOnlyClientInstance = ClientContext<"readOnly"> & {
2137
2205
  ` + 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. */
2138
2206
  ` + ` readonly $readOnly: () => ReadOnlyClientInstance;
2139
2207
  ` + 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.
@@ -2159,6 +2227,8 @@ function emitDts(params) {
2159
2227
  ...hasComputed ? ["ComputedConfig"] : []
2160
2228
  ].sort();
2161
2229
  const runtimeTypeImports = [
2230
+ "ClientContext",
2231
+ "QueryOperation",
2162
2232
  "SqlFragment",
2163
2233
  "ResultDecoder",
2164
2234
  "SqlColumn",
@@ -2166,6 +2236,7 @@ function emitDts(params) {
2166
2236
  "DbNow",
2167
2237
  "QueryEvent",
2168
2238
  "Skip",
2239
+ "StatementDiagnosticsOptions",
2169
2240
  "TransactionOptions",
2170
2241
  "WriteValidators",
2171
2242
  ...capabilitiesOf({ view }).advisoryLocks ? ["AdvisoryLockOptions"] : []
@@ -2173,7 +2244,6 @@ function emitDts(params) {
2173
2244
  const importLines = [
2174
2245
  `import type { ${runtimeTypeImports.join(", ")} } from "@vibeorm/runtime";`,
2175
2246
  `import type { ${schemaTypeImports.join(", ")} } from "@vibeorm/schema";`,
2176
- ...jsonTypeImportLines({ view }),
2177
2247
  ...baked.dtsImports,
2178
2248
  `import { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`
2179
2249
  ];
@@ -2235,7 +2305,7 @@ ${view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}:
2235
2305
  }
2236
2306
 
2237
2307
  // packages/generator/src/schema-view.ts
2238
- import { VibeError as VibeError4, assertValidSchemaIR, enumStorageOf, isEnumType } from "@vibeorm/schema";
2308
+ import { VibeError as VibeError5, assertValidSchemaIR, enumStorageOf, isEnumType } from "@vibeorm/schema";
2239
2309
  import { postgresDialect, renderRlsPolicy } from "@vibeorm/sql";
2240
2310
  var JSON_TYPE_RE = /@type\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+"([^"]+)"\s*\)/;
2241
2311
  function parseJsonTypeAnnotation(params) {
@@ -2254,7 +2324,7 @@ function buildFieldView(params) {
2254
2324
  const enumRef = isEnumType(field.type);
2255
2325
  const annotation = parseJsonTypeAnnotation({ comment: field.documentation });
2256
2326
  if (annotation !== null && (enumRef || field.type !== "Json")) {
2257
- throw new VibeError4({
2327
+ throw new VibeError5({
2258
2328
  code: "VIBE_SCHEMA_INVALID",
2259
2329
  message: `@type(...) annotation on ${model}.${field.name} \u2014 typed JSON is only valid on Json fields`,
2260
2330
  meta: { model, field: field.name }
@@ -2328,7 +2398,7 @@ function buildModelView(params) {
2328
2398
  target: relation.targetModel,
2329
2399
  type: relation.type,
2330
2400
  isList: relation.isList,
2331
- isOptional: relation.isOptional,
2401
+ isOptional: relation.isOptional || !relation.isList && (relation.fields?.length ?? 0) === 0,
2332
2402
  fkFields: relation.fields ?? [],
2333
2403
  inverseName: target === undefined ? null : resolveInverse({ relation, target })
2334
2404
  };
@@ -2391,12 +2461,15 @@ async function generateClient(params) {
2391
2461
  validateComputedConfig({ schema, computed: params.computed });
2392
2462
  computed = buildComputedManifest({ computed: params.computed });
2393
2463
  }
2464
+ if (params.module !== undefined && (!baked.empty || computed.length > 0))
2465
+ 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 } });
2394
2466
  const outputs = [
2395
2467
  { name: "index.js", content: emitJs({ view, baked, computed }) },
2396
2468
  { name: "index.d.ts", content: emitDts({ view, baked, computed }) },
2397
2469
  { name: "enums.js", content: emitEnumsJs({ view }) },
2398
2470
  { name: "enums.d.ts", content: emitEnumsDts({ view }) },
2399
- { name: "README.md", content: README }
2471
+ { name: "README.md", content: README },
2472
+ ...params.module === undefined ? [] : emitDatabaseModule({ view, module: params.module })
2400
2473
  ];
2401
2474
  await mkdir(outDir, { recursive: true });
2402
2475
  const files = [];
@@ -2412,4 +2485,4 @@ export {
2412
2485
  bakeExtensions
2413
2486
  };
2414
2487
 
2415
- //# debugId=43DBEE51B9E7189D64756E2164756E21
2488
+ //# debugId=732A6C5C8F7D397B64756E2164756E21