@neohm/nh-cli 1.1.4 → 1.2.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/README.md +146 -12
- package/package.json +1 -1
- package/src/commands/gen.flag.js +353 -0
- package/src/commands/gen.js +36 -78
- package/src/index.js +57 -2
- package/src/lib/collision.js +82 -0
- package/src/lib/format.js +54 -0
- package/src/lib/registry.js +120 -0
- package/src/lib/sync.engine.js +34 -7
- package/src/lib/templates.js +349 -32
package/src/lib/templates.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
+
toWords,
|
|
4
5
|
toDotCase,
|
|
5
6
|
toPascalCase,
|
|
6
7
|
toKebabCase,
|
|
@@ -16,6 +17,17 @@ const {
|
|
|
16
17
|
* across nestjs-boilerplate.
|
|
17
18
|
*/
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A named import, wrapped the way prettier's 80-column printer would: one line
|
|
22
|
+
* while it fits, one member per line once it doesn't. Generated files are
|
|
23
|
+
* committed as-is, so they have to come out already formatted.
|
|
24
|
+
*/
|
|
25
|
+
function namedImport(members, from) {
|
|
26
|
+
const oneLine = `import { ${members.join(', ')} } from '${from}';`;
|
|
27
|
+
if (oneLine.length <= 80) return oneLine;
|
|
28
|
+
return `import {\n${members.map((m) => ` ${m},`).join('\n')}\n} from '${from}';`;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/** Build the canonical names for a given entity-ish base (e.g. "Shape"). */
|
|
20
32
|
function names(base) {
|
|
21
33
|
const dot = toDotCase(base);
|
|
@@ -32,6 +44,8 @@ function names(base) {
|
|
|
32
44
|
attributesDtoFile: `${dot}.attributes.dto.ts`,
|
|
33
45
|
controllerClass: `${pascal}Controller`,
|
|
34
46
|
controllerFile: `${dot}.controller.ts`,
|
|
47
|
+
serviceClass: `${pascal}Service`,
|
|
48
|
+
serviceFile: `${dot}.service.ts`,
|
|
35
49
|
dtoClass: `Add${pascal}DataDto`,
|
|
36
50
|
dtoFile: `add.${dot}.data.dto.ts`,
|
|
37
51
|
processorClass: `${pascal}DataProcessor`,
|
|
@@ -42,6 +56,17 @@ function names(base) {
|
|
|
42
56
|
subscriberFile: `${dot}.subscriber.ts`,
|
|
43
57
|
jobClass: `${pascal}Job`,
|
|
44
58
|
jobFile: `${dot}.job.ts`,
|
|
59
|
+
// External (API-key) surface. Mirrors the internal names with an
|
|
60
|
+
// `external.` filename prefix and an `External` class prefix, matching
|
|
61
|
+
// external/controllers/external.test.controller.ts in the boilerplate.
|
|
62
|
+
externalControllerClass: `External${pascal}Controller`,
|
|
63
|
+
externalControllerFile: `external.${dot}.controller.ts`,
|
|
64
|
+
externalServiceClass: `External${pascal}Service`,
|
|
65
|
+
externalServiceFile: `external.${dot}.service.ts`,
|
|
66
|
+
externalProcessorClass: `External${pascal}DataProcessor`,
|
|
67
|
+
externalProcessorFile: `external.${dot}.data.processor.ts`,
|
|
68
|
+
externalListProcessorClass: `External${pascal}ListProcessor`,
|
|
69
|
+
externalListProcessorFile: `external.${dot}.list.processor.ts`,
|
|
45
70
|
// Fed through naming.migrationFileName/migrationClassName so the migration
|
|
46
71
|
// `nh gen` writes is named exactly like one from `nh migration`.
|
|
47
72
|
migrationName: `Add${pascal}Table`,
|
|
@@ -50,14 +75,26 @@ function names(base) {
|
|
|
50
75
|
|
|
51
76
|
function entity(base, tableName) {
|
|
52
77
|
const n = names(base);
|
|
53
|
-
return `import { Column, Entity } from 'typeorm';
|
|
54
|
-
import { CommonEntity } from '@neohm/nestend';
|
|
78
|
+
return `import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
|
|
79
|
+
import { CommonEntity, TenantEntity } from '@neohm/nestend';
|
|
55
80
|
import { ${n.attributesDtoClass} } from '../dtos/${n.dot}.attributes.dto';
|
|
56
81
|
|
|
57
82
|
@Entity({ name: '${tableName}' })
|
|
58
83
|
export class ${n.entityClass} extends CommonEntity {
|
|
84
|
+
// Tenant scope — the service and the list processor both filter on it.
|
|
85
|
+
@Column()
|
|
86
|
+
tenant_id: string;
|
|
87
|
+
|
|
88
|
+
// TODO: declare the columns ${n.pascal} owns.
|
|
89
|
+
|
|
59
90
|
@Column({ type: 'jsonb' })
|
|
60
91
|
attributes: ${n.attributesDtoClass};
|
|
92
|
+
|
|
93
|
+
// Relations and methods
|
|
94
|
+
|
|
95
|
+
@ManyToOne(() => TenantEntity)
|
|
96
|
+
@JoinColumn({ name: 'tenant_id' })
|
|
97
|
+
tenant: TenantEntity;
|
|
61
98
|
}
|
|
62
99
|
`;
|
|
63
100
|
}
|
|
@@ -70,32 +107,259 @@ function attributesDto(base) {
|
|
|
70
107
|
`;
|
|
71
108
|
}
|
|
72
109
|
|
|
73
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Controllers hold no business logic. They authenticate the caller, then
|
|
112
|
+
* delegate to the module's service — so the HTTP surface stays readable and
|
|
113
|
+
* the same operation is reusable from a job, a subscriber or another service.
|
|
114
|
+
*/
|
|
115
|
+
function controller(base, { processor = false, listProcessor = false } = {}) {
|
|
74
116
|
const n = names(base);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
const svcProp = `${n.camel}Service`;
|
|
118
|
+
const hasPost = processor || listProcessor;
|
|
119
|
+
|
|
120
|
+
// @nestjs/common members, alphabetised the way prettier leaves them.
|
|
121
|
+
const nestCommon = ['Controller', 'Get', 'Param'];
|
|
122
|
+
if (hasPost) nestCommon.push('Body', 'Post');
|
|
123
|
+
nestCommon.sort();
|
|
124
|
+
|
|
125
|
+
const nestendImports = ['AuthService'];
|
|
126
|
+
if (listProcessor) nestendImports.push('CommonListFilterDto');
|
|
127
|
+
nestendImports.push('CommonParamsDto');
|
|
128
|
+
|
|
129
|
+
const imports = [
|
|
130
|
+
namedImport(nestCommon, '@nestjs/common'),
|
|
131
|
+
// @ApiBearerAuth() attaches the requirement to these operations. The
|
|
132
|
+
// DocumentBuilder call only registers the scheme (under its default name,
|
|
133
|
+
// 'bearer') and draws the Authorize button — without the decorator
|
|
134
|
+
// swagger-ui shows no padlock and never sends the Authorization header.
|
|
135
|
+
`import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';`,
|
|
136
|
+
namedImport(nestendImports, '@neohm/nestend'),
|
|
137
|
+
`import { ${n.serviceClass} } from '../services/${n.dot}.service';`,
|
|
138
|
+
];
|
|
139
|
+
if (processor) {
|
|
140
|
+
imports.push(
|
|
141
|
+
`import { ${n.dtoClass} } from '../dtos/add.${n.dot}.data.dto';`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
78
144
|
|
|
145
|
+
const methods = [
|
|
146
|
+
` @Get(':id')
|
|
147
|
+
async get${n.pascal}(@Param() params: CommonParamsDto) {
|
|
148
|
+
const { user } = await this.authService.validateAccess(
|
|
149
|
+
'add-scope',
|
|
150
|
+
'add-role',
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return this.${svcProp}.get(user, params.id);
|
|
154
|
+
}`,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
if (processor) {
|
|
158
|
+
methods.push(` @Post()
|
|
159
|
+
async upsert${n.pascal}(@Body() body: ${n.dtoClass}) {
|
|
160
|
+
await this.authService.validateAccess('add-scope', 'add-role');
|
|
161
|
+
|
|
162
|
+
return this.${svcProp}.upsert(body);
|
|
163
|
+
}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (listProcessor) {
|
|
167
|
+
methods.push(` @Post('list')
|
|
168
|
+
async list(@Body() body: CommonListFilterDto) {
|
|
169
|
+
const { user } = await this.authService.validateAccess(
|
|
170
|
+
'add-scope',
|
|
171
|
+
'add-role',
|
|
172
|
+
);
|
|
173
|
+
return this.${svcProp}.list(user, body);
|
|
174
|
+
}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return `${imports.join('\n')}
|
|
178
|
+
|
|
179
|
+
// TODO: replace 'add-scope' / 'add-role' with this module's scope and role.
|
|
79
180
|
@ApiTags('${n.kebab}')
|
|
181
|
+
@ApiBearerAuth()
|
|
80
182
|
@Controller('api/v1/${n.kebab}')
|
|
81
183
|
export class ${n.controllerClass} {
|
|
82
|
-
constructor(
|
|
184
|
+
constructor(
|
|
185
|
+
private readonly ${svcProp}: ${n.serviceClass},
|
|
186
|
+
private readonly authService: AuthService,
|
|
187
|
+
) {}
|
|
188
|
+
|
|
189
|
+
${methods.join('\n\n')}
|
|
83
190
|
}
|
|
84
191
|
`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The module's service layer, always generated: single-record reads live here,
|
|
196
|
+
* and write / list work is handed to the processors. Injected into the
|
|
197
|
+
* controller, so nothing downstream has to know how a record is fetched.
|
|
198
|
+
*
|
|
199
|
+
* `get()` is unconditional — it is the one operation that needs no processor.
|
|
200
|
+
* `upsert()` appears with a data processor, `list()` with a listing processor.
|
|
201
|
+
*/
|
|
202
|
+
function service(base, { processor = false, listProcessor = false } = {}) {
|
|
203
|
+
const n = names(base);
|
|
204
|
+
const label = toWords(base).join(' ');
|
|
205
|
+
|
|
206
|
+
const nestendImports = [];
|
|
207
|
+
if (listProcessor) {
|
|
208
|
+
nestendImports.push('CommonListFilterDto', 'ListFilterService');
|
|
209
|
+
}
|
|
210
|
+
nestendImports.push('NotFoundException', 'UserEntity');
|
|
211
|
+
|
|
212
|
+
const imports = [
|
|
213
|
+
namedImport(nestendImports, '@neohm/nestend'),
|
|
214
|
+
`import { Injectable } from '@nestjs/common';`,
|
|
215
|
+
];
|
|
216
|
+
if (listProcessor) {
|
|
217
|
+
imports.push(
|
|
218
|
+
`import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (processor) {
|
|
222
|
+
imports.push(
|
|
223
|
+
`import { ${n.dtoClass} } from '../dtos/add.${n.dot}.data.dto';`,
|
|
224
|
+
`import { ${n.processorClass} } from '../libraries/${n.dot}.data.processor';`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
imports.push(
|
|
228
|
+
`import { ${n.entityClass} } from '../entities/${n.dot}.entity';`,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const members = [];
|
|
232
|
+
if (listProcessor) {
|
|
233
|
+
members.push(
|
|
234
|
+
` constructor(protected readonly listFilterService: ListFilterService) {}`,
|
|
235
|
+
);
|
|
85
236
|
}
|
|
86
|
-
return `import { Body, Controller, Post } from '@nestjs/common';
|
|
87
|
-
import { ApiTags } from '@nestjs/swagger';
|
|
88
|
-
import { CommonListFilterDto, SqlService } from '@neohm/nestend';
|
|
89
|
-
import { ${n.listProcessorClass} } from '../libraries/${n.dot}.list.processor';
|
|
90
237
|
|
|
91
|
-
|
|
92
|
-
|
|
238
|
+
// The tenant scope is part of the lookup, not a check after it: a row
|
|
239
|
+
// belonging to another tenant must read as "not found", never as "forbidden".
|
|
240
|
+
members.push(` async get(user: UserEntity, id: string) {
|
|
241
|
+
const ${n.camel} = await ${n.entityClass}.findOne({
|
|
242
|
+
where: { id, tenant_id: user.organization_id },
|
|
243
|
+
});
|
|
244
|
+
if (!${n.camel}) throw new NotFoundException('Invalid ${label} id');
|
|
245
|
+
|
|
246
|
+
return ${n.camel};
|
|
247
|
+
}`);
|
|
248
|
+
|
|
249
|
+
if (processor) {
|
|
250
|
+
// Insert or update: CommonDataDto carries an optional `id`, and the data
|
|
251
|
+
// processor decides which of the two the payload asks for.
|
|
252
|
+
members.push(` async upsert(data: ${n.dtoClass}) {
|
|
253
|
+
return new ${n.processorClass}().process(data);
|
|
254
|
+
}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (listProcessor) {
|
|
258
|
+
// Emit the call already wrapped the way prettier's 80-column printer
|
|
259
|
+
// would, so a long entity name doesn't leave the generated file dirty.
|
|
260
|
+
// Prettier's three tiers: keep the fluent one-liner; else break the
|
|
261
|
+
// .process() argument; else break the constructor arguments too.
|
|
262
|
+
const call = `new ${n.listProcessorClass}(this.listFilterService, user)`;
|
|
263
|
+
const oneLine = ` return ${call}.process(data);`;
|
|
264
|
+
const brokenArg = ` return ${call}.process(`;
|
|
265
|
+
let body;
|
|
266
|
+
if (oneLine.length <= 80) {
|
|
267
|
+
body = oneLine;
|
|
268
|
+
} else if (brokenArg.length <= 80) {
|
|
269
|
+
body = `${brokenArg}\n data,\n );`;
|
|
270
|
+
} else {
|
|
271
|
+
body = ` return new ${n.listProcessorClass}(\n this.listFilterService,\n user,\n ).process(data);`;
|
|
272
|
+
}
|
|
273
|
+
members.push(` async list(user: UserEntity, data: CommonListFilterDto) {
|
|
274
|
+
${body}
|
|
275
|
+
}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return `${imports.join('\n')}
|
|
279
|
+
|
|
280
|
+
@Injectable()
|
|
281
|
+
export class ${n.serviceClass} {
|
|
282
|
+
${members.join('\n\n')}
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Bare controller for `nh gen -c`: swagger tag, the caller-supplied route
|
|
289
|
+
* under api/v1, AuthService on hand, and one empty `check()` to build on.
|
|
290
|
+
*/
|
|
291
|
+
function bareController(base, routePath) {
|
|
292
|
+
const n = names(base);
|
|
293
|
+
return `import { Controller, Get } from '@nestjs/common';
|
|
294
|
+
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
|
295
|
+
import { AuthService } from '@neohm/nestend';
|
|
296
|
+
|
|
297
|
+
@ApiTags('${routePath}')
|
|
298
|
+
@ApiBearerAuth()
|
|
299
|
+
@Controller('api/v1/${routePath}')
|
|
93
300
|
export class ${n.controllerClass} {
|
|
94
|
-
constructor(private readonly
|
|
301
|
+
constructor(private readonly authService: AuthService) {}
|
|
95
302
|
|
|
96
|
-
@
|
|
97
|
-
async
|
|
98
|
-
|
|
303
|
+
@Get('check')
|
|
304
|
+
async check() {}
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Bare external controller for `nh gen -x -c`. The API-key surface differs
|
|
311
|
+
* from the internal one in three ways, all of them load-bearing: the route is
|
|
312
|
+
* api/e/v1, the key is declared to swagger with @ApiSecurity('api-key'), and
|
|
313
|
+
* access is checked through ApiKeyService rather than AuthService.
|
|
314
|
+
*/
|
|
315
|
+
function bareExternalController(base, routePath) {
|
|
316
|
+
const n = names(base);
|
|
317
|
+
return `import { Controller, Get } from '@nestjs/common';
|
|
318
|
+
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
|
319
|
+
import { ApiKeyService } from '@neohm/nestend';
|
|
320
|
+
|
|
321
|
+
@ApiTags('external-${routePath} (Api key)')
|
|
322
|
+
@ApiSecurity('api-key')
|
|
323
|
+
@Controller('api/e/v1/${routePath}')
|
|
324
|
+
export class ${n.externalControllerClass} {
|
|
325
|
+
constructor(private readonly apiKeyService: ApiKeyService) {}
|
|
326
|
+
|
|
327
|
+
@Get('check')
|
|
328
|
+
async check() {}
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Bare injectable service for `nh gen -s` / `nh gen -x -s`. */
|
|
334
|
+
function bareService(base, { external = false } = {}) {
|
|
335
|
+
const n = names(base);
|
|
336
|
+
const cls = external ? n.externalServiceClass : n.serviceClass;
|
|
337
|
+
return `import { Injectable } from '@nestjs/common';
|
|
338
|
+
|
|
339
|
+
@Injectable()
|
|
340
|
+
export class ${cls} {}
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Standalone job for `nh gen -j`. Unlike the entity job the wizard scaffolds,
|
|
346
|
+
* this one is not driven by a subscriber, so it takes no DatabaseEventDto and
|
|
347
|
+
* references no entity — whatever `dispatch()` was handed arrives as `data`.
|
|
348
|
+
*/
|
|
349
|
+
function additionalJob(base) {
|
|
350
|
+
const n = names(base);
|
|
351
|
+
return `import { CommonJob, QueueService } from '@neohm/nestend';
|
|
352
|
+
import { Injectable } from '@nestjs/common';
|
|
353
|
+
import { JobConstants } from '../../constants/job.constants';
|
|
354
|
+
|
|
355
|
+
@Injectable()
|
|
356
|
+
export class ${n.jobClass} extends CommonJob {
|
|
357
|
+
constructor(protected readonly queueService: QueueService) {
|
|
358
|
+
super(JobConstants.${n.camel}Job);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async handle(data: unknown): Promise<void> {
|
|
362
|
+
return;
|
|
99
363
|
}
|
|
100
364
|
}
|
|
101
365
|
`;
|
|
@@ -111,12 +375,16 @@ export class ${n.dtoClass} extends CommonDataDto {
|
|
|
111
375
|
`;
|
|
112
376
|
}
|
|
113
377
|
|
|
114
|
-
function processor(base) {
|
|
378
|
+
function processor(base, { external = false } = {}) {
|
|
115
379
|
const n = names(base);
|
|
380
|
+
// An external processor sits one level deeper (external/libraries/), so its
|
|
381
|
+
// reach back into the module's dtos/ gains a segment.
|
|
382
|
+
const up = external ? '../..' : '..';
|
|
383
|
+
const cls = external ? n.externalProcessorClass : n.processorClass;
|
|
116
384
|
return `import { CommonDataProcessor } from '@neohm/nestend';
|
|
117
|
-
import { ${n.dtoClass} } from '
|
|
385
|
+
import { ${n.dtoClass} } from '${up}/dtos/add.${n.dot}.data.dto';
|
|
118
386
|
|
|
119
|
-
export class ${
|
|
387
|
+
export class ${cls} extends CommonDataProcessor {
|
|
120
388
|
protected payload: ${n.dtoClass};
|
|
121
389
|
|
|
122
390
|
async process(data: ${n.dtoClass}) {
|
|
@@ -139,33 +407,63 @@ export class ${n.processorClass} extends CommonDataProcessor {
|
|
|
139
407
|
`;
|
|
140
408
|
}
|
|
141
409
|
|
|
142
|
-
function listProcessor(base, tableName) {
|
|
410
|
+
function listProcessor(base, tableName, { external = false } = {}) {
|
|
143
411
|
const n = names(base);
|
|
412
|
+
const cls = external ? n.externalListProcessorClass : n.listProcessorClass;
|
|
144
413
|
return `import {
|
|
145
414
|
CommonListFilterConfig,
|
|
146
415
|
CommonListFilterDto,
|
|
147
416
|
CommonListProcessor,
|
|
417
|
+
FilterValueTypeEnum,
|
|
418
|
+
ListFilterService,
|
|
148
419
|
SortDirection,
|
|
149
|
-
|
|
420
|
+
UserEntity,
|
|
150
421
|
} from '@neohm/nestend';
|
|
151
422
|
|
|
152
423
|
/**
|
|
153
|
-
* Lists / filters ${n.pascal}.
|
|
154
|
-
*
|
|
155
|
-
*
|
|
424
|
+
* Lists / filters ${n.pascal}.
|
|
425
|
+
*
|
|
426
|
+
* Two separate surfaces, and it matters which one a rule belongs to:
|
|
427
|
+
*
|
|
428
|
+
* - \`columns\` is the caller's allowlist. An identifier must appear here
|
|
429
|
+
* before a request can filter or sort on it, and \`searchable: true\` adds
|
|
430
|
+
* it to the free-text \`str\` sweep. Everything here is public. Declare a
|
|
431
|
+
* \`type\`: it decides how a caller's value is cast and which operators the
|
|
432
|
+
* column accepts — add \`operators\` to narrow that further.
|
|
433
|
+
*
|
|
434
|
+
* - \`predicates\` and \`applyCustomPredicates()\` are yours. Both compile
|
|
435
|
+
* inside the relation — before any join aggregate or LATERAL runs — and
|
|
436
|
+
* carry no public identifier, so a payload cannot name, weaken or compete
|
|
437
|
+
* with them. Lifecycle and tenant scoping belong here, never in
|
|
438
|
+
* \`columns\`. Standing literal clauses go in \`predicates\`; anything
|
|
439
|
+
* carrying a value goes in the method, where add() binds it.
|
|
156
440
|
*/
|
|
157
|
-
export class ${
|
|
441
|
+
export class ${cls} extends CommonListProcessor {
|
|
158
442
|
protected config: CommonListFilterConfig = {
|
|
159
443
|
query: '${tableName} a',
|
|
160
444
|
columns: [
|
|
161
|
-
{ key: 'a.id', identifier: 'id' },
|
|
162
|
-
{
|
|
163
|
-
|
|
445
|
+
{ key: 'a.id', identifier: 'id', type: FilterValueTypeEnum.UUID },
|
|
446
|
+
{
|
|
447
|
+
key: 'a.attributes',
|
|
448
|
+
identifier: 'attributes',
|
|
449
|
+
type: FilterValueTypeEnum.JSON,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
key: 'a.created_at',
|
|
453
|
+
identifier: 'created_at',
|
|
454
|
+
type: FilterValueTypeEnum.DATETIME,
|
|
455
|
+
},
|
|
164
456
|
],
|
|
165
|
-
|
|
457
|
+
// Soft-deleted rows are never listed. \`deleted_at\` is deliberately not a
|
|
458
|
+
// declared column: there is nothing a caller should be doing with it.
|
|
459
|
+
predicates: ['a.deleted_at is null'],
|
|
460
|
+
defaultSort: { field: 'created_at', direction: SortDirection.DESC },
|
|
166
461
|
};
|
|
167
462
|
|
|
168
|
-
constructor(
|
|
463
|
+
constructor(
|
|
464
|
+
protected readonly listFilterService: ListFilterService,
|
|
465
|
+
private readonly user: UserEntity,
|
|
466
|
+
) {
|
|
169
467
|
super();
|
|
170
468
|
}
|
|
171
469
|
|
|
@@ -174,6 +472,11 @@ export class ${n.listProcessorClass} extends CommonListProcessor {
|
|
|
174
472
|
|
|
175
473
|
return this.handle();
|
|
176
474
|
}
|
|
475
|
+
|
|
476
|
+
protected applyCustomPredicates() {
|
|
477
|
+
// Filter for the caller's tenant/organization
|
|
478
|
+
this.predicates.add('a.tenant_id = :tenant_id', this.user.organization_id);
|
|
479
|
+
}
|
|
177
480
|
}
|
|
178
481
|
`;
|
|
179
482
|
}
|
|
@@ -241,10 +544,19 @@ export class ${className} extends MigrationUtility {
|
|
|
241
544
|
|
|
242
545
|
process() {
|
|
243
546
|
this.rootPrimary();
|
|
547
|
+
|
|
548
|
+
this.foreign({ name: 'tenant_id', foreignTable: 'sys_tenants' });
|
|
244
549
|
this.foreign({ name: 'created_by', foreignTable: 'sys_users' });
|
|
245
|
-
|
|
550
|
+
this.foreign({ name: 'updated_by', foreignTable: 'sys_users' });
|
|
551
|
+
|
|
552
|
+
// TODO: add columns matching ${n.entityClass}.
|
|
553
|
+
|
|
246
554
|
this.json('attributes');
|
|
247
555
|
this.timestamps();
|
|
556
|
+
|
|
557
|
+
this.index('tenant_id');
|
|
558
|
+
this.index('created_by');
|
|
559
|
+
this.index('updated_by');
|
|
248
560
|
}
|
|
249
561
|
}
|
|
250
562
|
`;
|
|
@@ -384,6 +696,11 @@ module.exports = {
|
|
|
384
696
|
entity,
|
|
385
697
|
attributesDto,
|
|
386
698
|
controller,
|
|
699
|
+
service,
|
|
700
|
+
bareController,
|
|
701
|
+
bareExternalController,
|
|
702
|
+
bareService,
|
|
703
|
+
additionalJob,
|
|
387
704
|
dto,
|
|
388
705
|
processor,
|
|
389
706
|
listProcessor,
|