@vibeorm/generator 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { join } from "path";
5
5
  import { buildComputedManifest, validateComputedConfig } from "@vibeorm/schema";
6
6
 
7
7
  // packages/generator/src/emit-dts.ts
8
- import { VibeError as VibeError3 } from "@vibeorm/schema";
8
+ import { DIALECT_CAPABILITIES, VibeError as VibeError3, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
9
9
 
10
10
  // packages/generator/src/emit-js.ts
11
11
  import { internalError } from "@vibeorm/schema";
@@ -24,8 +24,10 @@ var EMPTY_BAKED_EXTENSIONS = {
24
24
  manifest: [],
25
25
  dtsImports: [],
26
26
  delegateDts: new Map,
27
+ readOnlyDelegateDts: new Map,
27
28
  moduleDts: [],
28
29
  clientDts: [],
30
+ readOnlyClientDts: [],
29
31
  empty: true
30
32
  };
31
33
 
@@ -46,6 +48,37 @@ class ImportCollector {
46
48
  function indent(lines) {
47
49
  return lines.map((line) => line.length === 0 ? line : ` ${line}`);
48
50
  }
51
+ function readOnlyClassification(params) {
52
+ const { extension, contribution } = params;
53
+ const readOnly = contribution.readOnly;
54
+ if (readOnly === undefined)
55
+ return;
56
+ const refuse = (problem) => {
57
+ throw new VibeError({
58
+ code: "VIBE_CONFIG",
59
+ message: `extension "${extension.name}": readOnly ${problem}`,
60
+ meta: { extension: extension.name, kind: contribution.kind }
61
+ });
62
+ };
63
+ if (contribution.kind === "method" && readOnly !== true) {
64
+ refuse('must be `true` on a "method" contribution \u2014 a member list classifies a namespace');
65
+ }
66
+ if (contribution.kind === "namespace") {
67
+ if (readOnly === true) {
68
+ refuse('must list the readable members on a "namespace" contribution \u2014 `true` would also classify members added later');
69
+ } else if (readOnly.length === 0) {
70
+ refuse("names no members \u2014 omit it instead, which denies the whole namespace");
71
+ }
72
+ }
73
+ return readOnly;
74
+ }
75
+ function readOnlyLine(params) {
76
+ const { owner, property, readOnly } = params;
77
+ if (readOnly === undefined)
78
+ return;
79
+ const access = `${owner}[${JSON.stringify(property)}]`;
80
+ return readOnly === true ? ` readonly ${property}: ${access};` : ` readonly ${property}: Pick<${access}, ${readOnly.map((member) => JSON.stringify(member)).join(" | ")}>;`;
81
+ }
49
82
  function bakeExtensions(params) {
50
83
  const { schema } = params;
51
84
  if (params.extensions.length === 0)
@@ -58,21 +91,33 @@ function bakeExtensions(params) {
58
91
  const dtsImports = new ImportCollector;
59
92
  const mounts = [];
60
93
  const delegateDts = new Map;
94
+ const readOnlyDelegateDts = new Map;
61
95
  const moduleDts = [];
62
96
  const clientDts = [];
97
+ const readOnlyClientDts = [];
63
98
  const useContribution = (paramsInner) => {
64
99
  const { extension, contribution, scope, model } = paramsInner;
65
100
  const property = scope === "model" ? extension.name : `$${extension.name}`;
101
+ const readOnly = readOnlyClassification({ extension, contribution });
66
102
  jsImports.add(contribution.imports);
67
103
  dtsImports.add(contribution.dtsImports);
68
104
  moduleDts.push(...contribution.dtsTypes ?? []);
69
- mounts.push(`{ extension: ${JSON.stringify(extension.name)}, scope: ${JSON.stringify(scope)}, ${model === undefined ? "" : `model: ${JSON.stringify(model)}, `}property: ${JSON.stringify(property)}, create: ${contribution.runtimeGlue} }`);
105
+ mounts.push(`{ extension: ${JSON.stringify(extension.name)}, scope: ${JSON.stringify(scope)}, ${model === undefined ? "" : `model: ${JSON.stringify(model)}, `}property: ${JSON.stringify(property)}, ${readOnly === undefined ? "" : `readOnly: ${JSON.stringify(readOnly)}, `}create: ${contribution.runtimeGlue} }`);
70
106
  if (scope === "model" && model !== undefined) {
71
107
  const lines = delegateDts.get(model) ?? [];
72
108
  lines.push(...indent(contribution.dts));
73
109
  delegateDts.set(model, lines);
110
+ const classified = readOnlyLine({ owner: `${model}Delegate`, property, readOnly });
111
+ if (classified !== undefined) {
112
+ const readOnlyLines = readOnlyDelegateDts.get(model) ?? [];
113
+ readOnlyLines.push(classified);
114
+ readOnlyDelegateDts.set(model, readOnlyLines);
115
+ }
74
116
  } else {
75
117
  clientDts.push(...indent(contribution.dts));
118
+ const classified = readOnlyLine({ owner: "VibeClientInstance", property, readOnly });
119
+ if (classified !== undefined)
120
+ readOnlyClientDts.push(classified);
76
121
  }
77
122
  };
78
123
  for (const extension of sorted) {
@@ -125,8 +170,10 @@ function bakeExtensions(params) {
125
170
  manifest: sorted.map((extension) => extensionManifestEntry({ extension })),
126
171
  dtsImports: dtsImports.lines({ typeOnly: true }),
127
172
  delegateDts,
173
+ readOnlyDelegateDts,
128
174
  moduleDts,
129
175
  clientDts,
176
+ readOnlyClientDts,
130
177
  empty: false
131
178
  };
132
179
  }
@@ -300,7 +347,8 @@ function emitJs(params) {
300
347
  const hasComputed = computed.length > 0;
301
348
  const parts = [];
302
349
  parts.push(HEADER);
303
- parts.push(`import { createClient } from "@vibeorm/runtime";`);
350
+ parts.push(`import { createClient, createSqlModel } from "@vibeorm/runtime";`);
351
+ parts.push(`export { sql, decode, sqlProjection } from "@vibeorm/runtime";`);
304
352
  if (baked.jsImports.length > 0)
305
353
  parts.push(baked.jsImports.join(`
306
354
  `));
@@ -309,6 +357,11 @@ function emitJs(params) {
309
357
  parts.push(`export { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`);
310
358
  parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
311
359
  export const schema = ${serializeSchema({ schema: view.schema })};`);
360
+ parts.push(`/** Mapped identifiers and explicit projection decoders. */
361
+ export const sqlModels = Object.freeze({
362
+ ${view.models.map((model) => ` [${tsString({ value: model.clientName })}]: (options = {}) => createSqlModel({ schema, model: ${tsString({ value: model.name })}, ...options }),`).join(`
363
+ `)}
364
+ });`);
312
365
  if (baked.empty && !hasComputed) {
313
366
  parts.push(`/** Build a typed client over a database adapter. */
314
367
  ` + `export function VibeClient(options) {
@@ -434,6 +487,12 @@ function updateOpsName(params) {
434
487
  function banner(params) {
435
488
  return `// \u2500\u2500\u2500 ${params.title} \u2500\u2500\u2500`;
436
489
  }
490
+ function capabilitiesOf(params) {
491
+ return DIALECT_CAPABILITIES[providerToDialect2({ provider: params.view.schema.datasource.provider })];
492
+ }
493
+ function literalUnion(params) {
494
+ return params.members.map((member) => tsString({ value: member })).join(" | ");
495
+ }
437
496
  function objectAlias(params) {
438
497
  const { name, lines } = params;
439
498
  if (lines.length === 0)
@@ -1030,7 +1089,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1030
1089
  const listRelations = model.relations.filter((relation) => relation.isList);
1031
1090
  const hasCounts = listRelations.length > 0;
1032
1091
  const countLine = ` readonly _count?: boolean | { readonly select: ${n}CountSelect };`;
1033
- const relationSelectValue = (relation) => `boolean | ${relation.target}${relation.isList ? "FindManyArgs" : "Args"}`;
1092
+ const relationSelectValue = (relation) => `boolean | ${relation.target}${relation.isList ? "RelationFindManyArgs" : "Args"}`;
1034
1093
  if (hasCounts) {
1035
1094
  parts.push(inputAlias({
1036
1095
  name: `${n}CountSelect`,
@@ -1082,6 +1141,8 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1082
1141
  lines: [` readonly is?: ${n}WhereInput | null;`, ` readonly isNot?: ${n}WhereInput | null;`]
1083
1142
  }));
1084
1143
  const whereLines = [
1144
+ ` /** Constructed raw SQL; refused on scoped, read-only and field-masked handles. */`,
1145
+ ` readonly $raw?: SqlFragment;`,
1085
1146
  ` readonly AND?: ${orMany({ type: `${n}WhereInput` })};`,
1086
1147
  ` readonly OR?: readonly ${n}WhereInput[];`,
1087
1148
  ` readonly NOT?: ${orMany({ type: `${n}WhereInput` })};`
@@ -1119,9 +1180,18 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1119
1180
  | ${members.join(`
1120
1181
  | `)};`);
1121
1182
  }
1183
+ const scalarOrderLines = model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: ${field.isOptional ? "SortOrderWithNulls" : "SortOrder"};`);
1184
+ parts.push(inputAlias({ name: `${n}ScalarOrderByInput`, lines: scalarOrderLines }));
1185
+ parts.push(inputAlias({
1186
+ name: `${n}RelationOrderByInput`,
1187
+ lines: model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: SortOrder;`)
1188
+ }));
1122
1189
  parts.push(inputAlias({
1123
1190
  name: `${n}OrderByInput`,
1124
- lines: model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: ${field.isOptional ? "SortOrderWithNulls" : "SortOrder"};`)
1191
+ lines: [
1192
+ ...scalarOrderLines,
1193
+ ...model.relations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: ${relation.isList ? "{ readonly _count?: SortOrder | Skip }" : `${relation.target}RelationOrderByInput`};`)
1194
+ ]
1125
1195
  }));
1126
1196
  parts.push(inputAlias({ name: `${n}CreateInput`, lines: checkedCreateLines({ model }) }));
1127
1197
  parts.push(inputAlias({ name: `${n}UncheckedCreateInput`, lines: uncheckedCreateLines({ model }) }));
@@ -1157,24 +1227,28 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1157
1227
  aggregateOpProps.push(` readonly _min?: ${n}MinAggregateInput;`);
1158
1228
  aggregateOpProps.push(` readonly _max?: ${n}MaxAggregateInput;`);
1159
1229
  }
1160
- parts.push(inputAlias({
1161
- name: `${n}FindManyArgs`,
1162
- lines: [
1163
- ` readonly where?: ${n}WhereInput;`,
1164
- ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
1165
- ` readonly select?: ${n}Select;`,
1166
- ` readonly include?: ${n}Include;`,
1167
- ` readonly omit?: ${n}OmitInput;`,
1168
- ` readonly cursor?: ${n}WhereUniqueInput;`,
1169
- ` readonly after?: KeysetToken;`,
1170
- ` readonly keyset?: KeysetOptions;`,
1171
- ` readonly take?: number;`,
1172
- ` readonly skip?: number;`,
1173
- ` readonly distinct?: ${orMany({ type: `${n}ScalarFieldEnum` })};`,
1174
- ` readonly relationStrategy?: RelationStrategy;`,
1175
- ` readonly lock?: "update";`
1176
- ]
1177
- }));
1230
+ for (const nested of [false, true]) {
1231
+ parts.push(inputAlias({
1232
+ name: `${n}${nested ? "RelationFindManyArgs" : "FindManyArgs"}`,
1233
+ lines: [
1234
+ ` readonly where?: ${n}WhereInput;`,
1235
+ ` readonly orderBy?: ${orMany({ type: `${n}${nested ? "ScalarOrderByInput" : "OrderByInput"}` })};`,
1236
+ ` readonly select?: ${n}Select;`,
1237
+ ` readonly include?: ${n}Include;`,
1238
+ ` readonly omit?: ${n}OmitInput;`,
1239
+ ` readonly take?: number;`,
1240
+ ` readonly skip?: number;`,
1241
+ ...!nested ? [
1242
+ ` readonly cursor?: ${n}WhereUniqueInput;`,
1243
+ ` readonly after?: KeysetToken;`,
1244
+ ` readonly keyset?: KeysetOptions;`,
1245
+ ` readonly distinct?: ${orMany({ type: `${n}ScalarFieldEnum` })};`,
1246
+ ` readonly relationStrategy?: RelationStrategy;`,
1247
+ lockArgumentLines()
1248
+ ] : []
1249
+ ]
1250
+ }));
1251
+ }
1178
1252
  parts.push(`export type ${n}FindFirstArgs = ${n}FindManyArgs;`);
1179
1253
  parts.push(inputAlias({
1180
1254
  name: `${n}FindUniqueArgs`,
@@ -1184,12 +1258,13 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1184
1258
  ` readonly include?: ${n}Include;`,
1185
1259
  ` readonly omit?: ${n}OmitInput;`,
1186
1260
  ` readonly relationStrategy?: RelationStrategy;`,
1187
- ` readonly lock?: "update";`
1261
+ lockArgumentLines()
1188
1262
  ]
1189
1263
  }));
1190
1264
  parts.push(inputAlias({
1191
1265
  name: `${n}CreateArgs`,
1192
1266
  lines: [
1267
+ ` readonly onConflict?: never;`,
1193
1268
  ` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
1194
1269
  ` readonly select?: ${n}Select;`,
1195
1270
  ` readonly include?: ${n}Include;`,
@@ -1197,14 +1272,9 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1197
1272
  ]
1198
1273
  }));
1199
1274
  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
- }));
1275
+ parts.push(`export type ${n}CreateOnConflictInput =
1276
+ ` + ` | { readonly action: "nothing"; readonly update?: never; readonly target?: ${n}ConflictTarget | Skip; readonly where?: string | Skip }` + (capabilitiesOf({ view }).returning ? `
1277
+ | { readonly action: "update"; readonly target: ${n}ConflictTarget; readonly update: { ${model.fields.map((field) => `readonly ${propKey({ name: field.name })}?: SqlFragment`).join("; ")} }; readonly where?: string | Skip }` : "") + ";");
1208
1278
  parts.push(inputAlias({
1209
1279
  name: `${n}CreateOrSkipArgs`,
1210
1280
  lines: [
@@ -1237,7 +1307,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
1237
1307
  ` readonly data: readonly ${n}CreateManyInput[];`,
1238
1308
  ` readonly conflictTarget: ${n}ConflictTarget;`,
1239
1309
  ` readonly conflictWhere?: string;`,
1240
- ` readonly update: readonly ${n}ScalarFieldEnum[];`,
1310
+ ` readonly update: readonly ${n}ScalarFieldEnum[] | { ${model.fields.map((field) => `readonly ${propKey({ name: field.name })}?: SqlFragment`).join("; ")} };`,
1241
1311
  ` readonly skipUpdatedAt?: boolean;`,
1242
1312
  ` readonly allowAnyUniqueConflict?: boolean;`
1243
1313
  ]
@@ -1301,7 +1371,7 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1301
1371
  lines: [
1302
1372
  ` readonly select?: ${n}CountAggregateInput;`,
1303
1373
  ` readonly where?: ${n}WhereInput;`,
1304
- ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
1374
+ ` readonly orderBy?: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1305
1375
  ` readonly cursor?: ${n}WhereUniqueInput;`,
1306
1376
  ` readonly take?: number;`,
1307
1377
  ` readonly skip?: number;`
@@ -1311,7 +1381,7 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1311
1381
  name: `${n}AggregateArgs`,
1312
1382
  lines: [
1313
1383
  ` readonly where?: ${n}WhereInput;`,
1314
- ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
1384
+ ` readonly orderBy?: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1315
1385
  ` readonly cursor?: ${n}WhereUniqueInput;`,
1316
1386
  ` readonly take?: number;`,
1317
1387
  ` readonly skip?: number;`,
@@ -1323,7 +1393,7 @@ export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
1323
1393
  lines: [
1324
1394
  ` readonly by: readonly ${n}ScalarFieldEnum[];`,
1325
1395
  ` readonly where?: ${n}WhereInput;`,
1326
- ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
1396
+ ` readonly orderBy?: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1327
1397
  ` readonly having?: ${n}WhereInput;`,
1328
1398
  ` readonly take?: number;`,
1329
1399
  ` readonly skip?: number;`,
@@ -1433,6 +1503,11 @@ ${relationCases}
1433
1503
  parts.push(`export type ${n}Delegate = {
1434
1504
  ${delegateMethods.join(`
1435
1505
  `)}
1506
+ };`);
1507
+ parts.push(`/** ${n}'s read surface on a \`$readOnly()\` client. Writes are not declared, and are refused at runtime with \`VIBE_READ_ONLY\`. */
1508
+ ` + `export type ${n}ReadOnlyDelegate = {
1509
+ ${[...readMethods, ...aggregateMethods, ...params.readOnlyExtensionLines ?? []].join(`
1510
+ `)}
1436
1511
  };`);
1437
1512
  return parts.join(`
1438
1513
 
@@ -1462,7 +1537,7 @@ function emitViewTypesForModel(params) {
1462
1537
  name: `${n}ViewNestedArgs`,
1463
1538
  lines: [
1464
1539
  ` readonly where?: ${n}WhereInput;`,
1465
- ` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
1540
+ ` readonly orderBy?: ${orMany({ type: `${n}ScalarOrderByInput` })};`,
1466
1541
  ` readonly take?: number;`,
1467
1542
  ` readonly skip?: number;`,
1468
1543
  ` readonly select?: ${n}ViewSelect;`,
@@ -1617,12 +1692,102 @@ ${lines.join(`
1617
1692
  };`
1618
1693
  ];
1619
1694
  }
1620
- function instanceSurfaceLines(params) {
1621
- const { view, selfType } = params;
1622
- const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`);
1695
+ function emitLockSection(params) {
1696
+ const capabilities = capabilitiesOf(params);
1697
+ const example = capabilities.rowLockWaitPolicies.includes("skipLocked") ? [
1698
+ ` * @example Claim one queued job without waiting for another worker's row`,
1699
+ ` * await db.$transaction(async (tx) => {`,
1700
+ ` * const [job] = await tx.job.findMany({`,
1701
+ ` * where: { status: "queued" },`,
1702
+ ` * take: 1,`,
1703
+ ` * lock: { strength: "update", wait: "skipLocked" },`,
1704
+ ` * });`,
1705
+ ` * if (job !== undefined) await tx.job.update({ where: { id: job.id }, data: { status: "running" } });`,
1706
+ ` * });`
1707
+ ] : [
1708
+ ` * @example Read a row you are about to write`,
1709
+ ` * await db.$transaction(async (tx) => {`,
1710
+ ` * const account = await tx.account.findUnique({ where: { id }, lock: "update" });`,
1711
+ ` * await tx.account.update({ where: { id }, data: { balance: account.balance - amount } });`,
1712
+ ` * });`
1713
+ ];
1714
+ const body = capabilities.rowLockStrengths.length === 0 ? [
1715
+ `/**`,
1716
+ ` * A row-locking read. This database has NO locking-read clause, so the`,
1717
+ ` * historical \`"update"\` request emits no clause at all \u2014 a documented`,
1718
+ ` * no-op that costs nothing here, because its write transactions already`,
1719
+ ` * exclude each other. Every other strength and wait policy is refused`,
1720
+ ` * rather than silently downgraded, so they are absent from this type.`,
1721
+ ` *`,
1722
+ ` * Still REQUIRES a transaction: outside one the call is refused`,
1723
+ ` * (\`VIBE_VALIDATION\`), so code written here stays portable.`,
1724
+ ` *`,
1725
+ ...example,
1726
+ ` */`,
1727
+ `export type RowLockRequest = "update";`
1728
+ ] : [
1729
+ `/**`,
1730
+ ` * A row-locking read (\`SELECT \u2026 FOR UPDATE SKIP LOCKED\`).`,
1731
+ ` *`,
1732
+ ` * REQUIRES a transaction \u2014 call it on the client \`$transaction\` hands its`,
1733
+ ` * callback. Outside one the lock would be released at statement end, so`,
1734
+ ` * the call is refused with \`VIBE_VALIDATION\` instead.`,
1735
+ ` *`,
1736
+ ` * \`"update"\` is the shorthand for \`{ strength: "update", wait: "wait" }\`.`,
1737
+ ` * \`wait\` is ONE policy, never several flags: \`"wait"\` blocks until the`,
1738
+ ` * holder finishes (the default), \`"nowait"\` fails immediately with`,
1739
+ ` * \`VIBE_LOCK_NOT_AVAILABLE\`, and \`"skipLocked"\` leaves locked rows out of`,
1740
+ ` * the result. Refused with \`include\`/relation \`select\`, with \`distinct\`,`,
1741
+ ` * and on views.`,
1742
+ ` *`,
1743
+ ...example,
1744
+ ` */`,
1745
+ `export type RowLockRequest =`,
1746
+ ` | "update"`,
1747
+ ` | {`,
1748
+ ` readonly strength: ${literalUnion({ members: capabilities.rowLockStrengths })};`,
1749
+ ` readonly wait?: ${literalUnion({ members: capabilities.rowLockWaitPolicies })};`,
1750
+ ` };`
1751
+ ];
1752
+ return [banner({ title: "Row locks" }), body.join(`
1753
+ `)];
1754
+ }
1755
+ function lockArgumentLines() {
1756
+ return [
1757
+ ` /** Row-locking read \u2014 see {@link RowLockRequest}. Only inside \`$transaction\`. */`,
1758
+ ` readonly lock?: RowLockRequest;`
1759
+ ].join(`
1760
+ `);
1761
+ }
1762
+ function advisoryLockLines(params) {
1763
+ if (!capabilitiesOf(params).advisoryLocks)
1764
+ return "";
1765
+ return ` /**
1766
+ ` + ` * Lock an application-defined resource for the rest of the transaction \u2014
1767
+ ` + ` * including one that has no row yet. BLOCKS until the lock is held.
1768
+ ` + ` *
1769
+ ` + ` * Released by commit and by rollback; there is no unlock call, so nothing
1770
+ ` + ` * can leak onto a pooled connection. Only inside \`$transaction\` (outside
1771
+ ` + ` * one the lock would be gone before the next statement).
1772
+ ` + ` *
1773
+ ` + ` * @example Serialize an import of a supplier's price list
1774
+ ` + ` * await ${params.selfLabel}.$transaction(async (tx) => {
1775
+ ` + ` * await tx.$advisoryLock({ classId: 1, objectId: supplierId });
1776
+ ` + ` * await rebuildPrices(tx);
1777
+ ` + ` * });
1778
+ ` + ` */
1779
+ ` + ` readonly $advisoryLock: (options: AdvisoryLockOptions) => Promise<void>;
1780
+ ` + ` /** The same lock WITHOUT waiting: \`true\` when acquired, \`false\` when someone else holds it. The transaction stays usable either way. */
1781
+ ` + ` readonly $tryAdvisoryLock: (options: AdvisoryLockOptions) => Promise<boolean>;
1782
+ `;
1783
+ }
1784
+ function delegateSurfaceLines(params) {
1785
+ const { view, delegateSuffix } = params;
1786
+ const typeOf = (model) => `${model.name}${delegateSuffix}`;
1787
+ const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${typeOf(model)};`);
1623
1788
  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;`];
1789
+ const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${typeOf(model)};`;
1790
+ return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${typeOf(model)};`];
1626
1791
  });
1627
1792
  return `${delegateProps.join(`
1628
1793
  `)}
@@ -1633,20 +1798,37 @@ ${modelsProps.join(`
1633
1798
  };
1634
1799
  ` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
1635
1800
  ` + ` 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. */
1801
+ `;
1802
+ }
1803
+ function transactionLines(params) {
1804
+ const { transactionType, reads } = params;
1805
+ const doc = reads ? ` /** ONE transaction of READS \u2014 a consistent snapshot. The callback's handle is read-only too, and the array form takes read operations only. */` : ` /** 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. */`;
1806
+ return `${doc}
1637
1807
  ` + ` readonly $transaction: {
1638
- ` + ` <T>(fn: (tx: ${selfType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
1808
+ ` + ` <T>(fn: (tx: ${transactionType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
1639
1809
  ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1640
1810
  ` + ` };
1811
+ `;
1812
+ }
1813
+ var RAW_SURFACE_LINES = ` readonly $querySql: <T>(params: { readonly query: SqlFragment; readonly decoder: ResultDecoder<T> }) => Promise<T[]>;
1814
+ ` + ` readonly $executeSql: (params: { readonly query: SqlFragment }) => Promise<number>;
1641
1815
  ` + ` readonly $queryRaw: <T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T[]>;
1642
1816
  ` + ` readonly $executeRaw: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
1643
1817
  ` + ` readonly $queryRawUnsafe: <T = Record<string, unknown>>(text: string, ...values: unknown[]) => Promise<T[]>;
1644
1818
  ` + ` readonly $executeRawUnsafe: (text: string, ...values: unknown[]) => Promise<number>;
1645
- ` + ` readonly $connect: () => Promise<void>;
1819
+ `;
1820
+ var CONNECTION_SURFACE_LINES = ` readonly $connect: () => Promise<void>;
1646
1821
  ` + ` readonly $disconnect: () => Promise<void>;
1647
- ` + ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
1822
+ `;
1823
+ var DEFINE_VIEW_LINE = ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
1648
1824
  ` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
1649
1825
  `;
1826
+ 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. */
1827
+ ` + ` readonly $readOnly: () => ReadOnlyClientInstance;
1828
+ `;
1829
+ function instanceSurfaceLines(params) {
1830
+ 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;
1650
1832
  }
1651
1833
  function telemetryDeclarations() {
1652
1834
  return [
@@ -1765,6 +1947,8 @@ function emitClient(params) {
1765
1947
  ` readonly adapter: DatabaseAdapter;`,
1766
1948
  ` readonly onQuery?: (event: QueryEvent) => void;`,
1767
1949
  ` readonly relationStrategy?: RelationStrategy;`,
1950
+ ` /** 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\`. */`,
1951
+ ` readonly relationLoading?: { readonly concurrency?: number };`,
1768
1952
  ` /** Append the primary key to ORDER BY when a find has no explicit orderBy (default false) \u2014 deterministic row order. */`,
1769
1953
  ` readonly defaultOrderByPk?: boolean;`,
1770
1954
  ` /** Append the primary key as a tie-breaker to \`distinct\` ordering (default true) \u2014 pins WHICH row represents each distinct key. */`,
@@ -1773,6 +1957,8 @@ function emitClient(params) {
1773
1957
  ` readonly validate?: WriteValidators;`,
1774
1958
  ` /** 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
1959
  ` readonly telemetry?: VibeTelemetryOptions;`,
1960
+ ` /** 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. */`,
1961
+ ` readonly diagnostics?: StatementDiagnosticsOptions;`,
1776
1962
  ...baked.empty ? [] : [
1777
1963
  ` /** The live extension instances \u2014 verified against the baked manifest. */`,
1778
1964
  ` readonly extensions?: readonly VibeExtension[];`
@@ -1792,7 +1978,21 @@ function emitClient(params) {
1792
1978
  `;
1793
1979
  if (nativeRls) {
1794
1980
  parts.push(`export type BoundVibeClientInstance = {
1795
- ` + instanceSurfaceLines({ view, selfType: "BoundVibeClientInstance" }) + clientExtensionLines + `};`);
1981
+ ` + instanceSurfaceLines({
1982
+ view,
1983
+ transactionType: "BoundVibeTransactionClientInstance",
1984
+ inTransaction: false,
1985
+ selfLabel: "db"
1986
+ }) + READ_ONLY_LINE + clientExtensionLines + `};`);
1987
+ parts.push(`/** The bound handle inside \`$transaction\`: same identity, plus the locks that require a transaction, minus the connection lifecycle the transaction owns. */
1988
+ ` + `export type BoundVibeTransactionClientInstance = {
1989
+ ` + instanceSurfaceLines({
1990
+ view,
1991
+ transactionType: "BoundVibeTransactionClientInstance",
1992
+ inTransaction: true,
1993
+ selfLabel: "tx"
1994
+ }) + READ_ONLY_LINE + clientExtensionLines + ` // Deliberately ABSENT: $connect / $disconnect (the open transaction owns the connection) and $withContext (identity is bound once, from the root).
1995
+ ` + `};`);
1796
1996
  parts.push(`export type VibeClientInstance = {
1797
1997
  ` + ` /** Bind the request's identity context \u2014 ALL of it, every declared slot \u2014 and get the client that can actually query. */
1798
1998
  ` + ` readonly $withContext: (context: RlsContext) => BoundVibeClientInstance;
@@ -1800,11 +2000,25 @@ function emitClient(params) {
1800
2000
  ` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
1801
2001
  ` + ` readonly $connect: () => Promise<void>;
1802
2002
  ` + ` readonly $disconnect: () => Promise<void>;
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.
2003
+ ` + ` // 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.
1804
2004
  ` + `};`);
1805
2005
  } else {
1806
2006
  parts.push(`export type VibeClientInstance = {
1807
- ` + instanceSurfaceLines({ view, selfType: "VibeClientInstance" }) + policyClientLines({ view }) + scopedClientLines({ view }) + clientExtensionLines + `};`);
2007
+ ` + instanceSurfaceLines({
2008
+ view,
2009
+ transactionType: "VibeTransactionClientInstance",
2010
+ inTransaction: false,
2011
+ selfLabel: "db"
2012
+ }) + policyClientLines({ view, selfType: "VibeClientInstance" }) + scopedClientLines({ view, scopedType: "ScopedClientInstance" }) + READ_ONLY_LINE + clientExtensionLines + `};`);
2013
+ 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. */
2014
+ ` + `export type VibeTransactionClientInstance = {
2015
+ ` + instanceSurfaceLines({
2016
+ view,
2017
+ transactionType: "VibeTransactionClientInstance",
2018
+ inTransaction: true,
2019
+ selfLabel: "tx"
2020
+ }) + policyClientLines({ view, selfType: "VibeTransactionClientInstance" }) + scopedClientLines({ view, scopedType: "ScopedTransactionClientInstance" }) + READ_ONLY_LINE + clientExtensionLines + ` // Deliberately ABSENT: $connect / $disconnect \u2014 the open transaction owns the connection, and both are refused at runtime here too.
2021
+ ` + `};`);
1808
2022
  }
1809
2023
  parts.push(`/** Build a typed client over a database adapter. */
1810
2024
  export declare function VibeClient(options: VibeClientOptions): VibeClientInstance;`);
@@ -1843,17 +2057,7 @@ function emitScopedSection(params) {
1843
2057
  const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`;
1844
2058
  return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${scopedDelegateType(model)};`];
1845
2059
  });
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(`
2060
+ const scopedSurface = (params2) => `${delegateProps.join(`
1857
2061
  `)}
1858
2062
  ` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
1859
2063
  ` + ` readonly $models: {
@@ -1864,14 +2068,27 @@ ${modelsProps.join(`
1864
2068
  ` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
1865
2069
  ` + ` /** ONE transaction; the callback receives a handle that INHERITS the scope. The array form takes un-awaited scoped delegate calls. */
1866
2070
  ` + ` readonly $transaction: {
1867
- ` + ` <T>(fn: (tx: ScopedClientInstance) => Promise<T>, options?: TransactionOptions): Promise<T>;
2071
+ ` + ` <T>(fn: (tx: ${params2.transactionType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
1868
2072
  ` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
1869
2073
  ` + ` };
1870
- ` + ` readonly $connect: () => Promise<void>;
1871
- ` + ` readonly $disconnect: () => Promise<void>;
1872
- ` + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
2074
+ ` + (params2.inTransaction ? advisoryLockLines({ view, selfLabel: "tx" }) : CONNECTION_SURFACE_LINES) + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
1873
2075
  ` + ` 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).
2076
+ ` + READ_ONLY_LINE;
2077
+ return [
2078
+ banner({ title: "Tenant scoping ($scoped)" }),
2079
+ `/** 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. */
2080
+ export type ScopedNestedMode = "verify" | "refuse";`,
2081
+ `/** Per-model tenancy classification \u2014 TOTAL: every model must be named ("none" = global, passes through untouched). */
2082
+ export type ScopedModelsConfig = {
2083
+ ${entries.join(`
2084
+ `)}
2085
+ };`,
2086
+ `export type ScopedClientInstance = {
2087
+ ` + 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).
2088
+ ` + `};`,
2089
+ `/** The scoped handle inside \`$transaction\`: same scope, plus the locks that require a transaction. */
2090
+ ` + `export type ScopedTransactionClientInstance = {
2091
+ ` + scopedSurface({ transactionType: "ScopedTransactionClientInstance", inTransaction: true }) + ` // Deliberately ABSENT: the raw-SQL doors, $scoped, and $connect / $disconnect (the open transaction owns the connection).
1875
2092
  ` + `};`
1876
2093
  ];
1877
2094
  }
@@ -1881,7 +2098,7 @@ function scopedClientLines(params) {
1881
2098
  if (params.view.models.some((model) => model.policy !== undefined))
1882
2099
  return "";
1883
2100
  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;
2101
+ ` + ` readonly $scoped: (config: { readonly scope: string | number | bigint | null; readonly models: ScopedModelsConfig }) => ${params.scopedType};
1885
2102
  `;
1886
2103
  }
1887
2104
  function policyClientLines(params) {
@@ -1897,10 +2114,35 @@ function policyClientLines(params) {
1897
2114
  }
1898
2115
  const contextType = `{ ${[...contextKeys.entries()].map(([key, type]) => `readonly ${propKey({ name: key })}: ${type};`).join(" ")} }`;
1899
2116
  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;
2117
+ ` + ` readonly $withPolicy: (context: ${contextType}) => ${params.selfType};
2118
+ ` + ` /** Deliberate, greppable full-access escape from policy scoping. It escapes the ROW policy, never a read-only restriction. */
2119
+ ` + ` readonly $bypassPolicy: () => ${params.selfType};
2120
+ `;
2121
+ }
2122
+ function emitReadOnlySection(params) {
2123
+ const { view, baked } = params;
2124
+ const extensionLines = baked.readOnlyClientDts.length === 0 ? "" : `${baked.readOnlyClientDts.join(`
2125
+ `)}
1903
2126
  `;
2127
+ return [
2128
+ banner({ title: "Read-only clients ($readOnly)" }),
2129
+ `/**
2130
+ ` + ` * A read-only handle. Application-level, not a database privilege: it refuses
2131
+ ` + ` * the calls VibeORM would issue, and cannot stop another connection or a role
2132
+ ` + ` * with write rights \u2014 use a read-only database role or transaction when the
2133
+ ` + ` * database itself must enforce it.
2134
+ ` + ` *
2135
+ ` + ` * @example Hand a query resolver something that cannot write
2136
+ ` + ` * const reader = db.$readOnly();
2137
+ ` + ` * await reader.user.findMany(); // fine
2138
+ ` + ` * await reader.user.upsertMany({ \u2026 }); // VIBE_READ_ONLY, and a compile error
2139
+ ` + ` */
2140
+ ` + `export type ReadOnlyClientInstance = {
2141
+ ` + 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. */
2142
+ ` + ` readonly $readOnly: () => ReadOnlyClientInstance;
2143
+ ` + 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.
2144
+ ` + `};`
2145
+ ];
1904
2146
  }
1905
2147
  function emitDts(params) {
1906
2148
  const { view } = params;
@@ -1920,8 +2162,21 @@ function emitDts(params) {
1920
2162
  ...baked.empty ? [] : ["VibeExtension"],
1921
2163
  ...hasComputed ? ["ComputedConfig"] : []
1922
2164
  ].sort();
2165
+ const runtimeTypeImports = [
2166
+ "SqlFragment",
2167
+ "ResultDecoder",
2168
+ "SqlColumn",
2169
+ "DatabaseAdapter",
2170
+ "DbNow",
2171
+ "QueryEvent",
2172
+ "Skip",
2173
+ "StatementDiagnosticsOptions",
2174
+ "TransactionOptions",
2175
+ "WriteValidators",
2176
+ ...capabilitiesOf({ view }).advisoryLocks ? ["AdvisoryLockOptions"] : []
2177
+ ].sort();
1923
2178
  const importLines = [
1924
- `import type { DatabaseAdapter, DbNow, QueryEvent, Skip, TransactionOptions, WriteValidators } from "@vibeorm/runtime";`,
2179
+ `import type { ${runtimeTypeImports.join(", ")} } from "@vibeorm/runtime";`,
1925
2180
  `import type { ${schemaTypeImports.join(", ")} } from "@vibeorm/schema";`,
1926
2181
  ...jsonTypeImportLines({ view }),
1927
2182
  ...baked.dtsImports,
@@ -1936,6 +2191,14 @@ export type { VibeErrorCode } from "@vibeorm/schema";
1936
2191
  export type { Skip } from "@vibeorm/runtime";
1937
2192
  ` + `export { ${enumEntryExports({ view }).join(", ")} };`);
1938
2193
  parts.push(PREAMBLE);
2194
+ parts.push(`export { sql, decode, sqlProjection } from "@vibeorm/runtime";`);
2195
+ parts.push(`export type { SqlFragment, ResultDecoder, ExtendedDateTime, SqlColumn } from "@vibeorm/runtime";`);
2196
+ parts.push(`/** Mapped SQL references. No database handle or execution privileges are captured. */
2197
+ export declare const sqlModels: {
2198
+ ${view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: (options?: { readonly alias?: string }) => { readonly table: SqlFragment; readonly fields: { ${model.fields.map((field) => `readonly ${propKey({ name: field.name })}: SqlColumn<${field.scalar === "Json" ? "unknown" : payloadFieldType({ field })}>`).join("; ")} } };`).join(`
2199
+ `)}
2200
+ };`);
2201
+ parts.push(...emitLockSection({ view }));
1939
2202
  parts.push(...filterSection());
1940
2203
  for (const declared of view.enums)
1941
2204
  parts.push(emitEnum({ declared }));
@@ -1943,12 +2206,14 @@ export type { Skip } from "@vibeorm/runtime";
1943
2206
  const modelParts = [];
1944
2207
  for (const model of view.models) {
1945
2208
  const extensionLines = baked.delegateDts.get(model.name);
2209
+ const readOnlyExtensionLines = baked.readOnlyDelegateDts.get(model.name);
1946
2210
  const modelComputed = computedByModel.get(model.name);
1947
2211
  modelParts.push(emitModel({
1948
2212
  model,
1949
2213
  view,
1950
2214
  registry,
1951
2215
  ...extensionLines === undefined ? {} : { extensionLines },
2216
+ ...readOnlyExtensionLines === undefined ? {} : { readOnlyExtensionLines },
1952
2217
  ...modelComputed === undefined ? {} : { computed: modelComputed }
1953
2218
  }));
1954
2219
  }
@@ -1965,6 +2230,7 @@ export type { Skip } from "@vibeorm/runtime";
1965
2230
  }
1966
2231
  parts.push(...emitViewSection({ view }));
1967
2232
  parts.push(...emitScopedSection({ view }));
2233
+ parts.push(...emitReadOnlySection({ view, baked }));
1968
2234
  parts.push(...emitRlsSection({ view }));
1969
2235
  parts.push(emitClient({ view, baked, hasComputed }));
1970
2236
  return `${parts.join(`
@@ -2151,4 +2417,4 @@ export {
2151
2417
  bakeExtensions
2152
2418
  };
2153
2419
 
2154
- //# debugId=AC114FAAB4DBE66364756E2164756E21
2420
+ //# debugId=F1C487D9764B944164756E2164756E21