@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.
@@ -152,6 +152,7 @@ class Generate extends base_command_1.default {
152
152
  relationships: entityRelationships,
153
153
  allEntities: entities,
154
154
  apiType: lowerCaseApiType,
155
+ auth,
155
156
  }),
156
157
  generator.generateService({
157
158
  entity,
@@ -368,6 +368,7 @@ class McpServe extends base_command_1.default {
368
368
  relationships: entityRelationships,
369
369
  allEntities: entities,
370
370
  apiType: lowerCaseApiType,
371
+ auth,
371
372
  }),
372
373
  generator.generateService({
373
374
  entity,
@@ -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, _b;
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 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
+ 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 || request.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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.47.1",
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.1",
9
+ "version": "0.47.2",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@apso/schema-tools": "^0.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.47.1",
2
+ "version": "0.47.2",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.47.1",
3
+ "version": "0.47.2",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",