Skip to content

Commit 73340f2

Browse files
committed
feat: remove features enable/disable
1 parent cda3c1d commit 73340f2

12 files changed

Lines changed: 24 additions & 142 deletions

File tree

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,13 @@ docker compose up -d
180180

181181
## Optional Features
182182

183-
Optional features are installed from independent folders in `installed_features`.
183+
Optional features are installed and enabled by placing independent folders in `installed_features`.
184184
Each feature folder needs a `feature.json` or `config.json` manifest:
185185

186186
```json
187187
{
188188
"name": "foodorders",
189189
"version": "0.0.1",
190-
"enabledByDefault": false,
191190
"navbar": {
192191
"insert": true,
193192
"href": "/foodorders",
@@ -198,17 +197,14 @@ Each feature folder needs a `feature.json` or `config.json` manifest:
198197
"href": "/admin/foodorders",
199198
"translationKeyBase": "Admin.FeatureCards.foodorders"
200199
},
201-
"settings": {
202-
"translationKeyBase": "AdminSettings.Features.foodorders"
203-
},
204200
"db": {
205201
"migrations": [],
206202
"seeds": []
207203
}
208204
}
209205
```
210206

211-
On startup the app scans `installed_features/<featureName>`. If the feature version is newer than `config/features/<featureName>.json`, or if the installed config does not exist yet, the feature files are copied into the application.
207+
On startup the app scans `installed_features/<featureName>`. Folder presence enables the feature. If the feature version is newer than `config/features/<featureName>.json`, or if the installed config does not exist yet, the feature files are copied into the application.
212208

213209
Supported feature folders include application folders such as `api`, `lib`, `src`, `views`, `public`, and `config`. A top-level `templates` folder is installed into `config/templates`. Feature translations live in:
214210

api/settings.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ const { verifyBufferIsJPG, verifyBufferIsJPGMaxDimensions, convertToWebp } = req
1515
const { getBackups, createBackup, restoreBackup } = require('@lib/backup');
1616
const { generateManifest } = require('@lib/manifest');
1717
const { InvalidRouteInput } = require('@lib/errors');
18-
const { ViewRenderer } = require('@lib/template');
19-
const { loadFeatureDefinitions } = require('@lib/features');
2018
const { isEBGOAuthEnabled } = require('@lib/oauth');
2119
const Joi = require('@lib/sanitizer');
2220
const express = require('ultimate-express');
@@ -35,9 +33,8 @@ const uploadHandler = multer({
3533
limits: { fileSize: 2 * 1024 * 1024 * 1024 } // 2GB file size limit
3634
});
3735

