@apso/cli 0.4.2 → 0.6.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.
Files changed (71) hide show
  1. package/README.md +796 -6
  2. package/dist/commands/server/new.js +1 -1
  3. package/dist/commands/server/scaffold.js +20 -9
  4. package/dist/lib/apsorc-parser.d.ts +3 -0
  5. package/dist/lib/apsorc-parser.js +11 -6
  6. package/dist/lib/controller.js +3 -1
  7. package/dist/lib/dto.js +9 -9
  8. package/dist/lib/entity.js +6 -5
  9. package/dist/lib/guards.d.ts +47 -0
  10. package/dist/lib/guards.js +176 -0
  11. package/dist/lib/index.d.ts +1 -0
  12. package/dist/lib/index.js +5 -1
  13. package/dist/lib/module.js +3 -1
  14. package/dist/lib/templates/guards/auth.guard.eta +228 -0
  15. package/dist/lib/templates/guards/guards.module.eta +114 -0
  16. package/dist/lib/templates/guards/index.eta +22 -0
  17. package/dist/lib/templates/guards/scope.guard.eta +327 -0
  18. package/dist/lib/types/auth.d.ts +203 -0
  19. package/dist/lib/types/auth.js +60 -0
  20. package/dist/lib/types/entity.d.ts +36 -1
  21. package/dist/lib/types/index.d.ts +2 -1
  22. package/dist/lib/types/index.js +8 -0
  23. package/dist/lib/types/relationship.d.ts +1 -1
  24. package/dist/lib/utils/casing.js +5 -5
  25. package/dist/lib/utils/field.js +2 -1
  26. package/dist/lib/utils/file-system.js +1 -1
  27. package/dist/lib/utils/relationships/parse-v1.js +2 -1
  28. package/dist/lib/utils/relationships/parse.js +117 -32
  29. package/dist/lib/utils/relationships/parse.spec.js +168 -141
  30. package/dist/test-nested-relationships.js +10 -10
  31. package/dist/tests/templates/entities/types/array-template.spec.js +45 -45
  32. package/dist/tests/templates/entities/types/bigint-template.spec.js +43 -43
  33. package/dist/tests/templates/entities/types/boolean-template.spec.js +44 -44
  34. package/dist/tests/templates/entities/types/bytea-template.spec.js +35 -35
  35. package/dist/tests/templates/entities/types/char-template.spec.js +35 -35
  36. package/dist/tests/templates/entities/types/date-template.spec.js +29 -29
  37. package/dist/tests/templates/entities/types/decimal-field-validation.spec.js +46 -46
  38. package/dist/tests/templates/entities/types/decimal-template.spec.js +46 -46
  39. package/dist/tests/templates/entities/types/double-template.spec.js +37 -34
  40. package/dist/tests/templates/entities/types/enum-template.spec.js +42 -42
  41. package/dist/tests/templates/entities/types/float-template.spec.js +37 -34
  42. package/dist/tests/templates/entities/types/geometry-template.spec.js +31 -31
  43. package/dist/tests/templates/entities/types/inet-template.spec.js +36 -36
  44. package/dist/tests/templates/entities/types/int4range-template.spec.js +45 -33
  45. package/dist/tests/templates/entities/types/integer-template.spec.js +40 -40
  46. package/dist/tests/templates/entities/types/interval-template.spec.js +35 -35
  47. package/dist/tests/templates/entities/types/json-plain-template.spec.js +27 -27
  48. package/dist/tests/templates/entities/types/json-template.spec.js +19 -19
  49. package/dist/tests/templates/entities/types/jsonb-template.spec.js +19 -19
  50. package/dist/tests/templates/entities/types/money-template.spec.js +35 -35
  51. package/dist/tests/templates/entities/types/numeric-template.spec.js +44 -44
  52. package/dist/tests/templates/entities/types/point-template.spec.js +37 -31
  53. package/dist/tests/templates/entities/types/polygon-template.spec.js +43 -31
  54. package/dist/tests/templates/entities/types/real-template.spec.js +34 -34
  55. package/dist/tests/templates/entities/types/serial-template.spec.js +62 -62
  56. package/dist/tests/templates/entities/types/smallint-template.spec.js +43 -43
  57. package/dist/tests/templates/entities/types/string-template.spec.js +59 -59
  58. package/dist/tests/templates/entities/types/text-template.spec.js +37 -37
  59. package/dist/tests/templates/entities/types/time-template.spec.js +35 -35
  60. package/dist/tests/templates/entities/types/timestamp-template.spec.js +39 -39
  61. package/dist/tests/templates/entities/types/timetz-template.spec.js +35 -35
  62. package/dist/tests/templates/entities/types/tsvector-template.spec.js +28 -28
  63. package/dist/tests/templates/entities/types/uuid-template.spec.js +43 -43
  64. package/dist/tests/templates/entities/types/varchar-template.spec.js +59 -56
  65. package/dist/tests/templates/entities/types/xml-template.spec.js +28 -28
  66. package/dist/tests/templates/entities/utils/template-test-utils.d.ts +1 -1
  67. package/dist/tests/templates/entities/utils/template-test-utils.js +3 -3
  68. package/dist/tests/templates/entities/uuid-entity-template.spec.js +138 -138
  69. package/dist/tests/utils/relationship-deduplication.spec.js +11 -11
  70. package/oclif.manifest.json +1 -1
  71. package/package.json +1 -1
