@postxl/generator 0.73.6 → 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postxl/generator",
3
- "version": "0.73.6",
3
+ "version": "0.74.0",
4
4
  "main": "./dist/generator.js",
5
5
  "typings": "./dist/generator.d.ts",
6
6
  "bin": {