@apso/cli 0.5.0 → 0.7.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 (69) hide show
  1. package/README.md +510 -1
  2. package/dist/commands/server/new.js +1 -1
  3. package/dist/commands/server/scaffold.js +19 -10
  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 +4 -3
  10. package/dist/lib/guards.js +64 -25
  11. package/dist/lib/module.js +3 -1
  12. package/dist/lib/templates/guards/auth.guard.eta +228 -0
  13. package/dist/lib/templates/guards/guards.module.eta +73 -13
  14. package/dist/lib/templates/guards/index.eta +18 -0
  15. package/dist/lib/templates/guards/scope.guard.eta +12 -4
  16. package/dist/lib/types/auth.d.ts +203 -0
  17. package/dist/lib/types/auth.js +60 -0
  18. package/dist/lib/types/entity.d.ts +2 -2
  19. package/dist/lib/types/index.d.ts +1 -0
  20. package/dist/lib/types/index.js +8 -0
  21. package/dist/lib/types/relationship.d.ts +1 -1
  22. package/dist/lib/utils/casing.js +5 -5
  23. package/dist/lib/utils/field.js +2 -1
  24. package/dist/lib/utils/file-system.js +1 -1
  25. package/dist/lib/utils/relationships/parse-v1.js +2 -1
  26. package/dist/lib/utils/relationships/parse.js +117 -32
  27. package/dist/lib/utils/relationships/parse.spec.js +168 -141
  28. package/dist/test-nested-relationships.js +10 -10
  29. package/dist/tests/templates/entities/types/array-template.spec.js +45 -45
  30. package/dist/tests/templates/entities/types/bigint-template.spec.js +43 -43
  31. package/dist/tests/templates/entities/types/boolean-template.spec.js +44 -44
  32. package/dist/tests/templates/entities/types/bytea-template.spec.js +35 -35
  33. package/dist/tests/templates/entities/types/char-template.spec.js +35 -35
  34. package/dist/tests/templates/entities/types/date-template.spec.js +29 -29
  35. package/dist/tests/templates/entities/types/decimal-field-validation.spec.js +46 -46
  36. package/dist/tests/templates/entities/types/decimal-template.spec.js +46 -46
  37. package/dist/tests/templates/entities/types/double-template.spec.js +37 -34
  38. package/dist/tests/templates/entities/types/enum-template.spec.js +42 -42
  39. package/dist/tests/templates/entities/types/float-template.spec.js +37 -34
  40. package/dist/tests/templates/entities/types/geometry-template.spec.js +31 -31
  41. package/dist/tests/templates/entities/types/inet-template.spec.js +36 -36
  42. package/dist/tests/templates/entities/types/int4range-template.spec.js +45 -33
  43. package/dist/tests/templates/entities/types/integer-template.spec.js +40 -40
  44. package/dist/tests/templates/entities/types/interval-template.spec.js +35 -35
  45. package/dist/tests/templates/entities/types/json-plain-template.spec.js +27 -27
  46. package/dist/tests/templates/entities/types/json-template.spec.js +19 -19
  47. package/dist/tests/templates/entities/types/jsonb-template.spec.js +19 -19
  48. package/dist/tests/templates/entities/types/money-template.spec.js +35 -35
  49. package/dist/tests/templates/entities/types/numeric-template.spec.js +44 -44
  50. package/dist/tests/templates/entities/types/point-template.spec.js +37 -31
  51. package/dist/tests/templates/entities/types/polygon-template.spec.js +43 -31
  52. package/dist/tests/templates/entities/types/real-template.spec.js +34 -34
  53. package/dist/tests/templates/entities/types/serial-template.spec.js +62 -62
  54. package/dist/tests/templates/entities/types/smallint-template.spec.js +43 -43
  55. package/dist/tests/templates/entities/types/string-template.spec.js +59 -59
  56. package/dist/tests/templates/entities/types/text-template.spec.js +37 -37
  57. package/dist/tests/templates/entities/types/time-template.spec.js +35 -35
  58. package/dist/tests/templates/entities/types/timestamp-template.spec.js +39 -39
  59. package/dist/tests/templates/entities/types/timetz-template.spec.js +35 -35
  60. package/dist/tests/templates/entities/types/tsvector-template.spec.js +28 -28
  61. package/dist/tests/templates/entities/types/uuid-template.spec.js +43 -43
  62. package/dist/tests/templates/entities/types/varchar-template.spec.js +59 -56
  63. package/dist/tests/templates/entities/types/xml-template.spec.js +28 -28
  64. package/dist/tests/templates/entities/utils/template-test-utils.d.ts +1 -1
  65. package/dist/tests/templates/entities/utils/template-test-utils.js +3 -3
  66. package/dist/tests/templates/entities/uuid-entity-template.spec.js +138 -138
  67. package/dist/tests/utils/relationship-deduplication.spec.js +11 -11
  68. package/oclif.manifest.json +1 -1
  69. package/package.json +1 -1