@@ -72,7 +72,7 @@ class New extends base_command_1.default {
72
72
  let projectName = flags.name;
73
73
  let apiType = (_a = flags.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();
74
74
  if (!projectName) {
75
- projectName = '';
75
+ projectName = "";
76
76
  }
77
77
  if (!apiType) {
78
78
  apiType = "rest";
@@ -10,7 +10,7 @@ const perf_hooks_1 = require("perf_hooks");
10
10
  const Eta = tslib_1.__importStar(require("eta"));
11
11
  Eta.configure({
12
12
  views: path.join(__dirname, "../../lib/templates"),
13
- cache: false
13
+ cache: false,
14
14
  });
15
15
  class Scaffold extends base_command_1.default {
16
16
  async scaffoldServer(options) {
@@ -25,7 +25,7 @@ class Scaffold extends base_command_1.default {
25
25
  entity,
26
26
  relationships: entityRelationships,
27
27
  apiType,
28
- allEntities
28
+ allEntities,
29
29
  });
30
30
  switch (apiType) {
31
31
  case apsorc_parser_1.ApiType.Graphql:
@@ -45,18 +45,25 @@ class Scaffold extends base_command_1.default {
45
45
  const filePath = path.join(dir, entity.name);
46
46
  const entityRelationships = relationshipMap[entity.name] || [];
47
47
  const tDto = perf_hooks_1.performance.now();
48
- await (0, lib_1.createDto)(filePath, entity, entityRelationships, { apiType, allEntities });
48
+ await (0, lib_1.createDto)(filePath, entity, entityRelationships, {
49
+ apiType,
50
+ allEntities,
51
+ });
49
52
  if (process.env.DEBUG) {
50
53
  console.log(`[timing] createDto for ${entity.name}: ${(perf_hooks_1.performance.now() - tDto).toFixed(2)}ms`);
51
54
  const used = process.memoryUsage();
52
- console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
55
+ console.log(`[mem] heapUsed after createDto for ${entity.name}: ${(used.heapUsed /
56
+ 1024 /
57
+ 1024).toFixed(2)} MB`);
53
58
  }
54
59
  const tService = perf_hooks_1.performance.now();
55
60
  await (0, lib_1.createService)(filePath, entity, relationshipMap);
56
61
  if (process.env.DEBUG) {
57
62
  console.log(`[timing] createService for ${entity.name}: ${(perf_hooks_1.performance.now() - tService).toFixed(2)}ms`);
58
63
  const used = process.memoryUsage();
59
- console.log(`[mem] heapUsed after createService for ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
64
+ console.log(`[mem] heapUsed after createService for ${entity.name}: ${(used.heapUsed /
65
+ 1024 /
66
+ 1024).toFixed(2)} MB`);
60
67
  }
61
68
  const tController = perf_hooks_1.performance.now();
62
69
  await (0, lib_1.createController)(filePath, entity, relationshipMap);
@@ -71,7 +78,7 @@ class Scaffold extends base_command_1.default {
71
78
  }
72
79
  async run() {
73
80
  const totalBuildStart = perf_hooks_1.performance.now();
74
- const { rootFolder, entities, relationshipMap, apiType } = (0, lib_1.parseApsorc)();
81
+ const { rootFolder, entities, relationshipMap, apiType, auth } = (0, lib_1.parseApsorc)();
75
82
  const rootPath = path.join(process.cwd(), rootFolder);
76
83
  const autogenPath = path.join(rootPath, "autogen");
77
84
  const lowerCaseApiType = apiType.toLowerCase();
@@ -83,19 +90,23 @@ class Scaffold extends base_command_1.default {
83
90
  entity,
84
91
  relationshipMap,
85
92
  apiType: lowerCaseApiType,
86
- allEntities: entities
93
+ allEntities: entities,
87
94
  });
88
95
  if (process.env.DEBUG) {
89
96
  const used = process.memoryUsage();
90
- console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB`);
97
+ console.log(`[mem] heapUsed after ${entity.name}: ${(used.heapUsed /
98
+ 1024 /
99
+ 1024).toFixed(2)} MB`);
91
100
  }
92
101
  }));
102
+ // Generate guards (auth and/or scope) based on .apsorc configuration
103
+ await (0, lib_1.createGuards)(rootPath, entities, auth);
93
104
  await (0, lib_1.createIndexAppModule)(autogenPath, entities, lowerCaseApiType);
94
105
  const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
95
106
  console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
96
107
  const formatStart = perf_hooks_1.performance.now();
97
108
  console.log("[apso] Formatting files...");
98
- await this.runNpmCommand(["run", "format", "src/autogen/**/*.ts"], true);
109
+ await this.runNpmCommand(["run", "format", "src/autogen/**/*.ts", "src/guards/**/*.ts"], true);
99
110
  const formatTime = perf_hooks_1.performance.now() - formatStart;
100
111
  console.log(`[apso] Finished formatting in ${formatTime.toFixed(2)} ms`);
101
112
  }
@@ -1,5 +1,6 @@
1
1
  import { Entity } from "./types/entity";
2
2
  import { ApsorcRelationship, RelationshipMap } from "./types/relationship";
3
+ import { AuthConfig } from "./types/auth";
3
4
  export declare enum ApiType {
4
5
  Graphql = "graphql",
5
6
  Rest = "rest"
@@ -10,6 +11,7 @@ export type ApsorcType = {
10
11
  entities: Entity[];
11
12
  apiType: ApiType;
12
13
  relationships: ApsorcRelationship[];
14
+ auth?: AuthConfig;
13
15
  };
14
16
  type ParsedApsorcData = {
15
17
  entities: Entity[];
@@ -20,6 +22,7 @@ type ParsedApsorc = {
20
22
  apiType: string;
21
23
  entities: Entity[];
22
24
  relationshipMap: RelationshipMap;
25
+ auth?: AuthConfig;
23
26
  };
24
27
  export declare const parseApsorcV1: (apsorc: ApsorcType) => ParsedApsorcData;
25
28
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
@@ -34,12 +34,14 @@ const parseRc = () => {
34
34
  const version = apsoConfig.version || 1;
35
35
  const entities = apsoConfig.entities || [];
36
36
  const relationships = apsoConfig.relationships || [];
37
+ const auth = apsoConfig.auth;
37
38
  return {
38
39
  rootFolder,
39
40
  apiType,
40
41
  version,
41
42
  entities,
42
43
  relationships,
44
+ auth,
43
45
  };
44
46
  };
45
47
  const parseApsorc = () => {
@@ -57,6 +59,7 @@ const parseApsorc = () => {
57
59
  const result = {
58
60
  rootFolder: apsoConfig.rootFolder,
59
61
  apiType: apsoConfig.apiType,
62
+ auth: apsoConfig.auth,
60
63
  ...(0, exports.parseApsorcV1)(apsoConfig),
61
64
  };
62
65
  if (debug) {
@@ -71,6 +74,7 @@ const parseApsorc = () => {
71
74
  const result = {
72
75
  rootFolder: apsoConfig.rootFolder,
73
76
  apiType: apsoConfig.apiType,
77
+ auth: apsoConfig.auth,
74
78
  ...(() => {
75
79
  const relStart = perf_hooks_1.performance.now();
76
80
  const parsed = (0, exports.parseApsorcV2)(apsoConfig);
@@ -128,11 +132,12 @@ const loadConfig = (appName = "apso", defaults = {}) => {
128
132
  const configData = (0, rc_1.default)(appName, defaults);
129
133
  const rootFolder = configData.rootFolder || "src"; // Get rootFolder early
130
134
  // Check if rc found and loaded the config
131
- // rc returns an object with default keys if nothing is found,
135
+ // rc returns an object with default keys if nothing is found,
132
136
  // so check for more than just default keys or if configData.config exists
133
137
  const hasMeaningfulConfig = configData &&
134
- Object.keys(configData).some(key => !Object.prototype.hasOwnProperty.call(defaults, key) && key !== '_') &&
135
- configData.configs && configData.configs.length > 0;
138
+ Object.keys(configData).some((key) => !Object.prototype.hasOwnProperty.call(defaults, key) && key !== "_") &&
139
+ configData.configs &&
140
+ configData.configs.length > 0;
136
141
  if (!hasMeaningfulConfig) {
137
142
  // If rc didn't find it, try our manual search as a fallback
138
143
  const foundPath = (0, exports.findConfigPath)();
@@ -150,7 +155,7 @@ const loadConfig = (appName = "apso", defaults = {}) => {
150
155
  rootFolder: rootFolder,
151
156
  entities: configData.entities || [],
152
157
  apiType: configData.apiType || ApiType.Rest,
153
- relationships: configData.relationships || []
158
+ relationships: configData.relationships || [],
154
159
  };
155
160
  if (version === 1) {
156
161
  // Pass the correctly structured object
@@ -167,7 +172,7 @@ const loadConfig = (appName = "apso", defaults = {}) => {
167
172
  if (!parsedData.entities || !Array.isArray(parsedData.entities)) {
168
173
  throw new Error("Invalid config: 'entities' array is missing or invalid after parsing.");
169
174
  }
170
- if (!rootFolder || typeof rootFolder !== 'string') {
175
+ if (!rootFolder || typeof rootFolder !== "string") {
171
176
  throw new Error("Invalid config: 'rootFolder' string is missing or invalid.");
172
177
  }
173
178
  // Relationships are implicitly validated by parseRelationships/parseV1Relationships
@@ -177,7 +182,7 @@ const loadConfig = (appName = "apso", defaults = {}) => {
177
182
  // or adjust UnifiedConfig. For now, returning empty array as placeholder.
178
183
  // The relationshipMap is the primary output needed by downstream code.
179
184
  relationships: [],
180
- rootFolder: rootFolder
185
+ rootFolder: rootFolder,
181
186
  };
182
187
  };
183
188
  exports.loadConfig = loadConfig;
@@ -35,7 +35,9 @@ const createController = async (apiBaseDir, entity, relationshipMap) => {
35
35
  const startNested = perf_hooks_1.performance.now();
36
36
  const tmpDir = os_1.default.tmpdir();
37
37
  const nestedJoinsFile = path.join(tmpDir, `apso-nested-joins-${entityName}-${Date.now()}.eta`);
38
- const nestedJoinsStream = fs_1.default.createWriteStream(nestedJoinsFile, { encoding: "utf8" });
38
+ const nestedJoinsStream = fs_1.default.createWriteStream(nestedJoinsFile, {
39
+ encoding: "utf8",
40
+ });
39
41
  let nestedCount = 0;
40
42
  nestedCount = await (0, parse_1.streamNestedRelationships)(entityName, relationshipMap, nestedJoinsStream);
41
43
  nestedJoinsStream.end();
package/dist/lib/dto.js CHANGED
@@ -32,15 +32,15 @@ options) => {
32
32
  // console.log('[DTO DEBUG] relationshipsTemplate:', relationshipsTemplate);
33
33
  // }
34
34
  // Add foreign key columns for ManyToOne relationships if not present
35
- relationshipsTemplate.forEach(rel => {
35
+ relationshipsTemplate.forEach((rel) => {
36
36
  var _a;
37
- if (rel.type === 'ManyToOne') {
38
- const fkName = (0, casing_1.camelCase)(rel.referenceName || rel.name) + 'Id';
37
+ if (rel.type === "ManyToOne") {
38
+ const fkName = (0, casing_1.camelCase)(rel.referenceName || rel.name) + "Id";
39
39
  // Look up the referenced entity to determine the foreign key type
40
- const referencedEntity = (_a = options === null || options === void 0 ? void 0 : options.allEntities) === null || _a === void 0 ? void 0 : _a.find(e => e.name === rel.name);
41
- const referencedPrimaryKeyType = (referencedEntity === null || referencedEntity === void 0 ? void 0 : referencedEntity.primaryKeyType) || 'serial';
42
- const fkDataType = referencedPrimaryKeyType === 'uuid' ? 'string' : 'number';
43
- const fkType = referencedPrimaryKeyType === 'uuid' ? 'text' : 'integer';
40
+ const referencedEntity = (_a = options === null || options === void 0 ? void 0 : options.allEntities) === null || _a === void 0 ? void 0 : _a.find((e) => e.name === rel.name);
41
+ const referencedPrimaryKeyType = (referencedEntity === null || referencedEntity === void 0 ? void 0 : referencedEntity.primaryKeyType) || "serial";
42
+ const fkDataType = referencedPrimaryKeyType === "uuid" ? "string" : "number";
43
+ const fkType = referencedPrimaryKeyType === "uuid" ? "text" : "integer";
44
44
  // if (process.env.DEBUG) {
45
45
  // console.log('[DTO DEBUG]', {
46
46
  // relType: rel.type,
@@ -52,7 +52,7 @@ options) => {
52
52
  // columns: columns.map(c => c.name)
53
53
  // });
54
54
  // }
55
- if (!columns.some(col => col.name === fkName)) {
55
+ if (!columns.some((col) => col.name === fkName)) {
56
56
  columns.push({
57
57
  name: fkName,
58
58
  dataType: fkDataType,
@@ -65,7 +65,7 @@ options) => {
65
65
  const addDefaultPKProperty = primaryKeyColumns.length === 0;
66
66
  const createdAt = entity.created_at;
67
67
  const updatedAt = entity.updated_at;
68
- const primaryKeyType = entity.primaryKeyType || 'serial'; // Default to 'serial' if not specified
68
+ const primaryKeyType = entity.primaryKeyType || "serial"; // Default to 'serial' if not specified
69
69
  // Filter columns: remove id, createdAt, updatedAt
70
70
  const dtoColumns = columns.filter((col) => !["id", "createdAt", "updatedAt"].includes(col.name));
71
71
  // Class names
@@ -21,15 +21,14 @@ const Eta = tslib_1.__importStar(require("eta"));
21
21
  * @returns {Promise<void>} A promise that resolves when the entity file is created.
22
22
  */
23
23
  const createEntity = async (options) => {
24
- const { apiBaseDir, entity, relationships: relationshipsNew, apiType, allEntities } = options;
24
+ const { apiBaseDir, entity, relationships: relationshipsNew, apiType, allEntities, } = options;
25
25
  const { name, fields = [], indexes, uniques } = entity;
26
26
  const File = path.join(apiBaseDir, `${name}.entity.ts`);
27
27
  const columns = (0, field_1.getFieldForTemplate)(fields, name);
28
- const createPrimaryKey = columns.filter((column) => column.primary === true)
29
- .length === 0;
28
+ const createPrimaryKey = columns.filter((column) => column.primary === true).length === 0;
30
29
  const createdAt = entity.created_at;
31
30
  const updatedAt = entity.updated_at;
32
- const primaryKeyType = entity.primaryKeyType || 'serial'; // Default to 'serial' if not specified
31
+ const primaryKeyType = entity.primaryKeyType || "serial"; // Default to 'serial' if not specified
33
32
  const relationships = (0, relationships_1.getRelationshipForTemplate)(name, relationshipsNew, allEntities);
34
33
  const entitiesToImport = (0, relationships_1.getRelationshipsForImport)(name, relationships);
35
34
  const data = {
@@ -48,7 +47,9 @@ const createEntity = async (options) => {
48
47
  importEnums: (0, field_1.typeExistsInEntity)(entity, "enum") !== -1,
49
48
  apiType,
50
49
  };
51
- const templatePath = apiType === "graphql" ? "./graphql/gql-entity-graphql" : "./entities/entity";
50
+ const templatePath = apiType === "graphql"
51
+ ? "./graphql/gql-entity-graphql"
52
+ : "./entities/entity";
52
53
  const content = await Eta.renderFileAsync(templatePath, (0, file_system_1.withGeneratedMeta)(data));
53
54
  await (0, file_system_1.createFile)(File, content);
54
55
  };
@@ -0,0 +1,47 @@
1
+ import { Entity, AuthConfig } from "./types";
2
+ /**
3
+ * Represents a scope field configuration for template rendering
4
+ */
5
+ interface ScopeFieldConfig {
6
+ field: string;
7
+ contextKey: string;
8
+ direct: boolean;
9
+ path?: string;
10
+ }
11
+ /**
12
+ * Represents a scoped entity configuration for template rendering
13
+ */
14
+ interface ScopedEntityConfig {
15
+ name: string;
16
+ routeName: string;
17
+ repoName: string;
18
+ scopes: ScopeFieldConfig[];
19
+ injectOnCreate: boolean;
20
+ enforceOn: string[];
21
+ bypassRoles: string[];
22
+ }
23
+ /**
24
+ * Extracts scoped entity configurations from all entities.
25
+ *
26
+ * @param entities Array of all entities from the parsed .apsorc.
27
+ * @returns Array of scoped entity configurations for entities that have scopeBy defined.
28
+ */
29
+ export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfig[];
30
+ /**
31
+ * Checks if any entities have scopeBy configured.
32
+ *
33
+ * @param entities Array of all entities from the parsed .apsorc.
34
+ * @returns True if at least one entity has scopeBy defined.
35
+ */
36
+ export declare function hasScopedEntities(entities: Entity[]): boolean;
37
+ /**
38
+ * Generates the guards module directory and files.
39
+ * Creates auth.guard.ts, scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
40
+ *
41
+ * @param rootPath The root path of the generated source (e.g., 'src').
42
+ * @param entities All entities from the parsed .apsorc.
43
+ * @param auth Optional auth configuration from .apsorc.
44
+ * @returns {Promise<void>} A promise that resolves when the guard files are created.
45
+ */
46
+ export declare const createGuards: (rootPath: string, entities: Entity[], auth?: AuthConfig) => Promise<void>;
47
+ export {};
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createGuards = exports.hasScopedEntities = exports.getScopedEntities = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const Eta = tslib_1.__importStar(require("eta"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const fs = tslib_1.__importStar(require("fs"));
8
+ const file_system_1 = require("./utils/file-system");
9
+ const types_1 = require("./types");
10
+ const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
11
+ /**
12
+ * Default scope options when not specified
13
+ */
14
+ const DEFAULT_SCOPE_OPTIONS = {
15
+ injectOnCreate: true,
16
+ enforceOn: ["find", "get", "create", "update", "delete"],
17
+ bypassRoles: [],
18
+ };
19
+ /**
20
+ * Parses scopeBy configuration and returns structured scope fields.
21
+ *
22
+ * @param scopeBy The scope configuration - either a single field/path string or an array of them.
23
+ * @returns An array of scope field configurations.
24
+ */
25
+ function parseScopeBy(scopeBy) {
26
+ const scopes = Array.isArray(scopeBy) ? scopeBy : [scopeBy];
27
+ return scopes.map((scope) => {
28
+ const isDirect = !scope.includes(".");
29
+ const field = isDirect ? scope : scope.split(".")[0];
30
+ // For direct fields, contextKey is the same as field
31
+ // For nested paths like 'task.workspaceId', we extract 'workspaceId' as contextKey
32
+ const contextKey = isDirect ? scope : scope.split(".").pop();
33
+ return {
34
+ field,
35
+ contextKey,
36
+ direct: isDirect,
37
+ path: isDirect ? undefined : scope,
38
+ };
39
+ });
40
+ }
41
+ /**
42
+ * Converts entity name to lowercase pluralized route name.
43
+ * e.g., "Project" -> "projects", "DiscoverySession" -> "discoverysessions"
44
+ *
45
+ * @param entityName The entity name in PascalCase.
46
+ * @returns The lowercase pluralized route name.
47
+ */
48
+ function toRouteName(entityName) {
49
+ return (0, pluralize_1.default)(entityName).toLowerCase();
50
+ }
51
+ /**
52
+ * Converts entity name to repository variable name.
53
+ * e.g., "Project" -> "projectRepository", "DiscoverySession" -> "discoverySessionRepository"
54
+ *
55
+ * @param entityName The entity name in PascalCase.
56
+ * @returns The camelCase repository variable name.
57
+ */
58
+ function toRepoName(entityName) {
59
+ const camelCase = entityName.charAt(0).toLowerCase() + entityName.slice(1);
60
+ return `${camelCase}Repository`;
61
+ }
62
+ /**
63
+ * Extracts scoped entity configurations from all entities.
64
+ *
65
+ * @param entities Array of all entities from the parsed .apsorc.
66
+ * @returns Array of scoped entity configurations for entities that have scopeBy defined.
67
+ */
68
+ function getScopedEntities(entities) {
69
+ return entities
70
+ .filter((entity) => entity.scopeBy)
71
+ .map((entity) => {
72
+ const options = { ...DEFAULT_SCOPE_OPTIONS, ...entity.scopeOptions };
73
+ return {
74
+ name: entity.name,
75
+ routeName: toRouteName(entity.name),
76
+ repoName: toRepoName(entity.name),
77
+ scopes: parseScopeBy(entity.scopeBy),
78
+ injectOnCreate: options.injectOnCreate,
79
+ enforceOn: options.enforceOn,
80
+ bypassRoles: options.bypassRoles,
81
+ };
82
+ });
83
+ }
84
+ exports.getScopedEntities = getScopedEntities;
85
+ /**
86
+ * Checks if any entities have scopeBy configured.
87
+ *
88
+ * @param entities Array of all entities from the parsed .apsorc.
89
+ * @returns True if at least one entity has scopeBy defined.
90
+ */
91
+ function hasScopedEntities(entities) {
92
+ return entities.some((entity) => entity.scopeBy);
93
+ }
94
+ exports.hasScopedEntities = hasScopedEntities;
95
+ /**
96
+ * Normalizes auth config by applying defaults for DB session providers
97
+ * @param {AuthConfig | undefined} auth - The auth configuration to normalize
98
+ * @returns {DbSessionAuthConfig | undefined} The normalized auth config or undefined
99
+ */
100
+ function normalizeAuthConfig(auth) {
101
+ if (!auth)
102
+ return undefined;
103
+ if ((0, types_1.isDbSessionAuth)(auth)) {
104
+ return {
105
+ ...types_1.DB_SESSION_AUTH_DEFAULTS,
106
+ ...auth,
107
+ };
108
+ }
109
+ // For now, only DB session providers are supported
110
+ // JWT providers will be added later
111
+ console.log(`[apso] Auth provider '${auth.provider}' is not yet supported, skipping auth guard generation`);
112
+ return undefined;
113
+ }
114
+ /**
115
+ * Generates the guards module directory and files.
116
+ * Creates auth.guard.ts, scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
117
+ *
118
+ * @param rootPath The root path of the generated source (e.g., 'src').
119
+ * @param entities All entities from the parsed .apsorc.
120
+ * @param auth Optional auth configuration from .apsorc.
121
+ * @returns {Promise<void>} A promise that resolves when the guard files are created.
122
+ */
123
+ const createGuards = async (rootPath, entities, auth) => {
124
+ const hasScopes = hasScopedEntities(entities);
125
+ const normalizedAuth = normalizeAuthConfig(auth);
126
+ // Only generate guards if there are scoped entities OR auth config
127
+ if (!hasScopes && !normalizedAuth) {
128
+ console.log("[apso] No scopeBy or auth configurations found, skipping guards generation");
129
+ return;
130
+ }
131
+ const guardsDir = path.join(rootPath, "guards");
132
+ // Ensure guards directory exists
133
+ if (!fs.existsSync(guardsDir)) {
134
+ fs.mkdirSync(guardsDir, { recursive: true });
135
+ }
136
+ const scopedEntities = getScopedEntities(entities);
137
+ // Common template data
138
+ const templateData = {
139
+ scopedEntities,
140
+ authConfig: normalizedAuth,
141
+ generatedAt: new Date().toISOString(),
142
+ generatedBy: "Apso CLI",
143
+ };
144
+ // Generate auth.guard.ts if auth is configured
145
+ if (normalizedAuth) {
146
+ const authGuardPath = path.join(guardsDir, "auth.guard.ts");
147
+ const authGuardContent = await Eta.renderFileAsync("./guards/auth.guard.eta", templateData);
148
+ await (0, file_system_1.createFile)(authGuardPath, authGuardContent);
149
+ console.log(`[apso] Generated ${authGuardPath}`);
150
+ }
151
+ // Generate scope.guard.ts if there are scoped entities
152
+ if (hasScopes) {
153
+ const scopeGuardPath = path.join(guardsDir, "scope.guard.ts");
154
+ const scopeGuardContent = await Eta.renderFileAsync("./guards/scope.guard.eta", templateData);
155
+ await (0, file_system_1.createFile)(scopeGuardPath, scopeGuardContent);
156
+ console.log(`[apso] Generated ${scopeGuardPath}`);
157
+ }
158
+ // Generate guards.module.ts
159
+ const guardsModulePath = path.join(guardsDir, "guards.module.ts");
160
+ const guardsModuleContent = await Eta.renderFileAsync("./guards/guards.module.eta", templateData);
161
+ await (0, file_system_1.createFile)(guardsModulePath, guardsModuleContent);
162
+ console.log(`[apso] Generated ${guardsModulePath}`);
163
+ // Generate index.ts
164
+ const indexPath = path.join(guardsDir, "index.ts");
165
+ const indexContent = await Eta.renderFileAsync("./guards/index.eta", templateData);
166
+ await (0, file_system_1.createFile)(indexPath, indexContent);
167
+ console.log(`[apso] Generated ${indexPath}`);
168
+ // Summary
169
+ const parts = [];
170
+ if (normalizedAuth)
171
+ parts.push(`auth (${normalizedAuth.provider})`);
172
+ if (scopedEntities.length > 0)
173
+ parts.push(`${scopedEntities.length} scoped entities`);
174
+ console.log(`[apso] Generated guards for ${parts.join(" + ")}`);
175
+ };
176
+ exports.createGuards = createGuards;
@@ -8,3 +8,4 @@ export { createIndexAppModule } from "./index-module";
8
8
  export { createDto } from "./dto";
9
9
  export { createEnums } from "./enums";
10
10
  export { createGqlDTO } from "./gql-dto";
11
+ export { createGuards, hasScopedEntities, getScopedEntities } from "./guards";
package/dist/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createGqlDTO = exports.createEnums = exports.createDto = exports.createIndexAppModule = exports.createModule = exports.createService = exports.createController = exports.createEntity = exports.parseApsorc = void 0;
3
+ exports.getScopedEntities = exports.hasScopedEntities = exports.createGuards = exports.createGqlDTO = exports.createEnums = exports.createDto = exports.createIndexAppModule = exports.createModule = exports.createService = exports.createController = exports.createEntity = exports.parseApsorc = void 0;
4
4
  var apsorc_parser_1 = require("./apsorc-parser");
5
5
  Object.defineProperty(exports, "parseApsorc", { enumerable: true, get: function () { return apsorc_parser_1.parseApsorc; } });
6
6
  var entity_1 = require("./entity");
@@ -19,3 +19,7 @@ var enums_1 = require("./enums");
19
19
  Object.defineProperty(exports, "createEnums", { enumerable: true, get: function () { return enums_1.createEnums; } });
20
20
  var gql_dto_1 = require("./gql-dto");
21
21
  Object.defineProperty(exports, "createGqlDTO", { enumerable: true, get: function () { return gql_dto_1.createGqlDTO; } });
22
+ var guards_1 = require("./guards");
23
+ Object.defineProperty(exports, "createGuards", { enumerable: true, get: function () { return guards_1.createGuards; } });
24
+ Object.defineProperty(exports, "hasScopedEntities", { enumerable: true, get: function () { return guards_1.hasScopedEntities; } });
25
+ Object.defineProperty(exports, "getScopedEntities", { enumerable: true, get: function () { return guards_1.getScopedEntities; } });
@@ -31,7 +31,9 @@ const createModule = async (apiBaseDir, entity, options) => {
31
31
  entityName,
32
32
  };
33
33
  // Determine template based on API type
34
- const templatePath = (options === null || options === void 0 ? void 0 : options.apiType) === "graphql" ? "./graphql/gql-module-graphql" : "./rest/module-rest";
34
+ const templatePath = (options === null || options === void 0 ? void 0 : options.apiType) === "graphql"
35
+ ? "./graphql/gql-module-graphql"
36
+ : "./rest/module-rest";
35
37
  // Render the template
36
38
  const content = await Eta.renderFileAsync(templatePath, data);
37
39
  // Create the module file