@postxl/generator 0.73.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,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
  }
@@ -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;
@@ -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,67 +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
- if (field.name === 'name' && nameField === undefined) {
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) {
302
+ if (createdAtField != null) {
290
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) {
308
+ if (updatedAtField != null) {
296
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 - ${idField.name} and ${field.name}`);
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 - ${defaultField.name} and ${field.name}`);
316
- }
317
- defaultField = field;
318
- }
319
- //handle name field
320
- if (field.attributes.isNameField) {
321
- if (labelField !== undefined) {
322
- // Note: In case we have a field called "name" but assign the "@@Label" attribute to another field, we ignore the "name" field.
323
- if (labelField.name === 'name') {
324
- labelField = field;
325
- }
326
- else {
327
- (0, error_1.throwError)(`${fieldName} has multiple name fields - ${labelField.name} and ${field.name}`);
328
- }
329
- }
330
- labelField = field;
331
- }
332
325
  }
333
326
  if (!idField) {
334
327
  (0, error_1.throwError)(`Model ${(0, logger_1.highlight)(name)} does not have an id field`);
@@ -336,7 +329,7 @@ function validateFields({ fields, model: { name } }) {
336
329
  return {
337
330
  idField,
338
331
  defaultField,
339
- 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,
340
333
  createdAtField,
341
334
  updatedAtField,
342
335
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.73.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",