@postxl/generator 0.54.0 → 0.55.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.
@@ -9,6 +9,7 @@ const types_1 = require("../../lib/schema/types");
9
9
  */
10
10
  function generateImportExportExporterClass({ models, meta }) {
11
11
  const imports = imports_1.ImportsGenerator.from(meta.importExport.exporterClass.filePath);
12
+ const modelsMap = new Map(models.map((model) => [model.name, model]));
12
13
  imports.addImports({
13
14
  [meta.businessLogic.view.importPath]: [meta.businessLogic.view.serviceClassName],
14
15
  [meta.importExport.decoder.fullDecoderFilePath]: [
@@ -39,20 +40,50 @@ function generateImportExportExporterClass({ models, meta }) {
39
40
  }
40
41
  const linkedModelMeta = (0, meta_1.getModelMetadata)({ model: field.relationToModel });
41
42
  if (field.isRequired) {
42
- linkedItems.push(`await this.${linkedModelMeta.importExport.exportAddFunctionName}(item.${field.name})`);
43
+ linkedItems.push(`await this.${linkedModelMeta.importExport.exportAddFunctionName}({id: item.${field.name}, includeChildren: false})`);
43
44
  }
44
45
  else {
45
46
  linkedItems.push(`
46
47
  if (item.${field.name}) {
47
- await this.${linkedModelMeta.importExport.exportAddFunctionName}(item.${field.name})
48
+ await this.${linkedModelMeta.importExport.exportAddFunctionName}({id: item.${field.name}, includeChildren: false})
48
49
  }`);
49
50
  }
50
51
  }
52
+ const childItemCalls = [];
53
+ for (const relatedModelCore of model.relatedModels) {
54
+ const relatedModel = modelsMap.get(relatedModelCore.name);
55
+ // This should not happen as the related models are always present
56
+ if (!relatedModel) {
57
+ continue;
58
+ }
59
+ for (const field of relatedModel.fields) {
60
+ if (field.kind !== 'relation') {
61
+ continue;
62
+ }
63
+ if (field.relationToModel.name !== model.name) {
64
+ continue;
65
+ }
66
+ const linkedModelMeta = (0, meta_1.getModelMetadata)({ model: relatedModel });
67
+ const linkedFieldMeta = (0, meta_1.getFieldMetadata)({ field: field });
68
+ childItemCalls.push(`
69
+ // Add all ${linkedModelMeta.userFriendlyNamePlural} that are related to the ${modelMeta.userFriendlyName} via ${field.name}
70
+ for (const ${field.name} of await this.viewService.${linkedModelMeta.businessLogic.view.serviceVariableName}.data.${linkedFieldMeta.getByForeignKeyIdsMethodFnName}(id)) {
71
+ await this.${linkedModelMeta.importExport.exportAddFunctionName}({id: ${field.name}, includeChildren})
72
+ }`);
73
+ }
74
+ }
75
+ const childItems = childItemCalls.length > 0
76
+ ? `
77
+ if (includeChildren) {
78
+ ${childItemCalls.join('\n')}
79
+ }`
80
+ : '';
81
+ const signature = `{id, includeChildren = true}: {id: ${model.brandedIdType}, includeChildren?: boolean}`;
51
82
  addFunctions.push(`
52
83
  /**
53
84
  * Adds a ${modelMeta.userFriendlyName} and all related (and nested) dependencies to the export.
54
85
  */
55
- public async ${modelMeta.importExport.exportAddFunctionName}(id: ${model.brandedIdType}) {
86
+ public async ${modelMeta.importExport.exportAddFunctionName}(${signature}) {
56
87
  if (this.${modelMeta.internalPluralName}.has(id)) {
57
88
  return
58
89
  }
@@ -64,11 +95,17 @@ function generateImportExportExporterClass({ models, meta }) {
64
95
  this.${modelMeta.internalPluralName}.set(id, item)
65
96
 
66
97
  ${linkedItems.join('\n')}
98
+
99
+ ${childItems}
67
100
  }
68
101
  `);
69
102
  addAllCalls.push(`this.${modelMeta.internalPluralName} = await this.viewService.${modelMeta.businessLogic.view.serviceVariableName}.getAll()`);
70
103
  }
71
104
  return /* ts */ `
105
+ // For consistency, we include \`{ includeChildren = true }\` in the signature of every add function.
106
+ // If the model does not have any related models, the \`includeChildren\` parameter is ignored.
107
+ /* eslint-disable @typescript-eslint/no-unused-vars */
108
+
72
109
  import { Logger } from '@nestjs/common'
73
110
 
74
111
  import { mapValues } from '@backend/common'
@@ -79,7 +116,7 @@ ${imports.generate()}
79
116
  *
80
117
  * Usage:
81
118
  * - Create an instance of the Exporter
82
- * - Call the \`add{ModelName}(itemId)\` methods to add the items you want to export.
119
+ * - Call the \`add{ModelName}({id})\` methods to add the items you want to export.
83
120
  * This will automatically add all dependencies of the item.
84
121
  * - Call the \`exportData()\` method to get the encoded Excel data.
85
122
  * - Alternatively, you can call 'exportAll()' to dump all data.
@@ -44,18 +44,17 @@ function generateModelBusinessLogicView({ model, meta }) {
44
44
  const refMeta = (0, meta_1.getModelMetadata)({ model: refModel });
45
45
  const variableGetter = `await this.${viewServiceClassName}.${refMeta.businessLogic.view.serviceVariableName}.get(itemRaw.${relation.name})`;
46
46
  const variablePresenceCheck = `
47
- if (!${relation.relatedModelBacklinkFieldName}) {
47
+ if (!${relation.relationFieldName}) {
48
48
  throw new Error(\`Could not find ${refMeta.types.typeName} with id \${itemRaw.${relation.name}} for ${model.typeName}.${relation.name}!\`)
49
49
  }
50
50
  `;
51
- const relationVariableName = relation.relatedModelBacklinkFieldName;
51
+ const relationVariableName = relation.relationFieldName;
52
+ const relationVariableDefinition = relation.isRequired
53
+ ? `${variableGetter};${variablePresenceCheck}`
54
+ : `itemRaw.${relation.name} !== null ? ${variableGetter} : null`;
52
55
  variables.set(relation.name, {
53
56
  variableName: relationVariableName,
54
- variableDefinition: `
55
- const ${relationVariableName} = ${relation.isRequired
56
- ? `${variableGetter};${variablePresenceCheck}`
57
- : `itemRaw.${relation.name} !== null ? ${variableGetter} : null`}
58
- `,
57
+ variableDefinition: `const ${relationVariableName} = ${relationVariableDefinition}`,
59
58
  });
60
59
  }
61
60
  const hasLinkedItems = variables.size > 0;
@@ -11,12 +11,17 @@ const jsdoc_1 = require("../../lib/utils/jsdoc");
11
11
  * Generates types for a given model.
12
12
  */
13
13
  function generateModelTypes({ model, meta }) {
14
- var _a, _b;
14
+ var _a;
15
15
  const idField = model.idField;
16
16
  const imports = imports_1.ImportsGenerator.from(meta.types.filePath);
17
+ /**
18
+ * Tells whether model references other models.
19
+ */
17
20
  let hasLinkedItems = false;
18
21
  for (const relation of (0, fields_1.getRelationFields)(model)) {
22
+ hasLinkedItems = true;
19
23
  if (relation.relationToModel.typeName === model.typeName) {
24
+ // NOTE: All type definitions are already present in this file for this model.
20
25
  continue;
21
26
  }
22
27
  const refModel = relation.relationToModel;
@@ -29,7 +34,6 @@ function generateModelTypes({ model, meta }) {
29
34
  items: [refModel.brandedIdType, refMeta.types.typeName],
30
35
  from: refMeta.types.filePath,
31
36
  });
32
- hasLinkedItems = true;
33
37
  }
34
38
  for (const f of (0, fields_1.getEnumFields)(model)) {
35
39
  const refEnumMeta = (0, meta_1.getEnumMetadata)({ enumerator: f.enumerator });
@@ -44,30 +48,34 @@ function generateModelTypes({ model, meta }) {
44
48
  from: schemaMeta.types.dto.path,
45
49
  });
46
50
  const decoderNames = meta.types.zodDecoderFnNames;
51
+ const linkedTypeDefinition = `
52
+ export type ${meta.types.linkedTypeName} = {
53
+ ${model.fields
54
+ .map((f) => `
55
+ ${getFieldComment(f)}
56
+ ${getLinkedFieldType(f)}${f.isRequired ? '' : ' | null'}
57
+ `)
58
+ .join('\n')}
59
+ }
60
+ `;
47
61
  return /* ts */ `
48
62
  import { z } from 'zod'
49
63
 
50
64
  ${imports.generate()}
51
65
 
52
- ${(0, jsdoc_1.toJsDocComment)((_b = (_a = model.description) === null || _a === void 0 ? void 0 : _a.split('\n')) !== null && _b !== void 0 ? _b : [])}
66
+ ${(0, jsdoc_1.toJsDocComment)((_a = model.description) === null || _a === void 0 ? void 0 : _a.split('\n'))}
53
67
  export type ${meta.types.typeName} = {
54
68
  ${model.fields
55
- .map((f) => `
56
- ${getFieldComment(f)}
57
- ${f.name}: ${getFieldType(f)}${f.isRequired ? '' : ' | null'}`)
69
+ .map((f) => {
70
+ return `
71
+ ${getFieldComment(f)}
72
+ ${f.name}: ${getFieldType(f)}${f.isRequired ? '' : ' | null'}
73
+ `;
74
+ })
58
75
  .join('\n')}
59
76
  }
60
77
 
61
- ${!hasLinkedItems
62
- ? ``
63
- : `
64
- export type ${meta.types.linkedTypeName} = {
65
- ${model.fields
66
- .map((f) => `
67
- ${getFieldComment(f)}
68
- ${getLinkedFieldType(f)}${f.isRequired ? '' : ' | null'}`)
69
- .join('\n')}
70
- }`}
78
+ ${hasLinkedItems ? linkedTypeDefinition : ``}
71
79
 
72
80
  /**
73
81
  * Branded Id type that should be used to identify an instance of a ${meta.userFriendlyName}.
@@ -187,7 +195,7 @@ function getLinkedFieldType(f) {
187
195
  case 'enum':
188
196
  return `${f.name}: ${f.typeName}`;
189
197
  case 'relation':
190
- return `${f.relatedModelBacklinkFieldName}: ${f.relationToModel.typeName}`;
198
+ return `${f.relationFieldName}: ${f.relationToModel.typeName}`;
191
199
  case 'id':
192
200
  return `${f.name}: ${f.model.brandedIdType}`;
193
201
  case 'scalar':
@@ -170,6 +170,10 @@ export type ModelFields = {
170
170
  * All fields of the model
171
171
  */
172
172
  fields: Field[];
173
+ /**
174
+ * A list of models that reference this model in their relations.
175
+ */
176
+ relatedModels: ModelCore[];
173
177
  };
174
178
  /**
175
179
  * A field of a model.
@@ -313,12 +317,12 @@ export type FieldRelation = Prettify<Omit<FieldCore, 'name'> & {
313
317
  */
314
318
  name: Types.FieldName;
315
319
  /**
316
- * Name of the field in the related model that references this model (e.g. `aggregation`).
320
+ * Name of the relation field in the model.
317
321
  *
318
322
  * NOTE: This does not reference the database column name, but the field name
319
323
  * in the model (e.g. not `aggregationId`, but `aggregation`).
320
324
  */
321
- relatedModelBacklinkFieldName: Types.FieldName;
325
+ relationFieldName: Types.FieldName;
322
326
  /**
323
327
  * Name of the unbranded TypeScript type of the field, e.g. string
324
328
  *
@@ -329,7 +333,7 @@ export type FieldRelation = Prettify<Omit<FieldCore, 'name'> & {
329
333
  */
330
334
  unbrandedTypeName: Types.TypeName;
331
335
  /**
332
- * The referenced model
336
+ * The referenced model.
333
337
  */
334
338
  relationToModel: ModelCore;
335
339
  }>;
@@ -343,12 +347,12 @@ export declare const isFieldRelation: (field: Field) => field is Prettify<Omit<F
343
347
  */
344
348
  name: Types.FieldName;
345
349
  /**
346
- * Name of the field in the related model that references this model (e.g. `aggregation`).
350
+ * Name of the relation field in the model.
347
351
  *
348
352
  * NOTE: This does not reference the database column name, but the field name
349
353
  * in the model (e.g. not `aggregationId`, but `aggregation`).
350
354
  */
351
- relatedModelBacklinkFieldName: Types.FieldName;
355
+ relationFieldName: Types.FieldName;
352
356
  /**
353
357
  * Name of the unbranded TypeScript type of the field, e.g. string
354
358
  *
@@ -359,7 +363,7 @@ export declare const isFieldRelation: (field: Field) => field is Prettify<Omit<F
359
363
  */
360
364
  unbrandedTypeName: Types.TypeName;
361
365
  /**
362
- * The referenced model
366
+ * The referenced model.
363
367
  */
364
368
  relationToModel: ModelCore;
365
369
  }>;
@@ -136,34 +136,43 @@ function parseModel({ dmmfModel, enums, models, config, }) {
136
136
  if (core.attributes.ignore) {
137
137
  return undefined;
138
138
  }
139
- // NOTE: We assume that each relation may only reference one field. Because of this,
140
- // we can "relate" a given relation to a scalar field used in the relation.
141
- // Since Prisma doesn't mark those fields as relations, we need to preprocess
142
- // relations and then figure out which scalar fields are used in relations.
143
- const relations = {};
139
+ // NOTE: We assume that each relation may only reference one field.
140
+ // Because of this, we can "relate" a given relation to a scalar field
141
+ // used as a foreign key.
142
+ //
143
+ // Since Prisma doesn't mark foreign-key fields as relations,
144
+ // we need to preprocess relations and then figure out which scalar
145
+ // fields are actually foreign-keys.
146
+ const referencedModels = {};
147
+ /**
148
+ * A map of models that are referenced in any way in the relations.
149
+ */
150
+ const relatedModels = {};
144
151
  for (const dmmfField of dmmfModel.fields) {
145
- if (dmmfField.kind !== 'object' ||
146
- !dmmfField.relationName ||
147
- !dmmfField.relationFromFields ||
148
- !dmmfField.relationToFields) {
152
+ if (dmmfField.kind !== 'object' || !dmmfField.relationName) {
149
153
  continue;
150
154
  }
151
- if (dmmfField.relationFromFields.length > 1) {
152
- (0, error_1.throwError)(`Field ${highlight(`${dmmfModel.name}.${dmmfField.relationName}`)} has more than one "from" field`);
153
- }
154
155
  const referencedModel = models.find((m) => m.sourceName === dmmfField.type);
155
156
  if (!referencedModel) {
156
157
  (0, error_1.throwError)(`Field ${highlight(`${dmmfModel.name}.${dmmfField.name}`)} references unknown model ${highlight(dmmfField.type)}.`);
157
158
  }
159
+ if (!dmmfField.relationFromFields || dmmfField.relationFromFields.length === 0) {
160
+ // NOTE: This field has no foreign-key values in this model so it must be a back-relation.
161
+ relatedModels[dmmfField.type] = referencedModel;
162
+ continue;
163
+ }
164
+ if (dmmfField.relationFromFields.length > 1) {
165
+ (0, error_1.throwError)(`Field ${highlight(`${dmmfModel.name}.${dmmfField.relationName}`)} has more than one "from" field`);
166
+ }
158
167
  if (dmmfField.relationOnDelete && dmmfField.relationOnDelete !== 'NoAction') {
159
168
  (0, error_1.throwError)(`Investigate model ${highlight(dmmfModel.name)}: "onDelete" attribute for relationship ${highlight(dmmfField.relationName)} must be "NoAction": any deletes must be handled in the application layer, e.g. to update repository and search caches!`);
160
169
  }
161
170
  // NOTE: At this point, we only have the `ModelCore`. After all models are parsed, we need to updated
162
171
  // the relations with the full `Model`. This is done in the `linkModels` function.
163
- relations[dmmfField.relationFromFields[0]] = referencedModel;
172
+ referencedModels[dmmfField.relationFromFields[0]] = referencedModel;
164
173
  }
165
174
  const relationFields = dmmfModel.fields
166
- .filter((f) => f.kind === 'object' && f.relationToFields && f.relationToFields.length === 1)
175
+ .filter((f) => { var _a; return f.kind === 'object' && ((_a = f.relationToFields) === null || _a === void 0 ? void 0 : _a.length) === 1; })
167
176
  .reduce((acc, f) => {
168
177
  if (f.relationFromFields && f.relationFromFields[0]) {
169
178
  acc[f.relationFromFields[0]] = f;
@@ -171,9 +180,14 @@ function parseModel({ dmmfModel, enums, models, config, }) {
171
180
  return acc;
172
181
  }, {});
173
182
  const fields = dmmfModel.fields
174
- .filter((dmmfField) =>
175
- // if field is a "backlink", i.e. the receiver of a relation, we ignore it
176
- dmmfField.kind !== 'object')
183
+ .filter((dmmfField) => {
184
+ // NOTE: This is a relation field that we'll handle when we process its foreign-key. If it's a back relation
185
+ // then it won't have a foreign-key and we simply ignore it.
186
+ if (dmmfField.kind === 'object') {
187
+ return false;
188
+ }
189
+ return true;
190
+ })
177
191
  .map((dmmfField) => {
178
192
  const attributes = (0, attributes_1.getFieldAttributes)(dmmfField);
179
193
  const fieldName = highlight(`${dmmfModel.name}.${dmmfField.name}`);
@@ -187,17 +201,19 @@ function parseModel({ dmmfModel, enums, models, config, }) {
187
201
  schemaType: dmmfField.type,
188
202
  };
189
203
  // NOTE: We mark scalar fields which are used in relations as relation fields by Purple Schema standards.
190
- if (dmmfField.name in relations) {
191
- const refModel = relations[dmmfField.name];
204
+ if (dmmfField.name in referencedModels) {
205
+ const refModel = referencedModels[dmmfField.name];
192
206
  const refField = relationFields[dmmfField.name];
193
207
  if (!refField) {
194
208
  (0, error_1.throwError)(`${fieldName}: Relation field ${highlight(dmmfField.name)} not found.`);
195
209
  }
196
- return Object.assign(Object.assign({ kind: 'relation' }, shared), { relatedModelBacklinkFieldName: Types.toFieldName(refField.name), typeName: Types.toTypeName(dmmfField.type), unbrandedTypeName: getTsTypeForId(dmmfField), relationToModel: refModel });
210
+ const _field = Object.assign(Object.assign({ kind: 'relation' }, shared), { relationFieldName: Types.toFieldName(refField.name), unbrandedTypeName: getTsTypeForId(dmmfField), relationToModel: refModel });
211
+ return _field;
197
212
  }
198
213
  if (dmmfField.isId) {
199
214
  const isGeneratedField = isAutoIncrementField(dmmfField) || isUUIDField(dmmfField);
200
- return Object.assign(Object.assign({ kind: 'id' }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
215
+ const _field = Object.assign(Object.assign({ kind: 'id' }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isGeneratedField, unbrandedTypeName: getTsTypeForId(dmmfField), model: core });
216
+ return _field;
201
217
  }
202
218
  if (dmmfField.kind === 'scalar') {
203
219
  let validation = undefined;
@@ -210,14 +226,16 @@ function parseModel({ dmmfModel, enums, models, config, }) {
210
226
  if (dmmfField.type === 'Float') {
211
227
  validation = { type: 'float' };
212
228
  }
213
- return Object.assign(Object.assign({ kind: 'scalar', validation }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isAutoIncrementField(dmmfField), tsTypeName: getTsTypeForScalar(dmmfField) });
229
+ const _field = Object.assign(Object.assign({ kind: 'scalar', validation }, shared), { isUnique: isUniqueField(dmmfField), isGenerated: isAutoIncrementField(dmmfField), tsTypeName: getTsTypeForScalar(dmmfField) });
230
+ return _field;
214
231
  }
215
232
  if (dmmfField.kind === 'enum') {
216
233
  const fieldEnumDef = enums.find((e) => e.sourceName === dmmfField.type);
217
234
  if (!fieldEnumDef) {
218
235
  (0, error_1.throwError)(`${fieldName}: Field references unknown enum ${highlight(dmmfField.type)}.`);
219
236
  }
220
- return Object.assign(Object.assign({ kind: 'enum' }, shared), { typeName: getTsTypeForEnum(dmmfField), enumerator: fieldEnumDef });
237
+ const _field = Object.assign(Object.assign({ kind: 'enum' }, shared), { typeName: getTsTypeForEnum(dmmfField), enumerator: fieldEnumDef });
238
+ return _field;
221
239
  }
222
240
  (0, error_1.throwError)(`${fieldName} is not scalar, enum nor relation.`);
223
241
  })
@@ -228,7 +246,7 @@ function parseModel({ dmmfModel, enums, models, config, }) {
228
246
  nameField,
229
247
  fields,
230
248
  createdAtField,
231
- updatedAtField });
249
+ updatedAtField, relatedModels: Object.values(relatedModels) });
232
250
  }
233
251
  /**
234
252
  * Checks that there is exactly one id field and that there is at most one default field.
@@ -290,7 +308,13 @@ function validateFields({ fields, model: { name } }) {
290
308
  if (!idField) {
291
309
  (0, error_1.throwError)(`Model ${highlight(name)} does not have an id field`);
292
310
  }
293
- return { idField, defaultField, nameField: (_a = labelField !== null && labelField !== void 0 ? labelField : nameField) !== null && _a !== void 0 ? _a : idField, createdAtField, updatedAtField };
311
+ return {
312
+ idField,
313
+ defaultField,
314
+ nameField: (_a = labelField !== null && labelField !== void 0 ? labelField : nameField) !== null && _a !== void 0 ? _a : idField,
315
+ createdAtField,
316
+ updatedAtField,
317
+ };
294
318
  }
295
319
  function isAutoIncrementField(fieldDmmf) {
296
320
  if (fieldDmmf.default === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {