@postxl/generator 0.33.4 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,7 @@ const imports_1 = require("../../lib/imports");
28
28
  const meta_1 = require("../../lib/meta");
29
29
  const fields_1 = require("../../lib/schema/fields");
30
30
  const Types = __importStar(require("../../lib/schema/types"));
31
+ const ast_1 = require("../../lib/utils/ast");
31
32
  const repository_generator_1 = require("./repository.generator");
32
33
  /**
33
34
  * Generates business logic for a given model.
@@ -188,72 +189,114 @@ export class ${meta.businessLogic.serviceClassName} {
188
189
  * Deletes the ${meta.userFriendlyName} with the given id.
189
190
  *
190
191
  * Note: This does NOT delete any linked items.
191
- * If the items is a dependency of another item, the deletion will fail!
192
+ * If the item is a dependency of another item, the deletion will fail!
192
193
  */
193
194
  public async delete(id: ${methodTypeSignatures.delete.parameters[0]}): ${methodTypeSignatures.delete.returnType} {
194
195
  return this.${modelRepositoryVariableName}.delete(id)
195
196
  }
196
197
  }
197
198
 
198
- const compare = (a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) => {
199
- switch (field) {
200
- ${model.fields.filter((f) => f.kind === 'scalar' && f.tsTypeName === 'string').length === 0
201
- ? ''
202
- : `${model.fields
203
- .filter((f) => f.kind === 'scalar' && f.tsTypeName === 'string')
204
- .map((f) => `case '${f.name}':`)
205
- .join('\n')}
206
- return (a[field] || '').localeCompare(b[field] || '')`}
207
- default:
208
- return 0
209
- }
199
+ // Utility Functions
200
+
201
+ ${_createModelCompareFn({ model })}
202
+
203
+ ${_createModelFilterFn({ model })}
204
+ `;
210
205
  }
206
+ exports.generateModelBusinessLogic = generateModelBusinessLogic;
207
+ /**
208
+ * Generates a utility filter function for the given model that can be used to filter out instances
209
+ * of a model using a given operator on a given field.
210
+ */
211
+ function _createModelFilterFn({ model }) {
212
+ const stringFieldFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
213
+ return {
214
+ match: `"${f.name}"`,
215
+ block: `
216
+ if (typeof value !== 'string') {
217
+ return false
218
+ }
211
219
 
212
- const filterFn = (item: ${model.typeName}, field: keyof ${model.typeName}, operator: FilterOperator, value: string | number): boolean => {
213
- switch (field) {
214
- ${model.fields.filter((f) => f.kind === 'scalar' && f.tsTypeName === 'string').length === 0
215
- ? ''
216
- : `${model.fields
217
- .filter((f) => f.kind === 'scalar' && f.tsTypeName === 'string')
218
- .map((f) => `case '${f.name}':`)
219
- .join('\n')}
220
- {
221
- if (typeof value !== 'string') return false
222
- switch (operator) {
223
- case 'contains': {
224
- return (item[field] || '').toLowerCase().includes(value.toLowerCase())
225
- }
226
- default:
227
- return false
228
- }
229
- }`}
230
- ${model.fields.filter((f) => f.kind === 'scalar' && f.tsTypeName === 'number').length === 0
231
- ? ''
232
- : `${model.fields
233
- .filter((f) => f.kind === 'scalar' && f.tsTypeName === 'number')
234
- .map((f) => `case '${f.name}':`)
235
- .join('\n')}
236
- {
237
- const toCompare = item[field]
238
- if (typeof value !== 'number' || toCompare === null) return false
239
- switch (operator) {
240
- case 'eq': {
241
- return item[field] === value
242
- }
243
- case 'gt': {
244
- return toCompare > value
245
- }
246
- case 'lt': {
247
- return toCompare < value
248
- }
249
- default:
250
- return false
251
- }
252
- }`}
253
- default:
254
- return false
255
- }
220
+ switch (operator) {
221
+ case 'contains': {
222
+ return (item[field] || '').toLowerCase().includes(value.toLowerCase())
223
+ }
224
+ default:
225
+ return false
226
+ }
227
+ `,
228
+ };
229
+ });
230
+ const numberFieldsFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'number' }).map((f) => {
231
+ return {
232
+ match: `"${f.name}"`,
233
+ block: `
234
+ const toCompare = item[field]
235
+ if (typeof value !== 'number' || toCompare === null) {
236
+ return false
237
+ }
238
+
239
+ switch (operator) {
240
+ case 'eq': {
241
+ return item[field] === value
242
+ }
243
+ case 'gt': {
244
+ return toCompare > value
245
+ }
246
+ case 'lt': {
247
+ return toCompare < value
248
+ }
249
+ default: {
250
+ return false
251
+ }
252
+ }
253
+ `,
254
+ };
255
+ });
256
+ const fnBlock = (0, ast_1.createSwitchStatement)({
257
+ field: 'field',
258
+ cases: [...stringFieldFilters, ...numberFieldsFilters],
259
+ defaultBlock: 'return false',
260
+ });
261
+ return `
262
+ /**
263
+ * Filters the given ${model.typeName} by the given field, using provided operator and value.
264
+ */
265
+ function filterFn(
266
+ item: ${model.typeName},
267
+ field: keyof ${model.typeName},
268
+ operator: FilterOperator,
269
+ value: string | number
270
+ ): boolean {
271
+ ${fnBlock}
256
272
  }
257
273
  `;
258
274
  }
259
- exports.generateModelBusinessLogic = generateModelBusinessLogic;
275
+ /**
276
+ * Returns a utility compare function that lets you compare two instances of a given model
277
+ * by a given field.
278
+ */
279
+ function _createModelCompareFn({ model }) {
280
+ const stringFieldComparators = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
281
+ return {
282
+ match: `"${f.name}"`,
283
+ block: `
284
+ return (a[field] || '').localeCompare(b[field] || '')
285
+ `,
286
+ };
287
+ });
288
+ const fnBlock = (0, ast_1.createSwitchStatement)({
289
+ field: 'field',
290
+ cases: [...stringFieldComparators],
291
+ defaultBlock: 'return 0',
292
+ });
293
+ return `
294
+ /**
295
+ * Compares two ${model.typeName} instances by the given field.
296
+ */
297
+ function compare(a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) {
298
+ ${fnBlock}
299
+ }
300
+
301
+ `;
302
+ }
@@ -52,6 +52,9 @@ export const ${modals.createComponentName} = ({ show, onHide }: { show: boolean;
52
52
  () => createTypedForm<CreateInputData>().with({ ${(() => {
53
53
  const components = new Set();
54
54
  for (const field of fields.values()) {
55
+ if (field.attributes.isReadonly) {
56
+ continue;
57
+ }
55
58
  switch (field.kind) {
56
59
  case 'enum': {
57
60
  const enumMeta = (0, meta_1.getEnumMetadata)({ enumerator: field.enumerator });
@@ -79,7 +82,7 @@ export const ${modals.createComponentName} = ({ show, onHide }: { show: boolean;
79
82
  <Typed.Formik
80
83
  initialValues={{
81
84
  ${fields
82
- .filter((f) => f.kind !== 'id')
85
+ .filter((f) => f.kind !== 'id' && !f.attributes.isReadonly)
83
86
  .map((field) => `${getFormikFieldName(field.name)}: null,`)
84
87
  .join('\n')}
85
88
  }}
@@ -195,6 +198,7 @@ export const ${components.modals.editComponentName} = ({
195
198
  <Typed.Formik
196
199
  initialValues={{
197
200
  ${fields
201
+ .filter((f) => !f.attributes.isReadonly)
198
202
  .map((field) => {
199
203
  switch (field.kind) {
200
204
  case 'enum':
@@ -387,6 +391,9 @@ function getFormImports({ model, meta }) {
387
391
  function getCreateFormInputFields({ model }) {
388
392
  const form = new serializer_1.Serializer();
389
393
  for (const field of model.fields.values()) {
394
+ if (field.attributes.isReadonly) {
395
+ continue;
396
+ }
390
397
  switch (field.kind) {
391
398
  case 'id':
392
399
  continue;
@@ -407,12 +414,13 @@ function getCreateFormInputFields({ model }) {
407
414
  }
408
415
  function getFormikCreateMutationData({ model: { fields } }) {
409
416
  return fields
417
+ .filter((f) => !f.attributes.isReadonly)
410
418
  .map((field) => {
411
419
  const formikFieldName = getFormikFieldName(field.name);
412
420
  switch (field.kind) {
413
421
  case 'id':
414
422
  case 'scalar':
415
- // NOTE: Create methods generate the ID field upon submision.
423
+ // NOTE: Create methods generate the ID field upon submission.
416
424
  if (field.kind === 'id') {
417
425
  return '';
418
426
  }
@@ -454,6 +462,9 @@ function getFormikCreateMutationData({ model: { fields } }) {
454
462
  function getEditFormInputFields({ model }) {
455
463
  const form = new serializer_1.Serializer();
456
464
  for (const field of model.fields.values()) {
465
+ if (field.attributes.isReadonly) {
466
+ continue;
467
+ }
457
468
  switch (field.kind) {
458
469
  case 'id':
459
470
  continue;
@@ -489,6 +500,7 @@ function getEditFormInputFields({ model }) {
489
500
  }
490
501
  function getEditFormikMutationData({ model: { fields } }) {
491
502
  return fields
503
+ .filter((f) => !f.attributes.isReadonly)
492
504
  .map((field) => {
493
505
  const formikFieldName = getFormikFieldName(field.name);
494
506
  switch (field.kind) {
@@ -520,11 +532,15 @@ function getFormFieldComponents({ model }) {
520
532
  var _a;
521
533
  const form = new serializer_1.Serializer();
522
534
  for (const field of model.fields.values()) {
535
+ // By default, we hide generated fields like createdAt, updatedAt, deletedAt.
536
+ if (field.attributes.isReadonly) {
537
+ continue;
538
+ }
523
539
  const formikFieldName = getFormikFieldName(field.name);
524
540
  const label = StringUtils.toPascalCase(field.name);
525
541
  switch (field.kind) {
526
542
  case 'id': {
527
- // NOTE: We never show the ID field in the form even if it's in the type signiture of the form input.
543
+ // NOTE: We never show the ID field in the form even if it's in the type signature of the form input.
528
544
  break;
529
545
  }
530
546
  case 'scalar': {
@@ -604,7 +620,7 @@ function getFormFieldComponents({ model }) {
604
620
  function getFormikValidationCases({ model }) {
605
621
  const form = new serializer_1.Serializer();
606
622
  for (const field of model.fields.values()) {
607
- if (field.kind === 'id') {
623
+ if (field.kind === 'id' || field.attributes.isReadonly) {
608
624
  continue;
609
625
  }
610
626
  const formikFieldName = getFormikFieldName(field.name);
@@ -27,7 +27,7 @@ function generateRepository({ model, meta }) {
27
27
  items: [meta.types.toBrandedIdTypeFnName],
28
28
  from: meta.types.importPath,
29
29
  });
30
- // NOTE: We first generate different parts of the code responisble for a particular task
30
+ // NOTE: We first generate different parts of the code responsible for a particular task
31
31
  // and then we combine them into the final code block.
32
32
  //
33
33
  // Based on the model, the repository is generated slightly differently:
@@ -132,36 +132,6 @@ export class ${meta.data.repositoryClassName} implements Repository<${model.type
132
132
  public count(): number {
133
133
  return this.data.size
134
134
  }
135
-
136
- /**${(0, jsdoc_1.convertToJsDocComments)([
137
- `Checks that item has the ${idField.name} field.`,
138
- `In case none exists, ${idBlocks.verifyFunctionComment}`,
139
- uniqueStringFieldsBlocks.verifyFunctionComment,
140
- maxLengthBlocks.verifyFunctionComment,
141
- ])}
142
- */
143
-
144
- private verifyItem(item: ${idBlocks.verifyFunctionParameterType}): ${model.typeName} {
145
- ${idBlocks.verifyCode}
146
-
147
- ${maxLengthBlocks.verifyCode.join('\n')}
148
-
149
- ${uniqueStringFieldsBlocks.verifyCode.join('\n')}
150
-
151
- return {
152
- ${idField.name},
153
- ${[...model.fields.values()]
154
- .filter((f) => f.kind !== 'id')
155
- .map((f) => `${f.name}: item.${f.name}`)
156
- .join(',\n')}
157
- }
158
- }
159
-
160
- private toCreateItem(item: ${model.typeName}) {
161
- return {
162
- ${[...model.fields.values()].map((f) => `${f.sourceName}: item.${f.name}`).join(',\n')}
163
- }
164
- }
165
135
 
166
136
  ${mainBlocks.createCode}
167
137
 
@@ -247,9 +217,35 @@ function _generateMainBuildingBlocks_InMemoryOnly({ model, meta, blocks, }) {
247
217
  return this.init()
248
218
  }`,
249
219
  createCode: `
220
+ ${(0, jsdoc_1.jsDocComment)([
221
+ `Checks that item has the ${model.idField.name} field.`,
222
+ `In case none exists, ${blocks.idBlocks.verifyFunctionComment}`,
223
+ blocks.uniqueStringFieldsBlocks.verifyFunctionComment,
224
+ blocks.maxLengthBlocks.verifyFunctionComment,
225
+ ])}
226
+ private verifyItem(item: ${blocks.idBlocks.verifyFunctionParameterType}): ${model.name} {
227
+ ${blocks.idBlocks.verifyCode}
228
+
229
+ ${blocks.maxLengthBlocks.verifyCode.join('\n')}
230
+
231
+ ${blocks.uniqueStringFieldsBlocks.verifyCode.join('\n')}
232
+
233
+ return {
234
+ ${model.idField.name},
235
+ ${model.fields
236
+ .filter((f) => f.kind !== 'id' && !f.attributes.isReadonly)
237
+ .map((f) => `${f.name}: item.${f.name}`)
238
+ .join(',\n')},
239
+ ${model.createdAtField ? `${model.createdAtField.sourceName}: new Date(),` : ''}
240
+ ${model.updatedAtField ? `${model.updatedAtField.sourceName}: new Date(),` : ''}
241
+ }
242
+ }
243
+
250
244
  // Non-mocked version is async - so we keep type-compatible signatures for create() and createWithId()
251
245
  // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
252
- public async create(item: ${methodTypeSignatures.create.parameters[0]}): ${methodTypeSignatures.create.returnType} {
246
+ public async create(
247
+ item: ${methodTypeSignatures.create.parameters[0]}
248
+ ): ${methodTypeSignatures.create.returnType} {
253
249
  const newItem = await Promise.resolve(this.verifyItem(item))
254
250
 
255
251
  this.set(newItem)
@@ -281,7 +277,8 @@ function _generateMainBuildingBlocks_InMemoryOnly({ model, meta, blocks, }) {
281
277
  ${blocks.uniqueStringFieldsBlocks.updateCode.join('\n')}
282
278
 
283
279
  const newItem = await Promise.resolve({ ...existingItem, ...item })
284
-
280
+ ${model.updatedAtField ? `newItem.${model.updatedAtField.name} = new Date()` : ''}
281
+
285
282
  this.remove(existingItem)
286
283
  this.set(newItem)
287
284
 
@@ -371,7 +368,42 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
371
368
  }
372
369
  `,
373
370
  createCode: `
374
- public async create(item: ${methodTypeSignatures.create.parameters[0]}): ${methodTypeSignatures.create.returnType} {
371
+ ${(0, jsdoc_1.jsDocComment)([
372
+ `Checks that item has the ${idField.name} field.`,
373
+ `In case none exists, ${blocks.idBlocks.verifyFunctionComment}`,
374
+ blocks.uniqueStringFieldsBlocks.verifyFunctionComment,
375
+ blocks.maxLengthBlocks.verifyFunctionComment,
376
+ ])}
377
+ private verifyItem(
378
+ item: ${blocks.idBlocks.verifyFunctionParameterType}
379
+ ): ${blocks.idBlocks.createFunctionParameterType} {
380
+ ${blocks.idBlocks.verifyCode}
381
+
382
+ ${blocks.maxLengthBlocks.verifyCode.join('\n')}
383
+
384
+ ${blocks.uniqueStringFieldsBlocks.verifyCode.join('\n')}
385
+
386
+ return {
387
+ ${idField.name},
388
+ ${model.fields
389
+ .filter((f) => f.kind !== 'id' && !f.attributes.isReadonly)
390
+ .map((f) => `${f.name}: item.${f.name}`)
391
+ .join(',\n')}
392
+ }
393
+ }
394
+
395
+ private toCreateItem(item: ${blocks.idBlocks.createFunctionParameterType}) {
396
+ return {
397
+ ${model.fields
398
+ .filter((f) => !f.attributes.isReadonly)
399
+ .map((f) => `${f.sourceName}: item.${f.name}`)
400
+ .join(',\n')},
401
+ }
402
+ }
403
+
404
+ public async create(
405
+ item: ${methodTypeSignatures.create.parameters[0]}
406
+ ): ${methodTypeSignatures.create.returnType} {
375
407
  const newItem = this.${decoderFunctionName}(
376
408
  await this.db.${meta.data.repository.getMethodFnName}.create({
377
409
  data: this.toCreateItem(this.verifyItem(item)),
@@ -387,11 +419,19 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
387
419
 
388
420
  await this.db.${meta.data.repository.getMethodFnName}.createMany({ data: newItems.map(i => this.toCreateItem(i)) })
389
421
 
390
- for (const item of newItems) {
422
+ const dbItems = await this.db.${meta.data.repository.getMethodFnName}.findMany({
423
+ where: {
424
+ ${model.idField.sourceName}: { in: newItems.map(i => i.${model.idField.name}) }
425
+ }
426
+ })
427
+
428
+ const result = dbItems.map((item) => this.${decoderFunctionName}(item))
429
+
430
+ for (const item of result) {
391
431
  this.set(item)
392
432
  }
393
433
 
394
- return newItems
434
+ return result
395
435
  }`,
396
436
  // prettier-ignore
397
437
  updateCode: `
@@ -411,8 +451,8 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
411
451
  ${idField.sourceName}: item.${idField.name},
412
452
  },
413
453
  data: {
414
- ${[...model.fields.values()]
415
- .filter((f) => f.kind !== 'id')
454
+ ${Array.from(model.fields.values())
455
+ .filter((f) => f.kind !== 'id' && !f.attributes.isReadonly)
416
456
  .map((f) => f.isRequired
417
457
  ? `${f.sourceName}: item.${f.name} ?? existingItem.${f.name}`
418
458
  : `${f.sourceName}: item.${f.name}`)
@@ -442,7 +482,9 @@ function generateMainBuildingBlocks_InDatabase({ model, meta, imports, blocks, }
442
482
  /**
443
483
  * Utility function that converts a given Database object to a TypeScript model instance
444
484
  */
445
- private ${decoderFunctionName}(item: Pick<DbType, ${Array.from(model.fields.values()).map((f) => `'${f.sourceName}'`).join(' | ')}>): ${model.typeName} {
485
+ private ${decoderFunctionName}(
486
+ item: Pick<DbType, ${Array.from(model.fields.values()).map((f) => `'${f.sourceName}'`).join(' | ')}>
487
+ ): ${model.typeName} {
446
488
  return ${meta.types.zodDecoderFnName}.parse({
447
489
  ${Array.from(model.fields.values()).map((f) => `${f.name}: item.${f.sourceName}`).join(',\n')}
448
490
  })
@@ -507,6 +549,7 @@ function _generateIdBlocks_NoGeneration({ idField, model, meta, }) {
507
549
  }
508
550
  const ${idField.name} = ${meta.types.toBrandedIdTypeFnName}(item.${idField.name})`,
509
551
  setCode: '',
552
+ createFunctionParameterType: model.typeName,
510
553
  };
511
554
  }
