@vibeorm/generator 2.0.1 → 2.1.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/emit-dts.d.ts.map +1 -1
- package/dist/emit-js.d.ts.map +1 -1
- package/dist/extension-bake.d.ts +9 -0
- package/dist/extension-bake.d.ts.map +1 -1
- package/dist/index.js +332 -71
- package/dist/index.js.map +5 -5
- package/package.json +3 -3
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 ? "
|
|
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:
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
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(
|
|
1201
|
-
|
|
1202
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
1621
|
-
const
|
|
1622
|
-
const
|
|
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
|
|
1625
|
-
return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${model
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
1819
|
+
`;
|
|
1820
|
+
var CONNECTION_SURFACE_LINES = ` readonly $connect: () => Promise<void>;
|
|
1646
1821
|
` + ` readonly $disconnect: () => Promise<void>;
|
|
1647
|
-
|
|
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 [
|
|
@@ -1792,7 +1974,21 @@ function emitClient(params) {
|
|
|
1792
1974
|
`;
|
|
1793
1975
|
if (nativeRls) {
|
|
1794
1976
|
parts.push(`export type BoundVibeClientInstance = {
|
|
1795
|
-
` + instanceSurfaceLines({
|
|
1977
|
+
` + instanceSurfaceLines({
|
|
1978
|
+
view,
|
|
1979
|
+
transactionType: "BoundVibeTransactionClientInstance",
|
|
1980
|
+
inTransaction: false,
|
|
1981
|
+
selfLabel: "db"
|
|
1982
|
+
}) + READ_ONLY_LINE + clientExtensionLines + `};`);
|
|
1983
|
+
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 = {
|
|
1985
|
+
` + instanceSurfaceLines({
|
|
1986
|
+
view,
|
|
1987
|
+
transactionType: "BoundVibeTransactionClientInstance",
|
|
1988
|
+
inTransaction: true,
|
|
1989
|
+
selfLabel: "tx"
|
|
1990
|
+
}) + READ_ONLY_LINE + clientExtensionLines + ` // Deliberately ABSENT: $connect / $disconnect (the open transaction owns the connection) and $withContext (identity is bound once, from the root).
|
|
1991
|
+
` + `};`);
|
|
1796
1992
|
parts.push(`export type VibeClientInstance = {
|
|
1797
1993
|
` + ` /** Bind the request's identity context \u2014 ALL of it, every declared slot \u2014 and get the client that can actually query. */
|
|
1798
1994
|
` + ` readonly $withContext: (context: RlsContext) => BoundVibeClientInstance;
|
|
@@ -1800,11 +1996,25 @@ function emitClient(params) {
|
|
|
1800
1996
|
` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
|
|
1801
1997
|
` + ` readonly $connect: () => Promise<void>;
|
|
1802
1998
|
` + ` 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.
|
|
1999
|
+
` + ` // 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
2000
|
` + `};`);
|
|
1805
2001
|
} else {
|
|
1806
2002
|
parts.push(`export type VibeClientInstance = {
|
|
1807
|
-
` + instanceSurfaceLines({
|
|
2003
|
+
` + instanceSurfaceLines({
|
|
2004
|
+
view,
|
|
2005
|
+
transactionType: "VibeTransactionClientInstance",
|
|
2006
|
+
inTransaction: false,
|
|
2007
|
+
selfLabel: "db"
|
|
2008
|
+
}) + policyClientLines({ view, selfType: "VibeClientInstance" }) + scopedClientLines({ view, scopedType: "ScopedClientInstance" }) + READ_ONLY_LINE + clientExtensionLines + `};`);
|
|
2009
|
+
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 = {
|
|
2011
|
+
` + instanceSurfaceLines({
|
|
2012
|
+
view,
|
|
2013
|
+
transactionType: "VibeTransactionClientInstance",
|
|
2014
|
+
inTransaction: true,
|
|
2015
|
+
selfLabel: "tx"
|
|
2016
|
+
}) + 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.
|
|
2017
|
+
` + `};`);
|
|
1808
2018
|
}
|
|
1809
2019
|
parts.push(`/** Build a typed client over a database adapter. */
|
|
1810
2020
|
export declare function VibeClient(options: VibeClientOptions): VibeClientInstance;`);
|
|
@@ -1843,17 +2053,7 @@ function emitScopedSection(params) {
|
|
|
1843
2053
|
const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`;
|
|
1844
2054
|
return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${scopedDelegateType(model)};`];
|
|
1845
2055
|
});
|
|
1846
|
-
|
|
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(`
|
|
2056
|
+
const scopedSurface = (params2) => `${delegateProps.join(`
|
|
1857
2057
|
`)}
|
|
1858
2058
|
` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
|
|
1859
2059
|
` + ` readonly $models: {
|
|
@@ -1864,14 +2064,27 @@ ${modelsProps.join(`
|
|
|
1864
2064
|
` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
|
|
1865
2065
|
` + ` /** ONE transaction; the callback receives a handle that INHERITS the scope. The array form takes un-awaited scoped delegate calls. */
|
|
1866
2066
|
` + ` readonly $transaction: {
|
|
1867
|
-
` + ` <T>(fn: (tx:
|
|
2067
|
+
` + ` <T>(fn: (tx: ${params2.transactionType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
1868
2068
|
` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
|
|
1869
2069
|
` + ` };
|
|
1870
|
-
` + `
|
|
1871
|
-
` + ` readonly $disconnect: () => Promise<void>;
|
|
1872
|
-
` + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
|
|
2070
|
+
` + (params2.inTransaction ? advisoryLockLines({ view, selfLabel: "tx" }) : CONNECTION_SURFACE_LINES) + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
|
|
1873
2071
|
` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
|
|
1874
|
-
` +
|
|
2072
|
+
` + READ_ONLY_LINE;
|
|
2073
|
+
return [
|
|
2074
|
+
banner({ title: "Tenant scoping ($scoped)" }),
|
|
2075
|
+
`/** 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. */
|
|
2076
|
+
export type ScopedNestedMode = "verify" | "refuse";`,
|
|
2077
|
+
`/** Per-model tenancy classification \u2014 TOTAL: every model must be named ("none" = global, passes through untouched). */
|
|
2078
|
+
export type ScopedModelsConfig = {
|
|
2079
|
+
${entries.join(`
|
|
2080
|
+
`)}
|
|
2081
|
+
};`,
|
|
2082
|
+
`export type ScopedClientInstance = {
|
|
2083
|
+
` + 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).
|
|
2084
|
+
` + `};`,
|
|
2085
|
+
`/** The scoped handle inside \`$transaction\`: same scope, plus the locks that require a transaction. */
|
|
2086
|
+
` + `export type ScopedTransactionClientInstance = {
|
|
2087
|
+
` + scopedSurface({ transactionType: "ScopedTransactionClientInstance", inTransaction: true }) + ` // Deliberately ABSENT: the raw-SQL doors, $scoped, and $connect / $disconnect (the open transaction owns the connection).
|
|
1875
2088
|
` + `};`
|
|
1876
2089
|
];
|
|
1877
2090
|
}
|
|
@@ -1881,7 +2094,7 @@ function scopedClientLines(params) {
|
|
|
1881
2094
|
if (params.view.models.some((model) => model.policy !== undefined))
|
|
1882
2095
|
return "";
|
|
1883
2096
|
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 }) =>
|
|
2097
|
+
` + ` readonly $scoped: (config: { readonly scope: string | number | bigint | null; readonly models: ScopedModelsConfig }) => ${params.scopedType};
|
|
1885
2098
|
`;
|
|
1886
2099
|
}
|
|
1887
2100
|
function policyClientLines(params) {
|
|
@@ -1897,10 +2110,35 @@ function policyClientLines(params) {
|
|
|
1897
2110
|
}
|
|
1898
2111
|
const contextType = `{ ${[...contextKeys.entries()].map(([key, type]) => `readonly ${propKey({ name: key })}: ${type};`).join(" ")} }`;
|
|
1899
2112
|
return ` /** Row scoping (@@policy): bind a per-request context; policied models fail closed without one. */
|
|
1900
|
-
` + ` readonly $withPolicy: (context: ${contextType}) =>
|
|
1901
|
-
` + ` /** Deliberate, greppable full-access escape from policy scoping. */
|
|
1902
|
-
` + ` readonly $bypassPolicy: () =>
|
|
2113
|
+
` + ` readonly $withPolicy: (context: ${contextType}) => ${params.selfType};
|
|
2114
|
+
` + ` /** Deliberate, greppable full-access escape from policy scoping. It escapes the ROW policy, never a read-only restriction. */
|
|
2115
|
+
` + ` readonly $bypassPolicy: () => ${params.selfType};
|
|
2116
|
+
`;
|
|
2117
|
+
}
|
|
2118
|
+
function emitReadOnlySection(params) {
|
|
2119
|
+
const { view, baked } = params;
|
|
2120
|
+
const extensionLines = baked.readOnlyClientDts.length === 0 ? "" : `${baked.readOnlyClientDts.join(`
|
|
2121
|
+
`)}
|
|
1903
2122
|
`;
|
|
2123
|
+
return [
|
|
2124
|
+
banner({ title: "Read-only clients ($readOnly)" }),
|
|
2125
|
+
`/**
|
|
2126
|
+
` + ` * A read-only handle. Application-level, not a database privilege: it refuses
|
|
2127
|
+
` + ` * the calls VibeORM would issue, and cannot stop another connection or a role
|
|
2128
|
+
` + ` * with write rights \u2014 use a read-only database role or transaction when the
|
|
2129
|
+
` + ` * database itself must enforce it.
|
|
2130
|
+
` + ` *
|
|
2131
|
+
` + ` * @example Hand a query resolver something that cannot write
|
|
2132
|
+
` + ` * const reader = db.$readOnly();
|
|
2133
|
+
` + ` * await reader.user.findMany(); // fine
|
|
2134
|
+
` + ` * await reader.user.upsertMany({ \u2026 }); // VIBE_READ_ONLY, and a compile error
|
|
2135
|
+
` + ` */
|
|
2136
|
+
` + `export type ReadOnlyClientInstance = {
|
|
2137
|
+
` + 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
|
+
` + ` readonly $readOnly: () => ReadOnlyClientInstance;
|
|
2139
|
+
` + 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.
|
|
2140
|
+
` + `};`
|
|
2141
|
+
];
|
|
1904
2142
|
}
|
|
1905
2143
|
function emitDts(params) {
|
|
1906
2144
|
const { view } = params;
|
|
@@ -1920,8 +2158,20 @@ function emitDts(params) {
|
|
|
1920
2158
|
...baked.empty ? [] : ["VibeExtension"],
|
|
1921
2159
|
...hasComputed ? ["ComputedConfig"] : []
|
|
1922
2160
|
].sort();
|
|
2161
|
+
const runtimeTypeImports = [
|
|
2162
|
+
"SqlFragment",
|
|
2163
|
+
"ResultDecoder",
|
|
2164
|
+
"SqlColumn",
|
|
2165
|
+
"DatabaseAdapter",
|
|
2166
|
+
"DbNow",
|
|
2167
|
+
"QueryEvent",
|
|
2168
|
+
"Skip",
|
|
2169
|
+
"TransactionOptions",
|
|
2170
|
+
"WriteValidators",
|
|
2171
|
+
...capabilitiesOf({ view }).advisoryLocks ? ["AdvisoryLockOptions"] : []
|
|
2172
|
+
].sort();
|
|
1923
2173
|
const importLines = [
|
|
1924
|
-
`import type {
|
|
2174
|
+
`import type { ${runtimeTypeImports.join(", ")} } from "@vibeorm/runtime";`,
|
|
1925
2175
|
`import type { ${schemaTypeImports.join(", ")} } from "@vibeorm/schema";`,
|
|
1926
2176
|
...jsonTypeImportLines({ view }),
|
|
1927
2177
|
...baked.dtsImports,
|
|
@@ -1936,6 +2186,14 @@ export type { VibeErrorCode } from "@vibeorm/schema";
|
|
|
1936
2186
|
export type { Skip } from "@vibeorm/runtime";
|
|
1937
2187
|
` + `export { ${enumEntryExports({ view }).join(", ")} };`);
|
|
1938
2188
|
parts.push(PREAMBLE);
|
|
2189
|
+
parts.push(`export { sql, decode, sqlProjection } from "@vibeorm/runtime";`);
|
|
2190
|
+
parts.push(`export type { SqlFragment, ResultDecoder, ExtendedDateTime, SqlColumn } from "@vibeorm/runtime";`);
|
|
2191
|
+
parts.push(`/** Mapped SQL references. No database handle or execution privileges are captured. */
|
|
2192
|
+
export declare const sqlModels: {
|
|
2193
|
+
${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(`
|
|
2194
|
+
`)}
|
|
2195
|
+
};`);
|
|
2196
|
+
parts.push(...emitLockSection({ view }));
|
|
1939
2197
|
parts.push(...filterSection());
|
|
1940
2198
|
for (const declared of view.enums)
|
|
1941
2199
|
parts.push(emitEnum({ declared }));
|
|
@@ -1943,12 +2201,14 @@ export type { Skip } from "@vibeorm/runtime";
|
|
|
1943
2201
|
const modelParts = [];
|
|
1944
2202
|
for (const model of view.models) {
|
|
1945
2203
|
const extensionLines = baked.delegateDts.get(model.name);
|
|
2204
|
+
const readOnlyExtensionLines = baked.readOnlyDelegateDts.get(model.name);
|
|
1946
2205
|
const modelComputed = computedByModel.get(model.name);
|
|
1947
2206
|
modelParts.push(emitModel({
|
|
1948
2207
|
model,
|
|
1949
2208
|
view,
|
|
1950
2209
|
registry,
|
|
1951
2210
|
...extensionLines === undefined ? {} : { extensionLines },
|
|
2211
|
+
...readOnlyExtensionLines === undefined ? {} : { readOnlyExtensionLines },
|
|
1952
2212
|
...modelComputed === undefined ? {} : { computed: modelComputed }
|
|
1953
2213
|
}));
|
|
1954
2214
|
}
|
|
@@ -1965,6 +2225,7 @@ export type { Skip } from "@vibeorm/runtime";
|
|
|
1965
2225
|
}
|
|
1966
2226
|
parts.push(...emitViewSection({ view }));
|
|
1967
2227
|
parts.push(...emitScopedSection({ view }));
|
|
2228
|
+
parts.push(...emitReadOnlySection({ view, baked }));
|
|
1968
2229
|
parts.push(...emitRlsSection({ view }));
|
|
1969
2230
|
parts.push(emitClient({ view, baked, hasComputed }));
|
|
1970
2231
|
return `${parts.join(`
|
|
@@ -2151,4 +2412,4 @@ export {
|
|
|
2151
2412
|
bakeExtensions
|
|
2152
2413
|
};
|
|
2153
2414
|
|
|
2154
|
-
//# debugId=
|
|
2415
|
+
//# debugId=43DBEE51B9E7189D64756E2164756E21
|