@@ -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
  };
@@ -1,4 +1,4 @@
1
- import { Entity } from "./types";
1
+ import { Entity, AuthConfig } from "./types";
2
2
  /**
3
3
  * Represents a scope field configuration for template rendering
4
4
  */
@@ -36,11 +36,12 @@ export declare function getScopedEntities(entities: Entity[]): ScopedEntityConfi
36
36
  export declare function hasScopedEntities(entities: Entity[]): boolean;
37
37
  /**
38
38
  * Generates the guards module directory and files.
39
- * Creates scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
39
+ * Creates auth.guard.ts, scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
40
40
  *
41
41
  * @param rootPath The root path of the generated source (e.g., 'src').
42
42
  * @param entities All entities from the parsed .apsorc.
43
+ * @param auth Optional auth configuration from .apsorc.
43
44
  * @returns {Promise<void>} A promise that resolves when the guard files are created.
44
45
  */
45
- export declare const createGuards: (rootPath: string, entities: Entity[]) => Promise<void>;
46
+ export declare const createGuards: (rootPath: string, entities: Entity[], auth?: AuthConfig) => Promise<void>;
46
47
  export {};
@@ -6,13 +6,14 @@ const Eta = tslib_1.__importStar(require("eta"));
6
6
  const path = tslib_1.__importStar(require("path"));
7
7
  const fs = tslib_1.__importStar(require("fs"));
8
8
  const file_system_1 = require("./utils/file-system");
9
+ const types_1 = require("./types");
9
10
  const pluralize_1 = tslib_1.__importDefault(require("pluralize"));
10
11
  /**
11
12
  * Default scope options when not specified
12
13
  */
13
14
  const DEFAULT_SCOPE_OPTIONS = {
14
15
  injectOnCreate: true,
15
- enforceOn: ['find', 'get', 'create', 'update', 'delete'],
16
+ enforceOn: ["find", "get", "create", "update", "delete"],
16
17
  bypassRoles: [],
17
18
  };
18
19
  /**
@@ -23,12 +24,12 @@ const DEFAULT_SCOPE_OPTIONS = {
23
24
  */