512
555
  /**
@@ -514,6 +557,8 @@ function _generateIdBlocks_NoGeneration({ idField, model, meta, }) {
514
557
  * Given chunks make sure that the id is unique if provided or generate a new one if not.
515
558
  */
516
559
  function _generateIdBlock_Int({ idField, model, meta, }) {
560
+ const generatedFields = model.fields.filter((f) => f.kind === 'id' || f.attributes.isReadonly);
561
+ const readonlyFields = model.fields.filter((f) => f.attributes.isReadonly);
517
562
  return {
518
563
  libraryImports: '',
519
564
  generateNextIdFunctionName: `
@@ -523,8 +568,14 @@ function _generateIdBlock_Int({ idField, model, meta, }) {
523
568
  }`,
524
569
  initCode: `this.currentMaxId = (await this.db.${meta.data.repository.getMethodFnName}.aggregate({ _max: { ${idField.sourceName}: true } }))._max.${idField.sourceName} ?? 0`,
525
570
  verifyFunctionComment: 'the id is generated by increasing the highest former id and assigned to the item.',
526
- verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
571
+ // prettier-ignore
572
+ verifyFunctionParameterType: `(Omit<${model.typeName}, ${generatedFields.map((f) => `'${f.name}'`).join(' | ')}> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
527
573
  verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
574
+ createFunctionParameterType:
575
+ // NOTE: In case we have readonly fields, we need to omit them from the create function.
576
+ readonlyFields.length === 0
577
+ ? model.typeName
578
+ : `Omit<${model.typeName}, ${readonlyFields.map((f) => `'${f.name}'`).join(' |')}>`,
528
579
  setCode: `if (item.id > this.currentMaxId) { this.currentMaxId = item.id }`,
529
580
  };
530
581
  }
@@ -533,6 +584,8 @@ function _generateIdBlock_Int({ idField, model, meta, }) {
533
584
  * It allows you to provide a custom id or generates a new one if not.
534
585
  */
535
586
  function _generateIdBlock_UUID({ idField, model, meta, }) {
587
+ const dbGeneratedFields = model.fields.filter((f) => f.kind === 'id' || f.attributes.isReadonly);
588
+ const readonlyFields = model.fields.filter((f) => f.attributes.isReadonly);
536
589
  return {
537
590
  libraryImports: `import { randomUUID } from 'crypto'`,
538
591
  generateNextIdFunctionName: `
@@ -541,8 +594,14 @@ function _generateIdBlock_UUID({ idField, model, meta, }) {
541
594
  }`,
542
595
  initCode: '',
543
596
  verifyFunctionComment: 'a new UUID is generated and assigned to the item.',
544
- verifyFunctionParameterType: `(Omit<${model.typeName}, '${idField.name}'> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
597
+ // prettier-ignore
598
+ verifyFunctionParameterType: `(Omit<${model.typeName}, ${dbGeneratedFields.map((f) => `'${f.name}'`).join(' | ')}> & Partial<{${idField.name}: ${idField.unbrandedTypeName}}>)`,
545
599
  verifyCode: `const ${idField.name} = (item.${idField.name} !== undefined) ? ${meta.types.toBrandedIdTypeFnName}(item.${idField.name}) : this.generateNextId()`,
600
+ createFunctionParameterType:
601
+ // NOTE: In case we have readonly fields, we need to omit them from the create function.
602
+ readonlyFields.length === 0
603
+ ? model.typeName
604
+ : `Omit<${model.typeName}, ${readonlyFields.map((f) => `'${f.name}'`).join(' |')}>`,
546
605
  setCode: '',
547
606
  };
548
607
  }
@@ -77,7 +77,7 @@ export const ${meta.trpc.routerName} = router({
77
77
  exports.generateRoute = generateRoute;
78
78
  function getCreateMethod({ model: { fields }, meta }) {
79
79
  const parameters = fields
80
- .filter((f) => f.kind !== 'id')
80
+ .filter((f) => f.kind !== 'id' && !f.attributes.isReadonly)
81
81
  .map((field) => `${field.name}: z.${(0, zod_1.getZodDecoderDefinition)({ field })}`)
82
82
  .join(',');
83
83
  return `
@@ -89,6 +89,7 @@ function getCreateMethod({ model: { fields }, meta }) {
89
89
  }
90
90
  function getUpdateMethod({ model: { fields }, meta }) {
91
91
  const parameters = fields
92
+ .filter((f) => !f.attributes.isReadonly)
92
93
  .map((field) => `${field.name}: z.${(0, zod_1.getZodDecoderDefinition)({ field, allowAnyOptionalField: field.kind !== 'id' })}`)
93
94
  .join(',');
94
95
  return `update: procedure
@@ -30,19 +30,44 @@ export type ModelAttributes = {
30
30
  export type FieldAttributes = {
31
31
  /**
32
32
  * Schema tag: ´@@Ignore()`
33
+ *
34
+ * Field will be ignored by the generator and not be exposed from the database.
33
35
  */
34
36
  ignore: boolean;
37
+ /**
38
+ * Schema tag: ´@@Readonly()`
39
+ *
40
+ * Field is generated by the database and shall not be edited by the user. It is readable though.
41
+ */
42
+ isReadonly: boolean;
43
+ /**
44
+ * Derived from Prisma's @updatedAt attribute.
45
+ *
46
+ * Field represents the last time the record was updated.
47
+ */
48
+ isUpdatedAt: boolean;
49
+ /**
50
+ * Automatically set is field name is "createdAt".
51
+ * Field represents the time the record was created.
52
+ */
53
+ isCreatedAt: boolean;
35
54
  /**
36
55
  * Schema tag: ´@@Description("Description of the field")`
56
+ *
57
+ * The description that is generated as a JSDoc comment in the code.
37
58
  */
38
59
  description?: string;
39
60
  /**
40
- * Schema tag: ´@@Examples("Example1", "Example2")`
61
+ * Schema tag: ´@@Examples("Example1", "Example2")` or `@@Example("Example1")`
62
+ *
63
+ * Examples that are generated as a JSDoc comment in the code and used as seed/test data.
41
64
  */
42
- examples?: (string | number | boolean)[];
65
+ examples?: (string | number | boolean | null)[];
43
66
  /**
44
67
  * Schema tag: ´@@DefaultField()`
45
68
  * The property of the model that identifies the default row.
69
+ *
70
+ * If this is set, the repository will verify that exactly one record has this field set to true - and use it as the default record.
46
71
  */
47
72
  isDefaultField?: boolean;
48
73
  /**
@@ -1,17 +1,20 @@
1
1
  import { Field, FieldEnum, FieldRelation, FieldScalar } from './schema';
2
2
  /**
3
3
  * The default field of the model.
4
- * Note: A model can only have one or no default field!
5
- * This is enforced by the schema validation.
4
+ *
5
+ * NOTE: A model can only have one or no default field! We enforce this by schema validation.
6
6
  */
7
7
  export declare const getDefaultField: ({ fields }: {
8
8
  fields: Field[];
9
9
  }) => FieldScalar | undefined;
10
10
  /**
11
- * List of all scalar fields of the model (excluding foreing keys and the id)
11
+ * List of all scalar fields of the model (excluding foreing keys and the id).
12
+ *
13
+ * If you provide an optional `tsTypeName` parameter, only scalar fields of that type are returned.
12
14
  */
13
- export declare const getScalarFields: ({ fields }: {
15
+ export declare const getScalarFields: ({ fields, tsTypeName }: {
14
16
  fields: Field[];
17
+ tsTypeName?: string | undefined;
15
18
  }) => FieldScalar[];
16
19
  /**
17
20
  * List of all relation fields of the model.
@@ -3,15 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isMaxLengthStringField = exports.isUniqueStringField = exports.getDefaultValueForType = exports.getEnumFields = exports.getRelationFields = exports.getScalarFields = exports.getDefaultField = void 0;
4
4
  /**
5
5
  * The default field of the model.
6
- * Note: A model can only have one or no default field!
7
- * This is enforced by the schema validation.
6
+ *
7
+ * NOTE: A model can only have one or no default field! We enforce this by schema validation.
8
8
  */
9
9
  const getDefaultField = ({ fields }) => fields.find((f) => f.attributes.isDefaultField);
10
10
  exports.getDefaultField = getDefaultField;
11
11
  /**
12
- * List of all scalar fields of the model (excluding foreing keys and the id)
12
+ * List of all scalar fields of the model (excluding foreing keys and the id).
13
+ *
14
+ * If you provide an optional `tsTypeName` parameter, only scalar fields of that type are returned.
13
15
  */
14
- const getScalarFields = ({ fields }) => fields.filter((f) => f.kind === 'scalar');
16
+ const getScalarFields = ({ fields, tsTypeName }) => {
17
+ if (tsTypeName) {
18
+ return fields.filter((f) => f.kind === 'scalar' && f.tsTypeName === tsTypeName);
19
+ }
20
+ return fields.filter((f) => f.kind === 'scalar');
21
+ };
15
22
  exports.getScalarFields = getScalarFields;
16
23
  /**
17
24
  * List of all relation fields of the model.
@@ -148,6 +148,14 @@ export type ModelFields = {
148
148
  * The id field of the model
149
149
  */
150
150
  idField: FieldId;
151
+ /**
152
+ * The field of the model that identifies when the entry was created.
153
+ */
154
+ createdAtField?: FieldScalar;
155
+ /**
156
+ * The field of the model that identifies when the entry was last updated.
157
+ */
158
+ updatedAtField?: FieldScalar;
151
159
  /**
152
160
  * The property of the model that identifies the default row.
153
161
  */
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A collection of utility functions that let you more easily write TypeScript code.
3
+ */
4
+ /**
5
+ * Utility function that lets you easily create a TypeScript switch statement.
6
+ */
7
+ export declare function createSwitchStatement({ field, cases, defaultBlock, }: {
8
+ /**
9
+ * The variable name to switch on.
10
+ */
11
+ field: string;
12
+ /**
13
+ * The cases to switch on.
14
+ */
15
+ cases: {
16
+ /**
17
+ * The value to match.
18
+ */
19
+ match: string;
20
+ /**
21
+ * The body of the switch case.
22
+ */
23
+ block: string;
24
+ }[];
25
+ /**
26
+ * The execution block of the default case.
27
+ */
28
+ defaultBlock?: string;
29
+ }): string;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ /**
3
+ * A collection of utility functions that let you more easily write TypeScript code.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createSwitchStatement = void 0;
7
+ /**
8
+ * Utility function that lets you easily create a TypeScript switch statement.
9
+ */
10
+ function createSwitchStatement({ field, cases, defaultBlock, }) {
11
+ const _cases = cases.map(_createSwitchCase).join('\n');
12
+ const _default = defaultBlock ? `default: {\n${defaultBlock}\n}` : '';
13
+ return `
14
+ switch (${field}) {
15
+ ${_cases}
16
+ ${_default}
17
+ }
18
+ `;
19
+ }
20
+ exports.createSwitchStatement = createSwitchStatement;
21
+ function _createSwitchCase({ match, block, }) {
22
+ return `case ${match}: {\n${block}\n}`;
23
+ }
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Returns a string of JSDoc comments from an array of lines.
3
3
  */
4
- export declare function convertToJsDocComments(comments: string[]): string;
4
+ export declare function jsDocComment(lines: string[]): string;
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.convertToJsDocComments = void 0;
3
+ exports.jsDocComment = void 0;
4
4
  /**
5
5
  * Returns a string of JSDoc comments from an array of lines.
6
6
  */
7
- function convertToJsDocComments(comments) {
8
- return comments
7
+ function jsDocComment(lines) {
8
+ const _lines = lines
9
9
  .filter((c) => c !== '')
10
10
  .map((c) => `\n * ${c}`)
11
11
  .join('');
12
+ return `/**${_lines}\n */`;
12
13
  }
13
- exports.convertToJsDocComments = convertToJsDocComments;
14
+ exports.jsDocComment = jsDocComment;
@@ -4,9 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getFieldAttributes = exports.getEnumAttributes = exports.getModelAttributes = exports.parseArgumentToStringOrStringArray = exports.parseAttributesFromDocumentation = void 0;
7
- const remeda_1 = require("remeda");
8
- const string_1 = require("../lib/utils/string");
9
7
  const zod_1 = __importDefault(require("zod"));
8
+ const string_1 = require("../lib/utils/string");
10
9
  /**
11
10
  * Parses attributes from a given string using provided prefix.
12
11
  */
@@ -113,31 +112,47 @@ exports.getEnumAttributes = getEnumAttributes;
113
112
  */
114
113
  function getFieldAttributes(field) {
115
114
  const attributes = parseAttributesFromDocumentation(field);
116
- if (attributes.examples === undefined && attributes.example !== undefined) {
117
- attributes.examples = attributes.example;
118
- }
119
115
  // Prisma also has an "@ignore" attribute - see https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#ignore
120
116
  // we handle this the same way as our custom "ignore" attribute
121
117
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
122
118
  const isPrismaIgnored = field.isIgnored === true;
123
- const isPXLIgnored = Object.hasOwn(attributes, 'ignore');
124
- return {
125
- ignore: isPrismaIgnored || isPXLIgnored,
126
- description: attributes.description && (0, remeda_1.isString)(attributes.description) ? attributes.description : undefined,
127
- isDefaultField: Object.hasOwn(attributes, 'isDefault'),
128
- isLabel: Object.hasOwn(attributes, 'isLabel'),
129
- examples: !attributes.examples
130
- ? undefined
131
- : Array.isArray(attributes.examples)
132
- ? attributes.examples
133
- : [attributes.examples],
134
- maxLength: Object.hasOwn(attributes, 'maxLength')
135
- ? (0, remeda_1.isString)(attributes.maxLength)
136
- ? parseInt(attributes.maxLength, 10)
137
- : (0, remeda_1.isNumber)(attributes.maxLength)
138
- ? attributes.maxLength
139
- : undefined
140
- : undefined,
141
- };
119
+ const exampleDecoder = zod_1.default.union([zod_1.default.string(), zod_1.default.number(), zod_1.default.boolean(), zod_1.default.null()]);
120
+ const examplesDecoder = exampleDecoder
121
+ .transform((obj) => [obj])
122
+ .or(zod_1.default.array(exampleDecoder))
123
+ .optional();
124
+ const decoder = zod_1.default
125
+ .object({
126
+ ignore: blankStringBooleanDecoder,
127
+ description: zod_1.default.string().optional(),
128
+ isDefault: blankStringBooleanDecoder,
129
+ label: blankStringBooleanDecoder,
130
+ example: examplesDecoder,
131
+ examples: examplesDecoder,
132
+ maxLength: zod_1.default
133
+ .number()
134
+ .or(zod_1.default.string().transform((s) => parseInt(s, 10)))
135
+ .optional(),
136
+ readonly: blankStringBooleanDecoder,
137
+ })
138
+ .transform((obj) => {
139
+ var _a;
140
+ return ({
141
+ ignore: obj.ignore || isPrismaIgnored,
142
+ description: obj.description,
143
+ isDefaultField: obj.isDefault,
144
+ isLabel: obj.label,
145
+ examples: obj.examples || obj.example,
146
+ maxLength: obj.maxLength,
147
+ isReadonly: obj.readonly || field.isGenerated || field.isUpdatedAt || field.name === 'createdAt',
148
+ isUpdatedAt: (_a = field.isUpdatedAt) !== null && _a !== void 0 ? _a : false,
149
+ isCreatedAt: field.name === 'createdAt',
150
+ });
151
+ });
152
+ const result = decoder.safeParse(attributes);
153
+ if (!result.success) {
154
+ throw new Error(`Field ${field.name} has invalid field attributes: ${result.error.toString()}`);
155
+ }
156
+ return result.data;
142
157
  }
143
158
  exports.getFieldAttributes = getFieldAttributes;
@@ -146,17 +146,21 @@ function parseModel({ dmmfModel, enums, models, config, }) {
146
146
  (0, error_1.throwError)(`Investigate: Field ${shared.sourceName}.${shared.sourceName} is not scalar, enum nor relation.`);
147
147
  })
148
148
  .filter((field) => !isFieldIgnored({ field }));
149
- const { idField, defaultField, nameField } = validateFields({ fields, model: core });
149
+ const { idField, defaultField, nameField, createdAtField, updatedAtField } = validateFields({ fields, model: core });
150
150
  return Object.assign(Object.assign({}, core), { idField,
151
151
  defaultField,
152
152
  nameField,
153
- fields });
153
+ fields,
154
+ createdAtField,
155
+ updatedAtField });
154
156
  }
155
157
  /**
156
158
  * Checks that there is exactly one id field and that there is at most one default field.
157
159
  */
158
160
  function validateFields({ fields, model: { name } }) {
159
161
  let idField = undefined;
162
+ let createdAtField = undefined;
163
+ let updatedAtField = undefined;
160
164
  let nameField = undefined;
161
165
  let nameFieldFallback = undefined;
162
166
  let defaultField = undefined;
@@ -166,6 +170,18 @@ function validateFields({ fields, model: { name } }) {
166
170
  if (field.name === 'name') {
167
171
  nameFieldFallback = field;
168
172
  }
173
+ if (field.attributes.isCreatedAt) {
174
+ if (createdAtField) {
175
+ throw new Error(`❌❌❌ Model ${name} has multiple createdAt fields`);
176
+ }
177
+ createdAtField = field;
178
+ }
179
+ if (field.attributes.isUpdatedAt) {
180
+ if (updatedAtField) {
181
+ throw new Error(`❌❌❌ Model ${name} has multiple updatedAt fields`);
182
+ }
183
+ updatedAtField = field;
184
+ }
169
185
  break;
170
186
  case 'id':
171
187
  if (idField) {
@@ -199,7 +215,7 @@ function validateFields({ fields, model: { name } }) {
199
215
  if (!nameField && nameFieldFallback) {
200
216
  nameField = nameFieldFallback;
201
217
  }
202
- return { idField, defaultField, nameField };
218
+ return { idField, defaultField, nameField, createdAtField, updatedAtField };
203
219
  }
204
220
  function isAutoIncrementField(fieldDmmf) {
205
221
  if (fieldDmmf.default === undefined) {
@@ -232,9 +248,6 @@ function isUniqueField(fieldRaw) {
232
248
  * Tells whether the parsed schema should skip a given field.
233
249
  */
234
250
  function isFieldIgnored({ field }) {
235
- if (field.name === 'createdAt' || field.name === 'updatedAt' || field.name === 'deletedAt') {
236
- return true;
237
- }
238
251
  if (Object.hasOwn(field.attributes, 'ignore') && field.attributes.ignore) {
239
252
  return true;
240
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.33.4",
3
+ "version": "0.34.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {