@vibeorm/generator 1.1.2 → 1.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeorm/generator",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "TypeScript client generator for VibeORM — produces typed delegates, inputs, and Zod schemas from a Prisma schema",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -37,6 +37,6 @@
37
37
  "bun": ">=1.1.0"
38
38
  },
39
39
  "dependencies": {
40
- "@vibeorm/parser": "1.1.2"
40
+ "@vibeorm/parser": "1.1.3"
41
41
  }
42
42
  }
@@ -1,4 +1,4 @@
1
- import type { Schema } from "@vibeorm/parser";
1
+ import type { Model, Schema } from "@vibeorm/parser";
2
2
  import { fileHeader, toCamelCase } from "../utils.ts";
3
3
 
4
4
  /**
@@ -49,7 +49,7 @@ export function generateClient(params: { schema: Schema }): string {
49
49
 
50
50
  // Generate the VibeClient type (intersection of all delegates)
51
51
  const clientProperties = schema.models
52
- .map((m) => ` ${toCamelCase({ str: m.name })}: ${m.name}Delegate;`)
52
+ .map((m) => generateClientProperty({ model: m }))
53
53
  .join("\n");
54
54
 
55
55
  parts.push(`export type VibeClientInstance = {
@@ -78,6 +78,38 @@ ${clientProperties}
78
78
  return parts.join("\n");
79
79
  }
80
80
 
81
+ function generateClientProperty(params: { model: Model }): string {
82
+ const { model } = params;
83
+ const modelVar = toCamelCase({ str: model.name });
84
+
85
+ return ` /**
86
+ * Operations for the ${model.name} model.
87
+ *
88
+ * @example
89
+ * const rows = await db.${modelVar}.findMany();
90
+ *
91
+ * @example
92
+ * const filteredRows = await db.${modelVar}.findMany({
93
+ * where: {
94
+ * // Add ${model.name}-specific filters here
95
+ * },
96
+ * });
97
+ *
98
+ * @example
99
+ * const page = await db.${modelVar}.findMany({
100
+ * where: {
101
+ * // Add ${model.name}-specific filters here
102
+ * },
103
+ * orderBy: {
104
+ * // e.g. createdAt: "desc"
105
+ * },
106
+ * take: 20,
107
+ * skip: 0,
108
+ * });
109
+ */
110
+ ${modelVar}: ${model.name}Delegate;`;
111
+ }
112
+
81
113
  function generateModelMeta(params: { schema: Schema }): string {
82
114
  const { schema } = params;
83
115
 
@@ -1,4 +1,4 @@
1
- import type { Model, Schema, ScalarField, EnumField } from "@vibeorm/parser";
1
+ import type { Model, Schema, ScalarField } from "@vibeorm/parser";
2
2
  import { fileHeader, toCamelCase } from "../utils.ts";
3
3
 
4
4
  const NUMERIC_PRISMA_TYPES = new Set(["Int", "Float", "Decimal", "BigInt"]);
@@ -47,7 +47,7 @@ export function generateDelegates(params: { schema: Schema }): string {
47
47
  const extraImports = hasJsonField ? ", JsonValue" : "";
48
48
  parts.push(`import type { ${payloadImports}${extraImports} } from "./models.ts";`);
49
49
 
50
- parts.push(`import type { GetResult } from "./result.ts";`);
50
+ parts.push(`import type { GetResult, Exact } from "./result.ts";`);
51
51
 
52
52
  // Import enums if any models have enum fields
53
53
  if (schema.enums.length > 0) {
@@ -69,68 +69,251 @@ export function generateDelegates(params: { schema: Schema }): string {
69
69
  function generateModelDelegate(params: { model: Model }): string {
70
70
  const { model } = params;
71
71
  const p = `$${model.name}Payload`;
72
+ const modelVar = toCamelCase({ str: model.name });
72
73
 
73
- return `export type ${model.name}Delegate = {
74
+ const findManyWhereBlock = buildFindManyWhereBlock({ model });
75
+ const uniqueWhereBlock = buildUniqueWhereBlock({ model });
76
+ const createDataBlock = buildObjectBlock({
77
+ key: "data",
78
+ lines: getExampleDataLines({ model }),
79
+ });
80
+ const updateDataBlock = buildObjectBlock({
81
+ key: "data",
82
+ lines: getExampleUpdateLines({ model }),
83
+ });
84
+ const createManyDataBlock = buildArrayOfObjectsBlock({
85
+ key: "data",
86
+ lines: getExampleDataLines({ model }),
87
+ });
88
+ const upsertCreateBlock = buildObjectBlock({
89
+ key: "create",
90
+ lines: getExampleDataLines({ model }),
91
+ });
92
+ const upsertUpdateBlock = buildObjectBlock({
93
+ key: "update",
94
+ lines: getExampleUpdateLines({ model }),
95
+ });
96
+ const includeBlock = buildIncludeBlock({ model });
97
+ const orderByField = getOrderByFieldName({ model });
98
+
99
+ return `/**
100
+ * Delegate API for the ${model.name} model.
101
+ * Access this via \`db.${modelVar}\`.
102
+ */
103
+ export type ${model.name}Delegate = {
104
+ /**
105
+ * Find zero or more ${model.name} records.
106
+ *
107
+ * @example
108
+ * const rows = await db.${modelVar}.findMany();
109
+ *
110
+ * @example
111
+ * const filteredRows = await db.${modelVar}.findMany({
112
+ ${findManyWhereBlock}
113
+ * });
114
+ *
115
+ * @example
116
+ * const page = await db.${modelVar}.findMany({
117
+ ${findManyWhereBlock}${includeBlock}
118
+ * orderBy: {
119
+ * ${orderByField}: "desc",
120
+ * },
121
+ * take: 20,
122
+ * skip: 0,
123
+ * });
124
+ */
74
125
  findMany<T extends ${model.name}FindManyArgs>(
75
- args?: T,
126
+ args?: Exact<T, ${model.name}FindManyArgs>,
76
127
  ): Promise<GetResult<${p}, T, "findMany">>;
77
128
 
129
+ /**
130
+ * Find the first ${model.name} record matching a filter.
131
+ *
132
+ * @example
133
+ * const row = await db.${modelVar}.findFirst({
134
+ ${findManyWhereBlock}
135
+ * orderBy: {
136
+ * ${orderByField}: "desc",
137
+ * },
138
+ * });
139
+ */
78
140
  findFirst<T extends ${model.name}FindFirstArgs>(
79
- args?: T,
141
+ args?: Exact<T, ${model.name}FindFirstArgs>,
80
142
  ): Promise<GetResult<${p}, T, "findFirst">>;
81
143
 
144
+ /**
145
+ * Find one ${model.name} record by a unique field.
146
+ *
147
+ * @example
148
+ * const row = await db.${modelVar}.findUnique({
149
+ ${uniqueWhereBlock}
150
+ * });
151
+ */
82
152
  findUnique<T extends ${model.name}FindUniqueArgs>(
83
- args: T,
153
+ args: Exact<T, ${model.name}FindUniqueArgs>,
84
154
  ): Promise<GetResult<${p}, T, "findUnique">>;
85
155
 
156
+ /**
157
+ * Find one ${model.name} record by a unique field, or throw if not found.
158
+ *
159
+ * @example
160
+ * const row = await db.${modelVar}.findUniqueOrThrow({
161
+ ${uniqueWhereBlock}
162
+ * });
163
+ */
86
164
  findUniqueOrThrow<T extends ${model.name}FindUniqueArgs>(
87
- args: T,
165
+ args: Exact<T, ${model.name}FindUniqueArgs>,
88
166
  ): Promise<GetResult<${p}, T, "findUniqueOrThrow">>;
89
167
 
168
+ /**
169
+ * Find the first ${model.name} record matching a filter, or throw if not found.
170
+ */
90
171
  findFirstOrThrow<T extends ${model.name}FindFirstArgs>(
91
- args?: T,
172
+ args?: Exact<T, ${model.name}FindFirstArgs>,
92
173
  ): Promise<GetResult<${p}, T, "findFirstOrThrow">>;
93
174
 
175
+ /**
176
+ * Create one ${model.name} record.
177
+ *
178
+ * @example
179
+ * const row = await db.${modelVar}.create({
180
+ ${createDataBlock}
181
+ * });
182
+ */
94
183
  create<T extends ${model.name}CreateArgs>(
95
- args: T,
184
+ args: Exact<T, ${model.name}CreateArgs>,
96
185
  ): Promise<GetResult<${p}, T, "create">>;
97
186
 
187
+ /**
188
+ * Create multiple ${model.name} records in one query.
189
+ *
190
+ * @example
191
+ * const result = await db.${modelVar}.createMany({
192
+ ${createManyDataBlock}
193
+ * skipDuplicates: true,
194
+ * });
195
+ */
98
196
  createMany(
99
197
  args: ${model.name}CreateManyArgs,
100
198
  ): Promise<{ count: number }>;
101
199
 
200
+ /**
201
+ * Create multiple ${model.name} records and return selected fields.
202
+ *
203
+ * @example
204
+ * const rows = await db.${modelVar}.createManyAndReturn({
205
+ ${createManyDataBlock}
206
+ * select: {
207
+ * // Choose fields to return
208
+ * },
209
+ * });
210
+ */
102
211
  createManyAndReturn<T extends ${model.name}CreateManyAndReturnArgs>(
103
- args: T,
212
+ args: Exact<T, ${model.name}CreateManyAndReturnArgs>,
104
213
  ): Promise<GetResult<${p}, T, "createManyAndReturn">>;
105
214
 
215
+ /**
216
+ * Update one ${model.name} record.
217
+ *
218
+ * @example
219
+ * const row = await db.${modelVar}.update({
220
+ ${uniqueWhereBlock}
221
+ ${updateDataBlock}
222
+ * });
223
+ */
106
224
  update<T extends ${model.name}UpdateArgs>(
107
- args: T,
225
+ args: Exact<T, ${model.name}UpdateArgs>,
108
226
  ): Promise<GetResult<${p}, T, "update">>;
109
227
 
228
+ /**
229
+ * Update one ${model.name} record if it exists, or create it otherwise.
230
+ *
231
+ * @example
232
+ * const row = await db.${modelVar}.upsert({
233
+ ${uniqueWhereBlock}
234
+ ${upsertCreateBlock}
235
+ ${upsertUpdateBlock}
236
+ * });
237
+ */
110
238
  upsert<T extends ${model.name}UpsertArgs>(
111
- args: T,
239
+ args: Exact<T, ${model.name}UpsertArgs>,
112
240
  ): Promise<GetResult<${p}, T, "upsert">>;
113
241
 
242
+ /**
243
+ * Delete one ${model.name} record by a unique field.
244
+ *
245
+ * @example
246
+ * const row = await db.${modelVar}.delete({
247
+ ${uniqueWhereBlock}
248
+ * });
249
+ */
114
250
  delete<T extends ${model.name}DeleteArgs>(
115
- args: T,
251
+ args: Exact<T, ${model.name}DeleteArgs>,
116
252
  ): Promise<GetResult<${p}, T, "delete">>;
117
253
 
254
+ /**
255
+ * Delete multiple ${model.name} records.
256
+ *
257
+ * @example
258
+ * const result = await db.${modelVar}.deleteMany({
259
+ ${findManyWhereBlock}
260
+ * });
261
+ */
118
262
  deleteMany(
119
263
  args?: ${model.name}DeleteManyArgs,
120
264
  ): Promise<{ count: number }>;
121
265
 
266
+ /**
267
+ * Update multiple ${model.name} records.
268
+ *
269
+ * @example
270
+ * const result = await db.${modelVar}.updateMany({
271
+ ${findManyWhereBlock}
272
+ ${updateDataBlock}
273
+ * });
274
+ */
122
275
  updateMany(
123
276
  args: ${model.name}UpdateManyArgs,
124
277
  ): Promise<{ count: number }>;
125
278
 
279
+ /**
280
+ * Count ${model.name} records.
281
+ *
282
+ * @example
283
+ * const total = await db.${modelVar}.count({
284
+ ${findManyWhereBlock}
285
+ * });
286
+ */
126
287
  count(
127
288
  args?: ${model.name}CountArgs,
128
289
  ): Promise<number>;
129
290
 
291
+ /**
292
+ * Run aggregate calculations on ${model.name} records.
293
+ *
294
+ * @example
295
+ * const stats = await db.${modelVar}.aggregate({
296
+ ${findManyWhereBlock}
297
+ * _count: true,
298
+ * _avg: {
299
+ * // Choose numeric fields
300
+ * },
301
+ * });
302
+ */
130
303
  aggregate(
131
304
  args: ${model.name}AggregateArgs,
132
305
  ): Promise<Aggregate${model.name}Result>;
133
306
 
307
+ /**
308
+ * Group ${model.name} records by scalar fields.
309
+ *
310
+ * @example
311
+ * const grouped = await db.${modelVar}.groupBy({
312
+ * by: ["${orderByField}"],
313
+ ${findManyWhereBlock}
314
+ * _count: true,
315
+ * });
316
+ */
134
317
  groupBy(
135
318
  args: ${model.name}GroupByArgs,
136
319
  ): Promise<${model.name}GroupByResult[]>;
@@ -138,6 +321,194 @@ function generateModelDelegate(params: { model: Model }): string {
138
321
  `;
139
322
  }
