@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/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 +337 -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 [
|
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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
|
-
` + `
|
|
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
|
-
` +
|
|
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 }) =>
|
|
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}) =>
|
|
1901
|
-
` + ` /** Deliberate, greppable full-access escape from policy scoping. */
|
|
1902
|
-
` + ` readonly $bypassPolicy: () =>
|
|
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 {
|
|
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=
|
|
2420
|
+
//# debugId=F1C487D9764B944164756E2164756E21
|