24
25
  function parseScopeBy(scopeBy) {
25
26
  const scopes = Array.isArray(scopeBy) ? scopeBy : [scopeBy];
26
- return scopes.map(scope => {
27
- const isDirect = !scope.includes('.');
28
- const field = isDirect ? scope : scope.split('.')[0];
27
+ return scopes.map((scope) => {
28
+ const isDirect = !scope.includes(".");
29
+ const field = isDirect ? scope : scope.split(".")[0];
29
30
  // For direct fields, contextKey is the same as field
30
31
  // For nested paths like 'task.workspaceId', we extract 'workspaceId' as contextKey
31
- const contextKey = isDirect ? scope : scope.split('.').pop();
32
+ const contextKey = isDirect ? scope : scope.split(".").pop();
32
33
  return {
33
34
  field,
34
35
  contextKey,
@@ -66,8 +67,8 @@ function toRepoName(entityName) {
66
67
  */
67
68
  function getScopedEntities(entities) {
68
69
  return entities
69
- .filter(entity => entity.scopeBy)
70
- .map(entity => {
70
+ .filter((entity) => entity.scopeBy)
71
+ .map((entity) => {
71
72
  const options = { ...DEFAULT_SCOPE_OPTIONS, ...entity.scopeOptions };
72
73
  return {
73
74
  name: entity.name,
@@ -88,24 +89,46 @@ exports.getScopedEntities = getScopedEntities;
88
89
  * @returns True if at least one entity has scopeBy defined.
89
90
  */
90
91
  function hasScopedEntities(entities) {
91
- return entities.some(entity => entity.scopeBy);
92
+ return entities.some((entity) => entity.scopeBy);
92
93
  }
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
+ }
94
114
  /**
95
115
  * Generates the guards module directory and files.
96
- * Creates scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
116
+ * Creates auth.guard.ts, scope.guard.ts, guards.module.ts, and index.ts in the guards directory.
97
117
  *
98
118
  * @param rootPath The root path of the generated source (e.g., 'src').
99
119
  * @param entities All entities from the parsed .apsorc.
120
+ * @param auth Optional auth configuration from .apsorc.
100
121
  * @returns {Promise<void>} A promise that resolves when the guard files are created.
101
122
  */
102
- const createGuards = async (rootPath, entities) => {
103
- // Only generate guards if there are scoped entities
104
- if (!hasScopedEntities(entities)) {
105
- console.log('[apso] No scopeBy configurations found, skipping guards generation');
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");
106
129
  return;
107
130
  }
108
- const guardsDir = path.join(rootPath, 'guards');
131
+ const guardsDir = path.join(rootPath, "autogen", "guards");
109
132
  // Ensure guards directory exists
110
133
  if (!fs.existsSync(guardsDir)) {
111
134
  fs.mkdirSync(guardsDir, { recursive: true });
@@ -114,24 +137,40 @@ const createGuards = async (rootPath, entities) => {
114
137
  // Common template data
115
138
  const templateData = {
116
139
  scopedEntities,
140
+ authConfig: normalizedAuth,
117
141
  generatedAt: new Date().toISOString(),
118
- generatedBy: 'Apso CLI',
142
+ generatedBy: "Apso CLI",
119
143
  };
120
- // Generate scope.guard.ts
121
- const scopeGuardPath = path.join(guardsDir, 'scope.guard.ts');
122
- const scopeGuardContent = await Eta.renderFileAsync('./guards/scope.guard', templateData);
123
- await (0, file_system_1.createFile)(scopeGuardPath, scopeGuardContent);
124
- console.log(`[apso] Generated ${scopeGuardPath}`);
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
+ }
125
158
  // Generate guards.module.ts
126
- const guardsModulePath = path.join(guardsDir, 'guards.module.ts');
127
- const guardsModuleContent = await Eta.renderFileAsync('./guards/guards.module', templateData);
159
+ const guardsModulePath = path.join(guardsDir, "guards.module.ts");
160
+ const guardsModuleContent = await Eta.renderFileAsync("./guards/guards.module.eta", templateData);
128
161
  await (0, file_system_1.createFile)(guardsModulePath, guardsModuleContent);
129
162
  console.log(`[apso] Generated ${guardsModulePath}`);
130
163
  // Generate index.ts
131
- const indexPath = path.join(guardsDir, 'index.ts');
132
- const indexContent = await Eta.renderFileAsync('./guards/index', templateData);
164
+ const indexPath = path.join(guardsDir, "index.ts");
165
+ const indexContent = await Eta.renderFileAsync("./guards/index.eta", templateData);
133
166
  await (0, file_system_1.createFile)(indexPath, indexContent);
134
167
  console.log(`[apso] Generated ${indexPath}`);
135
- console.log(`[apso] Generated guards for ${scopedEntities.length} scoped entities`);
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(" + ")}`);
136
175
  };
137
176
  exports.createGuards = createGuards;
@@ -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
@@ -0,0 +1,228 @@
1
+ /**
2
+ * AuthGuard - Authentication guard for <%= it.authConfig.provider %> provider
3
+ *
4
+ * Auto-generated by Apso CLI from .apsorc auth configuration
5
+ * Generated at: <%= it.generatedAt %>
6
+ *
7
+ * This guard:
8
+ * - Validates session tokens from cookies/headers
9
+ * - Loads user and organization context
10
+ * - Attaches normalized AuthContext to requests
11
+ *
12
+ * DO NOT EDIT - Changes will be overwritten on next scaffold
13
+ */
14
+ import {
15
+ Injectable,
16
+ CanActivate,
17
+ ExecutionContext,
18
+ UnauthorizedException,
19
+ } from '@nestjs/common';
20
+ import { Reflector } from '@nestjs/core';
21
+ import { InjectRepository } from '@nestjs/typeorm';
22
+ import { Repository } from 'typeorm';
23
+
24
+ // Entity imports (from sibling autogen folders)
25
+ import { <%= it.authConfig.sessionEntity %> } from '../<%= it.authConfig.sessionEntity %>/<%= it.authConfig.sessionEntity %>.entity';
26
+ import { <%= it.authConfig.userEntity %> } from '../<%= it.authConfig.userEntity %>/<%= it.authConfig.userEntity %>.entity';
27
+ <% if (it.authConfig.accountUserEntity) { %>
28
+ import { <%= it.authConfig.accountUserEntity %> } from '../<%= it.authConfig.accountUserEntity %>/<%= it.authConfig.accountUserEntity %>.entity';
29
+ <% } %>
30
+
31
+ /**
32
+ * Metadata key for marking routes as public (no auth required)
33
+ */
34
+ export const IS_PUBLIC_KEY = 'isPublic';
35
+
36
+ /**
37
+ * Decorator to mark a route as public (skips authentication)
38
+ * Usage: @Public()
39
+ */
40
+ export const Public =
41
+ () => (target: any, key?: string, descriptor?: PropertyDescriptor) => {
42
+ Reflect.defineMetadata(IS_PUBLIC_KEY, true, descriptor?.value || target);
43
+ };
44
+
45
+ /**
46
+ * Normalized auth context attached to authenticated requests.
47
+ * This is what scopeBy and RBAC consume.
48
+ */
49
+ export interface AuthContext {
50
+ userId?: string;
51
+ email?: string;
52
+ workspaceId?: string;
53
+ organizationId?: string;
54
+ roles: string[];
55
+ user?: <%= it.authConfig.userEntity %>;
56
+ session?: <%= it.authConfig.sessionEntity %>;
57
+ }
58
+
59
+ /**
60
+ * Extended request type with auth context
61
+ */
62
+ export interface AuthenticatedRequest extends Request {
63
+ auth: AuthContext;
64
+ user: <%= it.authConfig.userEntity %>;
65
+ session: <%= it.authConfig.sessionEntity %>;
66
+ // Legacy compatibility aliases
67
+ accountId?: string;
68
+ accountRole?: string;
69
+ organizationId?: string;
70
+ }
71
+
72
+ @Injectable()
73
+ export class AuthGuard implements CanActivate {
74
+ constructor(
75
+ private reflector: Reflector,
76
+ @InjectRepository(<%= it.authConfig.sessionEntity %>)
77
+ private sessionRepository: Repository<<%= it.authConfig.sessionEntity %>>,
78
+ @InjectRepository(<%= it.authConfig.userEntity %>)
79
+ private userRepository: Repository<<%= it.authConfig.userEntity %>>,
80
+ <% if (it.authConfig.accountUserEntity) { %>
81
+ @InjectRepository(<%= it.authConfig.accountUserEntity %>)
82
+ private accountUserRepository: Repository<<%= it.authConfig.accountUserEntity %>>,
83
+ <% } %>
84
+ ) {}
85
+
86
+ async canActivate(context: ExecutionContext): Promise<boolean> {
87
+ // Check if route is marked as public
88
+ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
89
+ context.getHandler(),
90
+ context.getClass(),
91
+ ]);
92
+
93
+ if (isPublic) {
94
+ return true;
95
+ }
96
+
97
+ const request = context.switchToHttp().getRequest();
98
+ const token = this.extractToken(request);
99
+
100
+ if (!token) {
101
+ throw new UnauthorizedException('No session token provided');
102
+ }
103
+
104
+ try {
105
+ // Find session by token
106
+ const sessionRecord = await this.sessionRepository.findOne({
107
+ where: { token },
108
+ });
109
+
110
+ if (!sessionRecord) {
111
+ throw new UnauthorizedException('Invalid session token');
112
+ }
113
+
114
+ // Check if session is expired
115
+ if (new Date(sessionRecord.expiresAt) < new Date()) {
116
+ throw new UnauthorizedException('Session expired');
117
+ }
118
+
119
+ // Get user from session
120
+ const user = await this.userRepository.findOne({
121
+ where: { id: sessionRecord.userId },
122
+ });
123
+
124
+ if (!user) {
125
+ throw new UnauthorizedException('User not found');
126
+ }
127
+
128
+ <% if (it.authConfig.accountUserEntity) { %>
129
+ // Get user's organization through <%= it.authConfig.accountUserEntity %> junction
130
+ const accountUser = await this.accountUserRepository.findOne({
131
+ where: { userId: user.id },
132
+ });
133
+
134
+ if (!accountUser) {
135
+ throw new UnauthorizedException('User not associated with any organization');
136
+ }
137
+
138
+ const organizationId = accountUser.<%= it.authConfig.organizationField %>;
139
+ const role = accountUser.<%= it.authConfig.roleField %> || 'member';
140
+ <% } else { %>
141
+ // No account user entity configured - using user-level organization
142
+ const organizationId = (user as any).<%= it.authConfig.organizationField %> || undefined;
143
+ const role = (user as any).<%= it.authConfig.roleField %> || 'member';
144
+ <% } %>
145
+
146
+ // Build normalized AuthContext
147
+ const authContext: AuthContext = {
148
+ userId: user.id,
149
+ email: user.email,
150
+ workspaceId: organizationId,
151
+ organizationId: organizationId,
152
+ roles: [role],
153
+ user: user,
154
+ session: sessionRecord,
155
+ };
156
+
157
+ // Attach to request
158
+ (request as any).auth = authContext;
159
+ (request as any).user = user;
160
+ (request as any).session = sessionRecord;
161
+
162
+ // Legacy compatibility - attach directly to request root
163
+ (request as any).accountId = organizationId;
164
+ (request as any).accountRole = role;
165
+ (request as any).organizationId = organizationId;
166
+
167
+ return true;
168
+ } catch (error) {
169
+ if (error instanceof UnauthorizedException) {
170
+ throw error;
171
+ }
172
+ console.error('Auth guard error:', error);
173
+ throw new UnauthorizedException('Authentication failed');
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Extract session token from request
179
+ * Checks: Authorization header (Bearer), cookies, X-Session-Token header
180
+ */
181
+ private extractToken(request: any): string | undefined {
182
+ // Check Authorization header first (Bearer token)
183
+ const authHeader = request.headers.authorization;
184
+ if (authHeader && authHeader.startsWith('Bearer ')) {
185
+ return authHeader.substring(7);
186
+ }
187
+
188
+ // Check for session token in cookies
189
+ const cookies = request.cookies || {};
190
+ <% if (it.authConfig.cookiePrefix) { %>
191
+ const cookieName = '<%= it.authConfig.cookiePrefix %>.session_token';
192
+ const sessionToken =
193
+ cookies[cookieName] ||
194
+ cookies['better-auth.session_token'] ||
195
+ cookies['session_token'];
196
+ <% } else { %>
197
+ const sessionToken =
198
+ cookies['better-auth.session_token'] ||
199
+ cookies['session_token'];
200
+ <% } %>
201
+
202
+ if (sessionToken) {
203
+ return sessionToken;
204
+ }
205
+
206
+ // Check X-Session-Token header
207
+ return request.headers['x-session-token'];
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Helper function to get AuthContext from request
213
+ * Use this in controllers/services to access the authenticated context
214
+ */
215
+ export function getAuthContext(request: any): AuthContext | undefined {
216
+ return request?.auth;
217
+ }
218
+
219
+ /**
220
+ * Helper function to require AuthContext (throws if not present)
221
+ */
222
+ export function requireAuthContext(request: any): AuthContext {
223
+ const ctx = getAuthContext(request);
224
+ if (!ctx) {
225
+ throw new UnauthorizedException('Authentication required');
226
+ }
227
+ return ctx;
228
+ }