@postxl/generator 0.73.5 → 0.74.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.
package/dist/generator.js CHANGED
@@ -421,20 +421,31 @@ function generate(_a) {
421
421
  }
422
422
  // NOTE: Lastly we generate the log of the changes.
423
423
  const log = lock_1.ConsoleUtils.getFilesChangelog(results.map((result) => {
424
- if (result.status === 'write') {
425
- if (result.prevDisk === undefined) {
426
- return { path: result.path, status: 'new' };
427
- }
428
- if (result.disk === undefined) {
429
- return { path: result.path, status: 'deleted' };
430
- }
431
- if (runtime_1.BufferUtils.equals(result.prevDisk.content, result.disk.content) &&
432
- result.prevDisk.mode === result.disk.mode) {
433
- return { path: result.path, status: 'unchanged' };
424
+ switch (result.status) {
425
+ case 'write': {
426
+ if (result.prevDisk === undefined) {
427
+ return { path: result.path, status: 'new' };
428
+ }
429
+ if (result.disk === undefined) {
430
+ return { path: result.path, status: 'deleted' };
431
+ }
432
+ if (runtime_1.BufferUtils.equals(result.prevDisk.content, result.disk.content) &&
433
+ result.prevDisk.mode === result.disk.mode) {
434
+ return { path: result.path, status: 'unchanged' };
435
+ }
436
+ return { path: result.path, status: 'changed' };
434
437
  }
435
- return { path: result.path, status: 'changed' };
438
+ case 'skip':
439
+ if (result.prevDisk &&
440
+ result.disk &&
441
+ runtime_1.BufferUtils.equals(result.prevDisk.content, result.disk.content) &&
442
+ result.prevDisk.mode === result.disk.mode) {
443
+ return { path: result.path, status: 'unchanged' };
444
+ }
445
+ return { path: result.path, status: 'skipped' };
446
+ default:
447
+ throw new types_2.ExhaustiveSwitchCheck(result);
436
448
  }
437
- return { path: result.path, status: 'skipped' };
438
449
  }));
439
450
  console.info(log);
440
451
  const perfEnd = performance.now();
@@ -39,12 +39,12 @@ function generateExporterClass({ models, meta }) {
39
39
  }
40
40
  const linkedModelMeta = (0, meta_1.getModelMetadata)({ model: field.relationToModel });
41
41
  if (field.isRequired) {
42
- linkedItems.push(`await this.${linkedModelMeta.export.exportAddFunctionName}({id: item.${field.name}, includeChildren: false})`);
42
+ linkedItems.push(`await this.${linkedModelMeta.export.exportAddFunctionName}({id: item.${field.name}, includeChildren: false, user})`);
43
43
  }
44
44
  else {
45
45
  linkedItems.push(`
46
46
  if (item.${field.name}) {
47
- await this.${linkedModelMeta.export.exportAddFunctionName}({id: item.${field.name}, includeChildren: false})
47
+ await this.${linkedModelMeta.export.exportAddFunctionName}({id: item.${field.name}, includeChildren: false, user})
48
48
  }`);
49
49
  }
50
50
  }
@@ -55,7 +55,7 @@ function generateExporterClass({ models, meta }) {
55
55
  childItemCalls.push(`
56
56
  // Add all ${linkedModelMeta.userFriendlyNamePlural} that are related to the ${modelMeta.userFriendlyName} via ${referencingField.name}
57
57
  for (const ${referencingField.name} of await this.viewService.${linkedModelMeta.view.serviceVariableName}.data.${linkedFieldMeta.getByForeignKeyIdsMethodFnName}(id)) {
58
- await this.${linkedModelMeta.export.exportAddFunctionName}({id: ${referencingField.name}, includeChildren})
58
+ await this.${linkedModelMeta.export.exportAddFunctionName}({id: ${referencingField.name}, includeChildren, user})
59
59
  }
60
60
  `);
61
61
  }
@@ -65,7 +65,7 @@ function generateExporterClass({ models, meta }) {
65
65
  ${childItemCalls.join('\n')}
66
66
  }`
67
67
  : '';
68
- const signature = `{id, includeChildren = true}: {id: ${model.brandedIdType}, includeChildren?: boolean}`;
68
+ const signature = `{id, includeChildren = true, user}: {id: ${model.brandedIdType}, includeChildren?: boolean, user: User}`;
69
69
  addFunctions.push(`
70
70
  /**
71
71
  * Adds a ${modelMeta.userFriendlyName} and all related (and nested) dependencies to the export.
@@ -74,7 +74,7 @@ function generateExporterClass({ models, meta }) {
74
74
  if (this.${modelMeta.internalPluralName}.has(id)) {
75
75
  return
76
76
  }
77
- const item = await this.viewService.${modelMeta.view.serviceVariableName}.get(id)
77
+ const item = await this.viewService.${modelMeta.view.serviceVariableName}.get({ id, user })
78
78
  if (!item) {
79
79
  this.logger.error(\`Cannot find ${modelMeta.userFriendlyName} \${id}\`)
80
80
  return
@@ -86,7 +86,7 @@ function generateExporterClass({ models, meta }) {
86
86
  ${childItems}
87
87
  }
88
88
  `);
89
- addAllCalls.push(`this.${modelMeta.internalPluralName} = await this.viewService.${modelMeta.view.serviceVariableName}.getAll()`);
89
+ addAllCalls.push(`this.${modelMeta.internalPluralName} = await this.viewService.${modelMeta.view.serviceVariableName}.getAll(user)`);
90
90
  }
91
91
  return /* ts */ `
92
92
  // For consistency, we include \`{ includeChildren = true }\` in the signature of every add function.
@@ -126,7 +126,7 @@ export class ${meta.export.exporterClass.name} {
126
126
  /**
127
127
  * Exports all data.
128
128
  */
129
- public async exportAll(): Promise<${meta.export.encoder.encodedExcelDataTypeName}> {
129
+ public async exportAll(user: User): Promise<${meta.export.encoder.encodedExcelDataTypeName}> {
130
130
  ${addAllCalls.join('\n')}
131
131
 
132
132
  return this.exportData()
@@ -35,10 +35,14 @@ export const ${meta.trpc.routerName} = router({
35
35
  ${defaultField ? defaultValueMethod : ''}
36
36
 
37
37
  get: procedure
38
+ .use(authMiddleware)
38
39
  .input(z.${idField.unbrandedTypeName}().transform(${meta.types.toBrandedIdTypeFnName}))
39
- .query(({ input, ctx }) => ctx.view.${meta.data.dataServiceName}.get(input)),
40
- getMap: procedure.query(({ ctx }) => ctx.view.${meta.data.dataServiceName}.getAll()),
40
+ .query(({ input, ctx }) => ctx.view.${meta.data.dataServiceName}.get({ id: input, user: ctx.user})),
41
+ getMap: procedure
42
+ .use(authMiddleware)
43
+ .query(({ ctx }) => ctx.view.${meta.data.dataServiceName}.getAll(ctx.user)),
41
44
  getList: procedure
45
+ .use(authMiddleware)
42
46
  .input(z.object({
43
47
  cursor: ${meta.view.cursorDecoder}.optional(),
44
48
  sort: z.object({
@@ -60,7 +64,7 @@ export const ${meta.trpc.routerName} = router({
60
64
  .optional(),
61
65
  }))
62
66
  .query(({ input, ctx }) => {
63
- return ctx.view.${meta.data.dataServiceName}.getList(input)
67
+ return ctx.view.${meta.data.dataServiceName}.getList({...input, user: ctx.user })
64
68
  }),
65
69
 
66
70
  create: procedure
@@ -22,7 +22,7 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
22
22
  const { view, update, types, data } = meta;
23
23
  const imports = imports_1.ImportsGenerator.from(meta.update.serviceClassLocation.path).addImports({
24
24
  [data.repository.location.import]: data.repository.className,
25
- [types.importPath]: [model.brandedIdType, types.typeName, types.toBrandedIdTypeFnName],
25
+ [types.importPath]: [model.brandedIdType, types.typeName, types.toBrandedIdTypeFnName, (0, types_1.toTypeName)('User')],
26
26
  [view.serviceLocation.import]: [view.serviceClassName],
27
27
  [schemaMeta.actions.execution.interfaceLocation.import]: [schemaMeta.actions.execution.interface],
28
28
  [schemaMeta.actions.dispatcher.definitionLocation.import]: [schemaMeta.actions.dispatcher.definition],
@@ -47,6 +47,8 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
47
47
 
48
48
  ${imports.generate()}
49
49
 
50
+ import { TRPCError } from '@trpc/server'
51
+
50
52
  export type Scope = "${meta.actions.actionScopeConstType}"
51
53
 
52
54
  export type Actions = {
@@ -136,6 +138,15 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
136
138
  data: CreatePayload;
137
139
  execution: ${schemaMeta.actions.execution.interface}
138
140
  }): Promise<${types.typeName}> {
141
+ // NOTE: User from execution.user should be authorized to create this item.
142
+ // eslint-disable-next-line no-constant-condition
143
+ if (false) {
144
+ throw new TRPCError({
145
+ code: 'UNAUTHORIZED',
146
+ message: \`You are not authorized to create a ${meta.userFriendlyName}\`,
147
+ })
148
+ }
149
+
139
150
  return this.data.create({ item: data, execution })
140
151
  }
141
152
 
@@ -144,6 +155,9 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
144
155
  data: UpdatePayload;
145
156
  execution: ${schemaMeta.actions.execution.interface}
146
157
  }): Promise<${types.typeName}> {
158
+ // NOTE: We need to make sure that the user is authorized to access the item
159
+ await this.authorize(data.id, execution.user)
160
+
147
161
  return this.data.update({ item: data, execution })
148
162
  }
149
163
 
@@ -152,6 +166,18 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
152
166
  data: UpsertPayload;
153
167
  execution: ${schemaMeta.actions.execution.interface}
154
168
  }): Promise<${types.typeName}> {
169
+ if ('id' in data) {
170
+ await this.authorize(data.id, execution.user)
171
+ } else {
172
+ // eslint-disable-next-line no-constant-condition
173
+ if (false) {
174
+ throw new TRPCError({
175
+ code: 'UNAUTHORIZED',
176
+ message: \`You are not authorized to create a ${meta.userFriendlyName}\`,
177
+ })
178
+ }
179
+ }
180
+
155
181
  return this.data.upsert({ item: data, execution })
156
182
  }
157
183
 
@@ -160,10 +186,34 @@ function generateModelBusinessLogicUpdate({ model, meta }) {
160
186
  data: ${model.brandedIdType};
161
187
  execution: ${schemaMeta.actions.execution.interface}
162
188
  }): Promise<${model.brandedIdType}> {
189
+ // NOTE: We need to make sure that the user is authorized to access the item
190
+ await this.authorize(data, execution.user)
191
+
163
192
  return this.data.delete({ id: data, execution })
164
193
  }
165
194
 
166
195
  ${cloneFn}
196
+
197
+ /**
198
+ * Authorizes access to ${meta.userFriendlyName}.
199
+ */
200
+ public async authorize(id: ${model.brandedIdType}, user: User): Promise<void> {
201
+ // NOTE: Root user is always authorized.
202
+ if (this.viewService.users.data.rootUser.id === user.id) {
203
+ return
204
+ }
205
+ const item = await this.data.get(id)
206
+ if (!item) {
207
+ throw new Error(\`${meta.userFriendlyName} with id \${id} not found\`)
208
+ }
209
+ // eslint-disable-next-line no-constant-condition
210
+ if (false) {
211
+ throw new TRPCError({
212
+ code: 'UNAUTHORIZED',
213
+ message: \`You are not authorized to access ${meta.userFriendlyName} with id \${id}\`,
214
+ })
215
+ }
216
+ }
167
217
  }
168
218
  `;
169
219
  }
