@postxl/generator 0.72.0 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/generator.js CHANGED
@@ -404,6 +404,20 @@ function generate(_a) {
404
404
  }
405
405
  }
406
406
  yield lock.writeToProjectRoot(root, { dryRun: false });
407
+ // NOTE: Detached mode tells whether any of the UTF-8 files were detached.
408
+ // We use this in CI template tests.
409
+ const isNoDetachedMode = process.env.POSTXL_NO_DETACHED_MODE === 'true';
410
+ if (isNoDetachedMode) {
411
+ console.log(`No detached mode enabled. Checking that all files are managed!`);
412
+ return;
413
+ }
414
+ if (isNoDetachedMode) {
415
+ const detachedFiles = results.filter((result) => { var _a; return result.status === 'skip' && !Buffer.isBuffer((_a = result.disk) === null || _a === void 0 ? void 0 : _a.content); });
416
+ if (detachedFiles.length > 0) {
417
+ console.log(`Detached files found: ${detachedFiles.map((f) => f.path).join(', ')}`);
418
+ }
419
+ process.exit(1);
420
+ }
407
421
  // NOTE: Lastly we generate the log of the changes.
408
422
  const log = lock_1.ConsoleUtils.getFilesChangelog(results.map((result) => {
409
423
  if (result.status === 'write') {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateModelExportEncoder = void 0;
4
4
  const imports_1 = require("../../../lib/imports");
5
+ const meta_1 = require("../../../lib/meta");
5
6
  const types_1 = require("../../../lib/schema/types");
6
7
  /**
7
8
  * Creates an encoder for the Seed Excel template.
@@ -13,15 +14,13 @@ function generateModelExportEncoder({ model, meta }) {
13
14
  [meta.import.decoder.location.import]: [meta.import.decoder.rowDecoderName],
14
15
  });
15
16
  const { userFriendlyName: userFriendly, userFriendlyNamePlural: userFriendlyPlural } = meta;
17
+ const fieldEncoders = generateFieldEncoders({ model });
16
18
  /* prettier-ignore */
17
19
  return `
18
20
  import * as z from 'zod'
19
21
 
20
- import { capitalizeKeys } from '@postxl/runtime'
21
-
22
22
  ${imports.generate()}
23
23
 
24
-
25
24
  /**
26
25
  * The type for rows in the ${userFriendly} table
27
26
  */
@@ -31,7 +30,9 @@ function generateModelExportEncoder({ model, meta }) {
31
30
  /**
32
31
  * Converts a ${userFriendly} to an Excel row
33
32
  */
34
- export const ${itemEncoderFunctionName} = (item: ${model.typeName}): ${encodedExcelType} => capitalizeKeys(item)
33
+ export const ${itemEncoderFunctionName} = (item: ${model.typeName}): ${encodedExcelType} => ({
34
+ ${fieldEncoders.join(',\n')}
35
+ })
35
36
 
36
37
  /**
37
38
  * Converts a list of ${userFriendlyPlural} to an Excel table
@@ -40,3 +41,11 @@ function generateModelExportEncoder({ model, meta }) {
40
41
  `;
41
42
  }
42
43
  exports.generateModelExportEncoder = generateModelExportEncoder;
44
+ function generateFieldEncoders({ model }) {
45
+ const fieldEncoders = [];
46
+ for (const field of model.fields) {
47
+ const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
48
+ fieldEncoders.push(`"${fieldMeta.excelColumnName}": item.${field.name}`);
49
+ }
50
+ return fieldEncoders;
51
+ }
@@ -17,11 +17,10 @@ function generateModelImportDecoder({ model, meta }) {
17
17
  });
18
18
  const { userFriendlyName: userFriendly, internalSingularNameCapitalized: singularCapitalized } = meta;
19
19
  const { fieldDecoders, blankFieldDecoders } = generateFieldDecoders({ model, meta, imports });
20
+ const fieldTransformers = generateFieldTransformers({ model });
20
21
  return `
21
22
  import * as z from 'zod'
22
23
 
23
- import { uncapitalizeKeys } from '@postxl/runtime'
24
-
25
24
  ${imports.generate()}
26
25
 
27
26
  /**
@@ -52,7 +51,10 @@ const blank${singularCapitalized}RowDecoder = z
52
51
  export const ${tableImportDecoder} = z
53
52
  .array(
54
53
  blank${singularCapitalized}RowDecoder
55
- .or(${meta.import.decoder.rowDecoderName}.transform(uncapitalizeKeys))
54
+ .or(${meta.import.decoder.rowDecoderName}
55
+ .transform(item => ({
56
+ ${fieldTransformers.join(',\n')}
57
+ })))
56
58
  )
57
59
  .transform((items) => items.filter(Boolean) as ${model.typeName}[])`;
58
60
  }
@@ -63,11 +65,11 @@ function generateFieldDecoders({ model, meta, imports, }) {
63
65
  for (const field of model.fields) {
64
66
  const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
65
67
  const optionalModifier = field.attributes.isUpdatedAt || field.attributes.isCreatedAt ? '.optional()' : '';
66
- blankFieldDecoders.push(`${fieldMeta.excelColumnName}: excelNullOrBlankDecoder${optionalModifier}`);
68
+ blankFieldDecoders.push(`'${fieldMeta.excelColumnName}': excelNullOrBlankDecoder${optionalModifier}`);
67
69
  switch (field.kind) {
68
70
  case 'id': {
69
71
  imports.addImport({ items: [meta.types.toBrandedIdTypeFnName], from: meta.types.importPath });
70
- fieldDecoders.push(`${fieldMeta.excelColumnName}: ${toExcelDecoder({
72
+ fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
71
73
  tsTypeName: field.unbrandedTypeName,
72
74
  nullable: false,
73
75
  imports,
@@ -75,7 +77,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
75
77
  break;
76
78
  }
77
79
  case 'scalar': {
78
- fieldDecoders.push(`${fieldMeta.excelColumnName}: ${toExcelDecoder({
80
+ fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
79
81
  tsTypeName: field.tsTypeName,
80
82
  nullable: !field.isRequired,
81
83
  imports,
@@ -86,7 +88,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
86
88
  const refModel = field.relationToModel;
87
89
  const refMeta = (0, meta_1.getModelMetadata)({ model: refModel });
88
90
  imports.addImport({ items: [refMeta.types.toBrandedIdTypeFnName], from: refMeta.types.importPath });
89
- fieldDecoders.push(`${fieldMeta.excelColumnName}: ${toExcelDecoder({
91
+ fieldDecoders.push(`'${fieldMeta.excelColumnName}': ${toExcelDecoder({
90
92
  tsTypeName: field.unbrandedTypeName,
91
93
  nullable: !field.isRequired,
92
94
  imports,
@@ -98,7 +100,7 @@ function generateFieldDecoders({ model, meta, imports, }) {
98
100
  break;
99
101
  }
100
102
  case 'enum': {
101
- fieldDecoders.push(`${fieldMeta.excelColumnName}: z.enum([
103
+ fieldDecoders.push(`'${fieldMeta.excelColumnName}': z.enum([
102
104
  ${field.enumerator.values.map((v) => `'${v}'`).join(', ')}
103
105
  ])${field.isRequired ? '' : '.nullable()'}${optionalModifier}`);
104
106
  break;
@@ -136,3 +138,11 @@ function toExcelDecoder({ tsTypeName, nullable, imports, }) {
136
138
  throw new Error('Unknown type');
137
139
  }
138
140
  }
141
+ function generateFieldTransformers({ model }) {
142
+ const fieldTransformers = [];
143
+ for (const field of model.fields) {
144
+ const fieldMeta = (0, meta_1.getFieldMetadata)({ field });
145
+ fieldTransformers.push(`${field.name}: item['${fieldMeta.excelColumnName}']`);
146
+ }
147
+ return fieldTransformers;
148
+ }
@@ -2,23 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateModelContext = void 0;
4
4
  const imports_1 = require("../../../lib/imports");
5
- const string_1 = require("../../../lib/utils/string");
6
5
  /**
7
6
  * Utility component that generates the definition of the React context for a given model.
8
7
  */
9
8
  function generateModelContext({ model, meta }) {
10
- const queryName = (0, string_1.toCamelCase)(model.name);
11
9
  const imports = imports_1.ImportsGenerator.from(meta.react.folderPath).addTypeImport({
12
- items: [model.typeName, model.brandedIdType],
13
10
  from: meta.types.importPath,
11
+ items: [model.typeName, model.brandedIdType],
14
12
  });
15
13
  const resultMapTypeDefinition = `Map<${model.brandedIdType}, ${model.typeName}>`;
16
14
  return `
17
- /* eslint-disable @typescript-eslint/no-unused-vars */
18
- import React, { useMemo } from 'react'
15
+ import { useMemo } from 'react'
19
16
 
20
17
  import { trpc } from '@lib/trpc'
21
- import { filterMap, mapMap } from '@postxl/runtime'
22
18
 
23
19
  ${imports.generate()}
24
20
 
@@ -32,17 +28,17 @@ type ProviderHookType = {
32
28
  * A React utility hook to access the model data in the generated forms.
33
29
  */
34
30
  export const ${meta.react.context.hookFnName} = (): ProviderHookType => {
35
- const ${queryName} = trpc.${meta.trpc.getMap.reactQueryMethod}.useQuery()
31
+ const dataQuery = trpc.${meta.trpc.getMap.reactQueryMethod}.useQuery()
36
32
 
37
33
  const value = useMemo<ProviderHookType>(() => {
38
- const data: ${resultMapTypeDefinition} = ${queryName}.data ?? new ${resultMapTypeDefinition}()
34
+ const data: ${resultMapTypeDefinition} = dataQuery.data ?? new ${resultMapTypeDefinition}()
39
35
 
40
36
  return {
41
- ready: ${queryName}.isLoading === false,
37
+ ready: dataQuery.isLoading === false,
42
38
  map: data,
43
39
  list: Array.from(data.values())
44
40
  }
45
- }, [${queryName}.isLoading, ${queryName}.data])
41
+ }, [dataQuery.isLoading, dataQuery.data])
46
42
 
47
43
  return value
48
44
  }
@@ -574,7 +574,7 @@ function getFormFieldComponents({ model, testIdCollector, }) {
574
574
  continue;
575
575
  }
576
576
  const formikFieldName = getFormikFieldName(field.name);
577
- const label = StringUtils.toPascalCase(field.name);
577
+ const label = field.attributes.label;
578
578
  switch (field.kind) {
579
579
  case 'id': {
580
580
  // NOTE: We never show the ID field in the form even if it's in the type signature of the form input.
@@ -5,9 +5,7 @@ const imports_1 = require("../../../lib/imports");
5
5
  const meta_1 = require("../../../lib/meta");
6
6
  const fields_1 = require("../../../lib/schema/fields");
7
7
  const types_1 = require("../../../lib/schema/types");
8
- const types_2 = require("../../../lib/types");
9
8
  const ast_1 = require("../../../lib/utils/ast");
10
- const jsdoc_1 = require("../../../lib/utils/jsdoc");
11
9
  /**
12
10
  * Generates view business logic for a given model.
13
11
  * The view logic exposes all information and links of a model. See template's readme for more info.
@@ -20,91 +18,64 @@ function generateModelBusinessLogicView({ model, meta }) {
20
18
  [meta.types.importPath]: [(0, types_1.toAnnotatedTypeName)(model.brandedIdType), (0, types_1.toAnnotatedTypeName)(meta.types.typeName)],
21
19
  [schemaMeta.view.serviceLocation.path]: schemaMeta.view.serviceClassName,
22
20
  });
23
- /**
24
- * The name of the variable that holds the central business logic service instance.
25
- * Instead of injecting a repository instance for each model, we inject this single instance
26
- * which then provides access to all models' business logic.
27
- */
28
- const viewServiceClassName = 'viewService';
29
- const constructorParameters = [
30
- `public readonly data: ${meta.data.repository.className}`,
31
- `@Inject(forwardRef(() => ${schemaMeta.view.serviceClassName})) private readonly ${viewServiceClassName}: ${schemaMeta.view.serviceClassName}`,
32
- ];
33
- /**
34
- * Variable names and their definitions indexed by the name of the relation they represent.
35
- */
36
- const variables = new Map();
37
- for (const relation of (0, fields_1.getRelationFields)(model)) {
38
- const refModel = relation.relationToModel;
39
- const refMeta = (0, meta_1.getModelMetadata)({ model: refModel });
40
- const variableGetter = `await this.${viewServiceClassName}.${refMeta.view.serviceVariableName}.get(itemRaw.${relation.name})`;
41
- const variablePresenceCheck = `
42
- if (!${relation.relationFieldName}) {
43
- throw new Error(\`Could not find ${refMeta.types.typeName} with id \${itemRaw.${relation.name}} for ${model.typeName}.${relation.name}!\`)
44
- }
45
- `;
46
- const relationVariableName = relation.relationFieldName;
47
- const relationVariableDefinition = relation.isRequired
48
- ? `${variableGetter};${variablePresenceCheck}`
49
- : `itemRaw.${relation.name} !== null ? ${variableGetter} : null`;
50
- variables.set(relation.name, {
51
- variableName: relationVariableName,
52
- variableDefinition: `const ${relationVariableName} = ${relationVariableDefinition}`,
53
- });
54
- if (relation.relationToModel.typeName !== model.typeName) {
55
- imports.addImport({ from: refMeta.types.importPath, items: [refMeta.types.toBrandedIdTypeFnName] });
56
- imports.addTypeImport({ from: refMeta.types.importPath, items: [refModel.brandedIdType, refMeta.types.typeName] });
57
- }
58
- }
59
- const hasLinkedItems = variables.size > 0;
60
- if (hasLinkedItems) {
61
- // NOTE: If we need to generate the linked item type, we need to import the enum types.
62
- for (const enumField of (0, fields_1.getEnumFields)(model)) {
63
- const enumMeta = (0, meta_1.getEnumMetadata)(enumField);
64
- imports.addTypeImport({ from: enumMeta.types.importPath, items: [enumField.typeName] });
21
+ const compareFnBlock = (0, ast_1.createSwitchStatement)({
22
+ field: 'field',
23
+ cases: (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => ({
24
+ match: `"${f.name}"`,
25
+ block: `return (a[field] || '').localeCompare(b[field] || '')`,
26
+ })),
27
+ defaultBlock: 'return 0',
28
+ });
29
+ const stringFieldFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
30
+ return {
31
+ match: `"${f.name}"`,
32
+ block: `
33
+ if (typeof value !== 'string') {
34
+ return false
65
35
  }
66
- }
67
- const linkedItemsGetterFn = `
68
- /**
69
- * Returns the linked ${meta.userFriendlyName} with the given id or null if it does not exist.
70
- * Linked: The ${meta.userFriendlyName} contains the linked (raw) items themselves, not only the ids.
71
- */
72
- public async getLinkedItem(id: ${model.brandedIdType}): Promise<${meta.types.linkedTypeName} | null> {
73
- const itemRaw = await this.data.get(id)
74
- if (!itemRaw) {
75
- return null
76
- }
77
36
 
78
- ${[...variables.values()].map((r) => r.variableDefinition).join('\n')}
79
-
80
- const item: ${meta.types.linkedTypeName} = {
81
- ${model.fields
82
- .map((f) => {
83
- if (f.kind !== 'relation') {
84
- return `${f.name}: itemRaw.${f.name}`;
37
+ switch (operator) {
38
+ case 'contains': {
39
+ return (item[field] || '').toLowerCase().includes(value.toLowerCase())
40
+ }
41
+ default: {
42
+ return false
43
+ }
85
44
  }
86
- const linkedRel = variables.get(f.name);
87
- if (!linkedRel) {
88
- throw new Error(`Could not find linked item for ${model.typeName}.${f.name}`);
45
+ `,
46
+ };
47
+ });
48
+ const numberFieldsFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'number' }).map((f) => {
49
+ return {
50
+ match: `"${f.name}"`,
51
+ block: `
52
+ const toCompare = item[field]
53
+ if (typeof value !== 'number' || toCompare === null) {
54
+ return false
89
55
  }
90
- return `${linkedRel.variableName}`;
91
- })
92
- .join(',\n')}
93
- }
94
56
 
95
- return item
96
- }
97
- `;
98
- const linkedTypeDefinition = `
99
- export type ${meta.types.linkedTypeName} = {
100
- ${model.fields
101
- .map((f) => `
102
- ${(0, jsdoc_1.getFieldComment)(f)}
103
- ${getLinkedFieldType(f)}${f.isRequired ? '' : ' | null'}
104
- `)
105
- .join('\n')}
106
- }
107
- `;
57
+ switch (operator) {
58
+ case 'eq': {
59
+ return item[field] === value
60
+ }
61
+ case 'gt': {
62
+ return toCompare > value
63
+ }
64
+ case 'lt': {
65
+ return toCompare < value
66
+ }
67
+ default: {
68
+ return false
69
+ }
70
+ }
71
+ `,
72
+ };
73
+ });
74
+ const filterFnBlock = (0, ast_1.createSwitchStatement)({
75
+ field: 'field',
76
+ cases: [...stringFieldFilters, ...numberFieldsFilters],
77
+ defaultBlock: 'return false',
78
+ });
108
79
  return /* ts */ `
109
80
  /* eslint-disable @typescript-eslint/no-unused-vars */
110
81
  import z from 'zod'
@@ -113,8 +84,6 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common'
113
84
 
114
85
  ${imports.generate()}
115
86
 
116
- ${hasLinkedItems ? linkedTypeDefinition : ''}
117
-
118
87
  /**
119
88
  * Cursor decoder.
120
89
  */
@@ -144,7 +113,11 @@ type FilterOperator = z.infer<typeof ${meta.view.filterOperatorDecoder}>
144
113
 
145
114
  @Injectable()
146
115
  export class ${meta.view.serviceClassName} {
147
- constructor(${constructorParameters.join(',\n')}) {}
116
+ constructor(
117
+ public readonly data: ${meta.data.repository.className},
118
+ @Inject(forwardRef(() => ${schemaMeta.view.serviceClassName}))
119
+ private readonly viewService: ${schemaMeta.view.serviceClassName}
120
+ ) {}
148
121
 
149
122
  /**
150
123
  * Returns the raw ${meta.userFriendlyName} with the given id or null if it does not exist.
@@ -154,8 +127,6 @@ export class ${meta.view.serviceClassName} {
154
127
  return this.data.get(id)
155
128
  }
156
129
 
157
- ${hasLinkedItems ? linkedItemsGetterFn : ''}
158
-
159
130
  /**
160
131
  * Returns a map of all ${meta.userFriendlyNamePlural}.
161
132
  */
@@ -192,68 +163,13 @@ export class ${meta.view.serviceClassName} {
192
163
 
193
164
  // Utility Functions
194
165
 
195
- ${_createModelCompareFn({ model })}
196
-
197
- ${_createModelFilterFn({ model })}
198
- `;
199
- }
200
- exports.generateModelBusinessLogicView = generateModelBusinessLogicView;
201
166
  /**
202
- * Generates a utility filter function for the given model that can be used to filter out instances
203
- * of a model using a given operator on a given field.
167
+ * Compares two ${model.typeName} instances by the given field.
204
168
  */
205
- function _createModelFilterFn({ model }) {
206
- const stringFieldFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
207
- return {
208
- match: `"${f.name}"`,
209
- block: `
210
- if (typeof value !== 'string') {
211
- return false
212
- }
213
-
214
- switch (operator) {
215
- case 'contains': {
216
- return (item[field] || '').toLowerCase().includes(value.toLowerCase())
217
- }
218
- default: {
219
- return false
220
- }
221
- }
222
- `,
223
- };
224
- });
225
- const numberFieldsFilters = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'number' }).map((f) => {
226
- return {
227
- match: `"${f.name}"`,
228
- block: `
229
- const toCompare = item[field]
230
- if (typeof value !== 'number' || toCompare === null) {
231
- return false
232
- }
169
+ function compare(a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) {
170
+ ${compareFnBlock}
171
+ }
233
172
 
234
- switch (operator) {
235
- case 'eq': {
236
- return item[field] === value
237
- }
238
- case 'gt': {
239
- return toCompare > value
240
- }
241
- case 'lt': {
242
- return toCompare < value
243
- }
244
- default: {
245
- return false
246
- }
247
- }
248
- `,
249
- };
250
- });
251
- const fnBlock = (0, ast_1.createSwitchStatement)({
252
- field: 'field',
253
- cases: [...stringFieldFilters, ...numberFieldsFilters],
254
- defaultBlock: 'return false',
255
- });
256
- return `
257
173
  /**
258
174
  * Filters the given ${model.typeName} by the given field, using provided operator and value.
259
175
  */
@@ -263,52 +179,8 @@ function filterFn(
263
179
  operator: FilterOperator,
264
180
  value: string | number
265
181
  ): boolean {
266
- ${fnBlock}
182
+ ${filterFnBlock}
267
183
  }
268
184
  `;
269
185
  }
270
- /**
271
- * Returns a utility compare function that lets you compare two instances of a given model
272
- * by a given field.
273
- */
274
- function _createModelCompareFn({ model }) {
275
- const stringFieldComparators = (0, fields_1.getScalarFields)({ fields: model.fields, tsTypeName: 'string' }).map((f) => {
276
- return {
277
- match: `"${f.name}"`,
278
- block: `
279
- return (a[field] || '').localeCompare(b[field] || '')
280
- `,
281
- };
282
- });
283
- const fnBlock = (0, ast_1.createSwitchStatement)({
284
- field: 'field',
285
- cases: [...stringFieldComparators],
286
- defaultBlock: 'return 0',
287
- });
288
- return `
289
- /**
290
- * Compares two ${model.typeName} instances by the given field.
291
- */
292
- function compare(a: ${model.typeName}, b: ${model.typeName}, field: keyof ${model.typeName}) {
293
- ${fnBlock}
294
- }
295
-
296
- `;
297
- }
298
- /**
299
- * Converts a field to a TypeScript type definition with linked fields.
300
- */
301
- function getLinkedFieldType(f) {
302
- switch (f.kind) {
303
- case 'enum':
304
- return `${f.name}: ${f.typeName}`;
305
- case 'relation':
306
- return `${f.relationFieldName}: ${f.relationToModel.typeName}`;
307
- case 'id':
308
- return `${f.name}: ${f.model.brandedIdType}`;
309
- case 'scalar':
310
- return `${f.name}: ${f.tsTypeName}`;
311
- default:
312
- throw new types_2.ExhaustiveSwitchCheck(f);
313
- }
314
- }
186
+ exports.generateModelBusinessLogicView = generateModelBusinessLogicView;
@@ -81,10 +81,21 @@ export type FieldAttributes = {
81
81
  */
82
82
  maxLength?: number;
83
83
  /**
84
- * Schema tag: `@@Label()`
84
+ * Schema tag: `@@IsNameField()`
85
85
  * The field that should be used as the default label for the model.
86
+ *
87
+ * If this is set, the repository will verify that exactly one record has this field set to true - and use it as the default label.
88
+ *
89
+ * If this attribute is set, this field will be used in the code/UI as the field that specifies the model's name. If not provided, we check for the existence of the`name` field. If that is not found, we use the `id` field as the name field.
90
+ */
91
+ isNameField?: boolean;
92
+ /**
93
+ * Schema tag: `@@Label("Label")`
94
+ * The human readable label/name of the field.
95
+ *
96
+ * If not provided, we use the field name.
86
97
  */
87
- isLabel?: boolean;
98
+ label: string;
88
99
  };
89
100
  export type EnumAttributes = {
90
101
  /**
package/dist/lib/meta.js CHANGED
@@ -402,7 +402,7 @@ function getFieldMetadata({ field }) {
402
402
  tsFieldName: Types.toVariableName((0, string_1.toCamelCase)(field.name)),
403
403
  getByForeignKeyMethodFnName: Types.toFunctionName(`getItemsFor${PascalCase}`),
404
404
  getByForeignKeyIdsMethodFnName: Types.toFunctionName(`getIdsFor${PascalCase}`),
405
- excelColumnName: (0, string_1.toPascalCase)(field.name),
405
+ excelColumnName: field.attributes.label,
406
406
  };
407
407
  }
408
408
  exports.getFieldMetadata = getFieldMetadata;
@@ -284,9 +284,9 @@ export type FieldScalar = Prettify<Omit<FieldCore, 'name'> & {
284
284
  export type FieldId = Prettify<Omit<FieldCore, 'name'> & {
285
285
  kind: 'id';
286
286
  /**
287
- * Name of the field and variable (e.g. id)
287
+ * Name of the field and variable (must be `id`)
288
288
  */
289
- name: Types.FieldName;
289
+ name: Types.IdFieldName;
290
290
  /**
291
291
  * Model that this ID field identifies.
292
292
  */
@@ -314,9 +314,9 @@ export type FieldId = Prettify<Omit<FieldCore, 'name'> & {
314
314
  export declare const isFieldId: (field: Field) => field is Prettify<Omit<FieldCore, "name"> & {
315
315
  kind: 'id';
316
316
  /**
317
- * Name of the field and variable (e.g. id)
317
+ * Name of the field and variable (must be `id`)
318
318
  */
319
- name: Types.FieldName;
319
+ name: Types.IdFieldName;
320
320
  /**
321
321
  * Model that this ID field identifies.
322
322
  */
@@ -71,6 +71,16 @@ export declare const isAnnotatedTypeName: (t: string | AnnotatedTypeName) => t i
71
71
  export type FieldName = string & {
72
72
  readonly ___type: 'FieldName';
73
73
  };
74
+ /**
75
+ * Brands a raw string to an IdFieldName identifier.
76
+ */
77
+ export declare const ID_FIELD_NAME: IdFieldName;
78
+ /**
79
+ * The name of the id field. Must be `id`.
80
+ */
81
+ export type IdFieldName = 'id' & {
82
+ readonly ___type: 'FieldName';
83
+ };
74
84
  /**
75
85
  * Brands a raw string to a FieldName identifier.
76
86
  */
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // MARK: - Generated content related types
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.toPackageName = exports.toPath = exports.toBackendModulePath = exports.toModuleLocation = exports.toFolderName = exports.toFileName = exports.toVariableName = exports.toFunctionName = exports.toEnumName = exports.toFieldName = exports.isAnnotatedTypeName = exports.toAnnotatedTypeName = exports.toTypeName = exports.toModelName = exports.toDiscriminantName = exports.toClassName = void 0;
4
+ exports.toPackageName = exports.toPath = exports.toBackendModulePath = exports.toModuleLocation = exports.toFolderName = exports.toFileName = exports.toVariableName = exports.toFunctionName = exports.toEnumName = exports.toFieldName = exports.ID_FIELD_NAME = exports.isAnnotatedTypeName = exports.toAnnotatedTypeName = exports.toTypeName = exports.toModelName = exports.toDiscriminantName = exports.toClassName = void 0;
5
5
  /**
6
6
  * Converts a raw string to a branded ClassName.
7
7
  */
@@ -35,6 +35,10 @@ exports.toAnnotatedTypeName = toAnnotatedTypeName;
35
35
  */
36
36
  const isAnnotatedTypeName = (t) => typeof t === 'object' && t.kind === 'TypeName';
37
37
  exports.isAnnotatedTypeName = isAnnotatedTypeName;
38
+ /**
39
+ * Brands a raw string to an IdFieldName identifier.
40
+ */
41
+ exports.ID_FIELD_NAME = 'id';
38
42
  /**
39
43
  * Brands a raw string to a FieldName identifier.
40
44
  */
@@ -34,7 +34,6 @@ function extractNestedErrors(value) {
34
34
  * As a fallback, it returns a JSON string representation of the error.
35
35
  */
36
36
  function formatZodError(e) {
37
- console.log(e);
38
37
  const formatted = e.format();
39
38
  if (typeof formatted === 'string') {
40
39
  return formatted;
@@ -138,7 +138,8 @@ function getFieldAttributes(field) {
138
138
  ignore: blankStringBooleanDecoder,
139
139
  description: zod_1.default.string().optional(),
140
140
  isDefault: blankStringBooleanDecoder,
141
- label: blankStringBooleanDecoder,
141
+ isNameField: blankStringBooleanDecoder,
142
+ label: zod_1.default.string().optional(),
142
143
  example: examplesDecoder,
143
144
  examples: examplesDecoder,
144
145
  maxLength: zod_1.default
@@ -148,7 +149,7 @@ function getFieldAttributes(field) {
148
149
  readonly: blankStringBooleanDecoder,
149
150
  })
150
151
  .transform((obj) => {
151
- var _a;
152
+ var _a, _b;
152
153
  if (isPrismaIgnored && !obj.ignore) {
153
154
  throw new Error(`Field ${field.name} is ignored by Prisma, but is missing the "ignore" PostXL attribute!`);
154
155
  }
@@ -156,11 +157,12 @@ function getFieldAttributes(field) {
156
157
  ignore: obj.ignore,
157
158
  description: obj.description,
158
159
  isDefaultField: obj.isDefault,
159
- isLabel: obj.label,
160
+ isNameField: obj.isNameField,
161
+ label: (_a = obj.label) !== null && _a !== void 0 ? _a : (0, string_1.toPascalCase)(field.name),
160
162
  examples: obj.examples || obj.example,
161
163
  maxLength: obj.maxLength,
162
164
  isReadonly: obj.readonly || field.isGenerated || field.isUpdatedAt || field.name === 'createdAt' || field.isId,
163
- isUpdatedAt: (_a = field.isUpdatedAt) !== null && _a !== void 0 ? _a : false,
165
+ isUpdatedAt: (_b = field.isUpdatedAt) !== null && _b !== void 0 ? _b : false,
164
166
  isCreatedAt: field.name === 'createdAt',
165
167
  };
166
168
  });
@@ -226,7 +226,7 @@ function parseModel({ dmmfModel, enums, models, config, }) {
226
226
  }
227
227
  if (dmmfField.isId) {
228
228
  const isGeneratedField = isAutoIncrementField(dmmfField) || isUUIDField(dmmfField);
229
- const _field = Object.assign(Object.assign({ kind: 'id' }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
229
+ const _field = Object.assign(Object.assign({ kind: 'id' }, shared), { name: Types.ID_FIELD_NAME, isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
230
230
  return _field;
231
231
  }
232
232
  if (dmmfField.kind === 'scalar') {
@@ -257,7 +257,7 @@ function parseModel({ dmmfModel, enums, models, config, }) {
257
257
  (0, error_1.throwError)(`${fieldName} is not scalar, enum nor relation.`);
258
258
  })
259
259
  .filter((field) => !isFieldIgnored({ field }));
260
- const { idField, defaultField, nameField, createdAtField, updatedAtField } = validateFields({ fields, model: core });
260
+ const { idField, defaultField, nameField, createdAtField, updatedAtField } = getTaggedFields({ fields, model: core });
261
261
  return Object.assign(Object.assign({}, core), { idField,
262
262
  defaultField,
263
263
  nameField,
@@ -268,61 +268,60 @@ function parseModel({ dmmfModel, enums, models, config, }) {
268
268
  references: [] });
269
269
  }
270
270
  /**
271
- * Checks that there is exactly one id field and that there is at most one default field.
271
+ * Returns special fields that are tagged with special attributes.
272
272
  */
273
- function validateFields({ fields, model: { name } }) {
273
+ function getTaggedFields({ fields, model: { name }, }) {
274
274
  var _a;
275
275
  let idField = undefined;
276
276
  let createdAtField = undefined;
277
277
  let updatedAtField = undefined;
278
- let labelField = undefined;
279
278
  let nameField = undefined;
279
+ let isNameField = undefined;
280
280
  let defaultField = undefined;
281
281
  for (const field of fields) {
282
282
  const fieldName = (0, logger_1.highlight)(`${name}.${field.name}`);
283
283
  switch (field.kind) {
284
+ case 'id': {
285
+ if (idField) {
286
+ (0, error_1.throwError)(`${fieldName} has multiple id fields - ${idField.name} and ${field.name}`);
287
+ }
288
+ idField = field;
289
+ break;
290
+ }
284
291
  case 'scalar':
285
292
  if (field.name === 'name') {
286
293
  nameField = field;
287
294
  }
295
+ if (field.attributes.isNameField) {
296
+ if (isNameField != null) {
297
+ (0, error_1.throwError)(`${fieldName} has multiple name fields - ${isNameField.name} and ${field.name}`);
298
+ }
299
+ isNameField = field;
300
+ }
288
301
  if (field.attributes.isCreatedAt) {
289
- if (createdAtField) {
290
- (0, error_1.throwError)(`${fieldName} has multiple createdAt fields`);
302
+ if (createdAtField != null) {
303
+ (0, error_1.throwError)(`${fieldName} has multiple createdAt fields - ${createdAtField.name} and ${field.name}`);
291
304
  }
292
305
  createdAtField = field;
293
306
  }
294
307
  if (field.attributes.isUpdatedAt) {
295
- if (updatedAtField) {
296
- (0, error_1.throwError)(`${fieldName} has multiple updatedAt fields`);
308
+ if (updatedAtField != null) {
309
+ (0, error_1.throwError)(`${fieldName} has multiple updatedAt fields - ${updatedAtField.name} and ${field.name}`);
297
310
  }
298
311
  updatedAtField = field;
299
312
  }
300
- break;
301
- case 'id':
302
- if (idField) {
303
- (0, error_1.throwError)(`${fieldName} has multiple id fields`);
313
+ if (field.attributes.isDefaultField) {
314
+ if (defaultField != null) {
315
+ (0, error_1.throwError)(`${fieldName} has multiple default fields - ${defaultField.name} and ${field.name}`);
316
+ }
317
+ defaultField = field;
304
318
  }
305
- idField = field;
306
319
  break;
307
320
  case 'relation':
308
321
  break;
309
322
  case 'enum':
310
323
  break;
311
324
  }
312
- //handle default case
313
- if (field.attributes.isDefaultField && field.kind === 'scalar') {
314
- if (defaultField !== undefined) {
315
- (0, error_1.throwError)(`${fieldName} has multiple default fields`);
316
- }
317
- defaultField = field;
318
- }
319
- //handle name field
320
- if (field.attributes.isLabel) {
321
- if (labelField !== undefined) {
322
- (0, error_1.throwError)(`${fieldName} has multiple name fields`);
323
- }
324
- labelField = field;
325
- }
326
325
  }
327
326
  if (!idField) {
328
327
  (0, error_1.throwError)(`Model ${(0, logger_1.highlight)(name)} does not have an id field`);
@@ -330,7 +329,7 @@ function validateFields({ fields, model: { name } }) {
330
329
  return {
331
330
  idField,
332
331
  defaultField,
333
- nameField: (_a = labelField !== null && labelField !== void 0 ? labelField : nameField) !== null && _a !== void 0 ? _a : idField,
332
+ nameField: (_a = isNameField !== null && isNameField !== void 0 ? isNameField : nameField) !== null && _a !== void 0 ? _a : idField,
334
333
  createdAtField,
335
334
  updatedAtField,
336
335
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.72.0",
3
+ "version": "0.73.1",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {
@@ -41,7 +41,6 @@
41
41
  "prisma": "5.8.1"
42
42
  },
43
43
  "scripts": {
44
- "test:generators": "./scripts/test-generators.sh",
45
44
  "test:setup": "./scripts/test-setup.sh",
46
45
  "test:jest": "jest",
47
46
  "test:watch": "jest --watch",