140
323
 
324
+ function buildObjectBlock(params: { key: string; lines: string[] }): string {
325
+ const { key, lines } = params;
326
+ const safeLines = lines.length > 0 ? lines : ["// Add model fields here"];
327
+ return ` * ${key}: {
328
+ ${safeLines.map((line) => ` * ${line}`).join("\n")}
329
+ * },`;
330
+ }
331
+
332
+ function buildArrayOfObjectsBlock(params: { key: string; lines: string[] }): string {
333
+ const { key, lines } = params;
334
+ const safeLines = lines.length > 0 ? lines : ["// Add model fields here"];
335
+ return ` * ${key}: [
336
+ * {
337
+ ${safeLines.map((line) => ` * ${line}`).join("\n")}
338
+ * },
339
+ * ],`;
340
+ }
341
+
342
+ function buildFindManyWhereBlock(params: { model: Model }): string {
343
+ return buildObjectBlock({
344
+ key: "where",
345
+ lines: getFindManyWhereLines({ model: params.model }),
346
+ });
347
+ }
348
+
349
+ function buildUniqueWhereBlock(params: { model: Model }): string {
350
+ return buildObjectBlock({
351
+ key: "where",
352
+ lines: getUniqueWhereLines({ model: params.model }),
353
+ });
354
+ }
355
+
356
+ function buildIncludeBlock(params: { model: Model }): string {
357
+ const relationField = params.model.fields.find((f) => f.kind === "relation");
358
+ if (!relationField) return "";
359
+ return `
360
+ * include: {
361
+ * ${relationField.name}: true,
362
+ * },`;
363
+ }
364
+
365
+ function getFindManyWhereLines(params: { model: Model }): string[] {
366
+ const field = getPreferredWhereField({ model: params.model });
367
+ if (!field) {
368
+ return ["// Add model-specific filters here"];
369
+ }
370
+
371
+ const fieldNameLower = field.name.toLowerCase();
372
+ if (field.prismaType === "String") {
373
+ if (fieldNameLower.includes("email")) {
374
+ return [`${field.name}: { endsWith: "@example.com" },`];
375
+ }
376
+ return [`${field.name}: { contains: "sample" },`];
377
+ }
378
+
379
+ if (field.prismaType === "DateTime") {
380
+ return [`${field.name}: { gte: new Date("2024-01-01T00:00:00.000Z") },`];
381
+ }
382
+
383
+ if (field.prismaType === "Boolean") {
384
+ return [`${field.name}: true,`];
385
+ }
386
+
387
+ if (field.prismaType === "BigInt") {
388
+ return [`${field.name}: { gte: 1n },`];
389
+ }
390
+
391
+ if (field.prismaType === "Decimal") {
392
+ return [`${field.name}: { gte: "1.00" },`];
393
+ }
394
+
395
+ if (field.prismaType === "Int" || field.prismaType === "Float") {
396
+ return [`${field.name}: { gte: 1 },`];
397
+ }
398
+
399
+ if (field.prismaType === "Json") {
400
+ return [`${field.name}: { equals: { key: "value" } },`];
401
+ }
402
+
403
+ if (field.prismaType === "Bytes") {
404
+ return [`${field.name}: { equals: new Uint8Array([1, 2, 3]) },`];
405
+ }
406
+
407
+ return [`${field.name}: { equals: ${getSampleValueForScalarField({ field })} },`];
408
+ }
409
+
410
+ function getUniqueWhereLines(params: { model: Model }): string[] {
411
+ const uniqueField = params.model.fields.find(
412
+ (f): f is ScalarField =>
413
+ f.kind === "scalar" && !f.isList && (f.isId || f.isUnique)
414
+ );
415
+
416
+ if (!uniqueField) {
417
+ return ["// Provide a unique field (for example id or email)"];
418
+ }
419
+
420
+ return [`${uniqueField.name}: ${getSampleValueForScalarField({ field: uniqueField })},`];
421
+ }
422
+
423
+ function getExampleDataLines(params: { model: Model }): string[] {
424
+ const candidates = params.model.fields.filter(
425
+ (f): f is ScalarField =>
426
+ f.kind === "scalar" &&
427
+ !f.isList &&
428
+ !f.isId &&
429
+ !f.isUpdatedAt &&
430
+ !f.default
431
+ );
432
+
433
+ const selected = candidates.length > 0
434
+ ? candidates
435
+ : params.model.fields.filter(
436
+ (f): f is ScalarField =>
437
+ f.kind === "scalar" && !f.isList && !f.isId && !f.isUpdatedAt
438
+ );
439
+
440
+ if (selected.length === 0) {
441
+ return ["// Add model fields here"];
442
+ }
443
+
444
+ return selected.slice(0, 2).map((field) => {
445
+ return `${field.name}: ${getSampleValueForScalarField({ field })},`;
446
+ });
447
+ }
448
+
449
+ function getExampleUpdateLines(params: { model: Model }): string[] {
450
+ const selected = params.model.fields.filter(
451
+ (f): f is ScalarField =>
452
+ f.kind === "scalar" && !f.isList && !f.isId && !f.isUpdatedAt
453
+ );
454
+
455
+ if (selected.length === 0) {
456
+ return ["// Add fields to update"];
457
+ }
458
+
459
+ return selected.slice(0, 2).map((field) => {
460
+ return `${field.name}: ${getSampleValueForScalarField({ field })},`;
461
+ });
462
+ }
463
+
464
+ function getPreferredWhereField(params: { model: Model }): ScalarField | undefined {
465
+ const fields = params.model.fields.filter(
466
+ (f): f is ScalarField => f.kind === "scalar" && !f.isList
467
+ );
468
+
469
+ return fields.find((f) => !f.isId && f.prismaType === "String")
470
+ ?? fields.find((f) => !f.isId && f.prismaType === "DateTime")
471
+ ?? fields.find((f) => !f.isId && NUMERIC_PRISMA_TYPES.has(f.prismaType))
472
+ ?? fields.find((f) => !f.isId && f.prismaType === "Boolean")
473
+ ?? fields.find((f) => !f.isId)
474
+ ?? fields[0];
475
+ }
476
+
477
+ function getOrderByFieldName(params: { model: Model }): string {
478
+ const scalarFields = params.model.fields.filter(
479
+ (f): f is ScalarField => f.kind === "scalar" && !f.isList
480
+ );
481
+
482
+ const preferred = scalarFields.find((f) => f.name === "createdAt")
483
+ ?? scalarFields.find((f) => f.name === "updatedAt")
484
+ ?? scalarFields.find((f) => f.isId)
485
+ ?? scalarFields[0];
486
+
487
+ return preferred?.name ?? "id";
488
+ }
489
+
490
+ function getSampleValueForScalarField(params: { field: ScalarField }): string {
491
+ const { field } = params;
492
+ const fieldNameLower = field.name.toLowerCase();
493
+
494
+ if (field.prismaType === "String") {
495
+ if (fieldNameLower.includes("email")) return '"user@example.com"';
496
+ if (fieldNameLower.includes("name")) return '"Example"';
497
+ if (fieldNameLower.includes("title")) return '"Example title"';
498
+ return '"sample"';
499
+ }
500
+
501
+ if (field.prismaType === "Boolean") return "true";
502
+ if (field.prismaType === "Int" || field.prismaType === "Float") return "1";
503
+ if (field.prismaType === "Decimal") return '"1.00"';
504
+ if (field.prismaType === "BigInt") return "1n";
505
+ if (field.prismaType === "DateTime") return 'new Date("2024-01-01T00:00:00.000Z")';
506
+ if (field.prismaType === "Json") return '{ key: "value" }';
507
+ if (field.prismaType === "Bytes") return "new Uint8Array([1, 2, 3])";
508
+
509
+ return "undefined";
510
+ }
511
+
141
512
  function generateAggregateResultType(params: { model: Model }): string {
142
513
  const { model } = params;
143
514
 
@@ -36,6 +36,25 @@ type IsAny<T> = 0 extends 1 & T ? true : false;
36
36
  /** Flatten intersection types for cleaner hover tooltips */
37
37
  type Compute<T> = T extends Function ? T : { [K in keyof T]: T[K] } & unknown;
38
38
 
39
+ /**
40
+ * Enforces strict type safety by recursively rejecting excess properties.
41
+ *
42
+ * TypeScript's structural subtyping allows objects to have extra properties
43
+ * when passed through generics (\`<T extends Args>\`). This utility type
44
+ * maps any key in A that is NOT in W to \`never\`, causing a compile-time
45
+ * error when unknown keys (e.g. typos in where/select/include) are used.
46
+ *
47
+ * Primitives, arrays, functions, and Dates are passed through unchanged
48
+ * to preserve literal types for return type narrowing via GetFindResult.
49
+ */
50
+ export type Exact<A, W> = W extends unknown
51
+ ? A extends W
52
+ ? A extends Record<string, unknown>
53
+ ? { [K in keyof A]: K extends keyof W ? Exact<A[K], W[K]> : never }
54
+ : A
55
+ : W
56
+ : never;
57
+
39
58
  /** Extract scalar fields from a payload (default selection) */
40
59
  type DefaultSelection<P extends OperationPayload> = P["scalars"];
41
60