@apso/cli 0.47.1 → 0.47.2
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 -0
- package/dist/commands/mcp/serve.js +1 -0
- package/dist/lib/generators/go.js +4 -0
- package/dist/lib/generators/typescript.js +18 -4
- package/dist/lib/templates/go/dto/dto.eta +1 -1
- package/dist/lib/templates/guards/scope.guard.eta +1 -1
- package/dist/lib/templates/rest/controller-rest.eta +22 -2
- package/dist/lib/types/generator.d.ts +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -244,8 +244,10 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
244
244
|
];
|
|
245
245
|
}
|
|
246
246
|
async generateDto(options) {
|
|
247
|
+
var _a;
|
|
247
248
|
const { entity, relationships, allEntities, apiType } = options;
|
|
248
249
|
const { name: entityName, fields = [] } = entity;
|
|
250
|
+
const scopeFields = new Set((_a = (0, guards_1.entityScope)(entity, Boolean(options.auth))) === null || _a === void 0 ? void 0 : _a.fields.map((f) => f.field));
|
|
249
251
|
const columns = this.getGoFieldsForTemplate(fields, entityName);
|
|
250
252
|
const relationshipsTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships, allEntities);
|
|
251
253
|
// Add Go-specific properties to relationships
|
|
@@ -258,6 +260,8 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
258
260
|
camelReferenceName: (0, casing_1.camelCase)(rel.referenceName || rel.name),
|
|
259
261
|
pluralName: (0, casing_1.pascalCase)((0, pluralize_1.default)(rel.name)),
|
|
260
262
|
camelPluralName: (0, casing_1.camelCase)((0, pluralize_1.default)(rel.name)),
|
|
263
|
+
// The service sets a scopeBy foreign key from the caller on create.
|
|
264
|
+
scoped: scopeFields.has(`${(0, casing_1.camelCase)(rel.referenceName || rel.name)}Id`),
|
|
261
265
|
};
|
|
262
266
|
});
|
|
263
267
|
const primaryKeyColumns = columns.filter((column) => column.primary === true);
|
|
@@ -109,7 +109,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
109
109
|
];
|
|
110
110
|
}
|
|
111
111
|
async generateController(options) {
|
|
112
|
-
var _a
|
|
112
|
+
var _a;
|
|
113
113
|
const { entity, relationships, relationshipMap, auth } = options;
|
|
114
114
|
const { name: entityName } = entity;
|
|
115
115
|
const relationshipsForTemplate = (0, relationships_1.getRelationshipForTemplate)(entityName, relationships);
|
|
@@ -128,9 +128,22 @@ 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
|
|
132
|
-
? (
|
|
133
|
-
:
|
|
131
|
+
const scoped = auth
|
|
132
|
+
? (0, guards_1.getScopedEntities)([entity], relationshipMap)[0]
|
|
133
|
+
: undefined;
|
|
134
|
+
const scopes = (_a = scoped === null || scoped === void 0 ? void 0 : scoped.scopes) !== null && _a !== void 0 ? _a : [];
|
|
135
|
+
// Only emitted when scopeOptions differ from the defaults, so services
|
|
136
|
+
// without them keep the same generated controller.
|
|
137
|
+
const scopeOptions = scoped &&
|
|
138
|
+
(scoped.bypassRoles.length > 0 ||
|
|
139
|
+
!scoped.injectOnCreate ||
|
|
140
|
+
["find", "get", "create", "update", "delete"].some((op) => !scoped.enforceOn.includes(op)))
|
|
141
|
+
? {
|
|
142
|
+
enforceOn: scoped.enforceOn,
|
|
143
|
+
bypassRoles: scoped.bypassRoles,
|
|
144
|
+
injectOnCreate: scoped.injectOnCreate,
|
|
145
|
+
}
|
|
146
|
+
: undefined;
|
|
134
147
|
const data = {
|
|
135
148
|
svcName,
|
|
136
149
|
ctrlName,
|
|
@@ -145,6 +158,7 @@ class TypeScriptGenerator extends base_1.BaseGenerator {
|
|
|
145
158
|
// ids ScopeGuard loaded for the caller into req.apsoScope.
|
|
146
159
|
crudAuth: scopes.filter((s) => s.direct),
|
|
147
160
|
crudAuthNested: scopes.filter((s) => s.fkField),
|
|
161
|
+
scopeOptions,
|
|
148
162
|
};
|
|
149
163
|
const controllerContent = await this.renderTemplate("./rest/controller-rest", data);
|
|
150
164
|
const specContent = await this.renderTemplate("./rest/controller-rest-spec", data);
|
|
@@ -51,7 +51,7 @@ type <%= it.entityName %>Create struct {
|
|
|
51
51
|
<% }); -%>
|
|
52
52
|
<% it.associations.forEach((assoc) => { -%>
|
|
53
53
|
<% if (assoc.type === 'ManyToOne' || (assoc.type === 'OneToOne' && assoc.isOwner)) { -%>
|
|
54
|
-
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id" validate:"required"
|
|
54
|
+
<%= assoc.referenceName || assoc.name %>ID <%= assoc.primaryKeyGoType || 'uint' %> `json:"<%= assoc.camelReferenceName || assoc.camelName %>Id"<% if (!assoc.scoped) { %> validate:"required"<% } %>`
|
|
55
55
|
<% } -%>
|
|
56
56
|
<% }) -%>
|
|
57
57
|
}
|
|
@@ -168,7 +168,7 @@ export class ScopeGuard implements CanActivate {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// Check if user has bypass role
|
|
171
|
-
const userRoles: string[] = request.user?.roles
|
|
171
|
+
const userRoles: string[] = request.auth?.roles ?? request.user?.roles ?? request.roles ?? [];
|
|
172
172
|
if (scopeConfig.bypassRoles.some(role => userRoles.includes(role))) {
|
|
173
173
|
return true;
|
|
174
174
|
}
|
|
@@ -15,6 +15,26 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
15
15
|
return { [fk]: ids.length ? { $in: ids } : { $isnull: true, $notnull: true } };
|
|
16
16
|
};
|
|
17
17
|
<% } %>
|
|
18
|
+
<% if (it.scopeOptions && (it.crudAuth.length || it.crudAuthNested.length)) { %>
|
|
19
|
+
// .apsorc scopeOptions: which requests the tenant filter covers and which
|
|
20
|
+
// roles skip it.
|
|
21
|
+
const ENFORCE_ON = [<%~ it.scopeOptions.enforceOn.map((op) => `'${op}'`).join(', ') %>];
|
|
22
|
+
const BYPASS_ROLES: string[] = [<%~ it.scopeOptions.bypassRoles.map((r) => `'${r}'`).join(', ') %>];
|
|
23
|
+
const scopeOperation = (req: any) => {
|
|
24
|
+
switch (req.method) {
|
|
25
|
+
case 'GET': return req.params?.id ? 'get' : 'find';
|
|
26
|
+
case 'POST': return 'create';
|
|
27
|
+
case 'PUT':
|
|
28
|
+
case 'PATCH': return 'update';
|
|
29
|
+
case 'DELETE': return 'delete';
|
|
30
|
+
default: return 'find';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const skipScope = (req: any) => {
|
|
34
|
+
const roles: string[] = req.auth?.roles ?? req.user?.roles ?? [];
|
|
35
|
+
return roles.some((r) => BYPASS_ROLES.includes(r)) || !ENFORCE_ON.includes(scopeOperation(req));
|
|
36
|
+
};
|
|
37
|
+
<% } %>
|
|
18
38
|
@Crud({
|
|
19
39
|
model: {
|
|
20
40
|
type: <%= it.entityName %>
|
|
@@ -63,7 +83,7 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
63
83
|
<% if (it.crudAuth.length || it.crudAuthNested.length) { %>
|
|
64
84
|
// Every query and write is limited to the caller's tenant (.apsorc scopeBy).
|
|
65
85
|
@CrudAuth({
|
|
66
|
-
filter: (req: any) => ({
|
|
86
|
+
filter: (req: any) => <% if (it.scopeOptions) { %>skipScope(req) ? {} : <% } %>({
|
|
67
87
|
<% it.crudAuth.forEach((s) => { %>
|
|
68
88
|
<%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId,
|
|
69
89
|
<% }) %>
|
|
@@ -71,7 +91,7 @@ const nestedScope = (req: any, fk: string) => {
|
|
|
71
91
|
...nestedScope(req, '<%= s.fkField %>'),
|
|
72
92
|
<% }) %>
|
|
73
93
|
}),
|
|
74
|
-
persist: (req: any) => ({
|
|
94
|
+
persist: (req: any) => <% if (it.scopeOptions) { %>skipScope(req)<% if (!it.scopeOptions.injectOnCreate) { %> || scopeOperation(req) === 'create'<% } %> ? {} : <% } %>({
|
|
75
95
|
<% it.crudAuth.forEach((s) => { %>
|
|
76
96
|
<%= s.field %>: req.<%= s.contextKey %> ?? req.user?.<%= s.contextKey %> ?? req.auth?.<%= s.contextKey %> ?? req.auth?.workspaceId,
|
|
77
97
|
<% }) %>
|
|
@@ -181,6 +181,8 @@ export interface DtoGenerationOptions extends EntityGenerationOptions {
|
|
|
181
181
|
* Include validation decorators/rules
|
|
182
182
|
*/
|
|
183
183
|
includeValidation?: boolean;
|
|
184
|
+
/** With scopeBy, the service sets the tenant field, so create doesn't require it. */
|
|
185
|
+
auth?: AuthConfig;
|
|
184
186
|
}
|
|
185
187
|
/**
|
|
186
188
|
* Options for generating migrations
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.47.
|
|
9
|
+
"version": "0.47.2",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@apso/schema-tools": "^0.2.0",
|
package/oclif.manifest.json
CHANGED