@vibeorm/generator 2.0.0-alpha.1 → 2.0.0-alpha.10
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 +10 -0
- package/dist/emit-dts.d.ts.map +1 -1
- package/dist/emit-js.d.ts +21 -3
- package/dist/emit-js.d.ts.map +1 -1
- package/dist/generate.d.ts +5 -2
- package/dist/generate.d.ts.map +1 -1
- package/dist/index.js +971 -282
- package/dist/index.js.map +8 -8
- package/dist/schema-view.d.ts +28 -2
- package/dist/schema-view.d.ts.map +1 -1
- package/dist/type-names.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
// packages/generator/src/generate.ts
|
|
3
3
|
import { mkdir, writeFile } from "fs/promises";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { buildComputedManifest, validateComputedConfig } from "@vibeorm/schema";
|
|
5
6
|
|
|
6
7
|
// packages/generator/src/emit-dts.ts
|
|
7
8
|
import { VibeError as VibeError3 } from "@vibeorm/schema";
|
|
8
9
|
|
|
10
|
+
// packages/generator/src/emit-js.ts
|
|
11
|
+
import { internalError } from "@vibeorm/schema";
|
|
12
|
+
|
|
9
13
|
// packages/generator/src/extension-bake.ts
|
|
10
14
|
import {
|
|
11
15
|
VibeError,
|
|
@@ -127,72 +131,6 @@ function bakeExtensions(params) {
|
|
|
127
131
|
};
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
// packages/generator/src/type-names.ts
|
|
131
|
-
var PAYLOAD_TYPES = {
|
|
132
|
-
String: "string",
|
|
133
|
-
Boolean: "boolean",
|
|
134
|
-
Int: "number",
|
|
135
|
-
BigInt: "bigint",
|
|
136
|
-
Float: "number",
|
|
137
|
-
Decimal: "string",
|
|
138
|
-
DateTime: "Date",
|
|
139
|
-
Json: "JsonValue",
|
|
140
|
-
Bytes: "Uint8Array"
|
|
141
|
-
};
|
|
142
|
-
var INPUT_TYPES = {
|
|
143
|
-
String: "string",
|
|
144
|
-
Boolean: "boolean",
|
|
145
|
-
Int: "number",
|
|
146
|
-
BigInt: "bigint | number",
|
|
147
|
-
Float: "number",
|
|
148
|
-
Decimal: "string | number",
|
|
149
|
-
DateTime: "Date | string",
|
|
150
|
-
Json: "InputJsonValue",
|
|
151
|
-
Bytes: "Uint8Array"
|
|
152
|
-
};
|
|
153
|
-
var FILTER_PREFIX = {
|
|
154
|
-
String: "String",
|
|
155
|
-
Boolean: "Bool",
|
|
156
|
-
Int: "Int",
|
|
157
|
-
BigInt: "BigInt",
|
|
158
|
-
Float: "Float",
|
|
159
|
-
Decimal: "Decimal",
|
|
160
|
-
DateTime: "DateTime",
|
|
161
|
-
Json: "Json",
|
|
162
|
-
Bytes: "Bytes"
|
|
163
|
-
};
|
|
164
|
-
var NUMERIC_SCALARS = new Set(["Int", "BigInt", "Float", "Decimal"]);
|
|
165
|
-
var MINMAX_SCALARS = new Set([
|
|
166
|
-
"String",
|
|
167
|
-
"Int",
|
|
168
|
-
"BigInt",
|
|
169
|
-
"Float",
|
|
170
|
-
"Decimal",
|
|
171
|
-
"DateTime"
|
|
172
|
-
]);
|
|
173
|
-
function payloadBaseType(params) {
|
|
174
|
-
const { field } = params;
|
|
175
|
-
return field.enumName ?? PAYLOAD_TYPES[field.scalar];
|
|
176
|
-
}
|
|
177
|
-
function inputBaseType(params) {
|
|
178
|
-
const { field } = params;
|
|
179
|
-
return field.enumName ?? INPUT_TYPES[field.scalar];
|
|
180
|
-
}
|
|
181
|
-
function filterName(params) {
|
|
182
|
-
const { field } = params;
|
|
183
|
-
const prefix = field.enumName ?? FILTER_PREFIX[field.scalar];
|
|
184
|
-
if (field.isList)
|
|
185
|
-
return `${prefix}ListFilter`;
|
|
186
|
-
return field.isOptional ? `${prefix}NullableFilter` : `${prefix}Filter`;
|
|
187
|
-
}
|
|
188
|
-
function updateOpsName(params) {
|
|
189
|
-
const { field } = params;
|
|
190
|
-
if (field.isList || field.scalar === null || !NUMERIC_SCALARS.has(field.scalar))
|
|
191
|
-
return null;
|
|
192
|
-
const prefix = FILTER_PREFIX[field.scalar];
|
|
193
|
-
return field.isOptional ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
134
|
// packages/generator/src/util.ts
|
|
197
135
|
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
198
136
|
function pascal(params) {
|
|
@@ -258,6 +196,240 @@ ${indent2}}`;
|
|
|
258
196
|
});
|
|
259
197
|
}
|
|
260
198
|
|
|
199
|
+
// packages/generator/src/emit-js.ts
|
|
200
|
+
var HEADER = `// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
|
|
201
|
+
// Regenerate with \`vibeorm generate\` after every schema change.`;
|
|
202
|
+
function enumEntryExports(params) {
|
|
203
|
+
return [...params.view.enums.map((declared) => declared.name), "enumValidators"];
|
|
204
|
+
}
|
|
205
|
+
function emitEnumsJs(params) {
|
|
206
|
+
const { view } = params;
|
|
207
|
+
const parts = [
|
|
208
|
+
`${HEADER}
|
|
209
|
+
// Lightweight entry: enum constants + boundary parsers. No runtime client, no schema module.`
|
|
210
|
+
];
|
|
211
|
+
if (view.enums.length === 0) {
|
|
212
|
+
parts.push(`/** Boundary parsers grouped by enum name \u2014 this schema declares no enums. */
|
|
213
|
+
` + `export const enumValidators = Object.freeze({});`);
|
|
214
|
+
return `${parts.join(`
|
|
215
|
+
|
|
216
|
+
`)}
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
parts.push(`import { VibeError } from "@vibeorm/schema";`);
|
|
220
|
+
for (const declared of view.enums) {
|
|
221
|
+
const entries = declared.values.map((value) => ` ${value === "__proto__" ? `[${tsString({ value })}]` : tsString({ value })}: ${tsString({ value })},`).join(`
|
|
222
|
+
`);
|
|
223
|
+
parts.push(`export const ${declared.name} = Object.freeze({
|
|
224
|
+
${entries}
|
|
225
|
+
});`);
|
|
226
|
+
}
|
|
227
|
+
parts.push(`/** Membership validator for one enum: \`values\`, \`is\`, \`parse\` (VIBE_VALIDATION on a non-member). */
|
|
228
|
+
` + `function enumValidator(params) {
|
|
229
|
+
` + ` const { name, values } = params;
|
|
230
|
+
` + ` const members = new Set(values);
|
|
231
|
+
` + ` const is = (value) => typeof value === "string" && members.has(value);
|
|
232
|
+
` + ` return Object.freeze({
|
|
233
|
+
` + ` values: Object.freeze([...values]),
|
|
234
|
+
` + ` is,
|
|
235
|
+
` + ` parse(input) {
|
|
236
|
+
` + ` const { value } = input;
|
|
237
|
+
` + ` if (is(value)) return value;
|
|
238
|
+
` + ` const received = typeof value === "string" ? value.slice(0, 80) : value === null ? "null" : typeof value;
|
|
239
|
+
` + ` throw new VibeError({
|
|
240
|
+
` + ` code: "VIBE_VALIDATION",
|
|
241
|
+
` + ` message: \`Invalid \${name} value \${JSON.stringify(received)} \u2014 expected one of \${values.join(", ")}\`,
|
|
242
|
+
` + ` meta: { enum: name, received },
|
|
243
|
+
` + ` });
|
|
244
|
+
` + ` },
|
|
245
|
+
` + ` });
|
|
246
|
+
` + `}`);
|
|
247
|
+
const validatorLines = view.enums.map((declared) => ` ${declared.name === "__proto__" ? `[${tsString({ value: declared.name })}]` : declared.name}: enumValidator({ name: ${tsString({ value: declared.name })}, values: [${declared.values.map((value) => tsString({ value })).join(", ")}] }),`).join(`
|
|
248
|
+
`);
|
|
249
|
+
parts.push(`/** Boundary parsers grouped by enum name, so an enum member named \`parse\` cannot collide. */
|
|
250
|
+
` + `export const enumValidators = Object.freeze({
|
|
251
|
+
${validatorLines}
|
|
252
|
+
});`);
|
|
253
|
+
return `${parts.join(`
|
|
254
|
+
|
|
255
|
+
`)}
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
function serializeSchema(params) {
|
|
259
|
+
const { schema } = params;
|
|
260
|
+
if (schema.rls === undefined)
|
|
261
|
+
return stableStringify({ value: schema });
|
|
262
|
+
return renderIr({ value: schema, indent: "", ordered: false });
|
|
263
|
+
}
|
|
264
|
+
function renderIr(params) {
|
|
265
|
+
const { value, indent: indent2, ordered } = params;
|
|
266
|
+
if (value === null)
|
|
267
|
+
return "null";
|
|
268
|
+
const kind = typeof value;
|
|
269
|
+
if (kind === "string" || kind === "number" || kind === "boolean")
|
|
270
|
+
return JSON.stringify(value);
|
|
271
|
+
if (Array.isArray(value)) {
|
|
272
|
+
if (value.length === 0)
|
|
273
|
+
return "[]";
|
|
274
|
+
const inner2 = `${indent2} `;
|
|
275
|
+
const items2 = value.map((item) => inner2 + renderIr({ value: item, indent: inner2, ordered }));
|
|
276
|
+
return `[
|
|
277
|
+
${items2.join(`,
|
|
278
|
+
`)}
|
|
279
|
+
${indent2}]`;
|
|
280
|
+
}
|
|
281
|
+
if (kind !== "object") {
|
|
282
|
+
throw internalError({ message: `internal: cannot serialize a ${kind} value into the generated client`, meta: { kind } });
|
|
283
|
+
}
|
|
284
|
+
const record = value;
|
|
285
|
+
const defined = Object.keys(record).filter((key) => record[key] !== undefined);
|
|
286
|
+
const keys = ordered ? defined : defined.sort();
|
|
287
|
+
if (keys.length === 0)
|
|
288
|
+
return "{}";
|
|
289
|
+
const inner = `${indent2} `;
|
|
290
|
+
const items = keys.map((key) => `${inner}${JSON.stringify(key)}: ${renderIr({ value: record[key], indent: inner, ordered: ordered || key === "rls" })}`);
|
|
291
|
+
return `{
|
|
292
|
+
${items.join(`,
|
|
293
|
+
`)}
|
|
294
|
+
${indent2}}`;
|
|
295
|
+
}
|
|
296
|
+
function emitJs(params) {
|
|
297
|
+
const { view } = params;
|
|
298
|
+
const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
|
|
299
|
+
const computed = params.computed ?? [];
|
|
300
|
+
const hasComputed = computed.length > 0;
|
|
301
|
+
const parts = [];
|
|
302
|
+
parts.push(HEADER);
|
|
303
|
+
parts.push(`import { createClient } from "@vibeorm/runtime";`);
|
|
304
|
+
if (baked.jsImports.length > 0)
|
|
305
|
+
parts.push(baked.jsImports.join(`
|
|
306
|
+
`));
|
|
307
|
+
parts.push(`export { VibeError } from "@vibeorm/schema";`);
|
|
308
|
+
parts.push(`export { SKIP } from "@vibeorm/runtime";`);
|
|
309
|
+
parts.push(`export { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`);
|
|
310
|
+
parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
|
|
311
|
+
export const schema = ${serializeSchema({ schema: view.schema })};`);
|
|
312
|
+
if (baked.empty && !hasComputed) {
|
|
313
|
+
parts.push(`/** Build a typed client over a database adapter. */
|
|
314
|
+
` + `export function VibeClient(options) {
|
|
315
|
+
` + ` const { adapter, ...clientOptions } = options;
|
|
316
|
+
` + ` return createClient({ schema, adapter, options: clientOptions });
|
|
317
|
+
` + `}`);
|
|
318
|
+
return `${parts.join(`
|
|
319
|
+
|
|
320
|
+
`)}
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
if (!baked.empty) {
|
|
324
|
+
parts.push(`/** Extension manifest \u2014 verified against the live extension list (VIBE_STALE_CLIENT). */
|
|
325
|
+
` + `const extensionManifest = ${stableStringify({ value: baked.manifest })};`);
|
|
326
|
+
parts.push(`/** Baked extension surfaces (extensions.md D9). */
|
|
327
|
+
` + `const extensionMounts = [
|
|
328
|
+
${baked.mounts.map((mount) => ` ${mount},`).join(`
|
|
329
|
+
`)}
|
|
330
|
+
];`);
|
|
331
|
+
}
|
|
332
|
+
if (hasComputed) {
|
|
333
|
+
parts.push(`/** Computed-field manifest \u2014 verified against the live computed config (VIBE_STALE_CLIENT). */
|
|
334
|
+
` + `const computedManifest = ${stableStringify({ value: computed })};`);
|
|
335
|
+
}
|
|
336
|
+
const destructured = [
|
|
337
|
+
"adapter",
|
|
338
|
+
...baked.empty ? [] : ["extensions"],
|
|
339
|
+
...hasComputed ? ["computed"] : [],
|
|
340
|
+
"...clientOptions"
|
|
341
|
+
].join(", ");
|
|
342
|
+
const createLines = [
|
|
343
|
+
` schema,`,
|
|
344
|
+
` adapter,`,
|
|
345
|
+
` options: clientOptions,`,
|
|
346
|
+
...baked.empty ? [] : [` extensions: { manifest: extensionManifest, mounts: extensionMounts, instances: extensions ?? [] },`],
|
|
347
|
+
...hasComputed ? [` computed: { manifest: computedManifest, specs: computed },`] : []
|
|
348
|
+
];
|
|
349
|
+
parts.push(`/** Build a typed client over a database adapter. */
|
|
350
|
+
` + `export function VibeClient(options) {
|
|
351
|
+
` + ` const { ${destructured} } = options;
|
|
352
|
+
` + ` return createClient({
|
|
353
|
+
${createLines.join(`
|
|
354
|
+
`)}
|
|
355
|
+
});
|
|
356
|
+
` + `}`);
|
|
357
|
+
return `${parts.join(`
|
|
358
|
+
|
|
359
|
+
`)}
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// packages/generator/src/type-names.ts
|
|
364
|
+
var PAYLOAD_TYPES = {
|
|
365
|
+
String: "string",
|
|
366
|
+
Boolean: "boolean",
|
|
367
|
+
Int: "number",
|
|
368
|
+
BigInt: "bigint",
|
|
369
|
+
Float: "number",
|
|
370
|
+
Decimal: "string",
|
|
371
|
+
DateTime: "Date",
|
|
372
|
+
Json: "JsonValue",
|
|
373
|
+
Bytes: "Uint8Array"
|
|
374
|
+
};
|
|
375
|
+
var INPUT_TYPES = {
|
|
376
|
+
String: "string",
|
|
377
|
+
Boolean: "boolean",
|
|
378
|
+
Int: "number",
|
|
379
|
+
BigInt: "bigint | number",
|
|
380
|
+
Float: "number",
|
|
381
|
+
Decimal: "string | number",
|
|
382
|
+
DateTime: "Date | string | DbNow",
|
|
383
|
+
Json: "InputJsonValue",
|
|
384
|
+
Bytes: "Uint8Array"
|
|
385
|
+
};
|
|
386
|
+
var FILTER_PREFIX = {
|
|
387
|
+
String: "String",
|
|
388
|
+
Boolean: "Bool",
|
|
389
|
+
Int: "Int",
|
|
390
|
+
BigInt: "BigInt",
|
|
391
|
+
Float: "Float",
|
|
392
|
+
Decimal: "Decimal",
|
|
393
|
+
DateTime: "DateTime",
|
|
394
|
+
Json: "Json",
|
|
395
|
+
Bytes: "Bytes"
|
|
396
|
+
};
|
|
397
|
+
var NUMERIC_SCALARS = new Set(["Int", "BigInt", "Float", "Decimal"]);
|
|
398
|
+
var MINMAX_SCALARS = new Set([
|
|
399
|
+
"String",
|
|
400
|
+
"Int",
|
|
401
|
+
"BigInt",
|
|
402
|
+
"Float",
|
|
403
|
+
"Decimal",
|
|
404
|
+
"DateTime"
|
|
405
|
+
]);
|
|
406
|
+
function payloadBaseType(params) {
|
|
407
|
+
const { field } = params;
|
|
408
|
+
if (field.jsonType !== undefined)
|
|
409
|
+
return field.jsonType.name;
|
|
410
|
+
return field.enumName ?? PAYLOAD_TYPES[field.scalar];
|
|
411
|
+
}
|
|
412
|
+
function inputBaseType(params) {
|
|
413
|
+
const { field } = params;
|
|
414
|
+
if (field.jsonType !== undefined)
|
|
415
|
+
return field.jsonType.name;
|
|
416
|
+
return field.enumName ?? INPUT_TYPES[field.scalar];
|
|
417
|
+
}
|
|
418
|
+
function filterName(params) {
|
|
419
|
+
const { field } = params;
|
|
420
|
+
const prefix = field.enumName ?? FILTER_PREFIX[field.scalar];
|
|
421
|
+
if (field.isList)
|
|
422
|
+
return `${prefix}ListFilter`;
|
|
423
|
+
return field.isOptional ? `${prefix}NullableFilter` : `${prefix}Filter`;
|
|
424
|
+
}
|
|
425
|
+
function updateOpsName(params) {
|
|
426
|
+
const { field } = params;
|
|
427
|
+
if (field.isList || field.scalar === null || !NUMERIC_SCALARS.has(field.scalar))
|
|
428
|
+
return null;
|
|
429
|
+
const prefix = FILTER_PREFIX[field.scalar];
|
|
430
|
+
return field.isOptional ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
|
|
431
|
+
}
|
|
432
|
+
|
|
261
433
|
// packages/generator/src/emit-dts.ts
|
|
262
434
|
function banner(params) {
|
|
263
435
|
return `// \u2500\u2500\u2500 ${params.title} \u2500\u2500\u2500`;
|
|
@@ -271,6 +443,23 @@ ${lines.join(`
|
|
|
271
443
|
`)}
|
|
272
444
|
};`;
|
|
273
445
|
}
|
|
446
|
+
var OPTIONAL_MEMBER = /^(\s*(?:readonly\s+)?(?:[A-Za-z_$][\w$]*|"(?:[^"\\]|\\.)*")\?:\s*)(.+);$/;
|
|
447
|
+
function skippableMembers(params) {
|
|
448
|
+
return params.block.split(`
|
|
449
|
+
`).map((line) => {
|
|
450
|
+
const match = OPTIONAL_MEMBER.exec(line);
|
|
451
|
+
const head = match?.[1];
|
|
452
|
+
const type = match?.[2];
|
|
453
|
+
if (head === undefined || type === undefined)
|
|
454
|
+
return line;
|
|
455
|
+
return `${head}${type.includes("=>") ? `(${type})` : type} | Skip;`;
|
|
456
|
+
}).join(`
|
|
457
|
+
`);
|
|
458
|
+
}
|
|
459
|
+
function inputAlias(params) {
|
|
460
|
+
const { name, lines } = params;
|
|
461
|
+
return objectAlias({ name, lines: lines.map((block) => skippableMembers({ block })) });
|
|
462
|
+
}
|
|
274
463
|
function orMany(params) {
|
|
275
464
|
const { type } = params;
|
|
276
465
|
return `${type} | readonly ${maybeParens({ type })}[]`;
|
|
@@ -316,18 +505,73 @@ export type SortOrderWithNulls = SortOrder | { readonly sort: SortOrder; readonl
|
|
|
316
505
|
/** How relations are loaded: batched WHERE-IN queries, or a single JOIN (pg/pglite). */
|
|
317
506
|
export type RelationStrategy = "query" | "join";
|
|
318
507
|
|
|
508
|
+
/**
|
|
509
|
+
* An opaque, versioned keyset position (\`after\`) \u2014 ordering values plus a
|
|
510
|
+
* unique tie-breaker, minted by a previous page. It is ENCODED, not encrypted:
|
|
511
|
+
* whoever holds it can read the ordering values inside. It is also NOT an
|
|
512
|
+
* authorization credential \u2014 it selects a position, never a row, and every
|
|
513
|
+
* query still applies its own where/scoped/policy/RLS filters.
|
|
514
|
+
*/
|
|
515
|
+
export type KeysetToken = string;
|
|
516
|
+
|
|
517
|
+
/** Per-query keyset options. */
|
|
518
|
+
export type KeysetOptions = {
|
|
519
|
+
/**
|
|
520
|
+
* Asserts that every DateTime ordering column in this query holds
|
|
521
|
+
* millisecond-exact values. Required on postgres and mysql, whose timestamps
|
|
522
|
+
* may be finer than a JavaScript Date; NOT safe for \`@default(now())\`
|
|
523
|
+
* columns on postgres, which are microsecond-precise.
|
|
524
|
+
*/
|
|
525
|
+
readonly timestampPrecision?: "milliseconds";
|
|
526
|
+
};
|
|
527
|
+
|
|
319
528
|
/** Result of createMany/updateMany/deleteMany. */
|
|
320
529
|
export type BatchPayload = { readonly count: number };
|
|
321
530
|
|
|
531
|
+
/**
|
|
532
|
+
* What upsertMany reports. \`count\` is the rows submitted after duplicate-key
|
|
533
|
+
* validation \u2014 every one of them was inserted or updated, since the statement
|
|
534
|
+
* has no skip arm and the whole operation is atomic. \`affectedRows\` is the
|
|
535
|
+
* driver's own number, and only where that number IS a row count: it is null on
|
|
536
|
+
* mysql, whose ON DUPLICATE KEY scores an insert 1, a changed update 2 and an
|
|
537
|
+
* unchanged update 0.
|
|
538
|
+
*/
|
|
539
|
+
export type UpsertManyPayload = { readonly count: number; readonly affectedRows: number | null };
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* An argument member as the RESULT machinery must read it. Absent and the
|
|
543
|
+
* \`SKIP\` sentinel both mean "not provided", so neither may narrow a payload:
|
|
544
|
+
* without this, \`{ omit?: UserOmitInput | Skip }\` would collapse the key
|
|
545
|
+
* helpers below to \`never\` and promise fields the query may still have dropped.
|
|
546
|
+
*/
|
|
547
|
+
type Provided<T> = Exclude<T, Skip | undefined>;
|
|
548
|
+
|
|
322
549
|
/** Keys of an omit object whose value is exactly true \u2014 the scalars the result drops. */
|
|
323
|
-
type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 }
|
|
550
|
+
type OmittedKeys<O> = keyof { [K in keyof O as O[K] extends true ? K : never]: 0 };
|
|
551
|
+
|
|
552
|
+
/** Keys a selection has not RULED OUT \u2014 everything except a literal false/undefined. */
|
|
553
|
+
type PossibleKeys<S> = keyof { [K in keyof S as S[K] extends false | undefined ? never : K]: 0 };
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Keys whose flag can STILL be false or missing when the query runs \u2014 a widened
|
|
557
|
+
* \`boolean\`, or an optional nested selection. They are emitted OPTIONAL: the
|
|
558
|
+
* result type must not promise a field a run-time flag can switch off.
|
|
559
|
+
*/
|
|
560
|
+
type UncertainKeys<S> = PossibleKeys<S> &
|
|
561
|
+
keyof { [K in keyof S as [false] extends [S[K]] ? K : [undefined] extends [S[K]] ? K : never]: 0 };
|
|
562
|
+
|
|
563
|
+
/** Keys the selection definitely turns on: a literal \`true\`, or a nested selection object. */
|
|
564
|
+
type ExactKeys<S> = Exclude<PossibleKeys<S>, UncertainKeys<S>>;
|
|
565
|
+
|
|
566
|
+
/** An aggregate object is absent when none of its selectors is enabled. */
|
|
567
|
+
type HasSelectedField<S> = [S] extends [true] ? true : [S] extends [object] ? ExactKeys<S> extends never ? false : true : false;`;
|
|
324
568
|
var SCALAR_FILTER_CONFIGS = [
|
|
325
569
|
{ prefix: "String", value: "string", comparable: true, text: true },
|
|
326
570
|
{ prefix: "Int", value: "number", comparable: true, text: false },
|
|
327
571
|
{ prefix: "Float", value: "number", comparable: true, text: false },
|
|
328
572
|
{ prefix: "BigInt", value: "bigint | number", comparable: true, text: false },
|
|
329
573
|
{ prefix: "Decimal", value: "string | number", comparable: true, text: false },
|
|
330
|
-
{ prefix: "DateTime", value: "Date | string", comparable: true, text: false },
|
|
574
|
+
{ prefix: "DateTime", value: "Date | string", comparable: true, text: false, clock: true },
|
|
331
575
|
{ prefix: "Bool", value: "boolean", comparable: false, text: false },
|
|
332
576
|
{ prefix: "Bytes", value: "Uint8Array", comparable: false, text: false }
|
|
333
577
|
];
|
|
@@ -335,17 +579,18 @@ function scalarFilterAlias(params) {
|
|
|
335
579
|
const { config, nullable } = params;
|
|
336
580
|
const name = nullable ? `${config.prefix}NullableFilter` : `${config.prefix}Filter`;
|
|
337
581
|
const value = config.value;
|
|
338
|
-
const
|
|
582
|
+
const point = config.clock === true ? `${value} | DbNow` : value;
|
|
583
|
+
const valueOrNull = nullable ? `${point} | null` : point;
|
|
339
584
|
const array = `readonly ${maybeParens({ type: value })}[]`;
|
|
340
585
|
const lines = [];
|
|
341
586
|
lines.push(` readonly equals?: ${valueOrNull};`);
|
|
342
587
|
lines.push(` readonly in?: ${array};`);
|
|
343
588
|
lines.push(` readonly notIn?: ${array};`);
|
|
344
589
|
if (config.comparable) {
|
|
345
|
-
lines.push(` readonly lt?: ${
|
|
346
|
-
lines.push(` readonly lte?: ${
|
|
347
|
-
lines.push(` readonly gt?: ${
|
|
348
|
-
lines.push(` readonly gte?: ${
|
|
590
|
+
lines.push(` readonly lt?: ${point};`);
|
|
591
|
+
lines.push(` readonly lte?: ${point};`);
|
|
592
|
+
lines.push(` readonly gt?: ${point};`);
|
|
593
|
+
lines.push(` readonly gte?: ${point};`);
|
|
349
594
|
}
|
|
350
595
|
if (config.text) {
|
|
351
596
|
lines.push(` readonly contains?: ${value};`);
|
|
@@ -354,13 +599,13 @@ function scalarFilterAlias(params) {
|
|
|
354
599
|
lines.push(` readonly mode?: QueryMode;`);
|
|
355
600
|
}
|
|
356
601
|
lines.push(` readonly not?: ${value} | ${name}${nullable ? " | null" : ""};`);
|
|
357
|
-
return
|
|
602
|
+
return inputAlias({ name, lines });
|
|
358
603
|
}
|
|
359
604
|
function scalarListFilterAlias(params) {
|
|
360
605
|
const { config } = params;
|
|
361
606
|
const value = config.value;
|
|
362
607
|
const array = `readonly ${maybeParens({ type: value })}[]`;
|
|
363
|
-
return
|
|
608
|
+
return inputAlias({
|
|
364
609
|
name: `${config.prefix}ListFilter`,
|
|
365
610
|
lines: [
|
|
366
611
|
` readonly equals?: ${array} | null;`,
|
|
@@ -380,7 +625,7 @@ var UPDATE_OPS_CONFIGS = [
|
|
|
380
625
|
function updateOpsAlias(params) {
|
|
381
626
|
const { prefix, value, nullable } = params;
|
|
382
627
|
const name = nullable ? `Nullable${prefix}FieldUpdateOperationsInput` : `${prefix}FieldUpdateOperationsInput`;
|
|
383
|
-
return
|
|
628
|
+
return inputAlias({
|
|
384
629
|
name,
|
|
385
630
|
lines: [
|
|
386
631
|
` readonly set?: ${nullable ? `${value} | null` : value};`,
|
|
@@ -391,6 +636,25 @@ function updateOpsAlias(params) {
|
|
|
391
636
|
]
|
|
392
637
|
});
|
|
393
638
|
}
|
|
639
|
+
function jsonFilterAlias(params) {
|
|
640
|
+
const { nullable } = params;
|
|
641
|
+
const document = nullable ? "InputJsonValue | null" : "InputJsonValue";
|
|
642
|
+
return inputAlias({
|
|
643
|
+
name: nullable ? "JsonNullableFilter" : "JsonFilter",
|
|
644
|
+
lines: [
|
|
645
|
+
` readonly path?: readonly (string | number)[];`,
|
|
646
|
+
` readonly mode?: "default" | "insensitive";`,
|
|
647
|
+
` readonly equals?: ${document};`,
|
|
648
|
+
` readonly not?: ${document};`,
|
|
649
|
+
` readonly string_contains?: string;`,
|
|
650
|
+
` readonly string_starts_with?: string;`,
|
|
651
|
+
` readonly string_ends_with?: string;`,
|
|
652
|
+
` readonly array_contains?: InputJsonValue;`,
|
|
653
|
+
` readonly array_starts_with?: InputJsonValue;`,
|
|
654
|
+
` readonly array_ends_with?: InputJsonValue;`
|
|
655
|
+
]
|
|
656
|
+
});
|
|
657
|
+
}
|
|
394
658
|
function filterSection() {
|
|
395
659
|
const parts = [banner({ title: "Scalar filters" })];
|
|
396
660
|
for (const config of SCALAR_FILTER_CONFIGS) {
|
|
@@ -398,15 +662,9 @@ function filterSection() {
|
|
|
398
662
|
parts.push(scalarFilterAlias({ config, nullable: true }));
|
|
399
663
|
parts.push(scalarListFilterAlias({ config }));
|
|
400
664
|
}
|
|
401
|
-
parts.push(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}));
|
|
405
|
-
parts.push(objectAlias({
|
|
406
|
-
name: "JsonNullableFilter",
|
|
407
|
-
lines: [` readonly equals?: InputJsonValue | null;`, ` readonly not?: InputJsonValue | null;`]
|
|
408
|
-
}));
|
|
409
|
-
parts.push(objectAlias({
|
|
665
|
+
parts.push(jsonFilterAlias({ nullable: false }));
|
|
666
|
+
parts.push(jsonFilterAlias({ nullable: true }));
|
|
667
|
+
parts.push(inputAlias({
|
|
410
668
|
name: "JsonListFilter",
|
|
411
669
|
lines: [
|
|
412
670
|
` readonly equals?: readonly InputJsonValue[] | null;`,
|
|
@@ -423,18 +681,55 @@ function filterSection() {
|
|
|
423
681
|
}
|
|
424
682
|
return parts;
|
|
425
683
|
}
|
|
426
|
-
|
|
684
|
+
var GENERATED_HEADER = `// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
|
|
685
|
+
// Regenerate with \`vibeorm generate\` after every schema change.`;
|
|
686
|
+
function emitEnumDeclaration(params) {
|
|
427
687
|
const { declared } = params;
|
|
428
688
|
const name = declared.name;
|
|
429
|
-
const parts = [banner({ title: `Enum ${name}` })];
|
|
430
689
|
const valueLines = declared.values.map((value) => ` readonly ${propKey({ name: value })}: ${tsString({ value })};`);
|
|
431
|
-
|
|
690
|
+
return [
|
|
691
|
+
banner({ title: `Enum ${name}` }),
|
|
692
|
+
`export declare const ${name}: {
|
|
432
693
|
${valueLines.join(`
|
|
433
694
|
`)}
|
|
434
|
-
}
|
|
435
|
-
|
|
695
|
+
};`,
|
|
696
|
+
`export type ${name} = ${declared.values.map((value) => tsString({ value })).join(" | ")};`,
|
|
697
|
+
`/** Boundary validator for ${name}: \`values\`, \`is\`, \`parse\` (throws VIBE_VALIDATION on a non-member). */
|
|
698
|
+
` + `export type ${name}Validator = {
|
|
699
|
+
` + ` readonly values: readonly ${name}[];
|
|
700
|
+
` + ` is(value: unknown): value is ${name};
|
|
701
|
+
` + ` parse(params: { readonly value: unknown }): ${name};
|
|
702
|
+
` + `};`
|
|
703
|
+
].join(`
|
|
704
|
+
|
|
705
|
+
`);
|
|
706
|
+
}
|
|
707
|
+
function emitEnumsDts(params) {
|
|
708
|
+
const { view } = params;
|
|
709
|
+
const parts = [
|
|
710
|
+
`${GENERATED_HEADER}
|
|
711
|
+
// Lightweight entry: enum constants + boundary parsers. No runtime client, no schema module.`
|
|
712
|
+
];
|
|
713
|
+
for (const declared of view.enums)
|
|
714
|
+
parts.push(emitEnumDeclaration({ declared }));
|
|
715
|
+
parts.push(banner({ title: "Enum validators" }));
|
|
716
|
+
const validatorLines = view.enums.map((declared) => ` readonly ${declared.name}: ${declared.name}Validator;`);
|
|
717
|
+
parts.push(`/** Boundary parsers grouped by enum name, so an enum member named \`parse\` cannot collide. */
|
|
718
|
+
` + (validatorLines.length === 0 ? `export declare const enumValidators: Record<string, never>;` : `export declare const enumValidators: {
|
|
719
|
+
${validatorLines.join(`
|
|
720
|
+
`)}
|
|
721
|
+
};`));
|
|
722
|
+
return `${parts.join(`
|
|
723
|
+
|
|
724
|
+
`)}
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
function emitEnum(params) {
|
|
728
|
+
const { declared } = params;
|
|
729
|
+
const name = declared.name;
|
|
730
|
+
const parts = [banner({ title: `Enum ${name} filters` })];
|
|
436
731
|
const array = `readonly ${name}[]`;
|
|
437
|
-
parts.push(
|
|
732
|
+
parts.push(inputAlias({
|
|
438
733
|
name: `${name}Filter`,
|
|
439
734
|
lines: [
|
|
440
735
|
` readonly equals?: ${name};`,
|
|
@@ -443,7 +738,7 @@ ${valueLines.join(`
|
|
|
443
738
|
` readonly not?: ${name} | ${name}Filter;`
|
|
444
739
|
]
|
|
445
740
|
}));
|
|
446
|
-
parts.push(
|
|
741
|
+
parts.push(inputAlias({
|
|
447
742
|
name: `${name}NullableFilter`,
|
|
448
743
|
lines: [
|
|
449
744
|
` readonly equals?: ${name} | null;`,
|
|
@@ -452,7 +747,7 @@ ${valueLines.join(`
|
|
|
452
747
|
` readonly not?: ${name} | ${name}NullableFilter | null;`
|
|
453
748
|
]
|
|
454
749
|
}));
|
|
455
|
-
parts.push(
|
|
750
|
+
parts.push(inputAlias({
|
|
456
751
|
name: `${name}ListFilter`,
|
|
457
752
|
lines: [
|
|
458
753
|
` readonly equals?: ${array} | null;`,
|
|
@@ -569,7 +864,7 @@ function registerNestedContext(params) {
|
|
|
569
864
|
register({
|
|
570
865
|
registry,
|
|
571
866
|
name: `${t}Create${suffix}Input`,
|
|
572
|
-
text:
|
|
867
|
+
text: inputAlias({
|
|
573
868
|
name: `${t}Create${suffix}Input`,
|
|
574
869
|
lines: checkedCreateLines({ model: target, omitRelation: inverse })
|
|
575
870
|
})
|
|
@@ -577,7 +872,7 @@ function registerNestedContext(params) {
|
|
|
577
872
|
register({
|
|
578
873
|
registry,
|
|
579
874
|
name: `${t}UncheckedCreate${suffix}Input`,
|
|
580
|
-
text:
|
|
875
|
+
text: inputAlias({
|
|
581
876
|
name: `${t}UncheckedCreate${suffix}Input`,
|
|
582
877
|
lines: uncheckedCreateLines({ model: target, omitRelation: inverse })
|
|
583
878
|
})
|
|
@@ -585,7 +880,7 @@ function registerNestedContext(params) {
|
|
|
585
880
|
register({
|
|
586
881
|
registry,
|
|
587
882
|
name: `${t}Update${suffix}Input`,
|
|
588
|
-
text:
|
|
883
|
+
text: inputAlias({
|
|
589
884
|
name: `${t}Update${suffix}Input`,
|
|
590
885
|
lines: checkedUpdateLines({ model: target, omitRelation: inverse })
|
|
591
886
|
})
|
|
@@ -593,7 +888,7 @@ function registerNestedContext(params) {
|
|
|
593
888
|
register({
|
|
594
889
|
registry,
|
|
595
890
|
name: `${t}UncheckedUpdate${suffix}Input`,
|
|
596
|
-
text:
|
|
891
|
+
text: inputAlias({
|
|
597
892
|
name: `${t}UncheckedUpdate${suffix}Input`,
|
|
598
893
|
lines: uncheckedUpdateLines({ model: target, omitRelation: inverse })
|
|
599
894
|
})
|
|
@@ -603,7 +898,7 @@ function registerNestedContext(params) {
|
|
|
603
898
|
register({
|
|
604
899
|
registry,
|
|
605
900
|
name: connectOrCreateName,
|
|
606
|
-
text:
|
|
901
|
+
text: inputAlias({
|
|
607
902
|
name: connectOrCreateName,
|
|
608
903
|
lines: [` readonly where: ${t}WhereUniqueInput;`, ` readonly create: ${createUnion};`]
|
|
609
904
|
})
|
|
@@ -613,7 +908,7 @@ function registerNestedContext(params) {
|
|
|
613
908
|
register({
|
|
614
909
|
registry,
|
|
615
910
|
name: `${t}CreateNestedMany${suffix}Input`,
|
|
616
|
-
text:
|
|
911
|
+
text: inputAlias({
|
|
617
912
|
name: `${t}CreateNestedMany${suffix}Input`,
|
|
618
913
|
lines: [
|
|
619
914
|
` readonly create?: ${orMany({ type: createUnion })};`,
|
|
@@ -627,7 +922,7 @@ function registerNestedContext(params) {
|
|
|
627
922
|
register({
|
|
628
923
|
registry,
|
|
629
924
|
name: updateWithWhere,
|
|
630
|
-
text:
|
|
925
|
+
text: inputAlias({
|
|
631
926
|
name: updateWithWhere,
|
|
632
927
|
lines: [` readonly where: ${t}WhereUniqueInput;`, ` readonly data: ${updateUnion};`]
|
|
633
928
|
})
|
|
@@ -636,7 +931,7 @@ function registerNestedContext(params) {
|
|
|
636
931
|
register({
|
|
637
932
|
registry,
|
|
638
933
|
name: updateManyWithWhere,
|
|
639
|
-
text:
|
|
934
|
+
text: inputAlias({
|
|
640
935
|
name: updateManyWithWhere,
|
|
641
936
|
lines: [` readonly where: ${t}WhereInput;`, ` readonly data: ${t}UpdateManyInput;`]
|
|
642
937
|
})
|
|
@@ -645,7 +940,7 @@ function registerNestedContext(params) {
|
|
|
645
940
|
register({
|
|
646
941
|
registry,
|
|
647
942
|
name: upsertWithWhere,
|
|
648
|
-
text:
|
|
943
|
+
text: inputAlias({
|
|
649
944
|
name: upsertWithWhere,
|
|
650
945
|
lines: [
|
|
651
946
|
` readonly where: ${t}WhereUniqueInput;`,
|
|
@@ -657,7 +952,7 @@ function registerNestedContext(params) {
|
|
|
657
952
|
register({
|
|
658
953
|
registry,
|
|
659
954
|
name: `${t}UpdateNestedMany${suffix}Input`,
|
|
660
|
-
text:
|
|
955
|
+
text: inputAlias({
|
|
661
956
|
name: `${t}UpdateNestedMany${suffix}Input`,
|
|
662
957
|
lines: [
|
|
663
958
|
` readonly create?: ${orMany({ type: createUnion })};`,
|
|
@@ -679,7 +974,7 @@ function registerNestedContext(params) {
|
|
|
679
974
|
register({
|
|
680
975
|
registry,
|
|
681
976
|
name: `${t}CreateNestedOne${suffix}Input`,
|
|
682
|
-
text:
|
|
977
|
+
text: inputAlias({
|
|
683
978
|
name: `${t}CreateNestedOne${suffix}Input`,
|
|
684
979
|
lines: [
|
|
685
980
|
` readonly create?: ${createUnion};`,
|
|
@@ -691,7 +986,7 @@ function registerNestedContext(params) {
|
|
|
691
986
|
register({
|
|
692
987
|
registry,
|
|
693
988
|
name: `${t}UpdateNestedOne${suffix}Input`,
|
|
694
|
-
text:
|
|
989
|
+
text: inputAlias({
|
|
695
990
|
name: `${t}UpdateNestedOne${suffix}Input`,
|
|
696
991
|
lines: [
|
|
697
992
|
` readonly create?: ${createUnion};`,
|
|
@@ -705,9 +1000,9 @@ function registerNestedContext(params) {
|
|
|
705
1000
|
});
|
|
706
1001
|
}
|
|
707
1002
|
}
|
|
708
|
-
var AGGREGATE_OPS_UNION = `"_count" | "_avg" | "_sum" | "_min" | "_max"`;
|
|
709
1003
|
function emitModel(params) {
|
|
710
1004
|
const { model, view, registry } = params;
|
|
1005
|
+
const computedEntries = params.computed ?? [];
|
|
711
1006
|
const n = model.name;
|
|
712
1007
|
const hasRelations = model.relations.length > 0;
|
|
713
1008
|
const parts = [banner({ title: `Model ${n}` })];
|
|
@@ -715,10 +1010,17 @@ function emitModel(params) {
|
|
|
715
1010
|
registerNestedContext({ registry, view, relation });
|
|
716
1011
|
}
|
|
717
1012
|
parts.push(`export type ${n} = {
|
|
718
|
-
${
|
|
1013
|
+
${[
|
|
1014
|
+
...model.fields.map((field) => ` ${propKey({ name: field.name })}: ${payloadFieldType({ field })};`),
|
|
1015
|
+
...computedEntries.map((entry) => ` ${propKey({ name: entry.field })}: ${entry.type};`)
|
|
1016
|
+
].join(`
|
|
719
1017
|
`)}
|
|
720
1018
|
};`);
|
|
721
1019
|
parts.push(`export type ${n}ScalarFieldEnum = ${model.fields.map((field) => tsString({ value: field.name })).join(" | ")};`);
|
|
1020
|
+
if (computedEntries.length > 0) {
|
|
1021
|
+
parts.push(`export type ${n}SelectableFieldEnum = ${n}ScalarFieldEnum | ${computedEntries.map((entry) => tsString({ value: entry.field })).join(" | ")};`);
|
|
1022
|
+
}
|
|
1023
|
+
const selectableEnum = computedEntries.length > 0 ? `${n}SelectableFieldEnum` : `${n}ScalarFieldEnum`;
|
|
722
1024
|
if (hasRelations) {
|
|
723
1025
|
parts.push(`export type ${n}Relations = {
|
|
724
1026
|
${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${relationPayloadType({ relation })};`).join(`
|
|
@@ -730,31 +1032,35 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
730
1032
|
const countLine = ` readonly _count?: boolean | { readonly select: ${n}CountSelect };`;
|
|
731
1033
|
const relationSelectValue = (relation) => `boolean | ${relation.target}${relation.isList ? "FindManyArgs" : "Args"}`;
|
|
732
1034
|
if (hasCounts) {
|
|
733
|
-
parts.push(
|
|
1035
|
+
parts.push(inputAlias({
|
|
734
1036
|
name: `${n}CountSelect`,
|
|
735
|
-
lines: listRelations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: boolean;`)
|
|
1037
|
+
lines: listRelations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: boolean | { readonly where?: ${relation.target}WhereInput };`)
|
|
736
1038
|
}));
|
|
737
1039
|
}
|
|
738
|
-
parts.push(
|
|
1040
|
+
parts.push(inputAlias({
|
|
739
1041
|
name: `${n}Select`,
|
|
740
1042
|
lines: [
|
|
741
1043
|
...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: boolean;`),
|
|
1044
|
+
...computedEntries.map((entry) => ` readonly ${propKey({ name: entry.field })}?: boolean;`),
|
|
742
1045
|
...model.relations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: ${relationSelectValue(relation)};`),
|
|
743
1046
|
...hasCounts ? [countLine] : []
|
|
744
1047
|
]
|
|
745
1048
|
}));
|
|
746
|
-
parts.push(
|
|
1049
|
+
parts.push(inputAlias({
|
|
747
1050
|
name: `${n}Include`,
|
|
748
1051
|
lines: [
|
|
749
1052
|
...model.relations.map((relation) => ` readonly ${propKey({ name: relation.name })}?: ${relationSelectValue(relation)};`),
|
|
750
1053
|
...hasCounts ? [countLine] : []
|
|
751
1054
|
]
|
|
752
1055
|
}));
|
|
753
|
-
parts.push(
|
|
1056
|
+
parts.push(inputAlias({
|
|
754
1057
|
name: `${n}OmitInput`,
|
|
755
|
-
lines:
|
|
1058
|
+
lines: [
|
|
1059
|
+
...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: boolean;`),
|
|
1060
|
+
...computedEntries.map((entry) => ` readonly ${propKey({ name: entry.field })}?: boolean;`)
|
|
1061
|
+
]
|
|
756
1062
|
}));
|
|
757
|
-
parts.push(
|
|
1063
|
+
parts.push(inputAlias({
|
|
758
1064
|
name: `${n}Args`,
|
|
759
1065
|
lines: [
|
|
760
1066
|
` readonly select?: ${n}Select;`,
|
|
@@ -763,7 +1069,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
763
1069
|
` readonly relationStrategy?: RelationStrategy;`
|
|
764
1070
|
]
|
|
765
1071
|
}));
|
|
766
|
-
parts.push(
|
|
1072
|
+
parts.push(inputAlias({
|
|
767
1073
|
name: `${n}ListRelationFilter`,
|
|
768
1074
|
lines: [
|
|
769
1075
|
` readonly some?: ${n}WhereInput;`,
|
|
@@ -771,7 +1077,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
771
1077
|
` readonly none?: ${n}WhereInput;`
|
|
772
1078
|
]
|
|
773
1079
|
}));
|
|
774
|
-
parts.push(
|
|
1080
|
+
parts.push(inputAlias({
|
|
775
1081
|
name: `${n}RelationFilter`,
|
|
776
1082
|
lines: [` readonly is?: ${n}WhereInput | null;`, ` readonly isNot?: ${n}WhereInput | null;`]
|
|
777
1083
|
}));
|
|
@@ -797,35 +1103,35 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
797
1103
|
whereLines.push(` readonly ${key}?: ${relation.target}RelationFilter | ${relation.target}WhereInput${relation.isOptional ? " | null" : ""};`);
|
|
798
1104
|
}
|
|
799
1105
|
}
|
|
800
|
-
parts.push(
|
|
1106
|
+
parts.push(inputAlias({ name: `${n}WhereInput`, lines: whereLines }));
|
|
801
1107
|
if (model.uniques.length === 0) {
|
|
802
1108
|
parts.push(`export type ${n}WhereUniqueInput = never;`);
|
|
803
1109
|
} else {
|
|
804
1110
|
const members = model.uniques.map((member) => {
|
|
805
1111
|
const first = member.fields[0];
|
|
806
1112
|
if (member.fields.length === 1 && first !== undefined) {
|
|
807
|
-
return `{ readonly ${propKey({ name: first.name })}: ${inputBaseType({ field: first })} }`;
|
|
1113
|
+
return `{ readonly ${propKey({ name: first.name })}: ${inputBaseType({ field: first })}${member.nullsNotDistinct && first.isOptional ? " | null" : ""} }`;
|
|
808
1114
|
}
|
|
809
|
-
const inner = member.fields.map((field) => `readonly ${propKey({ name: field.name })}: ${inputBaseType({ field })}`).join("; ");
|
|
1115
|
+
const inner = member.fields.map((field) => `readonly ${propKey({ name: field.name })}: ${inputBaseType({ field })}${member.nullsNotDistinct && field.isOptional ? " | null" : ""}`).join("; ");
|
|
810
1116
|
return `{ readonly ${propKey({ name: member.key })}: { ${inner} } }`;
|
|
811
1117
|
});
|
|
812
1118
|
parts.push(`export type ${n}WhereUniqueInput =
|
|
813
1119
|
| ${members.join(`
|
|
814
1120
|
| `)};`);
|
|
815
1121
|
}
|
|
816
|
-
parts.push(
|
|
1122
|
+
parts.push(inputAlias({
|
|
817
1123
|
name: `${n}OrderByInput`,
|
|
818
1124
|
lines: model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: ${field.isOptional ? "SortOrderWithNulls" : "SortOrder"};`)
|
|
819
1125
|
}));
|
|
820
|
-
parts.push(
|
|
821
|
-
parts.push(
|
|
1126
|
+
parts.push(inputAlias({ name: `${n}CreateInput`, lines: checkedCreateLines({ model }) }));
|
|
1127
|
+
parts.push(inputAlias({ name: `${n}UncheckedCreateInput`, lines: uncheckedCreateLines({ model }) }));
|
|
822
1128
|
parts.push(`export type ${n}CreateManyInput = ${n}UncheckedCreateInput;`);
|
|
823
|
-
parts.push(
|
|
824
|
-
parts.push(
|
|
1129
|
+
parts.push(inputAlias({ name: `${n}UpdateInput`, lines: checkedUpdateLines({ model }) }));
|
|
1130
|
+
parts.push(inputAlias({ name: `${n}UncheckedUpdateInput`, lines: uncheckedUpdateLines({ model }) }));
|
|
825
1131
|
parts.push(`export type ${n}UpdateManyInput = ${n}UncheckedUpdateInput;`);
|
|
826
1132
|
const numericFields = model.fields.filter((field) => !field.isList && field.scalar !== null && NUMERIC_SCALARS.has(field.scalar));
|
|
827
1133
|
const minmaxFields = model.fields.filter((field) => !field.isList && field.scalar !== null && MINMAX_SCALARS.has(field.scalar));
|
|
828
|
-
parts.push(
|
|
1134
|
+
parts.push(inputAlias({
|
|
829
1135
|
name: `${n}CountAggregateInput`,
|
|
830
1136
|
lines: [
|
|
831
1137
|
...model.fields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`),
|
|
@@ -834,7 +1140,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
834
1140
|
}));
|
|
835
1141
|
const aggregateOpProps = [` readonly _count?: true | ${n}CountAggregateInput;`];
|
|
836
1142
|
if (numericFields.length > 0) {
|
|
837
|
-
parts.push(
|
|
1143
|
+
parts.push(inputAlias({
|
|
838
1144
|
name: `${n}AvgAggregateInput`,
|
|
839
1145
|
lines: numericFields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`)
|
|
840
1146
|
}));
|
|
@@ -843,7 +1149,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
843
1149
|
aggregateOpProps.push(` readonly _sum?: ${n}SumAggregateInput;`);
|
|
844
1150
|
}
|
|
845
1151
|
if (minmaxFields.length > 0) {
|
|
846
|
-
parts.push(
|
|
1152
|
+
parts.push(inputAlias({
|
|
847
1153
|
name: `${n}MinAggregateInput`,
|
|
848
1154
|
lines: minmaxFields.map((field) => ` readonly ${propKey({ name: field.name })}?: true;`)
|
|
849
1155
|
}));
|
|
@@ -851,7 +1157,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
851
1157
|
aggregateOpProps.push(` readonly _min?: ${n}MinAggregateInput;`);
|
|
852
1158
|
aggregateOpProps.push(` readonly _max?: ${n}MaxAggregateInput;`);
|
|
853
1159
|
}
|
|
854
|
-
parts.push(
|
|
1160
|
+
parts.push(inputAlias({
|
|
855
1161
|
name: `${n}FindManyArgs`,
|
|
856
1162
|
lines: [
|
|
857
1163
|
` readonly where?: ${n}WhereInput;`,
|
|
@@ -860,24 +1166,28 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
860
1166
|
` readonly include?: ${n}Include;`,
|
|
861
1167
|
` readonly omit?: ${n}OmitInput;`,
|
|
862
1168
|
` readonly cursor?: ${n}WhereUniqueInput;`,
|
|
1169
|
+
` readonly after?: KeysetToken;`,
|
|
1170
|
+
` readonly keyset?: KeysetOptions;`,
|
|
863
1171
|
` readonly take?: number;`,
|
|
864
1172
|
` readonly skip?: number;`,
|
|
865
1173
|
` readonly distinct?: ${orMany({ type: `${n}ScalarFieldEnum` })};`,
|
|
866
|
-
` readonly relationStrategy?: RelationStrategy
|
|
1174
|
+
` readonly relationStrategy?: RelationStrategy;`,
|
|
1175
|
+
` readonly lock?: "update";`
|
|
867
1176
|
]
|
|
868
1177
|
}));
|
|
869
1178
|
parts.push(`export type ${n}FindFirstArgs = ${n}FindManyArgs;`);
|
|
870
|
-
parts.push(
|
|
1179
|
+
parts.push(inputAlias({
|
|
871
1180
|
name: `${n}FindUniqueArgs`,
|
|
872
1181
|
lines: [
|
|
873
1182
|
` readonly where: ${n}WhereUniqueInput;`,
|
|
874
1183
|
` readonly select?: ${n}Select;`,
|
|
875
1184
|
` readonly include?: ${n}Include;`,
|
|
876
1185
|
` readonly omit?: ${n}OmitInput;`,
|
|
877
|
-
` readonly relationStrategy?: RelationStrategy
|
|
1186
|
+
` readonly relationStrategy?: RelationStrategy;`,
|
|
1187
|
+
` readonly lock?: "update";`
|
|
878
1188
|
]
|
|
879
1189
|
}));
|
|
880
|
-
parts.push(
|
|
1190
|
+
parts.push(inputAlias({
|
|
881
1191
|
name: `${n}CreateArgs`,
|
|
882
1192
|
lines: [
|
|
883
1193
|
` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
|
|
@@ -886,48 +1196,97 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
886
1196
|
` readonly omit?: ${n}OmitInput;`
|
|
887
1197
|
]
|
|
888
1198
|
}));
|
|
889
|
-
parts.push(
|
|
1199
|
+
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
|
+
}));
|
|
1208
|
+
parts.push(inputAlias({
|
|
1209
|
+
name: `${n}CreateOrSkipArgs`,
|
|
1210
|
+
lines: [
|
|
1211
|
+
` readonly data: ${n}CreateInput | ${n}UncheckedCreateInput;`,
|
|
1212
|
+
` readonly onConflict: ${n}CreateOnConflictInput;`,
|
|
1213
|
+
` readonly select?: ${n}Select;`,
|
|
1214
|
+
` readonly include?: ${n}Include;`,
|
|
1215
|
+
` readonly omit?: ${n}OmitInput;`
|
|
1216
|
+
]
|
|
1217
|
+
}));
|
|
1218
|
+
parts.push(inputAlias({
|
|
890
1219
|
name: `${n}CreateManyArgs`,
|
|
891
1220
|
lines: [
|
|
892
1221
|
` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
|
|
893
|
-
` readonly skipDuplicates?: boolean;`
|
|
1222
|
+
` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`
|
|
894
1223
|
]
|
|
895
1224
|
}));
|
|
896
|
-
parts.push(
|
|
1225
|
+
parts.push(inputAlias({
|
|
897
1226
|
name: `${n}CreateManyAndReturnArgs`,
|
|
898
1227
|
lines: [
|
|
899
1228
|
` readonly data: ${orMany({ type: `${n}CreateManyInput` })};`,
|
|
900
|
-
` readonly skipDuplicates?: boolean;`,
|
|
1229
|
+
` readonly skipDuplicates?: boolean | { readonly target?: ${n}ConflictTarget; readonly where?: string };`,
|
|
901
1230
|
` readonly select?: ${n}Select;`,
|
|
902
1231
|
` readonly omit?: ${n}OmitInput;`
|
|
903
1232
|
]
|
|
904
1233
|
}));
|
|
905
|
-
parts.push(
|
|
1234
|
+
parts.push(inputAlias({
|
|
1235
|
+
name: `${n}UpsertManyArgs`,
|
|
1236
|
+
lines: [
|
|
1237
|
+
` readonly data: readonly ${n}CreateManyInput[];`,
|
|
1238
|
+
` readonly conflictTarget: ${n}ConflictTarget;`,
|
|
1239
|
+
` readonly conflictWhere?: string;`,
|
|
1240
|
+
` readonly update: readonly ${n}ScalarFieldEnum[];`,
|
|
1241
|
+
` readonly skipUpdatedAt?: boolean;`,
|
|
1242
|
+
` readonly allowAnyUniqueConflict?: boolean;`
|
|
1243
|
+
]
|
|
1244
|
+
}));
|
|
1245
|
+
parts.push(inputAlias({
|
|
906
1246
|
name: `${n}UpdateArgs`,
|
|
907
1247
|
lines: [
|
|
908
|
-
` readonly where: ${n}
|
|
1248
|
+
` readonly where: ${n}UpdateWhereInput;`,
|
|
909
1249
|
` readonly data: ${n}UpdateInput | ${n}UncheckedUpdateInput;`,
|
|
910
1250
|
` readonly select?: ${n}Select;`,
|
|
911
1251
|
` readonly include?: ${n}Include;`,
|
|
912
|
-
` readonly omit?: ${n}OmitInput
|
|
1252
|
+
` readonly omit?: ${n}OmitInput;`,
|
|
1253
|
+
` readonly skipUpdatedAt?: boolean;`
|
|
913
1254
|
]
|
|
914
1255
|
}));
|
|
915
|
-
parts.push(
|
|
1256
|
+
parts.push(`/** update()'s where: a concrete unique selector AND any additional predicates (optimistic-concurrency guards such as \`{ id, version }\`). */
|
|
1257
|
+
export type ${n}UpdateWhereInput = ${n}WhereUniqueInput & ${n}WhereInput;`);
|
|
1258
|
+
parts.push(inputAlias({
|
|
916
1259
|
name: `${n}UpdateManyArgs`,
|
|
917
|
-
lines: [
|
|
1260
|
+
lines: [
|
|
1261
|
+
` readonly where?: ${n}WhereInput;`,
|
|
1262
|
+
` readonly data: ${n}UpdateManyInput;`,
|
|
1263
|
+
` readonly skipUpdatedAt?: boolean;`
|
|
1264
|
+
]
|
|
918
1265
|
}));
|
|
919
|
-
parts.push(
|
|
1266
|
+
parts.push(inputAlias({
|
|
1267
|
+
name: `${n}UpdateManyAndReturnArgs`,
|
|
1268
|
+
lines: [
|
|
1269
|
+
` readonly include?: never;`,
|
|
1270
|
+
` readonly where?: ${n}WhereInput;`,
|
|
1271
|
+
` readonly data: ${n}UpdateManyInput;`,
|
|
1272
|
+
` readonly select?: ${n}Select;`,
|
|
1273
|
+
` readonly omit?: ${n}OmitInput;`,
|
|
1274
|
+
` readonly skipUpdatedAt?: boolean;`
|
|
1275
|
+
]
|
|
1276
|
+
}));
|
|
1277
|
+
parts.push(inputAlias({
|
|
920
1278
|
name: `${n}UpsertArgs`,
|
|
921
1279
|
lines: [
|
|
922
1280
|
` readonly where: ${n}WhereUniqueInput;`,
|
|
923
1281
|
` readonly create: ${n}CreateInput | ${n}UncheckedCreateInput;`,
|
|
924
1282
|
` readonly update: ${n}UpdateInput | ${n}UncheckedUpdateInput;`,
|
|
1283
|
+
` readonly onConflict?: { readonly constraint: string };`,
|
|
925
1284
|
` readonly select?: ${n}Select;`,
|
|
926
1285
|
` readonly include?: ${n}Include;`,
|
|
927
1286
|
` readonly omit?: ${n}OmitInput;`
|
|
928
1287
|
]
|
|
929
1288
|
}));
|
|
930
|
-
parts.push(
|
|
1289
|
+
parts.push(inputAlias({
|
|
931
1290
|
name: `${n}DeleteArgs`,
|
|
932
1291
|
lines: [
|
|
933
1292
|
` readonly where: ${n}WhereUniqueInput;`,
|
|
@@ -936,10 +1295,11 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
936
1295
|
` readonly omit?: ${n}OmitInput;`
|
|
937
1296
|
]
|
|
938
1297
|
}));
|
|
939
|
-
parts.push(
|
|
940
|
-
parts.push(
|
|
1298
|
+
parts.push(inputAlias({ name: `${n}DeleteManyArgs`, lines: [` readonly where?: ${n}WhereInput;`] }));
|
|
1299
|
+
parts.push(inputAlias({
|
|
941
1300
|
name: `${n}CountArgs`,
|
|
942
1301
|
lines: [
|
|
1302
|
+
` readonly select?: ${n}CountAggregateInput;`,
|
|
943
1303
|
` readonly where?: ${n}WhereInput;`,
|
|
944
1304
|
` readonly orderBy?: ${orMany({ type: `${n}OrderByInput` })};`,
|
|
945
1305
|
` readonly cursor?: ${n}WhereUniqueInput;`,
|
|
@@ -947,7 +1307,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
947
1307
|
` readonly skip?: number;`
|
|
948
1308
|
]
|
|
949
1309
|
}));
|
|
950
|
-
parts.push(
|
|
1310
|
+
parts.push(inputAlias({
|
|
951
1311
|
name: `${n}AggregateArgs`,
|
|
952
1312
|
lines: [
|
|
953
1313
|
` readonly where?: ${n}WhereInput;`,
|
|
@@ -958,7 +1318,7 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
958
1318
|
...aggregateOpProps
|
|
959
1319
|
]
|
|
960
1320
|
}));
|
|
961
|
-
parts.push(
|
|
1321
|
+
parts.push(inputAlias({
|
|
962
1322
|
name: `${n}GroupByArgs`,
|
|
963
1323
|
lines: [
|
|
964
1324
|
` readonly by: readonly ${n}ScalarFieldEnum[];`,
|
|
@@ -970,20 +1330,26 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
970
1330
|
...aggregateOpProps
|
|
971
1331
|
]
|
|
972
1332
|
}));
|
|
973
|
-
const
|
|
974
|
-
|
|
1333
|
+
const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
|
|
1334
|
+
const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
|
|
1335
|
+
parts.push(`type ${n}OmitBase<A> = A extends { readonly omit: infer O extends object }
|
|
1336
|
+
` + ` ? Omit<${n}, PossibleKeys<O> & keyof ${n}> & { [K in UncertainKeys<O> & keyof ${n}]?: ${n}[K] }
|
|
1337
|
+
` + ` : "omit" extends keyof A ? Omit<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}> & Partial<Pick<${n}, PossibleKeys<Provided<A["omit"]>> & keyof ${n}>> : ${n};`);
|
|
975
1338
|
if (hasCounts) {
|
|
976
1339
|
const listUnion = listRelations.map((relation) => tsString({ value: relation.name })).join(" | ");
|
|
977
1340
|
const allCounts = listRelations.map((relation) => `${propKey({ name: relation.name })}: number`).join("; ");
|
|
978
1341
|
parts.push(`type ${n}CountResult<C> = C extends { readonly select: infer CS extends object }
|
|
979
|
-
` + ` ? { [K in keyof CS & (${listUnion}) as ${
|
|
1342
|
+
` + ` ? { [K in keyof CS & (${listUnion}) as ${exactPart("CS")}]: number } &
|
|
1343
|
+
` + ` { [K in keyof CS & (${listUnion}) as ${uncertainPart("CS")}]?: number }
|
|
980
1344
|
` + ` : { ${allCounts} };`);
|
|
981
|
-
parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> }
|
|
1345
|
+
parts.push(`type ${n}CountPayload<S> = S extends { readonly _count: infer C extends true | object } ? { _count: ${n}CountResult<C> }
|
|
1346
|
+
` + ` : S extends { readonly _count: false | undefined } ? unknown
|
|
1347
|
+
` + ` : S extends { readonly _count?: infer C extends boolean | object } ? { _count?: ${n}CountResult<C> } : unknown;`);
|
|
982
1348
|
}
|
|
983
1349
|
if (hasRelations) {
|
|
984
1350
|
const relationCases = model.relations.map((relation) => {
|
|
985
1351
|
const nested = `${relation.target}GetPayload<A>`;
|
|
986
|
-
const guard = `A extends
|
|
1352
|
+
const guard = `A extends object`;
|
|
987
1353
|
let expression;
|
|
988
1354
|
if (relation.isList) {
|
|
989
1355
|
expression = `${guard} ? ${nested}[] : ${relation.target}[]`;
|
|
@@ -999,47 +1365,68 @@ ${model.relations.map((relation) => ` ${propKey({ name: relation.name })}: ${re
|
|
|
999
1365
|
${relationCases}
|
|
1000
1366
|
}[K];`);
|
|
1001
1367
|
parts.push(`type ${n}IncludePayload<A> = A extends { readonly include: infer I extends object }
|
|
1002
|
-
` + ` ? { [K in keyof I & keyof ${n}Relations as ${
|
|
1003
|
-
` + `
|
|
1368
|
+
` + ` ? { [K in keyof I & keyof ${n}Relations as ${exactPart("I")}]: ${n}RelationPayload<K, I[K]> } &
|
|
1369
|
+
` + ` { [K in keyof I & keyof ${n}Relations as ${uncertainPart("I")}]?: ${n}RelationPayload<K, I[K]> }${hasCounts ? ` &
|
|
1370
|
+
${n}CountPayload<I>` : ""}
|
|
1371
|
+
` + ` : "include" extends keyof A ? Partial<${n}IncludePayload<{ include: Provided<A["include"]> }>> : unknown;`);
|
|
1004
1372
|
parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
|
|
1005
|
-
` + ` ? { [K in keyof S & ${
|
|
1006
|
-
` + ` { [K in keyof S &
|
|
1373
|
+
` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
|
|
1374
|
+
` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] } &
|
|
1375
|
+
` + ` { [K in keyof S & keyof ${n}Relations as ${exactPart("S")}]: ${n}RelationPayload<K, S[K]> } &
|
|
1376
|
+
` + ` { [K in keyof S & keyof ${n}Relations as ${uncertainPart("S")}]?: ${n}RelationPayload<K, S[K]> }${hasCounts ? ` &
|
|
1007
1377
|
${n}CountPayload<S>` : ""}
|
|
1378
|
+
` + ` : "select" extends keyof A ? Partial<${n}>
|
|
1008
1379
|
` + ` : ${n}OmitBase<A> & ${n}IncludePayload<A>;`);
|
|
1009
1380
|
} else {
|
|
1010
1381
|
parts.push(`export type ${n}GetPayload<A> = A extends { readonly select: infer S extends object }
|
|
1011
|
-
` + ` ? { [K in keyof S & ${
|
|
1012
|
-
` + `
|
|
1382
|
+
` + ` ? { [K in keyof S & ${selectableEnum} as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
|
|
1383
|
+
` + ` { [K in keyof S & ${selectableEnum} as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
|
|
1384
|
+
` + ` : "select" extends keyof A ? Partial<${n}> : ${n}OmitBase<A>;`);
|
|
1013
1385
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
`
|
|
1017
|
-
` + `
|
|
1018
|
-
` + `
|
|
1019
|
-
` + `}
|
|
1386
|
+
const aggregateFields = (fields, value) => `{ ${fields.map((field) => `${propKey({ name: field.name })}: ${value(field)}`).join("; ")} }`;
|
|
1387
|
+
const countValues = aggregateFields(model.fields, () => "number").replace(" }", "; _all: number }");
|
|
1388
|
+
parts.push(`type ${n}AggregateValues = {
|
|
1389
|
+
` + ` _count: ${countValues};
|
|
1390
|
+
` + ` _avg: ${aggregateFields(numericFields, () => "number | null")};
|
|
1391
|
+
` + ` _sum: ${aggregateFields(numericFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
|
|
1392
|
+
` + ` _min: ${aggregateFields(minmaxFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
|
|
1393
|
+
` + ` _max: ${aggregateFields(minmaxFields, (field) => `${n}[${tsString({ value: field.name })}] | null`)};
|
|
1394
|
+
};`);
|
|
1395
|
+
parts.push(`type ${n}AggregateSelection<V, S> = S extends true ? number : S extends object
|
|
1396
|
+
` + ` ? { [K in keyof S & keyof V as ${exactPart("S")}]: V[K] } &
|
|
1397
|
+
` + ` { [K in keyof S & keyof V as ${uncertainPart("S")}]?: V[K] } : never;`);
|
|
1398
|
+
parts.push(`type ${n}CountOutput<A> = A extends { readonly select: infer S extends object }
|
|
1399
|
+
` + ` ? ${n}AggregateSelection<${n}AggregateValues["_count"], S>
|
|
1400
|
+
` + ` : "select" extends keyof A ? number | ${n}AggregateSelection<${n}AggregateValues["_count"], Provided<A["select"]>> : number;`);
|
|
1401
|
+
parts.push(`export type ${n}AggregateResult<A> =
|
|
1402
|
+
` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? K : never]: ${n}AggregateSelection<${n}AggregateValues[K], A[K]> } &
|
|
1403
|
+
` + ` { [K in keyof A & keyof ${n}AggregateValues as HasSelectedField<A[K]> extends true ? never : K]?: ${n}AggregateSelection<${n}AggregateValues[K], Provided<A[K]>> };`);
|
|
1020
1404
|
parts.push(`export type ${n}GroupByResult<A extends ${n}GroupByArgs> =
|
|
1021
|
-
` + `
|
|
1405
|
+
` + ` (number extends A["by"]["length"] ? Partial<Pick<${n}, A["by"][number]>> : Pick<${n}, A["by"][number]>) & ${n}AggregateResult<A>;`);
|
|
1022
1406
|
const readMethods = [
|
|
1023
|
-
` findMany<A extends ${n}FindManyArgs =
|
|
1024
|
-
` findFirst<A extends ${n}FindFirstArgs =
|
|
1025
|
-
` findFirstOrThrow<A extends ${n}FindFirstArgs =
|
|
1407
|
+
` findMany<A extends ${n}FindManyArgs = {}>(args?: A): Promise<${n}GetPayload<A>[]>;`,
|
|
1408
|
+
` findFirst<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A> | null>;`,
|
|
1409
|
+
` findFirstOrThrow<A extends ${n}FindFirstArgs = {}>(args?: A): Promise<${n}GetPayload<A>>;`,
|
|
1026
1410
|
` findUnique<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
|
|
1027
1411
|
` findUniqueOrThrow<A extends ${n}FindUniqueArgs>(args: A): Promise<${n}GetPayload<A>>;`
|
|
1028
1412
|
];
|
|
1029
1413
|
const writeMethods = [
|
|
1414
|
+
` create<A extends ${n}CreateOrSkipArgs>(args: A): Promise<${n}GetPayload<A> | null>;`,
|
|
1030
1415
|
` create<A extends ${n}CreateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
|
|
1031
1416
|
` createMany(args: ${n}CreateManyArgs): Promise<BatchPayload>;`,
|
|
1032
1417
|
` createManyAndReturn<A extends ${n}CreateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
|
|
1033
1418
|
` update<A extends ${n}UpdateArgs>(args: A): Promise<${n}GetPayload<A>>;`,
|
|
1034
1419
|
` updateMany(args: ${n}UpdateManyArgs): Promise<BatchPayload>;`,
|
|
1420
|
+
` updateManyAndReturn<A extends ${n}UpdateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;`,
|
|
1035
1421
|
` upsert<A extends ${n}UpsertArgs>(args: A): Promise<${n}GetPayload<A>>;`,
|
|
1422
|
+
` upsertMany(args: ${n}UpsertManyArgs): Promise<UpsertManyPayload>;`,
|
|
1036
1423
|
` delete<A extends ${n}DeleteArgs>(args: A): Promise<${n}GetPayload<A>>;`,
|
|
1037
1424
|
` deleteMany(args?: ${n}DeleteManyArgs): Promise<BatchPayload>;`
|
|
1038
1425
|
];
|
|
1039
1426
|
const aggregateMethods = [
|
|
1040
|
-
` count
|
|
1427
|
+
` count<A extends ${n}CountArgs = {}>(args?: A): Promise<${n}CountOutput<A>>;`,
|
|
1041
1428
|
` aggregate<A extends ${n}AggregateArgs>(args: A): Promise<${n}AggregateResult<A>>;`,
|
|
1042
|
-
` groupBy<A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;`
|
|
1429
|
+
` groupBy<const A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;`
|
|
1043
1430
|
];
|
|
1044
1431
|
const delegateMethods = model.isView ? [...readMethods, ...aggregateMethods] : [...readMethods, ...writeMethods, ...aggregateMethods];
|
|
1045
1432
|
delegateMethods.push(...params.extensionLines ?? []);
|
|
@@ -1108,49 +1495,54 @@ function emitViewTypesForModel(params) {
|
|
|
1108
1495
|
` readonly relationStrategy?: RelationStrategy;`
|
|
1109
1496
|
]
|
|
1110
1497
|
}));
|
|
1111
|
-
const
|
|
1498
|
+
const exactPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? never : undefined extends ${source}[K] ? never : K`;
|
|
1499
|
+
const uncertainPart = (source) => `${source}[K] extends false | undefined ? never : false extends ${source}[K] ? K : undefined extends ${source}[K] ? K : never`;
|
|
1112
1500
|
parts.push(`type ${n}ViewSelectScalars<F, A> = A extends { readonly select: infer S extends object }
|
|
1113
|
-
` + ` ? { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${
|
|
1114
|
-
` + `
|
|
1501
|
+
` + ` ? { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${exactPart("S")}]: ${n}[K & keyof ${n}] } &
|
|
1502
|
+
` + ` { [K in keyof S & keyof F & ${n}ScalarFieldEnum as ${uncertainPart("S")}]?: ${n}[K & keyof ${n}] }
|
|
1503
|
+
` + ` : "select" extends keyof A ? Partial<${n}ViewScalars<F>> : ${n}ViewScalars<F>;`);
|
|
1115
1504
|
parts.push(`type ${n}ViewReveal<A> = A extends { readonly reveal: infer R extends object }
|
|
1116
|
-
` + ` ? { [K in keyof R & ${n}ScalarFieldEnum as ${
|
|
1117
|
-
` + `
|
|
1505
|
+
` + ` ? { [K in keyof R & ${n}ScalarFieldEnum as ${exactPart("R")}]: ${n}[K & keyof ${n}] } &
|
|
1506
|
+
` + ` { [K in keyof R & ${n}ScalarFieldEnum as ${uncertainPart("R")}]?: ${n}[K & keyof ${n}] }
|
|
1507
|
+
` + ` : "reveal" extends keyof A ? Partial<${n}ViewReveal<{ reveal: NonNullable<A["reveal"]> }>> : unknown;`);
|
|
1118
1508
|
if (hasRelations) {
|
|
1119
1509
|
const relationCases = model.relations.map((relation) => {
|
|
1120
1510
|
const target = relation.target;
|
|
1121
1511
|
const targetClientKey = view.modelByName.get(target)?.clientName ?? target;
|
|
1122
1512
|
const guard = `D extends { readonly ${propKey({ name: targetClientKey })}: infer RF extends object }`;
|
|
1123
|
-
const masked = `ViewRow<${target}
|
|
1513
|
+
const masked = `ViewRow<${target}ViewSelectScalars<RF, A> & ${target}ViewReveal<A>>`;
|
|
1514
|
+
const unmasked = `(A extends object ? ${target}GetPayload<A> : ${target})`;
|
|
1124
1515
|
let expression;
|
|
1125
1516
|
if (relation.isList) {
|
|
1126
|
-
expression = `${guard} ? ${masked}[] : ${
|
|
1517
|
+
expression = `${guard} ? ${masked}[] : ${unmasked}[]`;
|
|
1127
1518
|
} else if (relation.isOptional) {
|
|
1128
|
-
expression = `(${guard} ? ${masked} : ${
|
|
1519
|
+
expression = `(${guard} ? ${masked} : ${unmasked}) | null`;
|
|
1129
1520
|
} else {
|
|
1130
|
-
expression = `${guard} ? ${masked} : ${
|
|
1521
|
+
expression = `${guard} ? ${masked} : ${unmasked}`;
|
|
1131
1522
|
}
|
|
1132
1523
|
return ` ${propKey({ name: relation.name })}: ${expression};`;
|
|
1133
1524
|
}).join(`
|
|
1134
1525
|
`);
|
|
1135
|
-
parts.push(`type ${n}ViewRelationResult<K extends keyof ${n}Relations, D> = {
|
|
1526
|
+
parts.push(`type ${n}ViewRelationResult<K extends keyof ${n}Relations, D, A> = {
|
|
1136
1527
|
${relationCases}
|
|
1137
1528
|
}[K];`);
|
|
1138
1529
|
parts.push(`type ${n}ViewIncludeResult<A, D> = A extends { readonly include: infer I extends object }
|
|
1139
|
-
` + ` ? { [K in keyof I & keyof ${n}Relations as ${
|
|
1140
|
-
` + `
|
|
1530
|
+
` + ` ? { [K in keyof I & keyof ${n}Relations as ${exactPart("I")}]: ${n}ViewRelationResult<K, D, I[K]> } &
|
|
1531
|
+
` + ` { [K in keyof I & keyof ${n}Relations as ${uncertainPart("I")}]?: ${n}ViewRelationResult<K, D, I[K]> }
|
|
1532
|
+
` + ` : "include" extends keyof A ? Partial<${n}ViewIncludeResult<{ include: NonNullable<A["include"]> }, D>> : unknown;`);
|
|
1141
1533
|
parts.push(`export type ${n}ViewResult<F, A, D> = ViewRow<${n}ViewSelectScalars<F, A> & ${n}ViewReveal<A> & ${n}ViewIncludeResult<A, D>>;`);
|
|
1142
1534
|
} else {
|
|
1143
1535
|
parts.push(`export type ${n}ViewResult<F, A, D> = ViewRow<${n}ViewSelectScalars<F, A> & ${n}ViewReveal<A>>;`);
|
|
1144
1536
|
}
|
|
1145
1537
|
parts.push(`export type ${n}ViewDelegate<F, D extends ViewDefinitionInput = ViewDefinitionInput> = {
|
|
1146
|
-
` + ` findMany<A extends ${n}ViewFindManyArgs =
|
|
1147
|
-
` + ` findFirst<A extends ${n}ViewFindManyArgs =
|
|
1148
|
-
` + ` findFirstOrThrow<A extends ${n}ViewFindManyArgs =
|
|
1538
|
+
` + ` findMany<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D>[]>;
|
|
1539
|
+
` + ` findFirst<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D> | null>;
|
|
1540
|
+
` + ` findFirstOrThrow<A extends ${n}ViewFindManyArgs = {}>(args?: A): Promise<${n}ViewResult<F, A, D>>;
|
|
1149
1541
|
` + ` findUnique<A extends ${n}ViewFindUniqueArgs>(args: A): Promise<${n}ViewResult<F, A, D> | null>;
|
|
1150
1542
|
` + ` findUniqueOrThrow<A extends ${n}ViewFindUniqueArgs>(args: A): Promise<${n}ViewResult<F, A, D>>;
|
|
1151
|
-
` + ` count
|
|
1543
|
+
` + ` count<A extends ${n}CountArgs = {}>(args?: A): Promise<${n}CountOutput<A>>;
|
|
1152
1544
|
` + ` aggregate<A extends ${n}AggregateArgs>(args: A): Promise<${n}AggregateResult<A>>;
|
|
1153
|
-
` + ` groupBy<A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;
|
|
1545
|
+
` + ` groupBy<const A extends ${n}GroupByArgs>(args: A): Promise<${n}GroupByResult<A>[]>;
|
|
1154
1546
|
` + `};`);
|
|
1155
1547
|
return parts.join(`
|
|
1156
1548
|
|
|
@@ -1184,38 +1576,236 @@ ${view.models.map((model) => ` ${propKey({ name: model.clientName })}: ${model.
|
|
|
1184
1576
|
}
|
|
1185
1577
|
return parts;
|
|
1186
1578
|
}
|
|
1579
|
+
function jsonTypeImportLines(params) {
|
|
1580
|
+
const bySpecifier = new Map;
|
|
1581
|
+
for (const model of params.view.models) {
|
|
1582
|
+
for (const field of model.fields) {
|
|
1583
|
+
if (field.jsonType === undefined)
|
|
1584
|
+
continue;
|
|
1585
|
+
const names = bySpecifier.get(field.jsonType.from) ?? new Set;
|
|
1586
|
+
names.add(field.jsonType.name);
|
|
1587
|
+
bySpecifier.set(field.jsonType.from, names);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return [...bySpecifier.keys()].sort().map((from) => `import type { ${[...bySpecifier.get(from)].sort().join(", ")} } from ${tsString({ value: from })};`);
|
|
1591
|
+
}
|
|
1592
|
+
function hasNativeRls(params) {
|
|
1593
|
+
return params.view.schema.rls !== undefined;
|
|
1594
|
+
}
|
|
1595
|
+
var RLS_CONTEXT_TYPES = {
|
|
1596
|
+
String: "string",
|
|
1597
|
+
Boolean: "boolean",
|
|
1598
|
+
Int: "number",
|
|
1599
|
+
BigInt: "bigint",
|
|
1600
|
+
Float: "number",
|
|
1601
|
+
Decimal: "string",
|
|
1602
|
+
DateTime: "Date"
|
|
1603
|
+
};
|
|
1604
|
+
function emitRlsSection(params) {
|
|
1605
|
+
const rls = params.view.schema.rls;
|
|
1606
|
+
if (rls === undefined)
|
|
1607
|
+
return [];
|
|
1608
|
+
const lines = Object.entries(rls.context).map(([key, slot]) => ` readonly ${propKey({ name: key })}: ${RLS_CONTEXT_TYPES[slot.type]}${slot.nullable ? " | null" : ""};`);
|
|
1609
|
+
const doc = `/** The identity context every query on this schema must be bound to \u2014 all slots required, an explicit null included. */`;
|
|
1610
|
+
return [
|
|
1611
|
+
banner({ title: "Native row-level security" }),
|
|
1612
|
+
lines.length === 0 ? `${doc}
|
|
1613
|
+
export type RlsContext = Record<string, never>;` : `${doc}
|
|
1614
|
+
export type RlsContext = {
|
|
1615
|
+
${lines.join(`
|
|
1616
|
+
`)}
|
|
1617
|
+
};`
|
|
1618
|
+
];
|
|
1619
|
+
}
|
|
1620
|
+
function instanceSurfaceLines(params) {
|
|
1621
|
+
const { view, selfType } = params;
|
|
1622
|
+
const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`);
|
|
1623
|
+
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;`];
|
|
1626
|
+
});
|
|
1627
|
+
return `${delegateProps.join(`
|
|
1628
|
+
`)}
|
|
1629
|
+
` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
|
|
1630
|
+
` + ` readonly $models: {
|
|
1631
|
+
${modelsProps.join(`
|
|
1632
|
+
`)}
|
|
1633
|
+
};
|
|
1634
|
+
` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
|
|
1635
|
+
` + ` 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. */
|
|
1637
|
+
` + ` readonly $transaction: {
|
|
1638
|
+
` + ` <T>(fn: (tx: ${selfType}) => Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
1639
|
+
` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
|
|
1640
|
+
` + ` };
|
|
1641
|
+
` + ` readonly $queryRaw: <T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]) => Promise<T[]>;
|
|
1642
|
+
` + ` readonly $executeRaw: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
|
|
1643
|
+
` + ` readonly $queryRawUnsafe: <T = Record<string, unknown>>(text: string, ...values: unknown[]) => Promise<T[]>;
|
|
1644
|
+
` + ` readonly $executeRawUnsafe: (text: string, ...values: unknown[]) => Promise<number>;
|
|
1645
|
+
` + ` readonly $connect: () => Promise<void>;
|
|
1646
|
+
` + ` readonly $disconnect: () => Promise<void>;
|
|
1647
|
+
` + ` /** Field-masking views (v1 defineView parity): a read-only masked projection of models. */
|
|
1648
|
+
` + ` readonly $defineView: <D extends ViewDefinitionInput>(options: { readonly name: string; readonly definition: D }) => ViewClient<D>;
|
|
1649
|
+
`;
|
|
1650
|
+
}
|
|
1651
|
+
function telemetryDeclarations() {
|
|
1652
|
+
return [
|
|
1653
|
+
`/** One SQL execution inside an operation. Parameter values are redacted; \`sql\` and \`parameters\` appear only with the matching capture option. */`,
|
|
1654
|
+
`export type VibeTelemetryStatementEvent = {`,
|
|
1655
|
+
` readonly operationId: string;`,
|
|
1656
|
+
` readonly index: number;`,
|
|
1657
|
+
` readonly kind: "rows" | "affected" | "raw" | "extension";`,
|
|
1658
|
+
` readonly model: string | null;`,
|
|
1659
|
+
` readonly method: string;`,
|
|
1660
|
+
` readonly dialect: string;`,
|
|
1661
|
+
` readonly provider: string;`,
|
|
1662
|
+
` /** Stable normalized grouping key \u2014 bounded and literal-free, safe as a metric label. */`,
|
|
1663
|
+
` readonly fingerprint: string;`,
|
|
1664
|
+
` readonly outcome: "success" | "failure";`,
|
|
1665
|
+
` /** Adapter-call elapsed time: networking and possibly pool wait included \u2014 not pure server execution time. */`,
|
|
1666
|
+
` readonly durationMs: number;`,
|
|
1667
|
+
` readonly parameterCount: number;`,
|
|
1668
|
+
` readonly inTransaction: boolean;`,
|
|
1669
|
+
` readonly rowCount?: number;`,
|
|
1670
|
+
` readonly errorCode?: string;`,
|
|
1671
|
+
` readonly slow?: boolean;`,
|
|
1672
|
+
` readonly sql?: string;`,
|
|
1673
|
+
` readonly parameters?: readonly unknown[];`,
|
|
1674
|
+
`};`,
|
|
1675
|
+
``,
|
|
1676
|
+
`/** One public ORM call, start to terminal outcome. Emitted exactly once. Unknown values are ABSENT, never zero. */`,
|
|
1677
|
+
`export type VibeTelemetryOperationEvent = {`,
|
|
1678
|
+
` readonly operationId: string;`,
|
|
1679
|
+
` readonly parentOperationId?: string;`,
|
|
1680
|
+
` readonly context?: unknown;`,
|
|
1681
|
+
` readonly kind: "model" | "raw" | "transaction" | "extension";`,
|
|
1682
|
+
` readonly model: string | null;`,
|
|
1683
|
+
` readonly method: string;`,
|
|
1684
|
+
` readonly dialect: string;`,
|
|
1685
|
+
` readonly provider: string;`,
|
|
1686
|
+
` readonly outcome: "success" | "failure";`,
|
|
1687
|
+
` readonly stage: "refused" | "validation" | "statement" | "decode" | "transaction" | "completed" | "unknown";`,
|
|
1688
|
+
` readonly dispatched: boolean;`,
|
|
1689
|
+
` readonly startedAt?: number;`,
|
|
1690
|
+
` readonly durationMs?: number;`,
|
|
1691
|
+
` /** Creation to dispatch \u2014 labelled separately, never folded into durationMs. */`,
|
|
1692
|
+
` readonly queueWaitMs?: number;`,
|
|
1693
|
+
` readonly statementCount: number;`,
|
|
1694
|
+
` readonly statementDurationMs?: number;`,
|
|
1695
|
+
` readonly rowCount?: number;`,
|
|
1696
|
+
` readonly errorCode?: string;`,
|
|
1697
|
+
` readonly detail?: { readonly renderMs?: number; readonly adapterMs?: number; readonly processingMs?: number };`,
|
|
1698
|
+
` readonly statements: readonly VibeTelemetryStatementEvent[];`,
|
|
1699
|
+
` readonly droppedStatements?: number;`,
|
|
1700
|
+
` readonly slow?: boolean;`,
|
|
1701
|
+
` readonly sampled: true;`,
|
|
1702
|
+
`};`,
|
|
1703
|
+
``,
|
|
1704
|
+
`/** Where events go. A throw from any callback is contained: it can never change a database outcome. */`,
|
|
1705
|
+
`export type VibeTelemetryExporter = {`,
|
|
1706
|
+
` readonly onOperation?: (event: VibeTelemetryOperationEvent) => void;`,
|
|
1707
|
+
` readonly onStatement?: (event: VibeTelemetryStatementEvent) => void;`,
|
|
1708
|
+
` readonly onDrop?: (event: { readonly reason: "buffer_full" | "statement_limit"; readonly count: number }) => void;`,
|
|
1709
|
+
` readonly flush?: () => void | Promise<void>;`,
|
|
1710
|
+
` /** NEVER called by the client \u2014 \`$disconnect()\` flushes only, because the provider is yours. */`,
|
|
1711
|
+
` readonly shutdown?: () => void | Promise<void>;`,
|
|
1712
|
+
`};`,
|
|
1713
|
+
``,
|
|
1714
|
+
`/** Complete counters (spans may be sampled, these are not) and explicit lifecycle. */`,
|
|
1715
|
+
`export type VibeTelemetryHandle = {`,
|
|
1716
|
+
` readonly metrics: () => {`,
|
|
1717
|
+
` readonly countsRepresent: "all";`,
|
|
1718
|
+
` readonly sampleRate: number;`,
|
|
1719
|
+
` readonly sampledOperations: number;`,
|
|
1720
|
+
` readonly operations: { readonly total: number; readonly failed: number; readonly refused: number };`,
|
|
1721
|
+
` readonly statements: { readonly total: number; readonly failed: number };`,
|
|
1722
|
+
` readonly rows: { readonly known: number; readonly total: number };`,
|
|
1723
|
+
` readonly operationLatencyMs: { readonly bounds: readonly number[]; readonly counts: readonly number[]; readonly sum: number; readonly count: number };`,
|
|
1724
|
+
` readonly statementLatencyMs: { readonly bounds: readonly number[]; readonly counts: readonly number[]; readonly sum: number; readonly count: number };`,
|
|
1725
|
+
` readonly dropped: { readonly operations: number; readonly statements: number };`,
|
|
1726
|
+
` };`,
|
|
1727
|
+
` readonly flush: () => Promise<void>;`,
|
|
1728
|
+
` readonly shutdown: () => Promise<void>;`,
|
|
1729
|
+
`};`,
|
|
1730
|
+
``,
|
|
1731
|
+
`/** Opt-in operation telemetry. Defaults are conservative: no SQL text, no parameter values, no sampling. */`,
|
|
1732
|
+
`export type VibeTelemetryOptions = {`,
|
|
1733
|
+
` readonly exporter: VibeTelemetryExporter;`,
|
|
1734
|
+
` /** Attach rendered SQL text (default false \u2014 raw text can embed secrets). */`,
|
|
1735
|
+
` readonly captureSql?: boolean;`,
|
|
1736
|
+
` /** Attach parameter VALUES (default false \u2014 only the count is reported). */`,
|
|
1737
|
+
` readonly captureParameterValues?: boolean;`,
|
|
1738
|
+
` /** Split render / adapter / processing time on the operation event (default false). */`,
|
|
1739
|
+
` readonly detailedTimings?: boolean;`,
|
|
1740
|
+
` /** Span sampling probability, 0..1 (default 1). Counters still count every execution. */`,
|
|
1741
|
+
` readonly sampleRate?: number;`,
|
|
1742
|
+
` readonly sampler?: (input: { readonly kind: "model" | "raw" | "transaction" | "extension"; readonly model: string | null; readonly method: string; readonly context?: unknown }) => boolean;`,
|
|
1743
|
+
` /** Read your current parent context SYNCHRONOUSLY at call time, so an application span survives the async boundary. */`,
|
|
1744
|
+
` readonly context?: () => unknown;`,
|
|
1745
|
+
` readonly delivery?: "immediate" | "buffered";`,
|
|
1746
|
+
` readonly maxBufferedOperations?: number;`,
|
|
1747
|
+
` readonly maxStatementsPerOperation?: number;`,
|
|
1748
|
+
` readonly slowStatementMs?: number;`,
|
|
1749
|
+
` readonly slowOperationMs?: number;`,
|
|
1750
|
+
` readonly onExporterError?: (params: { readonly error: unknown; readonly phase: "operation" | "statement" | "drop" | "flush" | "shutdown" | "sampler" | "context" }) => void;`,
|
|
1751
|
+
` /** Called once while the client is built, with the handle that owns metrics and lifecycle. */`,
|
|
1752
|
+
` readonly onReady?: (handle: VibeTelemetryHandle) => void;`,
|
|
1753
|
+
`};`
|
|
1754
|
+
].join(`
|
|
1755
|
+
`);
|
|
1756
|
+
}
|
|
1187
1757
|
function emitClient(params) {
|
|
1188
|
-
const { view, baked } = params;
|
|
1758
|
+
const { view, baked, hasComputed } = params;
|
|
1759
|
+
const nativeRls = hasNativeRls({ view });
|
|
1189
1760
|
const parts = [banner({ title: "Client" })];
|
|
1761
|
+
parts.push(telemetryDeclarations());
|
|
1190
1762
|
parts.push(objectAlias({
|
|
1191
1763
|
name: "VibeClientOptions",
|
|
1192
1764
|
lines: [
|
|
1193
1765
|
` readonly adapter: DatabaseAdapter;`,
|
|
1194
1766
|
` readonly onQuery?: (event: QueryEvent) => void;`,
|
|
1195
1767
|
` readonly relationStrategy?: RelationStrategy;`,
|
|
1768
|
+
` /** Append the primary key to ORDER BY when a find has no explicit orderBy (default false) \u2014 deterministic row order. */`,
|
|
1769
|
+
` readonly defaultOrderByPk?: boolean;`,
|
|
1770
|
+
` /** Append the primary key as a tie-breaker to \`distinct\` ordering (default true) \u2014 pins WHICH row represents each distinct key. */`,
|
|
1771
|
+
` readonly distinctOrderByPk?: boolean;`,
|
|
1772
|
+
` /** Opt-in write-boundary validators, per model \xD7 field: \`{ User: { email: zodSchema } }\`. Any Standard Schema validator (zod, valibot, arktype) works; they run on every write path before encoding. */`,
|
|
1773
|
+
` readonly validate?: WriteValidators;`,
|
|
1774
|
+
` /** 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
|
+
` readonly telemetry?: VibeTelemetryOptions;`,
|
|
1196
1776
|
...baked.empty ? [] : [
|
|
1197
1777
|
` /** The live extension instances \u2014 verified against the baked manifest. */`,
|
|
1198
1778
|
` readonly extensions?: readonly VibeExtension[];`
|
|
1199
|
-
]
|
|
1779
|
+
],
|
|
1780
|
+
...hasComputed ? [
|
|
1781
|
+
` /** The live computed-field config \u2014 verified against the baked manifest (VIBE_STALE_CLIENT). */`,
|
|
1782
|
+
` readonly computed: ComputedConfig;`
|
|
1783
|
+
] : [],
|
|
1784
|
+
...nativeRls ? [
|
|
1785
|
+
` /** Native row-level security: the non-bypassing database role the runtime binds each transaction to (required on PGlite; server connections log in as it). */`,
|
|
1786
|
+
` readonly nativeRls?: { readonly runtimeRole?: string };`
|
|
1787
|
+
] : []
|
|
1200
1788
|
]
|
|
1201
1789
|
}));
|
|
1202
|
-
const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${model.name}Delegate;`);
|
|
1203
1790
|
const clientExtensionLines = baked.clientDts.length === 0 ? "" : `${baked.clientDts.join(`
|
|
1204
1791
|
`)}
|
|
1205
1792
|
`;
|
|
1206
|
-
|
|
1207
|
-
`
|
|
1208
|
-
`)}
|
|
1209
|
-
`
|
|
1210
|
-
` + `
|
|
1211
|
-
` + ` readonly $
|
|
1212
|
-
` + `
|
|
1213
|
-
` + ` readonly $
|
|
1793
|
+
if (nativeRls) {
|
|
1794
|
+
parts.push(`export type BoundVibeClientInstance = {
|
|
1795
|
+
` + instanceSurfaceLines({ view, selfType: "BoundVibeClientInstance" }) + clientExtensionLines + `};`);
|
|
1796
|
+
parts.push(`export type VibeClientInstance = {
|
|
1797
|
+
` + ` /** Bind the request's identity context \u2014 ALL of it, every declared slot \u2014 and get the client that can actually query. */
|
|
1798
|
+
` + ` readonly $withContext: (context: RlsContext) => BoundVibeClientInstance;
|
|
1799
|
+
` + ` /** Subscribe to client events ("query" fires after every statement, with the same payload as onQuery \u2014 both fire). Returns an unsubscribe function. */
|
|
1800
|
+
` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
|
|
1214
1801
|
` + ` readonly $connect: () => Promise<void>;
|
|
1215
1802
|
` + ` readonly $disconnect: () => Promise<void>;
|
|
1216
|
-
` + `
|
|
1217
|
-
` + `
|
|
1218
|
-
|
|
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.
|
|
1804
|
+
` + `};`);
|
|
1805
|
+
} else {
|
|
1806
|
+
parts.push(`export type VibeClientInstance = {
|
|
1807
|
+
` + instanceSurfaceLines({ view, selfType: "VibeClientInstance" }) + policyClientLines({ view }) + scopedClientLines({ view }) + clientExtensionLines + `};`);
|
|
1808
|
+
}
|
|
1219
1809
|
parts.push(`/** Build a typed client over a database adapter. */
|
|
1220
1810
|
export declare function VibeClient(options: VibeClientOptions): VibeClientInstance;`);
|
|
1221
1811
|
parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
|
|
@@ -1224,22 +1814,127 @@ export declare const schema: SchemaIR;`);
|
|
|
1224
1814
|
|
|
1225
1815
|
`);
|
|
1226
1816
|
}
|
|
1817
|
+
function scopedColumnCandidates(params) {
|
|
1818
|
+
return params.model.fields.filter((field) => !field.isList && (field.scalar === "String" || field.scalar === "Int" || field.scalar === "BigInt")).map((field) => field.name);
|
|
1819
|
+
}
|
|
1820
|
+
function emitScopedSection(params) {
|
|
1821
|
+
const { view } = params;
|
|
1822
|
+
if (hasNativeRls({ view }))
|
|
1823
|
+
return [];
|
|
1824
|
+
if (view.models.some((model) => model.policy !== undefined))
|
|
1825
|
+
return [];
|
|
1826
|
+
const entries = view.models.map((model) => {
|
|
1827
|
+
const arms = [`"none"`];
|
|
1828
|
+
const columns = scopedColumnCandidates({ model });
|
|
1829
|
+
if (columns.length > 0) {
|
|
1830
|
+
const union = columns.map((name) => `"${name}"`).join(" | ");
|
|
1831
|
+
arms.push(`{ readonly column: ${union}; readonly nested?: ScopedNestedMode }`);
|
|
1832
|
+
}
|
|
1833
|
+
const throughs = model.relations.filter((relation) => !relation.isList).map((relation) => relation.name);
|
|
1834
|
+
if (throughs.length > 0) {
|
|
1835
|
+
const union = throughs.map((name) => `"${name}"`).join(" | ");
|
|
1836
|
+
arms.push(`{ readonly through: ${union}; readonly nested?: ScopedNestedMode }`);
|
|
1837
|
+
}
|
|
1838
|
+
return ` readonly ${propKey({ name: model.name })}: ${arms.join(" | ")};`;
|
|
1839
|
+
});
|
|
1840
|
+
const scopedDelegateType = (model) => `${model.name}Delegate`;
|
|
1841
|
+
const delegateProps = view.models.map((model) => ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`);
|
|
1842
|
+
const modelsProps = view.models.flatMap((model) => {
|
|
1843
|
+
const byClientName = ` readonly ${propKey({ name: model.clientName })}: ${scopedDelegateType(model)};`;
|
|
1844
|
+
return model.name === model.clientName ? [byClientName] : [byClientName, ` readonly ${propKey({ name: model.name })}: ${scopedDelegateType(model)};`];
|
|
1845
|
+
});
|
|
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(`
|
|
1857
|
+
`)}
|
|
1858
|
+
` + ` /** Delegates keyed by BOTH camelCase client name and model name \u2014 the same objects as the top-level properties (dynamic access). */
|
|
1859
|
+
` + ` readonly $models: {
|
|
1860
|
+
${modelsProps.join(`
|
|
1861
|
+
`)}
|
|
1862
|
+
};
|
|
1863
|
+
` + ` /** Subscribe to client events \u2014 shared with the base client. */
|
|
1864
|
+
` + ` readonly $on: (event: "query", listener: (event: QueryEvent) => void) => () => void;
|
|
1865
|
+
` + ` /** ONE transaction; the callback receives a handle that INHERITS the scope. The array form takes un-awaited scoped delegate calls. */
|
|
1866
|
+
` + ` readonly $transaction: {
|
|
1867
|
+
` + ` <T>(fn: (tx: ScopedClientInstance) => Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
1868
|
+
` + ` <T extends readonly unknown[]>(operations: readonly [...T], options?: TransactionOptions): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>;
|
|
1869
|
+
` + ` };
|
|
1870
|
+
` + ` readonly $connect: () => Promise<void>;
|
|
1871
|
+
` + ` readonly $disconnect: () => Promise<void>;
|
|
1872
|
+
` + ` /** Field-masking views compose over the scoped handle (columns \xD7 rows). */
|
|
1873
|
+
` + ` 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).
|
|
1875
|
+
` + `};`
|
|
1876
|
+
];
|
|
1877
|
+
}
|
|
1878
|
+
function scopedClientLines(params) {
|
|
1879
|
+
if (hasNativeRls(params))
|
|
1880
|
+
return "";
|
|
1881
|
+
if (params.view.models.some((model) => model.policy !== undefined))
|
|
1882
|
+
return "";
|
|
1883
|
+
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;
|
|
1885
|
+
`;
|
|
1886
|
+
}
|
|
1887
|
+
function policyClientLines(params) {
|
|
1888
|
+
const policied = params.view.models.filter((model) => model.policy !== undefined);
|
|
1889
|
+
if (policied.length === 0)
|
|
1890
|
+
return "";
|
|
1891
|
+
const CONTEXT_SCALAR_TYPES = { String: "string", Int: "number", BigInt: "bigint" };
|
|
1892
|
+
const contextKeys = new Map;
|
|
1893
|
+
for (const model of policied) {
|
|
1894
|
+
const policy = model.policy;
|
|
1895
|
+
const field = model.fields.find((entry) => entry.name === policy.field);
|
|
1896
|
+
contextKeys.set(policy.context, field?.scalar == null ? "string" : CONTEXT_SCALAR_TYPES[field.scalar] ?? "string");
|
|
1897
|
+
}
|
|
1898
|
+
const contextType = `{ ${[...contextKeys.entries()].map(([key, type]) => `readonly ${propKey({ name: key })}: ${type};`).join(" ")} }`;
|
|
1899
|
+
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;
|
|
1903
|
+
`;
|
|
1904
|
+
}
|
|
1227
1905
|
function emitDts(params) {
|
|
1228
1906
|
const { view } = params;
|
|
1229
1907
|
const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
|
|
1908
|
+
const computed = params.computed ?? [];
|
|
1909
|
+
const hasComputed = computed.length > 0;
|
|
1910
|
+
const computedByModel = new Map;
|
|
1911
|
+
for (const entry of computed) {
|
|
1912
|
+
const list = computedByModel.get(entry.model) ?? [];
|
|
1913
|
+
list.push(entry);
|
|
1914
|
+
computedByModel.set(entry.model, list);
|
|
1915
|
+
}
|
|
1230
1916
|
const parts = [];
|
|
1231
|
-
parts.push(
|
|
1232
|
-
|
|
1917
|
+
parts.push(GENERATED_HEADER);
|
|
1918
|
+
const schemaTypeImports = [
|
|
1919
|
+
"SchemaIR",
|
|
1920
|
+
...baked.empty ? [] : ["VibeExtension"],
|
|
1921
|
+
...hasComputed ? ["ComputedConfig"] : []
|
|
1922
|
+
].sort();
|
|
1233
1923
|
const importLines = [
|
|
1234
|
-
`import type { DatabaseAdapter, QueryEvent, TransactionOptions } from "@vibeorm/runtime";`,
|
|
1235
|
-
`import type {
|
|
1236
|
-
...
|
|
1924
|
+
`import type { DatabaseAdapter, DbNow, QueryEvent, Skip, TransactionOptions, WriteValidators } from "@vibeorm/runtime";`,
|
|
1925
|
+
`import type { ${schemaTypeImports.join(", ")} } from "@vibeorm/schema";`,
|
|
1926
|
+
...jsonTypeImportLines({ view }),
|
|
1927
|
+
...baked.dtsImports,
|
|
1928
|
+
`import { ${enumEntryExports({ view }).join(", ")} } from "./enums.js";`
|
|
1237
1929
|
];
|
|
1238
1930
|
parts.push(`${importLines.join(`
|
|
1239
1931
|
`)}
|
|
1240
1932
|
|
|
1241
1933
|
` + `export { VibeError } from "@vibeorm/schema";
|
|
1242
|
-
export type { VibeErrorCode } from "@vibeorm/schema"
|
|
1934
|
+
export type { VibeErrorCode } from "@vibeorm/schema";
|
|
1935
|
+
` + `export { SKIP } from "@vibeorm/runtime";
|
|
1936
|
+
export type { Skip } from "@vibeorm/runtime";
|
|
1937
|
+
` + `export { ${enumEntryExports({ view }).join(", ")} };`);
|
|
1243
1938
|
parts.push(PREAMBLE);
|
|
1244
1939
|
parts.push(...filterSection());
|
|
1245
1940
|
for (const declared of view.enums)
|
|
@@ -1248,11 +1943,13 @@ export type { VibeErrorCode } from "@vibeorm/schema";`);
|
|
|
1248
1943
|
const modelParts = [];
|
|
1249
1944
|
for (const model of view.models) {
|
|
1250
1945
|
const extensionLines = baked.delegateDts.get(model.name);
|
|
1946
|
+
const modelComputed = computedByModel.get(model.name);
|
|
1251
1947
|
modelParts.push(emitModel({
|
|
1252
1948
|
model,
|
|
1253
1949
|
view,
|
|
1254
1950
|
registry,
|
|
1255
|
-
...extensionLines === undefined ? {} : { extensionLines }
|
|
1951
|
+
...extensionLines === undefined ? {} : { extensionLines },
|
|
1952
|
+
...modelComputed === undefined ? {} : { computed: modelComputed }
|
|
1256
1953
|
}));
|
|
1257
1954
|
}
|
|
1258
1955
|
parts.push(...modelParts);
|
|
@@ -1267,62 +1964,9 @@ export type { VibeErrorCode } from "@vibeorm/schema";`);
|
|
|
1267
1964
|
parts.push(...baked.moduleDts);
|
|
1268
1965
|
}
|
|
1269
1966
|
parts.push(...emitViewSection({ view }));
|
|
1270
|
-
parts.push(
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
`)}
|
|
1274
|
-
`;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// packages/generator/src/emit-js.ts
|
|
1278
|
-
function emitJs(params) {
|
|
1279
|
-
const { view } = params;
|
|
1280
|
-
const baked = params.baked ?? EMPTY_BAKED_EXTENSIONS;
|
|
1281
|
-
const parts = [];
|
|
1282
|
-
parts.push(`// Generated by VibeORM (@vibeorm/generator) \u2014 DO NOT EDIT.
|
|
1283
|
-
// Regenerate with \`vibeorm generate\` after every schema change.`);
|
|
1284
|
-
parts.push(`import { createClient } from "@vibeorm/runtime";`);
|
|
1285
|
-
if (baked.jsImports.length > 0)
|
|
1286
|
-
parts.push(baked.jsImports.join(`
|
|
1287
|
-
`));
|
|
1288
|
-
parts.push(`export { VibeError } from "@vibeorm/schema";`);
|
|
1289
|
-
parts.push(`/** The normalized Schema IR this client was generated from (plain data). */
|
|
1290
|
-
export const schema = ${stableStringify({ value: view.schema })};`);
|
|
1291
|
-
for (const declared of view.enums) {
|
|
1292
|
-
const entries = declared.values.map((value) => ` ${tsString({ value })}: ${tsString({ value })},`).join(`
|
|
1293
|
-
`);
|
|
1294
|
-
parts.push(`export const ${declared.name} = Object.freeze({
|
|
1295
|
-
${entries}
|
|
1296
|
-
});`);
|
|
1297
|
-
}
|
|
1298
|
-
if (baked.empty) {
|
|
1299
|
-
parts.push(`/** Build a typed client over a database adapter. */
|
|
1300
|
-
` + `export function VibeClient(options) {
|
|
1301
|
-
` + ` const { adapter, ...clientOptions } = options;
|
|
1302
|
-
` + ` return createClient({ schema, adapter, options: clientOptions });
|
|
1303
|
-
` + `}`);
|
|
1304
|
-
return `${parts.join(`
|
|
1305
|
-
|
|
1306
|
-
`)}
|
|
1307
|
-
`;
|
|
1308
|
-
}
|
|
1309
|
-
parts.push(`/** Extension manifest \u2014 verified against the live extension list (VIBE_STALE_CLIENT). */
|
|
1310
|
-
` + `const extensionManifest = ${stableStringify({ value: baked.manifest })};`);
|
|
1311
|
-
parts.push(`/** Baked extension surfaces (extensions.md D9). */
|
|
1312
|
-
` + `const extensionMounts = [
|
|
1313
|
-
${baked.mounts.map((mount) => ` ${mount},`).join(`
|
|
1314
|
-
`)}
|
|
1315
|
-
];`);
|
|
1316
|
-
parts.push(`/** Build a typed client over a database adapter. */
|
|
1317
|
-
` + `export function VibeClient(options) {
|
|
1318
|
-
` + ` const { adapter, extensions, ...clientOptions } = options;
|
|
1319
|
-
` + ` return createClient({
|
|
1320
|
-
` + ` schema,
|
|
1321
|
-
` + ` adapter,
|
|
1322
|
-
` + ` options: clientOptions,
|
|
1323
|
-
` + ` extensions: { manifest: extensionManifest, mounts: extensionMounts, instances: extensions ?? [] },
|
|
1324
|
-
` + ` });
|
|
1325
|
-
` + `}`);
|
|
1967
|
+
parts.push(...emitScopedSection({ view }));
|
|
1968
|
+
parts.push(...emitRlsSection({ view }));
|
|
1969
|
+
parts.push(emitClient({ view, baked, hasComputed }));
|
|
1326
1970
|
return `${parts.join(`
|
|
1327
1971
|
|
|
1328
1972
|
`)}
|
|
@@ -1330,13 +1974,31 @@ ${baked.mounts.map((mount) => ` ${mount},`).join(`
|
|
|
1330
1974
|
}
|
|
1331
1975
|
|
|
1332
1976
|
// packages/generator/src/schema-view.ts
|
|
1333
|
-
import { assertValidSchemaIR, isEnumType } from "@vibeorm/schema";
|
|
1977
|
+
import { VibeError as VibeError4, assertValidSchemaIR, enumStorageOf, isEnumType } from "@vibeorm/schema";
|
|
1978
|
+
import { postgresDialect, renderRlsPolicy } from "@vibeorm/sql";
|
|
1979
|
+
var JSON_TYPE_RE = /@type\(\s*([A-Za-z_$][A-Za-z0-9_$]*)\s+from\s+"([^"]+)"\s*\)/;
|
|
1980
|
+
function parseJsonTypeAnnotation(params) {
|
|
1981
|
+
if (params.comment === undefined)
|
|
1982
|
+
return null;
|
|
1983
|
+
const match = JSON_TYPE_RE.exec(params.comment);
|
|
1984
|
+
if (match === null)
|
|
1985
|
+
return null;
|
|
1986
|
+
return { name: match[1], from: match[2] };
|
|
1987
|
+
}
|
|
1334
1988
|
function byName(left, right) {
|
|
1335
1989
|
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
1336
1990
|
}
|
|
1337
1991
|
function buildFieldView(params) {
|
|
1338
|
-
const { field } = params;
|
|
1992
|
+
const { field, model } = params;
|
|
1339
1993
|
const enumRef = isEnumType(field.type);
|
|
1994
|
+
const annotation = parseJsonTypeAnnotation({ comment: field.documentation });
|
|
1995
|
+
if (annotation !== null && (enumRef || field.type !== "Json")) {
|
|
1996
|
+
throw new VibeError4({
|
|
1997
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1998
|
+
message: `@type(...) annotation on ${model}.${field.name} \u2014 typed JSON is only valid on Json fields`,
|
|
1999
|
+
meta: { model, field: field.name }
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
1340
2002
|
return {
|
|
1341
2003
|
name: field.name,
|
|
1342
2004
|
scalar: enumRef ? null : field.type,
|
|
@@ -1346,7 +2008,8 @@ function buildFieldView(params) {
|
|
|
1346
2008
|
isId: field.isId,
|
|
1347
2009
|
isUnique: field.isUnique,
|
|
1348
2010
|
hasDefault: field.default !== undefined,
|
|
1349
|
-
isUpdatedAt: field.isUpdatedAt
|
|
2011
|
+
isUpdatedAt: field.isUpdatedAt,
|
|
2012
|
+
...annotation === null ? {} : { jsonType: annotation }
|
|
1350
2013
|
};
|
|
1351
2014
|
}
|
|
1352
2015
|
function resolveInverse(params) {
|
|
@@ -1360,8 +2023,8 @@ function buildUniques(params) {
|
|
|
1360
2023
|
const fieldByName = new Map(fields.map((field) => [field.name, field]));
|
|
1361
2024
|
const members = [];
|
|
1362
2025
|
const seen = new Set;
|
|
1363
|
-
const add = (names) => {
|
|
1364
|
-
const key = names.join("_");
|
|
2026
|
+
const add = (names, nullsNotDistinct, clientName) => {
|
|
2027
|
+
const key = clientName ?? names.join("_");
|
|
1365
2028
|
if (seen.has(key))
|
|
1366
2029
|
return;
|
|
1367
2030
|
const resolved = [];
|
|
@@ -1372,24 +2035,28 @@ function buildUniques(params) {
|
|
|
1372
2035
|
resolved.push(field);
|
|
1373
2036
|
}
|
|
1374
2037
|
seen.add(key);
|
|
1375
|
-
members.push({ key, fields: resolved });
|
|
2038
|
+
members.push({ key, fields: resolved, nullsNotDistinct });
|
|
1376
2039
|
};
|
|
1377
2040
|
for (const field of fields) {
|
|
1378
2041
|
if (field.isId || field.isUnique)
|
|
1379
|
-
add([field.name]);
|
|
2042
|
+
add([field.name], false);
|
|
1380
2043
|
}
|
|
1381
|
-
if (model.primaryKey !== undefined
|
|
1382
|
-
|
|
2044
|
+
if (model.primaryKey !== undefined) {
|
|
2045
|
+
if (model.primaryKey.fields.length === 1)
|
|
2046
|
+
add(model.primaryKey.fields, false);
|
|
2047
|
+
else
|
|
2048
|
+
add(model.primaryKey.fields, false, model.primaryKey.clientName);
|
|
1383
2049
|
}
|
|
1384
2050
|
for (const index of model.indexes) {
|
|
1385
|
-
if (index.kind === "unique")
|
|
1386
|
-
add(index.fields.map((field) => field.name));
|
|
2051
|
+
if (index.kind === "unique" && index.where === undefined) {
|
|
2052
|
+
add(index.fields.map((field) => field.name), index.nullsNotDistinct === true, index.clientName);
|
|
2053
|
+
}
|
|
1387
2054
|
}
|
|
1388
2055
|
return members;
|
|
1389
2056
|
}
|
|
1390
2057
|
function buildModelView(params) {
|
|
1391
2058
|
const { model, modelIRByName } = params;
|
|
1392
|
-
const fields = model.fields.filter((field) => field.isUnsupported !== true).map((field) => buildFieldView({ field }));
|
|
2059
|
+
const fields = model.fields.filter((field) => field.isUnsupported !== true).map((field) => buildFieldView({ field, model: model.name }));
|
|
1393
2060
|
const fkFieldNames = new Set;
|
|
1394
2061
|
const relations = model.relations.map((relation) => {
|
|
1395
2062
|
for (const fk of relation.fields ?? [])
|
|
@@ -1412,23 +2079,38 @@ function buildModelView(params) {
|
|
|
1412
2079
|
relations,
|
|
1413
2080
|
uniques: buildUniques({ model, fields }),
|
|
1414
2081
|
fkFieldNames,
|
|
1415
|
-
isView: model.isView
|
|
2082
|
+
isView: model.isView,
|
|
2083
|
+
...model.policy === undefined ? {} : { policy: model.policy }
|
|
1416
2084
|
};
|
|
1417
2085
|
}
|
|
2086
|
+
function assertRlsRenderable(params) {
|
|
2087
|
+
for (const model of params.schema.models) {
|
|
2088
|
+
if (model.rls === undefined || model.rls === "unprotected")
|
|
2089
|
+
continue;
|
|
2090
|
+
renderRlsPolicy({ dialect: postgresDialect, schema: params.schema, model: model.name });
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
1418
2093
|
function buildSchemaView(params) {
|
|
1419
2094
|
const { schema } = params;
|
|
1420
2095
|
assertValidSchemaIR({ schema });
|
|
2096
|
+
if (schema.rls !== undefined)
|
|
2097
|
+
assertRlsRenderable({ schema });
|
|
1421
2098
|
const normalized = {
|
|
1422
2099
|
datasource: schema.datasource,
|
|
1423
2100
|
models: [...schema.models].sort(byName),
|
|
1424
2101
|
enums: [...schema.enums].sort(byName),
|
|
1425
|
-
extensions: [...schema.extensions].sort(byName)
|
|
2102
|
+
extensions: [...schema.extensions].sort(byName),
|
|
2103
|
+
...schema.rls === undefined ? {} : { rls: schema.rls }
|
|
1426
2104
|
};
|
|
1427
2105
|
const modelIRByName = new Map(normalized.models.map((model) => [model.name, model]));
|
|
1428
2106
|
const models = normalized.models.map((model) => buildModelView({ model, modelIRByName }));
|
|
1429
2107
|
return {
|
|
1430
2108
|
schema: normalized,
|
|
1431
|
-
enums: normalized.enums.map((declared) => ({
|
|
2109
|
+
enums: normalized.enums.map((declared) => ({
|
|
2110
|
+
name: declared.name,
|
|
2111
|
+
values: declared.values,
|
|
2112
|
+
storage: enumStorageOf({ enumDef: declared })
|
|
2113
|
+
})),
|
|
1432
2114
|
models,
|
|
1433
2115
|
modelByName: new Map(models.map((model) => [model.name, model]))
|
|
1434
2116
|
};
|
|
@@ -1443,9 +2125,16 @@ async function generateClient(params) {
|
|
|
1443
2125
|
const { schema, outDir } = params;
|
|
1444
2126
|
const view = buildSchemaView({ schema });
|
|
1445
2127
|
const baked = bakeExtensions({ schema: view.schema, extensions: params.extensions ?? [] });
|
|
2128
|
+
let computed = [];
|
|
2129
|
+
if (params.computed !== undefined && Object.keys(params.computed).length > 0) {
|
|
2130
|
+
validateComputedConfig({ schema, computed: params.computed });
|
|
2131
|
+
computed = buildComputedManifest({ computed: params.computed });
|
|
2132
|
+
}
|
|
1446
2133
|
const outputs = [
|
|
1447
|
-
{ name: "index.js", content: emitJs({ view, baked }) },
|
|
1448
|
-
{ name: "index.d.ts", content: emitDts({ view, baked }) },
|
|
2134
|
+
{ name: "index.js", content: emitJs({ view, baked, computed }) },
|
|
2135
|
+
{ name: "index.d.ts", content: emitDts({ view, baked, computed }) },
|
|
2136
|
+
{ name: "enums.js", content: emitEnumsJs({ view }) },
|
|
2137
|
+
{ name: "enums.d.ts", content: emitEnumsDts({ view }) },
|
|
1449
2138
|
{ name: "README.md", content: README }
|
|
1450
2139
|
];
|
|
1451
2140
|
await mkdir(outDir, { recursive: true });
|
|
@@ -1462,4 +2151,4 @@ export {
|
|
|
1462
2151
|
bakeExtensions
|
|
1463
2152
|
};
|
|
1464
2153
|
|
|
1465
|
-
//# debugId=
|
|
2154
|
+
//# debugId=AC114FAAB4DBE66364756E2164756E21
|