@apso/cli 0.46.0 → 0.46.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/commands/generate.js +1 -1
- package/dist/commands/mcp/serve.js +1 -1
- package/dist/lib/generators/base.d.ts +2 -2
- package/dist/lib/generators/typescript.d.ts +2 -2
- package/dist/lib/generators/typescript.js +13 -8
- package/dist/lib/guards.d.ts +12 -3
- package/dist/lib/guards.js +26 -3
- package/dist/lib/templates/guards/scope.guard.eta +65 -1
- package/dist/lib/templates/rest/controller-rest.eta +14 -2
- package/dist/lib/types/generator.d.ts +1 -1
- package/dist/lib/utils/relationships/parse.d.ts +1 -0
- package/dist/lib/utils/relationships/parse.js +4 -3
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -193,7 +193,7 @@ class Generate extends base_command_1.default {
|
|
|
193
193
|
console.log(`[apso] Finished building entity '${entity.name}' in ${entityBuildTime.toFixed(2)} ms`);
|
|
194
194
|
}
|
|
195
195
|
// Generate guards
|
|
196
|
-
const guardFiles = await generator.generateGuards(entities, auth);
|
|
196
|
+
const guardFiles = await generator.generateGuards(entities, auth, relationshipMap);
|
|
197
197
|
for (const file of guardFiles) {
|
|
198
198
|
// eslint-disable-next-line no-await-in-loop
|
|
199
199
|
await writeGenerated(file.path, file.content);
|
|
@@ -402,7 +402,7 @@ class McpServe extends base_command_1.default {
|
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
// Generate guards
|
|
405
|
-
const guardFiles = await generator.generateGuards(entities, auth);
|
|
405
|
+
const guardFiles = await generator.generateGuards(entities, auth, relationshipMap);
|
|
406
406
|
for (const file of guardFiles) {
|
|
407
407
|
const fullPath = path.join(autogenPath, file.path);
|
|
408
408
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as Eta from "eta";
|
|
2
|
-
import { TargetLanguage, GeneratedFile, ValidationResult, GeneratorConfig, EntityGenerationOptions, ControllerGenerationOptions, ServiceGenerationOptions, DtoGenerationOptions, MigrationGenerationOptions, LanguageGenerator, Entity, AuthConfig } from "../types";
|
|
2
|
+
import { TargetLanguage, GeneratedFile, ValidationResult, GeneratorConfig, EntityGenerationOptions, ControllerGenerationOptions, ServiceGenerationOptions, DtoGenerationOptions, MigrationGenerationOptions, LanguageGenerator, Entity, AuthConfig, RelationshipMap } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* Abstract base class for language-specific generators.
|
|
5
5
|
* Provides common functionality and enforces the LanguageGenerator interface.
|
|
@@ -60,7 +60,7 @@ export declare abstract class BaseGenerator implements LanguageGenerator {
|
|
|
60
60
|
/**
|
|
61
61
|
* Generate guard files (auth, scope, etc.)
|
|
62
62
|
*/
|
|
63
|
-
abstract generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
63
|
+
abstract generateGuards(entities: Entity[], auth?: AuthConfig, relationshipMap?: RelationshipMap): Promise<GeneratedFile[]>;
|
|
64
64
|
/**
|
|
65
65
|
* Generate shared query utility files (filter, sort, pagination).
|
|
66
66
|
* Default no-op. Override in language generators that need query utils.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseGenerator } from "./base";
|
|
2
|
-
import { TargetLanguage, GeneratedFile, ValidationResult, GeneratorConfig, EntityGenerationOptions, ControllerGenerationOptions, ServiceGenerationOptions, DtoGenerationOptions, MigrationGenerationOptions, Entity, AuthConfig } from "../types";
|
|
2
|
+
import { TargetLanguage, GeneratedFile, ValidationResult, GeneratorConfig, EntityGenerationOptions, ControllerGenerationOptions, ServiceGenerationOptions, DtoGenerationOptions, MigrationGenerationOptions, Entity, AuthConfig, RelationshipMap } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* TypeScript/NestJS generator that wraps the existing generation functions.
|
|
5
5
|
* This generator produces TypeORM entities, NestJS controllers, services, and DTOs.
|
|
@@ -20,7 +20,7 @@ export declare class TypeScriptGenerator extends BaseGenerator {
|
|
|
20
20
|
emitEvents?: boolean;
|
|
21
21
|
auth?: AuthConfig;
|
|
22
22
|
}): Promise<GeneratedFile[]>;
|
|
23
|
-
generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
23
|
+
generateGuards(entities: Entity[], auth?: AuthConfig, relationshipMap?: RelationshipMap): Promise<GeneratedFile[]>;
|
|
24
24
|
/**
|
|
25
25
|
* Emits a schema-derived manifest of the entities that have opted in to
|
|
26
26
|
* domain-event emission (`.apsorc` `emitEvents`).
|
|
@@ -128,6 +128,9 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
128
128
|
const ctrlName = `${entityName}Controller`;
|
|
129
129
|
const pluralEntityName = (0, pluralize_1.default)(entityName);
|
|
130
130
|
const primaryKeyType = entity.primaryKeyType || "serial";
|
|
131
|
+
const scopes = auth
|
|
132
|
+
? (_b = (_a = (0, guards_1.getScopedEntities)([entity], relationshipMap)[0]) === null || _a === void 0 ? void 0 : _a.scopes) !== null && _b !== void 0 ? _b : []
|
|
133
|
+
: [];
|
|
131
134
|
const data = {
|
|
132
135
|
svcName,
|
|
133
136
|
ctrlName,
|
|
@@ -136,12 +139,12 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
136
139
|
primaryKeyType,
|
|
137
140
|
associations: [...new Set(relationshipsForTemplate)],
|
|
138
141
|
nestedJoinsFile,
|
|
139
|
-
//
|
|
140
|
-
// authenticated.
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// scopeBy fields @CrudAuth adds to every query once a caller is
|
|
143
|
+
// authenticated. Direct fields match the caller's tenant; nested paths
|
|
144
|
+
// (task.workspaceId) match the foreign key (taskId) against the parent
|
|
145
|
+
// ids ScopeGuard loaded for the caller into req.apsoScope.
|
|
146
|
+
crudAuth: scopes.filter((s) => s.direct),
|
|
147
|
+
crudAuthNested: scopes.filter((s) => s.fkField),
|
|
145
148
|
};
|
|
146
149
|
const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
|
|
147
150
|
const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
|
|
@@ -328,7 +331,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
328
331
|
},
|
|
329
332
|
];
|
|
330
333
|
}
|
|
331
|
-
async generateGuards(entities, auth) {
|
|
334
|
+
async generateGuards(entities, auth, relationshipMap) {
|
|
332
335
|
const hasScopes = (0, guards_1.hasScopedEntities)(entities);
|
|
333
336
|
const normalizedAuth = this.normalizeAuthConfig(auth);
|
|
334
337
|
const jwt = auth && (0, auth_1.isJwtAuth)(auth) ? (0, auth_1.resolveJwtAuth)(auth) : undefined;
|
|
@@ -339,7 +342,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
339
342
|
if (!hasScopes && !hasAuth) {
|
|
340
343
|
return [];
|
|
341
344
|
}
|
|
342
|
-
const scopedEntities = (0, guards_1.getScopedEntities)(entities);
|
|
345
|
+
const scopedEntities = (0, guards_1.getScopedEntities)(entities, relationshipMap);
|
|
343
346
|
const files = [];
|
|
344
347
|
const authEntities = normalizedAuth
|
|
345
348
|
? [
|
|
@@ -354,6 +357,8 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
354
357
|
: [];
|
|
355
358
|
const templateData = {
|
|
356
359
|
scopedEntities,
|
|
360
|
+
// Nested paths are only enforced on list and create with a caller's tenant.
|
|
361
|
+
hasNestedFk: hasAuth && scopedEntities.some((e) => e.scopes.some((s) => s.fkField)),
|
|
357
362
|
authConfig: normalizedAuth,
|
|
358
363
|
hasAuth,
|
|
359
364
|
authProvider: auth === null || auth === void 0 ? void 0 : auth.provider,
|
package/dist/lib/guards.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Entity, ResolvedApiKeyAuth } from "./types";
|
|
1
|
+
import { Entity, RelationshipMap, ResolvedApiKeyAuth } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Represents a scope field configuration for template rendering
|
|
4
4
|
*/
|
|
@@ -7,6 +7,14 @@ export interface ScopeFieldConfig {
|
|
|
7
7
|
contextKey: string;
|
|
8
8
|
direct: boolean;
|
|
9
9
|
path?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Nested paths only, set when the relationship map is given: the entity's
|
|
12
|
+
* foreign key to the first hop (taskId), the entity it points to (Task),
|
|
13
|
+
* and the rest of the path on that entity (workspaceId).
|
|
14
|
+
*/
|
|
15
|
+
fkField?: string;
|
|
16
|
+
parentEntity?: string;
|
|
17
|
+
parentPath?: string;
|
|
10
18
|
}
|
|
11
19
|
/**
|
|
12
20
|
* Represents a scoped entity configuration for template rendering
|
|
@@ -21,9 +29,10 @@ export interface ScopedEntityConfig {
|
|
|
21
29
|
bypassRoles: string[];
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
|
-
* Extracts scoped entity configurations from all entities.
|
|
32
|
+
* Extracts scoped entity configurations from all entities. Pass the
|
|
33
|
+
* relationship map to resolve nested paths to their foreign keys.
|
|
25
34
|
*/
|
|
26
|
-
export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfig[];
|
|
35
|
+
export declare function getScopedEntities(entities: Entity[], relationshipMap?: RelationshipMap): ScopedEntityConfig[];
|
|
27
36
|
/**
|
|
28
37
|
* Checks if any entities have scopeBy configured.
|
|
29
38
|
*/
|
package/dist/lib/guards.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.nestedScopeEntities = exports.entityScope = exports.apiKeyTemplateData = exports.hasScopedEntities = exports.getScopedEntities = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
|
|
6
|
+
const parse_1 = require("./utils/relationships/parse");
|
|
6
7
|
/**
|
|
7
8
|
* Default scope options when not specified
|
|
8
9
|
*/
|
|
@@ -36,18 +37,40 @@ function toRepoName(entityName) {
|
|
|
36
37
|
return `${camelCase}Repository`;
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
|
-
*
|
|
40
|
+
* A nested path's first segment must be a ManyToOne relationship of the
|
|
41
|
+
* entity: its foreign key is what list queries filter on.
|
|
40
42
|
*/
|
|
41
|
-
function
|
|
43
|
+
function resolveNestedScope(entityName, scope, relationshipMap) {
|
|
44
|
+
var _a;
|
|
45
|
+
if (scope.direct || !scope.path)
|
|
46
|
+
return scope;
|
|
47
|
+
const relation = ((_a = relationshipMap[entityName]) !== null && _a !== void 0 ? _a : []).find((r) => r.type === "ManyToOne" && (0, parse_1.getRelationshipName)(r) === scope.field);
|
|
48
|
+
if (!relation)
|
|
49
|
+
throw new Error(`${entityName}.scopeBy "${scope.path}": "${scope.field}" must be a many-to-one relationship of ${entityName}`);
|
|
50
|
+
return {
|
|
51
|
+
...scope,
|
|
52
|
+
fkField: (0, parse_1.getRelationshipIdField)(relation),
|
|
53
|
+
parentEntity: relation.name,
|
|
54
|
+
parentPath: scope.path.split(".").slice(1).join("."),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extracts scoped entity configurations from all entities. Pass the
|
|
59
|
+
* relationship map to resolve nested paths to their foreign keys.
|
|
60
|
+
*/
|
|
61
|
+
function getScopedEntities(entities, relationshipMap) {
|
|
42
62
|
return entities
|
|
43
63
|
.filter((entity) => entity.scopeBy)
|
|
44
64
|
.map((entity) => {
|
|
45
65
|
const options = { ...DEFAULT_SCOPE_OPTIONS, ...entity.scopeOptions };
|
|
66
|
+
const scopes = parseScopeBy(entity.scopeBy);
|
|
46
67
|
return {
|
|
47
68
|
name: entity.name,
|
|
48
69
|
routeName: toRouteName(entity.name),
|
|
49
70
|
repoName: toRepoName(entity.name),
|
|
50
|
-
scopes:
|
|
71
|
+
scopes: relationshipMap
|
|
72
|
+
? scopes.map((s) => resolveNestedScope(entity.name, s, relationshipMap))
|
|
73
|
+
: scopes,
|
|
51
74
|
injectOnCreate: options.injectOnCreate,
|
|
52
75
|
enforceOn: options.enforceOn,
|
|
53
76
|
bypassRoles: options.bypassRoles,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from '@nestjs/common';
|
|
9
9
|
import { Reflector } from '@nestjs/core';
|
|
10
10
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
11
|
-
import { Repository } from 'typeorm';
|
|
11
|
+
import { <% if (it.hasNestedFk) { %>DataSource, <% } %>Repository } from 'typeorm';
|
|
12
12
|
|
|
13
13
|
<% /* Import all entities that have scoped relationships (from sibling autogen folders) */ %>
|
|
14
14
|
<% it.scopedEntities.forEach((entity) => { %>
|
|
@@ -73,6 +73,12 @@ export interface ScopeField {
|
|
|
73
73
|
direct: boolean;
|
|
74
74
|
/** For nested paths, the full path (e.g., 'task.workspaceId') */
|
|
75
75
|
path?: string;
|
|
76
|
+
/** Nested paths: the foreign key to the first hop (e.g., 'taskId') */
|
|
77
|
+
fkField?: string;
|
|
78
|
+
/** Nested paths: the entity the foreign key points to (e.g., 'Task') */
|
|
79
|
+
parentEntity?: string;
|
|
80
|
+
/** Nested paths: the rest of the path on that entity (e.g., 'workspaceId') */
|
|
81
|
+
parentPath?: string;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/**
|
|
@@ -90,6 +96,11 @@ export const ENTITY_SCOPES: Record<string, ScopeConfig> = {
|
|
|
90
96
|
direct: <%= scope.direct %>,
|
|
91
97
|
<% if (scope.path) { %>
|
|
92
98
|
path: '<%= scope.path %>',
|
|
99
|
+
<% } %>
|
|
100
|
+
<% if (scope.fkField) { %>
|
|
101
|
+
fkField: '<%= scope.fkField %>',
|
|
102
|
+
parentEntity: '<%= scope.parentEntity %>',
|
|
103
|
+
parentPath: '<%= scope.parentPath %>',
|
|
93
104
|
<% } %>
|
|
94
105
|
}<%= scopeIndex < entity.scopes.length - 1 ? ',' : '' %>
|
|
95
106
|
<% }) %>
|
|
@@ -109,6 +120,9 @@ export class ScopeGuard implements CanActivate {
|
|
|
109
120
|
@InjectRepository(<%= entity.name %>)
|
|
110
121
|
private <%= entity.repoName %>: Repository<<%= entity.name %>>,
|
|
111
122
|
<% }) %>
|
|
123
|
+
<% if (it.hasNestedFk) { %>
|
|
124
|
+
private dataSource: DataSource,
|
|
125
|
+
<% } %>
|
|
112
126
|
) {}
|
|
113
127
|
|
|
114
128
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
@@ -166,6 +180,12 @@ export class ScopeGuard implements CanActivate {
|
|
|
166
180
|
if (!scopeConfig.enforceOn.includes(operation)) {
|
|
167
181
|
return true;
|
|
168
182
|
}
|
|
183
|
+
<% if (it.hasNestedFk) { %>
|
|
184
|
+
|
|
185
|
+
// Nested paths (task.workspaceId): load the caller's parent ids for the
|
|
186
|
+
// controller's list filter, and refuse a parent from another tenant.
|
|
187
|
+
await this.loadNestedScopes(request, scopeConfig, operation);
|
|
188
|
+
<% } %>
|
|
169
189
|
|
|
170
190
|
// For GET requests with an ID, verify the resource belongs to this scope
|
|
171
191
|
if ((operation === 'get' || operation === 'update' || operation === 'delete') && request.params?.id) {
|
|
@@ -338,6 +358,50 @@ export class ScopeGuard implements CanActivate {
|
|
|
338
358
|
);
|
|
339
359
|
}
|
|
340
360
|
|
|
361
|
+
<% if (it.hasNestedFk) { %>
|
|
362
|
+
/**
|
|
363
|
+
* For each nested scope, find the ids of the parent rows in the caller's
|
|
364
|
+
* tenant and put them on request.apsoScope, where the controller's
|
|
365
|
+
* @CrudAuth filter reads them. A create or update that names a parent
|
|
366
|
+
* outside that set is refused.
|
|
367
|
+
*/
|
|
368
|
+
private async loadNestedScopes(
|
|
369
|
+
request: any,
|
|
370
|
+
scopeConfig: ScopeConfig,
|
|
371
|
+
operation: string,
|
|
372
|
+
): Promise<void> {
|
|
373
|
+
for (const scope of scopeConfig.scopes) {
|
|
374
|
+
if (!scope.fkField || !scope.parentEntity || !scope.parentPath) continue;
|
|
375
|
+
const scopeValue = this.getScopeValue(request, scope.contextKey);
|
|
376
|
+
if (!scopeValue) {
|
|
377
|
+
throw new ForbiddenException('No tenant in the auth context');
|
|
378
|
+
}
|
|
379
|
+
// ponytail: loads every parent id in the tenant per request; switch to a
|
|
380
|
+
// subquery filter if a tenant grows past tens of thousands of parents.
|
|
381
|
+
const repo = this.dataSource.getRepository(scope.parentEntity);
|
|
382
|
+
const pk = repo.metadata.primaryColumns[0].propertyName;
|
|
383
|
+
const where = scope.parentPath
|
|
384
|
+
.split('.')
|
|
385
|
+
.reverse()
|
|
386
|
+
.reduce<any>((inner, key) => ({ [key]: inner }), scopeValue);
|
|
387
|
+
const parents = await repo.find({ select: { [pk]: true } as any, where });
|
|
388
|
+
const ids = parents.map((p: any) => p[pk]);
|
|
389
|
+
request.apsoScope = { ...request.apsoScope, [scope.fkField]: ids };
|
|
390
|
+
|
|
391
|
+
if (operation === 'create' || operation === 'update') {
|
|
392
|
+
const body = request.body && typeof request.body === 'object' ? request.body : {};
|
|
393
|
+
const items = Array.isArray(body.bulk) ? body.bulk : [body];
|
|
394
|
+
for (const item of items) {
|
|
395
|
+
const fk = item?.[scope.fkField] ?? item?.[scope.field]?.[pk];
|
|
396
|
+
if (fk != null && !ids.some((id: any) => String(id) === String(fk))) {
|
|
397
|
+
throw new ForbiddenException('You do not have access to this resource');
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
<% } %>
|
|
341
405
|
private getNestedValue(obj: any, path: string): any {
|
|
342
406
|
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
343
407
|
}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
<%~ includeFile('./header.eta') %>
|
|
2
2
|
|
|
3
3
|
import { Controller, Body } from "@nestjs/common";
|
|
4
|
-
import { Crud, <% if (it.crudAuth.length) { %>CrudAuth, <% } %>CrudController, Override, ParsedRequest, CrudRequest, ParsedBody, CreateManyDto } from '@apso/crud';
|
|
4
|
+
import { Crud, <% if (it.crudAuth.length || it.crudAuthNested.length) { %>CrudAuth, <% } %>CrudController, Override, ParsedRequest, CrudRequest, ParsedBody, CreateManyDto } from '@apso/crud';
|
|
5
5
|
import { ApiTags, ApiResponse, ApiOperation, ApiBody } from '@nestjs/swagger';
|
|
6
6
|
import {<%= it.entityName %>, <%= it.entityName %>Create, <%= it.entityName %>Update} from './dtos/<%= it.entityName%>.dto'
|
|
7
7
|
import {<%= it.svcName %>} from './<%= it.entityName %>.service';
|
|
8
8
|
|
|
9
|
+
<% if (it.crudAuthNested.length) { %>
|
|
10
|
+
// A nested scopeBy path (task.workspaceId) limits rows to the parents ScopeGuard
|
|
11
|
+
// found in the caller's tenant (req.apsoScope). An empty list matches no rows.
|
|
12
|
+
const nestedScope = (req: any, fk: string) => {
|
|
13
|
+
const ids = req.apsoScope?.[fk];
|
|
14
|
+
if (!ids) return {};
|
|
15
|
+
return { [fk]: ids.length ? { $in: ids } : { $isnull: true, $notnull: true } };
|
|
16
|
+
};
|
|
17
|
+
<% } %>
|
|
9
18
|
@Crud({
|
|
10
19
|
model: {
|
|
11
20
|
type: <%= it.entityName %>
|
|
@@ -51,12 +60,15 @@ import {<%= it.svcName %>} from './<%= it.entityName %>.service';
|
|
|
51
60
|
<% } %>
|
|
52
61
|
}
|
|
53
62
|
})
|
|
54
|
-
<% if (it.crudAuth.length) { %>
|
|
63
|
+
<% if (it.crudAuth.length || it.crudAuthNested.length) { %>
|
|
55
64
|
// Every query and write is limited to the caller's tenant (.apsorc scopeBy).
|
|
56
65
|
@CrudAuth({
|
|
57
66
|
filter: (req: any) => ({
|
|
58
67
|
<% it.crudAuth.forEach((s) => { %>
|
|
59
68
|
<%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId,
|
|
69
|
+
<% }) %>
|
|
70
|
+
<% it.crudAuthNested.forEach((s) => { %>
|
|
71
|
+
...nestedScope(req, '<%= s.fkField %>'),
|
|
60
72
|
<% }) %>
|
|
61
73
|
}),
|
|
62
74
|
persist: (req: any) => ({
|
|
@@ -262,7 +262,7 @@ export interface LanguageGenerator {
|
|
|
262
262
|
/**
|
|
263
263
|
* Generate guard files (auth, scope, etc.)
|
|
264
264
|
*/
|
|
265
|
-
generateGuards(entities: Entity[], auth?: AuthConfig): Promise<GeneratedFile[]>;
|
|
265
|
+
generateGuards(entities: Entity[], auth?: AuthConfig, relationshipMap?: RelationshipMap): Promise<GeneratedFile[]>;
|
|
266
266
|
/**
|
|
267
267
|
* Generate shared query utility files (filter, sort, pagination)
|
|
268
268
|
*/
|
|
@@ -48,6 +48,7 @@ export declare const parseRelationship: (relationship: ApsorcRelationship, allRe
|
|
|
48
48
|
* @returns A comprehensive RelationshipMap mapping entity names to their Relationship objects.
|
|
49
49
|
*/
|
|
50
50
|
export declare const parseRelationships: (relationships: ApsorcRelationship[]) => RelationshipMap;
|
|
51
|
+
export declare const getRelationshipName: (relationship: Relationship) => string;
|
|
51
52
|
export declare const getRelationshipIdField: (relationship: Relationship) => string;
|
|
52
53
|
/**
|
|
53
54
|
* Transforms the parsed Relationship data into the final format needed by the entity/controller templates.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getRelationshipsForImport = exports.streamNestedRelationships = exports.getNestedRelationships = exports.generateNestedRelationships = exports.getRelationshipForTemplate = exports.getRelationshipIdField = exports.parseRelationships = exports.parseRelationship = exports.parseManyToMany = exports.parseManytoOne = exports.parseOneToMany = void 0;
|
|
3
|
+
exports.getRelationshipsForImport = exports.streamNestedRelationships = exports.getNestedRelationships = exports.generateNestedRelationships = exports.getRelationshipForTemplate = exports.getRelationshipIdField = exports.getRelationshipName = exports.parseRelationships = exports.parseRelationship = exports.parseManyToMany = exports.parseManytoOne = exports.parseOneToMany = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
|
|
6
6
|
const casing_1 = require("../casing");
|
|
@@ -413,8 +413,9 @@ const getRelationshipName = (relationship) => {
|
|
|
413
413
|
}
|
|
414
414
|
return (0, casing_1.camelCase)(relationship.name);
|
|
415
415
|
};
|
|
416
|
+
exports.getRelationshipName = getRelationshipName;
|
|
416
417
|
const getRelationshipIdField = (relationship) => {
|
|
417
|
-
return `${getRelationshipName(relationship)}Id`;
|
|
418
|
+
return `${(0, exports.getRelationshipName)(relationship)}Id`;
|
|
418
419
|
};
|
|
419
420
|
exports.getRelationshipIdField = getRelationshipIdField;
|
|
420
421
|
/**
|
|
@@ -572,7 +573,7 @@ function* generateNestedRelationships(entityName, relationshipMap, maxDepth = 4,
|
|
|
572
573
|
if (!relationships)
|
|
573
574
|
return;
|
|
574
575
|
for (const relationship of relationships) {
|
|
575
|
-
const referenceName = getRelationshipName(relationship);
|
|
576
|
+
const referenceName = (0, exports.getRelationshipName)(relationship);
|
|
576
577
|
const type = relationship.type;
|
|
577
578
|
// Format the property name based on relationship type
|
|
578
579
|
// OneToMany and ManyToMany relationships use plural forms
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.46.
|
|
9
|
+
"version": "0.46.1",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@apso/schema-tools": "^0.2.0",
|
package/oclif.manifest.json
CHANGED