38-
// Dynamically generate the schema for toggling settings based on available features
3936
const getSettingsToggleSchema = () => {
40-
const settingKeys = ['USER_SHOPPINGLIST_ACTIVE', 'DB_AUTOVACUUM', 'LOW_FUNDS_WARNING', ...Object.keys(loadFeatureDefinitions()).map((featureName) => `feature_${featureName}`)];
37+
const settingKeys = ['USER_SHOPPINGLIST_ACTIVE', 'DB_AUTOVACUUM', 'LOW_FUNDS_WARNING'];
4138
if (!isEBGOAuthEnabled()) settingKeys.unshift('REG_CODE_ACTIVE');
4239

4340
return Joi.object({
@@ -70,9 +67,6 @@ router.get('/', verifyRequest('app.admin.settings.read'), limiter(1), async (req
7067
router.post('/toggle', verifyRequest('app.admin.settings.write'), limiter(1), async (req, res) => {
7168
const body = await getSettingsToggleSchema().validateAsync(req.body);
7269
const result = await toggleSetting(body.setting_key);
73-
if (body.setting_key.startsWith('feature_')) {
74-
new ViewRenderer().flushAllCachesAndRenderStaticPages();
75-
}
7670
return res.json({ success: true, new_value: result });
7771
});
7872

config/application.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = {
2-
dbVersion: 1782000000, // Minimum database version
2+
dbVersion: 1782166516, // Minimum database version
33
}

config/locals_map.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,6 @@ module.exports = {
99
"Categories",
1010
"AdminUsers"
1111
],
12-
"/admin/:uuid/itembuyers": [
13-
"Admin",
14-
"Error",
15-
"Page",
16-
"Items",
17-
"Navbar"
18-
],
1912
"/admin/:uuid/edituser": [
2013
"Admin",
2114
"Error",
@@ -25,6 +18,13 @@ module.exports = {
2518
"Navbar",
2619
"AdminUsers"
2720
],
21+
"/admin/:uuid/itembuyers": [
22+
"Admin",
23+
"Error",
24+
"Page",
25+
"Items",
26+
"Navbar"
27+
],
2828
"/admin/:uuid/transactions": [
2929
"Admin",
3030
"AdminUsers",

lib/features.js

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ const manifestCandidates = ['feature.json', 'config.json'];
1515
* @typedef {Object} FeatureManifest
1616
* @property {String} name Safe feature identifier used for settings, config files, and install paths.
1717
* @property {String} version Feature version used to decide whether an installed feature should be updated.
18-
* @property {Boolean} enabledByDefault Whether migrations/settings should enable the feature by default.
1918
* @property {Object} navbar Navbar metadata used when a feature adds a navigation entry.
2019
* @property {Object} adminCard Admin feature-card metadata for the admin overview.
21-
* @property {Object} settings Admin settings metadata for the feature toggle.
2220
* @property {Object} db Database migration and seed file references.
2321
* @property {Object.<String, String[]>} localsMap Extra translation keys needed by route.
2422
*/
@@ -111,7 +109,6 @@ const normalizeManifest = (manifest, fallbackName) => {
111109
...manifest,
112110
name,
113111
version: manifest.version || '0.0.0',
114-
enabledByDefault: manifest.enabledByDefault === true,
115112
navbar: {
116113
insert: manifest.navbar?.insert === true,
117114
href: manifest.navbar?.href || `/${name}`,
@@ -123,9 +120,6 @@ const normalizeManifest = (manifest, fallbackName) => {
123120
href: manifest.adminCard?.href || manifest.href || `/admin/${name}`,
124121
translationKeyBase: manifest.adminCard?.translationKeyBase || manifest.translationKeyBase || `Admin.FeatureCards.${name}`,
125122
},
126-
settings: {
127-
translationKeyBase: manifest.settings?.translationKeyBase || manifest.settingsTranslationKeyBase || `AdminSettings.Features.${name}`,
128-
},
129123
db: {
130124
migrations: Array.isArray(manifest.db?.migrations) ? manifest.db.migrations : [],
131125
seeds: Array.isArray(manifest.db?.seeds) ? manifest.db.seeds : [],
@@ -346,39 +340,6 @@ const loadFeatureDefinitions = () => {
346340
}, {});
347341
};
348342

349-
/**
350-
* Returns only the enabled feature definitions and annotates each one with enabled: true.
351-
* @param {Object.<String, Boolean>} featureSettings Feature enablement map, keyed by feature name.
352-
* @returns {Object.<String, FeatureManifest & {enabled: Boolean}>} Enabled feature definitions keyed by feature name.
353-
*/
354-
const getEnabledFeatures = (featureSettings) => {
355-
const featureDefinitions = loadFeatureDefinitions();
356-
return Object.entries(featureDefinitions).reduce((enabledFeatures, [featureName, definition]) => {
357-
if (featureSettings[featureName] === true) {
358-
enabledFeatures[featureName] = {
359-
...definition,
360-
enabled: true,
361-
};
362-
}
363-
return enabledFeatures;
364-
}, {});
365-
};
366-
367-
/**
368-
* Returns only enabled feature definitions without adding extra fields.
369-
* @param {Object.<String, Boolean>} featureSettings Feature enablement map, keyed by feature name.
370-
* @returns {Object.<String, FeatureManifest>} Enabled feature definitions keyed by feature name.
371-
*/
372-
const getEnabledFeatureDefinitions = (featureSettings) => {
373-
const featureDefinitions = loadFeatureDefinitions();
374-
return Object.entries(featureDefinitions).reduce((enabledFeatureDefinitions, [featureName, definition]) => {
375-
if (featureSettings[featureName] === true) {
376-
enabledFeatureDefinitions[featureName] = definition;
377-
}
378-
return enabledFeatureDefinitions;
379-
}, {});
380-
};
381-
382343
/**
383344
* Resolves a public asset path for an installed feature.
384345
* @param {String} featureName Feature name whose installed_features/<feature>/public directory should be used.
@@ -404,8 +365,6 @@ module.exports = {
404365
featureLocalesDir,
405366
installFeatures,
406367
loadFeatureDefinitions,
407-
getEnabledFeatures,
408-
getEnabledFeatureDefinitions,
409368
getFeaturePublicFilePath,
410369
compareVersions,
411370
};

lib/sqlite/settings.js

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,38 +50,10 @@ const checkIfSettingTrue = async (key) => {
5050
return currentValue ? currentValue.setting_value === 'true' : false;
5151
};
5252

53-
/**
54-
* Get all feature flags from app settings.
55-
* Settings are stored as feature_<name> and exposed as { [name]: boolean }.
56-
* @returns {Object}
57-
*/
58-
const getFeatureSettings = () => {
59-
const settings = db.prepare("SELECT setting_key, setting_value FROM app_settings WHERE setting_key LIKE 'feature_%'").all();
60-
return settings.reduce((features, setting) => {
61-
const featureName = setting.setting_key.replace(/^feature_/, '');
62-
features[featureName] = setting.setting_value === 'true';
63-
return features;
64-
}, {});
65-
};
66-
67-
/**
68-
* Ensure every installed feature has a toggle setting.
69-
* @param {Object} featureDefinitions
70-
*/
71-
const ensureFeatureSettings = (featureDefinitions) => {
72-
const insertSetting = db.prepare('INSERT OR IGNORE INTO app_settings (setting_key, setting_value) VALUES (?, ?)');
73-
74-
Object.entries(featureDefinitions).forEach(([featureName, definition]) => {
75-
insertSetting.run(`feature_${featureName}`, definition.enabledByDefault === true ? 'true' : 'false');
76-
});
77-
};
78-
7953
module.exports = {
8054
getSettings,
8155
getSetting,
8256
toggleSetting,
8357
updateSetting,
84-
checkIfSettingTrue,
85-
getFeatureSettings,
86-
ensureFeatureSettings
58+
checkIfSettingTrue
8759
};

lib/template.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ const { deepMerge } = require('@lib/utils');
99

1010
const auth = require('@config/auth.js');
1111
const { default_group, default_member_group } = require('@config/permissions.js');
12-
const { loadFeatureDefinitions, getEnabledFeatures, getEnabledFeatureDefinitions } = require('@lib/features.js');
13-
const { getFeatureSettings } = require('@lib/sqlite/settings.js');
12+
const { loadFeatureDefinitions } = require('@lib/features.js');
1413
const { getEBGOAuthAuthorizeUrl, isEBGOAuthEnabled } = require('@lib/oauth.js');
1514

1615
const startupUnixTime = Math.floor(Date.now() / 1000); // Keeping global cuz the dynamic pages would trigger a lot of CDN overwrites
@@ -71,12 +70,9 @@ class ViewRenderer {
7170
return result;
7271
}, {});
7372

74-
const featureSettings = getFeatureSettings();
7573
const featureDefinitions = loadFeatureDefinitions();
7674
const featureLangData = process.availableFeatureLanguages?.[langCode] || {};
77-
const featureNames = page === '/admin/settings'
78-
? Object.keys(featureDefinitions)
79-
: Object.entries(featureSettings).filter(([, enabled]) => enabled).map(([featureName]) => featureName);
75+
const featureNames = Object.keys(featureDefinitions);
8076

8177
featureNames.forEach(featureName => {
8278
if (!featureLangData[featureName]) return;
@@ -401,7 +397,6 @@ class ViewRenderer {
401397
const getContextObject = () => {
402398
if (cachedContextObject) return cachedContextObject;
403399

404-
const featureSettings = getFeatureSettings();
405400
const featureDefinitions = loadFeatureDefinitions();
406401
cachedContextObject = {
407402
Github: {
@@ -411,9 +406,8 @@ const getContextObject = () => {
411406
OAuthUrl: `${auth.Google.url}?client_id=${auth.Google.clientID}&redirect_uri=${auth.Google.url_redirect}&response_type=code&scope=${auth.Google.url_scope.join(' ')}`,
412407
},
413408
curentUnixTime: startupUnixTime,
414-
features: getEnabledFeatures(featureSettings),
415-
featureDefinitions: getEnabledFeatureDefinitions(featureSettings),
416-
featureSettingsDefinitions: featureDefinitions,
409+
features: featureDefinitions,
410+
featureDefinitions,
417411
default_group: default_group,
418412
countryConfig: process.countryConfig,
419413
permissions_groups: Object.keys(process.permissions_config.groups),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DELETE FROM app_settings
2+
WHERE setting_key LIKE 'feature_%';

public/appjs/translate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ document.addEventListener("DOMContentLoaded", async () => {
8484
Object.entries(features)
8585
.map(([featureName, feature]) => ({
8686
name: featureName,
87-
config: typeof feature === 'object' ? feature : { enabled: feature === true },
87+
config: typeof feature === 'object' ? feature : {},
8888
}))
89-
.filter(({ config }) => config.enabled === true && config.navbar?.insert === true)
89+
.filter(({ config }) => config.navbar?.insert === true)
9090
.filter(({ config }) => !config.navbar.permission || checkPermission(config.navbar.permission).result)
9191
.sort((left, right) => (left.config.navbar.order || 100) - (right.config.navbar.order || 100))
9292
.forEach(({ name, config }) => {

src/app.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ const { getManifest } = require('@lib/manifest');
1414
const { countUsers } = require('@lib/sqlite/users');
1515
const { backfillStatistics } = require('@lib/sqlite/stats');
1616
const { getStaticFilePath } = require('@lib/imageStore');
17-
const { loadFeatureDefinitions, getFeaturePublicFilePath } = require('@lib/features');
18-
const { ensureFeatureSettings } = require('@lib/sqlite/settings');
17+
const { getFeaturePublicFilePath } = require('@lib/features');
1918
const { isEBGOAuthEnabled } = require('@lib/oauth');
2019
const { startNotificationWorker } = require('@lib/notifications');
2120

@@ -77,7 +76,6 @@ if (dbMigration === 0) {
7776
}
7877
}
7978

80-
ensureFeatureSettings(loadFeatureDefinitions());
8179
backfillStatistics(); // Backfill statistics data
8280

8381
// Redirect root to setup

0 commit comments

Comments
 (0)