Skip to content

Commit af0cc3f

Browse files
feat(config): per-module activated flag (#3384)
* feat(config): add per-module `activated` flag to exclude deactivated modules (#3383) Add `filterByActivation` to config helper that filters out routes, models, policies, and swagger YAML for modules with `activated: false`. Core modules (core, auth, users, home) are never filtered. Missing flag defaults to active. * fix(config): normalize path separators in extractModuleName for Windows compat * docs: add module activation migration entry
1 parent f77c3a1 commit af0cc3f

9 files changed

Lines changed: 194 additions & 0 deletions

File tree

MIGRATIONS.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ Breaking changes and upgrade notes for downstream projects.
44

55
---
66

7+
## Module Activation Config (2026-04-05)
8+
9+
Per-module `activated: true/false` config flag. When `activated: false`, the module's routes, policies, models, and swagger YAML are excluded from the app entirely.
10+
11+
### What changed
12+
13+
- New `filterByActivation(files, config)` in `lib/helpers/config.js` — filters all globbed file arrays by module activation status
14+
- `config/index.js` applies filtering after config merge to: routes, policies, models, swagger YAML, preRoutes, configs
15+
- Core modules (`core`, `auth`, `users`, `home`) are always active regardless of flag
16+
- New module config files with `activated: true` default: `audit`, `billing`, `organizations`, `uploads`, `tasks`
17+
18+
### Action for downstream
19+
20+
1. Run `/update-stack` to pull the change
21+
2. No breaking change — all modules default to `activated: true` (backward compatible)
22+
3. To deactivate a module, set `DEVKIT_NODE_{moduleName}_activated=false` in env vars or override in config:
23+
```js
24+
// config/defaults/development.config.js
25+
tasks: { activated: false }
26+
```
27+
4. If you have custom modules, add `activated: true` in their config file to be explicit
28+
29+
---
30+
731
## Decentralized Policy Subject Resolution (2026-04-03)
832

933
Subject resolution in `lib/middlewares/policy.js` is now registry-based instead of hardcoded. Each module's policy file exports a `*SubjectRegistration()` function that registers its own document-level and path-level subjects during `discoverPolicies()`.

config/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ const initGlobalConfig = async () => {
131131
config = deepMerge(config, { package: packageJSON });
132132
// Initialize global globbed files
133133
config = deepMerge(config, { files: await configHelper.initGlobalConfigFiles(assets) });
134+
// Filter files by module activation (deactivated modules are excluded)
135+
const fileKeys = ['swagger', 'mongooseModels', 'sequelizeModels', 'preRoutes', 'routes', 'configs', 'policies'];
136+
for (const key of fileKeys) {
137+
if (config.files[key]) {
138+
config.files[key] = configHelper.filterByActivation(config.files[key], config);
139+
}
140+
}
134141
// Init Secure SSL if can be used
135142
configHelper.initSecureMode(config);
136143
// Print a warning if config.domain is not set

lib/helpers/config.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,42 @@ const initSecureMode = (config) => {
9898
}
9999
};
100100

101+
/**
102+
* Core modules that are always active regardless of the `activated` flag.
103+
*/
104+
const CORE_MODULES = new Set(['core', 'auth', 'users', 'home']);
105+
106+
/**
107+
* Extract the module name from a file path.
108+
* E.g. `modules/tasks/routes/tasks.routes.js` → `tasks`
109+
* @param {string} filePath
110+
* @returns {string|null} module name or null if not inside modules/
111+
*/
112+
const extractModuleName = (filePath) => {
113+
const normalizedPath = String(filePath).replace(/\\/g, '/');
114+
const match = normalizedPath.match(/modules\/([^/]+)\//);
115+
return match ? match[1] : null;
116+
};
117+
118+
/**
119+
* Filter file paths by module activation config.
120+
* Files belonging to deactivated modules are excluded.
121+
* Core modules are never filtered out.
122+
* Missing `activated` key defaults to active (true).
123+
*
124+
* @param {string[]} files - array of file paths
125+
* @param {object} config - merged configuration object
126+
* @returns {string[]} filtered file paths
127+
*/
128+
const filterByActivation = (files, config) => files.filter((file) => {
129+
const moduleName = extractModuleName(file);
130+
if (!moduleName) return true; // not a module file, keep it
131+
if (CORE_MODULES.has(moduleName)) return true; // core modules always active
132+
const moduleConfig = config[moduleName];
133+
if (!moduleConfig || moduleConfig.activated === undefined) return true; // default to active
134+
return moduleConfig.activated !== false;
135+
});
136+
101137
/**
102138
* Initialize global configuration files
103139
*/
@@ -120,4 +156,6 @@ export default {
120156
validateJwtSecret,
121157
initSecureMode,
122158
initGlobalConfigFiles,
159+
filterByActivation,
160+
CORE_MODULES,
123161
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* Unit tests for filterByActivation in config helper.
3+
*/
4+
import configHelper from '../config.js';
5+
6+
const { filterByActivation, CORE_MODULES } = configHelper;
7+
8+
describe('filterByActivation', () => {
9+
const files = [
10+
'modules/core/routes/core.routes.js',
11+
'modules/auth/routes/auth.routes.js',
12+
'modules/users/routes/users.routes.js',
13+
'modules/home/routes/home.routes.js',
14+
'modules/tasks/routes/tasks.routes.js',
15+
'modules/billing/routes/billing.routes.js',
16+
'modules/audit/models/audit.mongoose.js',
17+
'modules/organizations/policies/organizations.policy.js',
18+
'modules/uploads/routes/uploads.routes.js',
19+
];
20+
21+
it('should keep all files when all modules are activated (default)', () => {
22+
const config = {
23+
tasks: { activated: true },
24+
billing: { activated: true },
25+
audit: { activated: true },
26+
organizations: { activated: true },
27+
uploads: { activated: true },
28+
};
29+
const result = filterByActivation(files, config);
30+
expect(result).toEqual(files);
31+
});
32+
33+
it('should filter out files from a deactivated non-core module', () => {
34+
const config = {
35+
tasks: { activated: false },
36+
billing: { activated: true },
37+
};
38+
const result = filterByActivation(files, config);
39+
expect(result).not.toContain('modules/tasks/routes/tasks.routes.js');
40+
expect(result).toContain('modules/billing/routes/billing.routes.js');
41+
});
42+
43+
it('should filter out multiple deactivated modules', () => {
44+
const config = {
45+
tasks: { activated: false },
46+
billing: { activated: false },
47+
audit: { activated: false },
48+
};
49+
const result = filterByActivation(files, config);
50+
expect(result).not.toContain('modules/tasks/routes/tasks.routes.js');
51+
expect(result).not.toContain('modules/billing/routes/billing.routes.js');
52+
expect(result).not.toContain('modules/audit/models/audit.mongoose.js');
53+
// Core modules still present
54+
expect(result).toContain('modules/core/routes/core.routes.js');
55+
expect(result).toContain('modules/auth/routes/auth.routes.js');
56+
expect(result).toContain('modules/users/routes/users.routes.js');
57+
expect(result).toContain('modules/home/routes/home.routes.js');
58+
});
59+
60+
it('should never filter out core modules even if activated is false', () => {
61+
const config = {
62+
core: { activated: false },
63+
auth: { activated: false },
64+
users: { activated: false },
65+
home: { activated: false },
66+
};
67+
const result = filterByActivation(files, config);
68+
expect(result).toContain('modules/core/routes/core.routes.js');
69+
expect(result).toContain('modules/auth/routes/auth.routes.js');
70+
expect(result).toContain('modules/users/routes/users.routes.js');
71+
expect(result).toContain('modules/home/routes/home.routes.js');
72+
});
73+
74+
it('should default to active when activated key is missing', () => {
75+
const config = {
76+
tasks: { someOtherSetting: true }, // no activated key
77+
};
78+
const result = filterByActivation(files, config);
79+
expect(result).toContain('modules/tasks/routes/tasks.routes.js');
80+
});
81+
82+
it('should default to active when module has no config at all', () => {
83+
const config = {}; // no module configs
84+
const result = filterByActivation(files, config);
85+
expect(result).toEqual(files);
86+
});
87+
88+
it('should keep non-module files (no modules/ in path)', () => {
89+
const mixedFiles = [
90+
'config/defaults/development.config.js',
91+
'lib/helpers/config.js',
92+
'modules/tasks/routes/tasks.routes.js',
93+
];
94+
const config = { tasks: { activated: false } };
95+
const result = filterByActivation(mixedFiles, config);
96+
expect(result).toContain('config/defaults/development.config.js');
97+
expect(result).toContain('lib/helpers/config.js');
98+
expect(result).not.toContain('modules/tasks/routes/tasks.routes.js');
99+
});
100+
101+
it('should handle empty file array', () => {
102+
const result = filterByActivation([], {});
103+
expect(result).toEqual([]);
104+
});
105+
106+
it('should define core, auth, users, home as core modules', () => {
107+
expect(CORE_MODULES.has('core')).toBe(true);
108+
expect(CORE_MODULES.has('auth')).toBe(true);
109+
expect(CORE_MODULES.has('users')).toBe(true);
110+
expect(CORE_MODULES.has('home')).toBe(true);
111+
expect(CORE_MODULES.has('tasks')).toBe(false);
112+
expect(CORE_MODULES.has('billing')).toBe(false);
113+
});
114+
});

modules/audit/config/audit.development.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const config = {
22
audit: {
3+
activated: true,
34
enabled: true,
45
ttlDays: 90,
56
captureIp: true,

modules/billing/config/billing.development.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const config = {
22
billing: {
3+
activated: true,
34
// Plans available for subscriptions — extend as needed
45
plans: ['free', 'starter', 'pro', 'enterprise'],
56
// Quotas — downstream projects override these per plan:

modules/organizations/config/organizations.development.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const config = {
22
organizations: {
3+
activated: true,
34
enabled: true, // false → B2C mode, organizations invisible
45
autoCreate: true, // automatically create/join orgs at signup
56
domainMatching: true, // match users to existing orgs by email domain
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const config = {
2+
tasks: {
3+
activated: true,
4+
},
5+
};
6+
7+
export default config;

modules/uploads/config/uploads.development.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const config = {
22
uploads: {
3+
activated: true,
34
sharp: {
45
// default sharp settings for all uploads
56
blur: 8,

0 commit comments

Comments
 (0)