@@ -15,7 +15,11 @@ function generateModelBusinessLogicView({ model, meta }) {
15
15
  const imports = imports_1.ImportsGenerator.from(meta.view.serviceLocation.path);
16
16
  imports.addImports({
17
17
  [meta.data.repository.location.import]: meta.data.repository.className,
18
- [meta.types.importPath]: [(0, types_1.toAnnotatedTypeName)(model.brandedIdType), (0, types_1.toAnnotatedTypeName)(meta.types.typeName)],
18
+ [meta.types.importPath]: [
19
+ (0, types_1.toAnnotatedTypeName)(model.brandedIdType),
20
+ (0, types_1.toAnnotatedTypeName)(meta.types.typeName),
21
+ (0, types_1.toAnnotatedTypeName)((0, types_1.toTypeName)('User')),
22
+ ],
19
23
  [schemaMeta.view.serviceLocation.path]: schemaMeta.view.serviceClassName,
20
24
  });
21
25
  const compareFnBlock = (0, ast_1.createSwitchStatement)({
@@ -84,6 +88,9 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common'
84
88
 
85
89
  ${imports.generate()}
86
90
 
91
+ import { TRPCError } from '@trpc/server'
92
+
93
+
87
94
  /**
88
95
  * Cursor decoder.
89
96
  */
@@ -123,15 +130,34 @@ export class ${meta.view.serviceClassName} {
123
130
  * Returns the raw ${meta.userFriendlyName} with the given id or null if it does not exist.
124
131
  * Raw: The ${meta.userFriendlyName} only contains linked Ids, not the linked items themselves.
125
132
  */
126
- public async get(id: ${model.brandedIdType}): Promise<${meta.types.typeName} | null> {
127
- return this.data.get(id)
133
+ public async get({ id, user }: { id: ${model.brandedIdType}, user: User }): Promise<${meta.types.typeName} | null> {
134
+ const item = await this.data.get(id)
135
+ if (!item) {
136
+ return null
137
+ }
138
+
139
+ // NOTE: Authorizing user to access the item.
140
+ await this.authorize({ item, user })
141
+
142
+ return item
128
143
  }
129
144
 
130
145
  /**
131
146
  * Returns a map of all ${meta.userFriendlyNamePlural}.
132
147
  */
133
- public async getAll(): Promise<Map<${meta.types.brandedIdType}, ${model.typeName}>> {
134
- return this.data.getAll()
148
+ public async getAll(user: User): Promise<Map<${meta.types.brandedIdType}, ${model.typeName}>> {
149
+ const result = await this.data.getAll()
150
+
151
+ // NOTE: Removing non-authorized items from the result.
152
+ for (const [key, value] of result.entries()) {
153
+ try {
154
+ await this.authorize({ item: value, user })
155
+ } catch (e) {
156
+ result.delete(key)
157
+ }
158
+ }
159
+
160
+ return result
135
161
  }
136
162
 
137
163
  /**
@@ -141,24 +167,51 @@ export class ${meta.view.serviceClassName} {
141
167
  filter,
142
168
  sort,
143
169
  cursor,
170
+ user,
144
171
  }: {
145
172
  filter?: { field: keyof ${model.typeName}; operator: FilterOperator; value: string | number }
146
173
  sort?: { field: keyof ${model.typeName}; ascending: boolean }
147
174
  cursor?: Cursor
148
- }) {
149
- const items = await this.data.getAllAsArray()
175
+ user: User,
176
+ },
177
+ ) {
178
+ const items = await this.data.getAll()
179
+
180
+ // NOTE: Removing non-authorized items from the result.
181
+ for (const [key, value] of items.entries()) {
182
+ try {
183
+ await this.authorize({ item: value, user })
184
+ } catch (e) {
185
+ items.delete(key)
186
+ }
187
+ }
188
+ const authorized = [...items.values()]
189
+
150
190
  const filtered = !filter
151
- ? items
152
- : items.filter((item) => filterFn(item, filter.field, filter.operator, filter.value))
191
+ ? authorized
192
+ : authorized.filter((item) => filterFn(item, filter.field, filter.operator, filter.value))
153
193
 
154
194
  const filteredAndSorted = !sort
155
195
  ? filtered
156
196
  : filtered.sort((a, b) => (sort.ascending ? compare(a, b, sort.field) : -compare(a, b, sort.field)))
157
197
 
158
198
  const paginated = !cursor ? filteredAndSorted : filteredAndSorted.slice(cursor.startRow, cursor.endRow)
159
- return { rows: paginated, count: items.length }
199
+ return { rows: paginated, count: authorized.length }
160
200
  }
161
201
 
202
+ /**
203
+ * Authorizes access to ${meta.userFriendlyName}.
204
+ */
205
+ // eslint-disable-next-line @typescript-eslint/require-await
206
+ public async authorize({ item, user}: { item: ${model.typeName}, user: User }): Promise<void> {
207
+ // eslint-disable-next-line no-constant-condition
208
+ if (false) {
209
+ throw new TRPCError({
210
+ code: 'UNAUTHORIZED',
211
+ message: \`You are not authorized to access ${meta.userFriendlyName} with id \${item.id}\`,
212
+ })
213
+ }
214
+ }
162
215
  }
163
216
 
164
217
  // Utility Functions
@@ -18,6 +18,10 @@ export declare const toPascalCase: (str: string) => string;
18
18
  * Returns a pluralized version of the given string based on the count.
19
19
  */
20
20
  export declare const pluralize: (s: string, count?: number) => string;
21
+ /**
22
+ * Returns true if the given string is already pluralized.
23
+ */
24
+ export declare const isPlural: (s: string) => boolean;
21
25
  /**
22
26
  * Converts each line of a string to a commented line
23
27
  */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.conjugateNames = exports.commentLines = exports.pluralize = exports.toPascalCase = exports.toCamelCase = exports.capitalize = exports.uncapitalize = void 0;
3
+ exports.conjugateNames = exports.commentLines = exports.isPlural = exports.pluralize = exports.toPascalCase = exports.toCamelCase = exports.capitalize = exports.uncapitalize = void 0;
4
4
  /**
5
5
  * Returns the same string with a lowercase first letter.
6
6
  */
@@ -88,6 +88,9 @@ const pluralize = (s, count = 2) => {
88
88
  return s.slice(0, -singular.length + 1) + plural.slice(1);
89
89
  }
90
90
  }
91
+ if (lower.endsWith('ss')) {
92
+ return s.slice(0, -2) + 'sses';
93
+ }
91
94
  if (s.endsWith('y') &&
92
95
  !s.endsWith('ay') &&
93
96
  !s.endsWith('ey') &&
@@ -102,6 +105,14 @@ const pluralize = (s, count = 2) => {
102
105
  return s + 's';
103
106
  };
104
107
  exports.pluralize = pluralize;
108
+ /**
109
+ * Returns true if the given string is already pluralized.
110
+ */
111
+ const isPlural = (s) => {
112
+ const plural = (0, exports.pluralize)(s);
113
+ return plural === s;
114
+ };
115
+ exports.isPlural = isPlural;
105
116
  /**
106
117
  * Converts each line of a string to a commented line
107
118
  */
@@ -34,6 +34,7 @@ const attributes_1 = require("./attributes");
34
34
  */
35
35
  function parsePrismaSchema({ datamodel: { enums: enumsRaw, models: modelsRaw }, config, }) {
36
36
  ensurePXLSystemModelsExist(modelsRaw);
37
+ ensureConsistency({ models: modelsRaw, enums: enumsRaw });
37
38
  // NOTE: We preprocess models and enums so that we can populate relationships.
38
39
  const models = modelsRaw.map((dmmfModel) => parseModelCore({ dmmfModel, config }));
39
40
  const enums = enumsRaw.map((dmmfEnum) => parseEnum({ dmmfEnum, config }));
@@ -72,6 +73,56 @@ function ensurePXLSystemModelsExist(models) {
72
73
  }
73
74
  }
74
75
  }
76
+ /**
77
+ * Validates:
78
+ * - That there are no duplicate model names
79
+ * - That model names are singular
80
+ * - That model attributes are valid
81
+ * - That field attributes are valid
82
+ * - That enum attributes are valid
83
+ */
84
+ function ensureConsistency({ models, enums }) {
85
+ const errors = [];
86
+ const modelNames = models.map((m) => m.name);
87
+ const duplicateModelName = modelNames.find((name, i) => modelNames.indexOf(name) !== i);
88
+ if (duplicateModelName) {
89
+ errors.push(`Model ${duplicateModelName} is defined more than once.`);
90
+ }
91
+ for (const model of models) {
92
+ if ((0, string_1.isPlural)(model.name)) {
93
+ errors.push(`Model ${(0, logger_1.highlight)(model.name)} is plural. Please use singular names for models.`);
94
+ }
95
+ try {
96
+ (0, attributes_1.getModelAttributes)(model);
97
+ }
98
+ catch (e) {
99
+ errors.push(`Model ${(0, logger_1.highlight)(model.name)} has invalid model attributes: ${(0, error_1.extractError)(e)}`);
100
+ }
101
+ }
102
+ for (const model of models) {
103
+ for (const field of model.fields) {
104
+ try {
105
+ (0, attributes_1.getFieldAttributes)(field);
106
+ }
107
+ catch (e) {
108
+ errors.push(`Model ${(0, logger_1.highlight)(model.name)} has invalid attributes for field ${(0, logger_1.highlight)(field.name)}:
109
+ ${(0, error_1.extractError)(e)}`);
110
+ }
111
+ }
112
+ }
113
+ for (const enumDef of enums) {
114
+ try {
115
+ (0, attributes_1.getEnumAttributes)(enumDef);
116
+ }
117
+ catch (e) {
118
+ errors.push(`Enum ${(0, logger_1.highlight)(enumDef.name)} has invalid attributes:
119
+ ${(0, error_1.extractError)(e)}`);
120
+ }
121
+ }
122
+ if (errors.length > 0) {
123
+ (0, error_1.throwError)(`${errors.length} ${(0, string_1.pluralize)('issue', errors.length)} detected in schema:\n * ${errors.join('\n * ')}`);
124
+ }
125
+ }
75
126
  function isModelNotIgnored(model) {
76
127
  return model !== undefined && !model.attributes.ignore;
77
128
  }
@@ -79,6 +130,9 @@ function isModelNotIgnored(model) {
79
130
  * Parses the core properties of a model without fields.
80
131
  */
81
132
  function parseModelCore({ dmmfModel, config, }) {
133
+ if ((0, string_1.isPlural)(dmmfModel.name)) {
134
+ (0, error_1.throwError)(`Model ${dmmfModel.name} is plural. Please use singular names for models.`);
135
+ }
82
136
  const attributes = (0, attributes_1.getModelAttributes)(dmmfModel);
83
137
  return {
84
138
  name: Types.toModelName((0, string_1.toPascalCase)(dmmfModel.name)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.73.5",
3
+ "version": "0.74.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {