kempo 4.8.0 → 4.9.1
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.
- package/README.md +7 -3
- package/bin/cli.js +23 -1
- package/dist/admin/component-overrides.js +1 -1
- package/dist/admin/components/AddGroupControl.js +1 -1
- package/dist/admin/components/AdminTableControl.js +1 -1
- package/dist/admin/components/ClearExpiredSessionsControl.js +1 -1
- package/dist/admin/components/Dashboard.js +1 -1
- package/dist/admin/components/EmailTemplates.js +1 -1
- package/dist/admin/components/Emails.js +1 -1
- package/dist/admin/components/FragmentEditor.js +1 -1
- package/dist/admin/components/GlobalContentEditor.js +1 -1
- package/dist/admin/components/GroupDeleteSelected.js +1 -1
- package/dist/admin/components/GroupRemoveRecord.js +1 -1
- package/dist/admin/components/Menus.js +1 -1
- package/dist/admin/components/Monaco.js +1 -1
- package/dist/admin/components/PageEditor.js +1 -1
- package/dist/admin/components/TableRowDelete.js +1 -1
- package/dist/admin/components/TableRowLink.js +1 -1
- package/dist/admin/components/TemplateEditor.js +1 -1
- package/dist/admin/components/Updates.js +117 -0
- package/dist/admin/components/Updates.js.map +1 -0
- package/dist/admin/components/ValueEditor.js +1 -1
- package/dist/admin/components/dashboardLayout.js +1 -1
- package/dist/admin/components/settings/AdminSettings.js +1 -1
- package/dist/admin/components/settings/BooleanSetting.js +1 -1
- package/dist/admin/components/settings/JsonSetting.js +1 -1
- package/dist/admin/components/settings/NumberSetting.js +1 -1
- package/dist/admin/components/settings/SecretSetting.js +1 -1
- package/dist/admin/components/settings/Setting.js +1 -1
- package/dist/admin/components/settings/SettingEditor.js +1 -1
- package/dist/admin/components/settings/SettingsPanel.js +1 -1
- package/dist/admin/components/settings/StringSetting.js +1 -1
- package/dist/admin/components/settings/humanize.js +1 -1
- package/dist/admin/components/tableControls/AddMember.js +1 -1
- package/dist/admin/components/tableControls/AddPermission.js +1 -1
- package/dist/admin/components/tableControls/ExtensionDetails.js +1 -1
- package/dist/admin/components/tableControls/InstallExtension.js +1 -1
- package/dist/admin/components/tableControls/ToggleExtension.js +1 -1
- package/dist/admin/components/tableControls/UninstallExtension.js +1 -1
- package/dist/admin/components/widgets/Content.js +1 -1
- package/dist/admin/components/widgets/Extensions.js +1 -1
- package/dist/admin/components/widgets/Notifications.js +1 -1
- package/dist/admin/components/widgets/Pages.js +1 -1
- package/dist/admin/components/widgets/Users.js +1 -1
- package/dist/admin/init.js +1 -1
- package/dist/admin/nav.fragment.html +6 -0
- package/dist/admin/updates/index.page.html +14 -0
- package/dist/kempo/api/admin-globals/DELETE.js +1 -1
- package/dist/kempo/api/admin-globals/GET.js +1 -1
- package/dist/kempo/api/admin-globals/POST.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/GET.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/PUT.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/disable/PUT.js +1 -1
- package/dist/kempo/api/admin-globals/[id]/enable/PUT.js +1 -1
- package/dist/kempo/api/auth/change-password/POST.js +1 -1
- package/dist/kempo/api/auth/login/email/POST.js +1 -1
- package/dist/kempo/api/auth/logout/POST.js +1 -1
- package/dist/kempo/api/auth/register/email/POST.js +1 -1
- package/dist/kempo/api/auth/reset-password/POST.js +1 -1
- package/dist/kempo/api/auth/session/GET.js +1 -1
- package/dist/kempo/api/auth/verify-email/POST.js +1 -1
- package/dist/kempo/api/dashboard/widgets/GET.js +1 -1
- package/dist/kempo/api/email-templates/GET.js +1 -1
- package/dist/kempo/api/email-templates/file/DELETE.js +1 -1
- package/dist/kempo/api/email-templates/file/GET.js +1 -1
- package/dist/kempo/api/email-templates/file/PUT.js +1 -1
- package/dist/kempo/api/email-templates/preview/GET.js +1 -1
- package/dist/kempo/api/emails/GET.js +1 -1
- package/dist/kempo/api/emails/[id]/GET.js +1 -1
- package/dist/kempo/api/emails/[id]/retry/POST.js +1 -1
- package/dist/kempo/api/emails/test/POST.js +1 -1
- package/dist/kempo/api/extensions/DELETE.js +1 -1
- package/dist/kempo/api/extensions/GET.js +1 -1
- package/dist/kempo/api/extensions/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/dependents/GET.js +1 -1
- package/dist/kempo/api/extensions/[name]/disable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/enable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/requirements/GET.js +1 -1
- package/dist/kempo/api/extensions/[name]/update/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/update-check/GET.js +1 -1
- package/dist/kempo/api/extensions/available/GET.js +1 -1
- package/dist/kempo/api/extensions/known/GET.js +1 -1
- package/dist/kempo/api/extensions/rating/PUT.js +1 -1
- package/dist/kempo/api/forgot-password/POST.js +1 -1
- package/dist/kempo/api/fragments/DELETE.js +1 -1
- package/dist/kempo/api/fragments/GET.js +1 -1
- package/dist/kempo/api/fragments/POST.js +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js +1 -1
- package/dist/kempo/api/fragments/file/GET.js +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js +1 -1
- package/dist/kempo/api/fragments/file/PUT.js +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js +1 -1
- package/dist/kempo/api/globals/DELETE.js +1 -1
- package/dist/kempo/api/globals/GET.js +1 -1
- package/dist/kempo/api/globals/POST.js +1 -1
- package/dist/kempo/api/globals/disable/PUT.js +1 -1
- package/dist/kempo/api/globals/enable/PUT.js +1 -1
- package/dist/kempo/api/globals/entry/GET.js +1 -1
- package/dist/kempo/api/globals/entry/PUT.js +1 -1
- package/dist/kempo/api/globals/lock/PUT.js +1 -1
- package/dist/kempo/api/groups/DELETE.js +1 -1
- package/dist/kempo/api/groups/GET.js +1 -1
- package/dist/kempo/api/groups/PATCH.js +1 -1
- package/dist/kempo/api/groups/POST.js +1 -1
- package/dist/kempo/api/groups/[name]/GET.js +1 -1
- package/dist/kempo/api/groups/[name]/members/DELETE.js +1 -1
- package/dist/kempo/api/groups/[name]/members/GET.js +1 -1
- package/dist/kempo/api/groups/[name]/members/POST.js +1 -1
- package/dist/kempo/api/groups/[name]/permissions/DELETE.js +1 -1
- package/dist/kempo/api/groups/[name]/permissions/GET.js +1 -1
- package/dist/kempo/api/groups/[name]/permissions/POST.js +1 -1
- package/dist/kempo/api/menus/DELETE.js +1 -1
- package/dist/kempo/api/menus/GET.js +1 -1
- package/dist/kempo/api/menus/POST.js +1 -1
- package/dist/kempo/api/menus/adopt/PUT.js +1 -1
- package/dist/kempo/api/menus/items/DELETE.js +1 -1
- package/dist/kempo/api/menus/items/POST.js +1 -1
- package/dist/kempo/api/menus/items/enabled/PUT.js +1 -1
- package/dist/kempo/api/menus/items/order/PUT.js +1 -1
- package/dist/kempo/api/menus/location/PUT.js +1 -1
- package/dist/kempo/api/notifications/GET.js +1 -1
- package/dist/kempo/api/notifications/[id]/DELETE.js +1 -1
- package/dist/kempo/api/notifications/[id]/handled/POST.js +1 -1
- package/dist/kempo/api/notifications/[id]/read/POST.js +1 -1
- package/dist/kempo/api/notifications/count/GET.js +1 -1
- package/dist/kempo/api/notifications/read-all/POST.js +1 -1
- package/dist/kempo/api/pages/DELETE.js +1 -1
- package/dist/kempo/api/pages/GET.js +1 -1
- package/dist/kempo/api/pages/POST.js +1 -1
- package/dist/kempo/api/pages/directories/GET.js +1 -1
- package/dist/kempo/api/pages/disable/PUT.js +1 -1
- package/dist/kempo/api/pages/enable/PUT.js +1 -1
- package/dist/kempo/api/pages/file/GET.js +1 -1
- package/dist/kempo/api/pages/file/PATCH.js +1 -1
- package/dist/kempo/api/pages/file/PUT.js +1 -1
- package/dist/kempo/api/pages/lock/PUT.js +1 -1
- package/dist/kempo/api/pages/rescan/POST.js +1 -1
- package/dist/kempo/api/pages/search/GET.js +1 -1
- package/dist/kempo/api/pages/templates/GET.js +1 -1
- package/dist/kempo/api/permissions/DELETE.js +1 -1
- package/dist/kempo/api/permissions/GET.js +1 -1
- package/dist/kempo/api/permissions/PATCH.js +1 -1
- package/dist/kempo/api/permissions/POST.js +1 -1
- package/dist/kempo/api/realtime/WS.js +1 -1
- package/dist/kempo/api/realtime/connections/GET.js +1 -1
- package/dist/kempo/api/settings/[owner]/GET.js +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/DELETE.js +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/GET.js +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/POST.js +1 -1
- package/dist/kempo/api/settings/public/GET.js +1 -1
- package/dist/kempo/api/templates/DELETE.js +1 -1
- package/dist/kempo/api/templates/GET.js +1 -1
- package/dist/kempo/api/templates/POST.js +1 -1
- package/dist/kempo/api/templates/disable/PUT.js +1 -1
- package/dist/kempo/api/templates/enable/PUT.js +1 -1
- package/dist/kempo/api/templates/file/GET.js +1 -1
- package/dist/kempo/api/templates/file/PATCH.js +1 -1
- package/dist/kempo/api/templates/file/PUT.js +1 -1
- package/dist/kempo/api/templates/lock/PUT.js +1 -1
- package/dist/kempo/api/updates/GET.js +2 -0
- package/dist/kempo/api/updates/GET.js.map +1 -0
- package/dist/kempo/api/updates/apply/POST.js +2 -0
- package/dist/kempo/api/updates/apply/POST.js.map +1 -0
- package/dist/kempo/api/updates/check/POST.js +2 -0
- package/dist/kempo/api/updates/check/POST.js.map +1 -0
- package/dist/kempo/api/updates/discard/POST.js +2 -0
- package/dist/kempo/api/updates/discard/POST.js.map +1 -0
- package/dist/kempo/api/updates/prepare/POST.js +2 -0
- package/dist/kempo/api/updates/prepare/POST.js.map +1 -0
- package/dist/kempo/api/updates/rollback/POST.js +2 -0
- package/dist/kempo/api/updates/rollback/POST.js.map +1 -0
- package/dist/kempo/api/user/DELETE.js +1 -1
- package/dist/kempo/api/user/GET.js +1 -1
- package/dist/kempo/api/user/PATCH.js +1 -1
- package/dist/kempo/api/user/[userid]/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/groups/DELETE.js +1 -1
- package/dist/kempo/api/user/[userid]/groups/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/groups/POST.js +1 -1
- package/dist/kempo/api/user/[userid]/name/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/permissions/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/permissions/all/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/permissions/list/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/permissions/some/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/sessions/DELETE.js +1 -1
- package/dist/kempo/api/user/[userid]/sessions/GET.js +1 -1
- package/dist/kempo/api/user/[userid]/sessions/expired/DELETE.js +1 -1
- package/dist/kempo/api/user/create/POST.js +1 -1
- package/dist/kempo/api/user/search/GET.js +1 -1
- package/dist/kempo/components/AdminNavFooter.js +1 -1
- package/dist/kempo/components/DashboardWidget.js +1 -1
- package/dist/kempo/components/Login.js +1 -1
- package/dist/kempo/components/LogoutBtn.js +1 -1
- package/dist/kempo/components/Menu.js +1 -1
- package/dist/kempo/components/NotificationBell.js +1 -1
- package/dist/kempo/components/NotificationList.js +1 -1
- package/dist/kempo/components/Permission.js +1 -1
- package/dist/kempo/components/PermissionTarget.js +1 -1
- package/dist/kempo/components/Register.js +1 -1
- package/dist/kempo/components/Role.js +1 -1
- package/dist/kempo/components/UserCreatedAt.js +1 -1
- package/dist/kempo/components/UserDisplayName.js +1 -1
- package/dist/kempo/components/UserEmail.js +1 -1
- package/dist/kempo/components/UserEmailVerified.js +1 -1
- package/dist/kempo/components/UserName.js +1 -1
- package/dist/kempo/fetch.js +2 -2
- package/dist/kempo/fetch.js.map +1 -1
- package/dist/kempo/realtime.js +1 -1
- package/dist/kempo/sdk.js +2 -2
- package/dist/kempo/sdk.js.map +1 -1
- package/dist/kempo/utils/safeNext.js +1 -1
- package/docker/.dockerignore +9 -0
- package/docker/Dockerfile +25 -0
- package/docker/docker-compose.prod.yml +39 -0
- package/docker/entrypoint.mjs +96 -0
- package/middleware/kempo.js +53 -3
- package/package.json +3 -1
- package/server/db/schema.js +12 -0
- package/server/db/upgrades/README.md +20 -0
- package/server/sdk.js +3 -0
- package/server/utils/extensions/checkExtensionUpdate.js +9 -36
- package/server/utils/extensions/reconcileExtensions.js +90 -0
- package/server/utils/extensions/updateExtension.js +14 -153
- package/server/utils/extensions/upgradeExtension.js +117 -0
- package/server/utils/extensions/writeSiteFile.js +66 -0
- package/server/utils/settings/systemSettings.js +1 -0
- package/server/utils/setup/runUpgrades.js +70 -0
- package/server/utils/setup/seedDatabase.js +1 -1
- package/server/utils/setup/seedIfNew.js +21 -0
- package/server/utils/setup/syncCoreSchema.js +130 -0
- package/server/utils/setup/systemPermissions.js +2 -0
- package/server/utils/updates/applyStagedUpdate.js +95 -0
- package/server/utils/updates/applyUpdate.js +76 -0
- package/server/utils/updates/backupDatabase.js +35 -0
- package/server/utils/updates/discardUpdate.js +11 -0
- package/server/utils/updates/extensionTargets.js +101 -0
- package/server/utils/updates/fetchManifest.js +26 -0
- package/server/utils/updates/getExtensionUpdates.js +70 -0
- package/server/utils/updates/getInstallInfo.js +25 -0
- package/server/utils/updates/getUpdateStatus.js +57 -0
- package/server/utils/updates/helpers.js +30 -0
- package/server/utils/updates/notifySchemaAttention.js +35 -0
- package/server/utils/updates/npm.js +41 -0
- package/server/utils/updates/prepareUpdate.js +109 -0
- package/server/utils/updates/releaseStatus.js +23 -0
- package/server/utils/updates/restart.js +64 -0
- package/server/utils/updates/rollbackUpdate.js +56 -0
- package/server/utils/updates/runUpdateCheck.js +52 -0
- package/server/utils/updates/runtime.js +15 -0
- package/server/utils/updates/siteLayout.js +200 -0
- package/server/utils/updates/starterFiles.js +118 -0
package/middleware/kempo.js
CHANGED
|
@@ -13,9 +13,16 @@ import triggerHook from '../server/utils/hooks/triggerHook.js';
|
|
|
13
13
|
import { ADMIN_GLOBALS_DIR } from '../server/utils/admin-global-content/helpers.js';
|
|
14
14
|
import { startScheduler } from '../server/utils/scheduler/scheduler.js';
|
|
15
15
|
import { ensureEncryptionKey } from '../server/utils/settings/ensureEncryptionKey.js';
|
|
16
|
+
import syncCoreSchema from '../server/utils/setup/syncCoreSchema.js';
|
|
17
|
+
import runUpgrades from '../server/utils/setup/runUpgrades.js';
|
|
18
|
+
import seedIfNew from '../server/utils/setup/seedIfNew.js';
|
|
19
|
+
import reconcileExtensions from '../server/utils/extensions/reconcileExtensions.js';
|
|
20
|
+
import { flagFailed, markHealthy } from '../server/utils/updates/siteLayout.js';
|
|
21
|
+
import notifySchemaAttention from '../server/utils/updates/notifySchemaAttention.js';
|
|
16
22
|
import { ensureSystemSettings } from '../server/utils/settings/systemSettings.js';
|
|
17
23
|
import { ensureSystemPermissions } from '../server/utils/setup/systemPermissions.js';
|
|
18
24
|
import registerJobs from '../server/utils/scheduler/registerJobs.js';
|
|
25
|
+
import '../server/utils/updates/runtime.js';
|
|
19
26
|
import { resolveAlias } from '../server/utils/extensions/aliases.js';
|
|
20
27
|
import { normalizeScopes } from '../server/utils/extensions/scopes.js';
|
|
21
28
|
|
|
@@ -187,6 +194,11 @@ const serveDir = async (dirPath, method, request, response, resolveDir, rootDir,
|
|
|
187
194
|
export default config => {
|
|
188
195
|
startScheduler();
|
|
189
196
|
|
|
197
|
+
/* A site that runs from a Docker volume has just switched to new code: once it has stayed up a minute the switch is final. */
|
|
198
|
+
if(process.env.KEMPO_SITE_ROOT && process.env.KEMPO_DB !== 'pglite'){
|
|
199
|
+
setTimeout(() => markHealthy(process.env.KEMPO_SITE_ROOT), 60000).unref();
|
|
200
|
+
}
|
|
201
|
+
|
|
190
202
|
/*
|
|
191
203
|
The key that encrypts stored credentials exists before any request needs it, generated into .env the
|
|
192
204
|
first time when the site has none. Opt out with `autoEncryptionKey: false` in the server config or
|
|
@@ -198,17 +210,55 @@ export default config => {
|
|
|
198
210
|
if(error) console.error(`[kempo] ${error.msg}`);
|
|
199
211
|
});
|
|
200
212
|
/*
|
|
201
|
-
What kempo added in a later version appears on an older site at start: its settings (so they show on the settings screen and can be
|
|
202
|
-
edited), its permissions, and its own scheduled
|
|
213
|
+
What kempo added in a later version appears on an older site at start: its tables and columns (additive only, see syncCoreSchema), its settings (so they show on the settings screen and can be
|
|
214
|
+
edited), its permissions, and its own scheduled jobs (the email retries and cleanup, and the check for a newer kempo). Only what is missing is added, and never in
|
|
203
215
|
the in-memory test database (nothing is set up there yet).
|
|
204
216
|
*/
|
|
205
217
|
const systemSettingsReady = process.env.KEMPO_DB === 'pglite'
|
|
206
218
|
? Promise.resolve()
|
|
207
219
|
: (async () => {
|
|
208
220
|
for(const [step, run] of [
|
|
221
|
+
['schema', async () => {
|
|
222
|
+
const [error, result] = await syncCoreSchema();
|
|
223
|
+
if(result?.applied.length) console.log(`[kempo] Database brought up to date: ${result.applied.join('; ')}`);
|
|
224
|
+
for(const { change, reason } of result?.failed ?? []) console.error(`[kempo] Database change failed: ${change}: ${reason}`);
|
|
225
|
+
return [error];
|
|
226
|
+
}],
|
|
227
|
+
['seed', async () => {
|
|
228
|
+
const [error, result] = await seedIfNew();
|
|
229
|
+
if(result?.seeded) console.log('[kempo] New database: its groups, permissions and settings were set up. Open /setup to create the first administrator.');
|
|
230
|
+
return [error];
|
|
231
|
+
}],
|
|
232
|
+
['upgrades', async () => {
|
|
233
|
+
const [error, result] = await runUpgrades();
|
|
234
|
+
if(result?.ran.length) console.log(`[kempo] Data migrations run: ${result.ran.join(', ')}`);
|
|
235
|
+
if(error){
|
|
236
|
+
console.error(`[kempo] ${error.msg}`);
|
|
237
|
+
if(error.breaking && process.env.KEMPO_SITE_ROOT){
|
|
238
|
+
flagFailed({ siteRoot: process.env.KEMPO_SITE_ROOT, reason: error.msg });
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return [null];
|
|
243
|
+
}],
|
|
209
244
|
['settings', ensureSystemSettings],
|
|
210
245
|
['permissions', ensureSystemPermissions],
|
|
211
|
-
['
|
|
246
|
+
['extensions', async () => {
|
|
247
|
+
const [error, result] = await reconcileExtensions();
|
|
248
|
+
for(const { name, from, to } of result?.upgraded ?? []) console.log(`[kempo] Extension ${name} brought up to date: ${from} -> ${to}`);
|
|
249
|
+
for(const { name, from, to } of result?.reverted ?? []) console.log(`[kempo] Extension ${name} is back on ${to} (was ${from})`);
|
|
250
|
+
for(const { name, reason } of result?.failed ?? []) console.error(`[kempo] Extension ${name} could not be brought up to date: ${reason}`);
|
|
251
|
+
if(result?.failed.some(item => item.major) && process.env.KEMPO_SITE_ROOT){
|
|
252
|
+
flagFailed({ siteRoot: process.env.KEMPO_SITE_ROOT, reason: result.failed.filter(item => item.major).map(item => `${item.name}: ${item.reason}`).join('; ') });
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
return [error];
|
|
256
|
+
}],
|
|
257
|
+
['schema notice', notifySchemaAttention],
|
|
258
|
+
['jobs', () => registerJobs({ owner: 'kempo', jobs: [
|
|
259
|
+
{ name: 'email-maintenance', every: '1m', callback: './server/utils/email/maintainEmails.js' },
|
|
260
|
+
{ name: 'update-check', every: '12h', callback: './server/utils/updates/runUpdateCheck.js' },
|
|
261
|
+
] })],
|
|
212
262
|
]){
|
|
213
263
|
const [error] = await run();
|
|
214
264
|
if(error) console.error(`[kempo] System ${step}: ${error.msg}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fullstack CMS framework for creating websites with user authentication",
|
|
6
6
|
"scripts": {
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"server/utils",
|
|
34
34
|
"server/db/index.js",
|
|
35
35
|
"server/db/schema.js",
|
|
36
|
+
"server/db/upgrades",
|
|
37
|
+
"docker",
|
|
36
38
|
"server/sdk.js",
|
|
37
39
|
"middleware/kempo.js",
|
|
38
40
|
"app-templates",
|
package/server/db/schema.js
CHANGED
|
@@ -115,6 +115,18 @@ export const extension = pgTable('extension', {
|
|
|
115
115
|
updatedAt: timestamp('updatedAt').notNull(),
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
/*
|
|
119
|
+
Which of kempo's own data migrations (server/db/upgrades/*.js) have run on this database. The additive changes to its
|
|
120
|
+
tables need no record, they are found by comparing; a migration that restructures data runs once and is recorded here.
|
|
121
|
+
The row with id 'baseline' marks the database as already holding what the shipped migrations produce.
|
|
122
|
+
*/
|
|
123
|
+
export const upgrade = pgTable('upgrade', {
|
|
124
|
+
id: text('id').primaryKey(),
|
|
125
|
+
description: text('description'),
|
|
126
|
+
breaking: boolean('breaking').notNull().default(false),
|
|
127
|
+
appliedAt: timestamp('appliedAt').notNull().defaultNow(),
|
|
128
|
+
});
|
|
129
|
+
|
|
118
130
|
/*
|
|
119
131
|
Scheduled jobs: recurring work an extension declares in its kempo-config.json `jobs`. A row per job
|
|
120
132
|
is the schedule and the lease: `nextRunAt` is when it is next due and `lockedUntil` is held while a
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Data migrations
|
|
2
|
+
|
|
3
|
+
Additive changes to kempo's tables (new tables, columns, indexes) need nothing here: they are applied when the server starts.
|
|
4
|
+
A change that restructures or moves data goes in a file in this folder, named so that they sort in the order they must run:
|
|
5
|
+
|
|
6
|
+
4.9.0-split-user-name.js
|
|
7
|
+
|
|
8
|
+
```js
|
|
9
|
+
export const meta = { description: 'Splits user.name into first and last', breaking: true };
|
|
10
|
+
|
|
11
|
+
export const up = async ({ db, sql }) => {
|
|
12
|
+
await db.execute(sql`...`);
|
|
13
|
+
};
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- It runs once, inside a transaction, when the first server of the new version starts, and is recorded in the `upgrade` table.
|
|
17
|
+
- `breaking: true` is for anything the previous version cannot run against (data moved or removed). The Updates screen warns
|
|
18
|
+
before such an update, kempo backs the database up first, and a failure puts the previous version and that backup back.
|
|
19
|
+
- Add one only in a release whose version is higher than the first one that has this folder: a site's `upgrade` table starts with
|
|
20
|
+
every file that was here when it first started recorded as already run.
|
package/server/sdk.js
CHANGED
|
@@ -221,6 +221,9 @@ export { default as installExtension } from './utils/extensions/installExtension
|
|
|
221
221
|
export { default as listExtensions } from './utils/extensions/listExtensions.js';
|
|
222
222
|
export { default as uninstallExtension } from './utils/extensions/uninstallExtension.js';
|
|
223
223
|
export { default as updateExtension } from './utils/extensions/updateExtension.js';
|
|
224
|
+
export { default as upgradeExtension } from './utils/extensions/upgradeExtension.js';
|
|
225
|
+
export { default as reconcileExtensions } from './utils/extensions/reconcileExtensions.js';
|
|
226
|
+
export { default as writeSiteFile } from './utils/extensions/writeSiteFile.js';
|
|
224
227
|
|
|
225
228
|
/*
|
|
226
229
|
Notifications
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
2
|
import { extension } from '../../db/schema.js';
|
|
3
3
|
import { eq } from 'drizzle-orm';
|
|
4
|
+
import { compareVersions, isVersion } from '../updates/helpers.js';
|
|
5
|
+
import fetchManifest from '../updates/fetchManifest.js';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if(!match) return null;
|
|
8
|
-
return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/main/${file}`;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const semverGt = (a, b) => {
|
|
12
|
-
const parse = v => (v || '0.0.0').replace(/^v/, '').split('.').map(Number);
|
|
13
|
-
const [aMaj, aMin, aPatch] = parse(a);
|
|
14
|
-
const [bMaj, bMin, bPatch] = parse(b);
|
|
15
|
-
if(aMaj !== bMaj) return aMaj > bMaj;
|
|
16
|
-
if(aMin !== bMin) return aMin > bMin;
|
|
17
|
-
return aPatch > bPatch;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export default async ({ name }) => {
|
|
7
|
+
/* Whether npm has a newer version of one installed extension than the one recorded for it. */
|
|
8
|
+
export default async ({ name, fetcher }) => {
|
|
21
9
|
if(!name){
|
|
22
10
|
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
23
11
|
}
|
|
@@ -32,25 +20,10 @@ export default async ({ name }) => {
|
|
|
32
20
|
return [{ code: 500, msg: 'Failed to find extension' }, null];
|
|
33
21
|
}
|
|
34
22
|
|
|
35
|
-
const
|
|
36
|
-
if(
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const rawUrl = toRawUrl(gitUrl);
|
|
41
|
-
if(!rawUrl){
|
|
42
|
-
return [null, { hasUpdate: false, reason: 'Unsupported git URL format' }];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let remotePkg;
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(rawUrl);
|
|
48
|
-
if(!res.ok) return [null, { hasUpdate: false, reason: 'Failed to fetch remote package.json' }];
|
|
49
|
-
remotePkg = await res.json();
|
|
50
|
-
} catch(error){
|
|
51
|
-
return [{ code: 502, msg: 'Failed to reach remote repository' }, null];
|
|
52
|
-
}
|
|
23
|
+
const [error, manifest] = await fetchManifest({ name, fetcher });
|
|
24
|
+
if(error?.code === 404) return [null, { hasUpdate: false, reason: 'This extension is not on npm' }];
|
|
25
|
+
if(error) return [error, null];
|
|
53
26
|
|
|
54
|
-
const hasUpdate =
|
|
55
|
-
return [null, { hasUpdate, currentVersion: record.version, latestVersion:
|
|
27
|
+
const hasUpdate = isVersion(record.version) && compareVersions(manifest.version, record.version) > 0;
|
|
28
|
+
return [null, { hasUpdate, currentVersion: record.version, latestVersion: manifest.version }];
|
|
56
29
|
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import db from '../../db/index.js';
|
|
4
|
+
import { extension } from '../../db/schema.js';
|
|
5
|
+
import readKempoConfig from './readKempoConfig.js';
|
|
6
|
+
import upgradeExtension from './upgradeExtension.js';
|
|
7
|
+
import { compareVersions, isBreakingBump, isVersion } from '../updates/helpers.js';
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
Brings every installed extension's database in step with the version on disk. Run when the server starts, so it is the one
|
|
11
|
+
place an update of an extension takes effect, whichever way the new version got there (the Updates screen, a switch of the
|
|
12
|
+
site's code, npm run by hand, an image that was rebuilt). It is what a deploy script would otherwise have to do.
|
|
13
|
+
|
|
14
|
+
on disk is newer than the row upgradeExtension: its tables, update.js and whatever it newly declares
|
|
15
|
+
on disk is older than the row the site went back: the row says so, and no script runs, so going back is safe
|
|
16
|
+
the same, or not installed nothing
|
|
17
|
+
|
|
18
|
+
The row is claimed first (its version moved to the new one only if it still holds the old), so of several servers starting
|
|
19
|
+
together exactly one upgrades an extension. A failed upgrade gives the row its old version back, so it is tried again at
|
|
20
|
+
the next start, and it is reported rather than stopping the server.
|
|
21
|
+
|
|
22
|
+
Extensions are taken in dependency order (what each declares in `dependencies`, in the new version's kempo-config.json), so
|
|
23
|
+
an extension's update script finds the ones it builds on already up to date: kempo-media after kempo-files and kempo-thumbs,
|
|
24
|
+
kempo-commerce after kempo-products and kempo-payments.
|
|
25
|
+
*/
|
|
26
|
+
let lastRun = { ran: false, upgraded: [], reverted: [], failed: [] };
|
|
27
|
+
|
|
28
|
+
export const getReconcileResult = () => lastRun;
|
|
29
|
+
|
|
30
|
+
const isBreaking = (from, to) => isVersion(from) && isVersion(to) && isBreakingBump(from, to);
|
|
31
|
+
|
|
32
|
+
const newer = (installed, recorded) => (isVersion(installed) && isVersion(recorded) ? compareVersions(installed, recorded) : (installed === recorded ? 0 : 1));
|
|
33
|
+
|
|
34
|
+
export default async ({ executor = db, root = process.cwd(), upgrade = upgradeExtension } = {}) => {
|
|
35
|
+
const result = { upgraded: [], reverted: [], failed: [] };
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const found = [];
|
|
39
|
+
for(const row of await executor.select().from(extension)){
|
|
40
|
+
const extRoot = join(root, 'node_modules', row.name);
|
|
41
|
+
try {
|
|
42
|
+
const { pkg, kempo } = await readKempoConfig(extRoot);
|
|
43
|
+
found.push({ row, extRoot, installed: pkg.version, requires: kempo.dependencies ?? row.kempo?.dependencies ?? [] });
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ordered = [];
|
|
50
|
+
const placed = new Set();
|
|
51
|
+
const place = item => {
|
|
52
|
+
if(placed.has(item.row.name)) return;
|
|
53
|
+
placed.add(item.row.name);
|
|
54
|
+
for(const name of item.requires){
|
|
55
|
+
const dependency = found.find(other => other.row.name === name);
|
|
56
|
+
if(dependency) place(dependency);
|
|
57
|
+
}
|
|
58
|
+
ordered.push(item);
|
|
59
|
+
};
|
|
60
|
+
found.forEach(place);
|
|
61
|
+
|
|
62
|
+
for(const { row, extRoot, installed } of ordered){
|
|
63
|
+
if(!installed || installed === row.version) continue;
|
|
64
|
+
|
|
65
|
+
const direction = row.version ? newer(installed, row.version) : 1;
|
|
66
|
+
if(direction < 0){
|
|
67
|
+
await executor.update(extension).set({ version: installed, updatedAt: new Date() }).where(eq(extension.name, row.name));
|
|
68
|
+
result.reverted.push({ name: row.name, from: row.version, to: installed });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const claim = and(eq(extension.name, row.name), row.version ? eq(extension.version, row.version) : undefined);
|
|
73
|
+
const [claimed] = await executor.update(extension).set({ version: installed }).where(claim).returning({ name: extension.name });
|
|
74
|
+
if(!claimed) continue;
|
|
75
|
+
|
|
76
|
+
const [error] = await upgrade({ name: row.name, record: row, extRoot });
|
|
77
|
+
if(error){
|
|
78
|
+
await executor.update(extension).set({ version: row.version }).where(eq(extension.name, row.name));
|
|
79
|
+
result.failed.push({ name: row.name, from: row.version, to: installed, major: isBreaking(row.version, installed), reason: error.msg });
|
|
80
|
+
} else {
|
|
81
|
+
result.upgraded.push({ name: row.name, from: row.version, to: installed });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch(error) {
|
|
85
|
+
return [{ code: 500, msg: `The extensions could not be brought up to date: ${String(error.cause?.message || error.message).slice(0, 300)}` }, result];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
lastRun = { ran: true, ...result };
|
|
89
|
+
return [null, result];
|
|
90
|
+
};
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import db from '../../db/index.js';
|
|
2
|
-
import { extension
|
|
3
|
-
import { eq
|
|
2
|
+
import { extension } from '../../db/schema.js';
|
|
3
|
+
import { eq } from 'drizzle-orm';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
6
|
-
import { randomUUID } from 'crypto';
|
|
7
6
|
import { execSync } from 'child_process';
|
|
8
|
-
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
9
|
-
import createHook from '../hooks/createHook.js';
|
|
10
|
-
import registerDashboardWidget from '../dashboard/registerDashboardWidget.js';
|
|
11
|
-
import unregisterDashboardWidget from '../dashboard/unregisterDashboardWidget.js';
|
|
12
|
-
import registerJobs from '../scheduler/registerJobs.js';
|
|
13
|
-
import setSetting from '../settings/setSetting.js';
|
|
14
|
-
import declaredValue from '../settings/declaredValue.js';
|
|
15
|
-
import createTablesFromSchema from './createTablesFromSchema.js';
|
|
16
7
|
import readKempoConfig from './readKempoConfig.js';
|
|
8
|
+
import upgradeExtension from './upgradeExtension.js';
|
|
17
9
|
import { normalizeAliases } from './aliases.js';
|
|
18
10
|
import { normalizeScopes, scopeNames } from './scopes.js';
|
|
19
11
|
import checkClaims from './checkClaims.js';
|
|
20
12
|
|
|
21
13
|
const requireFromProject = createRequire(join(process.cwd(), 'package.json'));
|
|
22
14
|
|
|
15
|
+
/*
|
|
16
|
+
Installs a newer version of one extension in place, and brings the database in step with it straight away, without a
|
|
17
|
+
restart (see upgradeExtension). For a site that runs from a Docker volume updates go through the Updates screen instead,
|
|
18
|
+
which stages them, shows what they change and can go back; this refuses there so it cannot bypass that.
|
|
19
|
+
*/
|
|
23
20
|
export default async ({ name, version = 'latest' }) => {
|
|
24
21
|
if(!name){
|
|
25
22
|
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
26
23
|
}
|
|
24
|
+
if(process.env.KEMPO_SITE_ROOT){
|
|
25
|
+
return [{ code: 409, msg: 'This site updates extensions from the Updates screen' }, null];
|
|
26
|
+
}
|
|
27
27
|
|
|
28
28
|
let oldRecord;
|
|
29
29
|
try {
|
|
@@ -35,27 +35,18 @@ export default async ({ name, version = 'latest' }) => {
|
|
|
35
35
|
return [{ code: 500, msg: 'Failed to find extension' }, null];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const oldKempo = oldRecord.kempo || {};
|
|
39
38
|
const oldVersion = oldRecord.version;
|
|
40
39
|
|
|
41
|
-
/*
|
|
42
|
-
Install updated package
|
|
43
|
-
*/
|
|
44
40
|
try {
|
|
45
41
|
execSync(`npm install ${name}@${version}`, { cwd: process.cwd(), stdio: 'pipe' });
|
|
46
42
|
} catch(error){
|
|
47
43
|
return [{ code: 500, msg: `Failed to update package: ${error.message}` }, null];
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
What the new version declares comes from its kempo-config.json (package.json's own `kempo` field is
|
|
52
|
-
only the fallback for an older extension). Reading package.json alone meant that no extension's new
|
|
53
|
-
permissions, settings, groups, hooks or widgets ever reached the database through this update.
|
|
54
|
-
*/
|
|
55
|
-
let extRoot, pkg, newKempo;
|
|
46
|
+
let extRoot, newKempo;
|
|
56
47
|
try {
|
|
57
48
|
extRoot = dirname(requireFromProject.resolve(`${name}/package.json`));
|
|
58
|
-
({
|
|
49
|
+
({ kempo: newKempo } = await readKempoConfig(extRoot));
|
|
59
50
|
} catch {
|
|
60
51
|
return [{ code: 404, msg: `Updated package "${name}" not found` }, null];
|
|
61
52
|
}
|
|
@@ -81,135 +72,5 @@ export default async ({ name, version = 'latest' }) => {
|
|
|
81
72
|
return [{ ...refusal, msg }, null];
|
|
82
73
|
}
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
Create any new tables from schema
|
|
86
|
-
*/
|
|
87
|
-
if(newKempo.schema){
|
|
88
|
-
try {
|
|
89
|
-
const schemaModule = await import(`${join(extRoot, newKempo.schema)}?t=${Date.now()}`);
|
|
90
|
-
const [err] = await createTablesFromSchema(schemaModule);
|
|
91
|
-
if(err) return [err, null];
|
|
92
|
-
} catch(error){
|
|
93
|
-
return [{ code: 500, msg: `Failed to load extension schema: ${error.message}` }, null];
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/*
|
|
98
|
-
Run update script with diff context
|
|
99
|
-
*/
|
|
100
|
-
try {
|
|
101
|
-
const updateModule = await import(`${join(extRoot, 'update.js')}?t=${Date.now()}`).catch(() => null);
|
|
102
|
-
if(updateModule?.default){
|
|
103
|
-
await updateModule.default({ oldVersion, newVersion: pkg.version, oldKempo, newKempo });
|
|
104
|
-
}
|
|
105
|
-
} catch(error){
|
|
106
|
-
return [{ code: 500, msg: `Extension update script failed: ${error.message}` }, null];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/*
|
|
110
|
-
Add new permissions (never delete — user data may depend on them)
|
|
111
|
-
*/
|
|
112
|
-
const oldPermNames = new Set((oldKempo.permissions || []).map(p => p.name));
|
|
113
|
-
for(const { name: permName, description } of newKempo.permissions || []){
|
|
114
|
-
if(!oldPermNames.has(permName)){
|
|
115
|
-
await db.insert(permission).values({
|
|
116
|
-
name: permName,
|
|
117
|
-
description: description || null,
|
|
118
|
-
owner: name,
|
|
119
|
-
createdAt: new Date(),
|
|
120
|
-
}).onConflictDoNothing();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/*
|
|
125
|
-
Add new settings only (never overwrite — user may have changed the value)
|
|
126
|
-
*/
|
|
127
|
-
const oldSettingNames = new Set((oldKempo.settings || []).map(s => s.name));
|
|
128
|
-
for(const { name: settingName, value, type, description } of newKempo.settings || []){
|
|
129
|
-
if(!oldSettingNames.has(settingName)){
|
|
130
|
-
await setSetting(name, settingName, declaredValue(value, type), type || null, false, description || null);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/*
|
|
135
|
-
Add new groups
|
|
136
|
-
*/
|
|
137
|
-
const oldGroupNames = new Set((oldKempo.groups || []).map(g => g.name));
|
|
138
|
-
for(const { name: groupName, description, permissions: groupPerms = [] } of newKempo.groups || []){
|
|
139
|
-
if(!oldGroupNames.has(groupName)){
|
|
140
|
-
await db.insert(group).values({
|
|
141
|
-
name: groupName,
|
|
142
|
-
description: description || null,
|
|
143
|
-
owner: name,
|
|
144
|
-
createdAt: new Date(),
|
|
145
|
-
}).onConflictDoNothing();
|
|
146
|
-
for(const permName of groupPerms){
|
|
147
|
-
await db.insert(groupPermission).values({
|
|
148
|
-
id: randomUUID(),
|
|
149
|
-
groupName,
|
|
150
|
-
permissionName: permName,
|
|
151
|
-
createdAt: new Date(),
|
|
152
|
-
}).onConflictDoNothing();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/*
|
|
158
|
-
Sync hooks: add new, remove deleted
|
|
159
|
-
*/
|
|
160
|
-
const oldHooks = oldKempo.hooks || {};
|
|
161
|
-
const newHooks = newKempo.hooks || {};
|
|
162
|
-
for(const [event, callback] of Object.entries(newHooks)){
|
|
163
|
-
if(!oldHooks[event]){
|
|
164
|
-
await createHook({ owner: name, event, callback });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
for(const event of Object.keys(oldHooks)){
|
|
168
|
-
if(!newHooks[event]){
|
|
169
|
-
await db.delete(hook).where(and(eq(hook.owner, name), eq(hook.event, event))).catch(() => {});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/*
|
|
174
|
-
Sync dashboard widgets: register or refresh every declared one, remove those no longer declared
|
|
175
|
-
*/
|
|
176
|
-
const newWidgetKeys = new Set((newKempo.widgets || []).map(w => w.key));
|
|
177
|
-
for(const widget of newKempo.widgets || []){
|
|
178
|
-
const [widgetError] = await registerDashboardWidget({ ...widget, owner: name });
|
|
179
|
-
if(widgetError){
|
|
180
|
-
console.warn(`[${name}] dashboard widget "${widget.key}" was not registered: ${widgetError.msg}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
for(const { key } of oldKempo.widgets || []){
|
|
184
|
-
if(!newWidgetKeys.has(key)){
|
|
185
|
-
await unregisterDashboardWidget({ owner: name, key });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/*
|
|
190
|
-
Sync scheduled jobs: the schedule becomes exactly what is declared now
|
|
191
|
-
*/
|
|
192
|
-
if((newKempo.jobs || []).length || (oldKempo.jobs || []).length){
|
|
193
|
-
const [jobsError] = await registerJobs({ owner: name, jobs: newKempo.jobs || [] });
|
|
194
|
-
if(jobsError){
|
|
195
|
-
console.warn(`[${name}] scheduled jobs were not registered: ${jobsError.msg}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const now = new Date();
|
|
200
|
-
try {
|
|
201
|
-
await db.update(extension).set({
|
|
202
|
-
version: pkg.version || null,
|
|
203
|
-
kempo: newKempo,
|
|
204
|
-
updatedAt: now,
|
|
205
|
-
}).where(eq(extension.name, name));
|
|
206
|
-
} catch(error){
|
|
207
|
-
return [{ code: 500, msg: 'Failed to update extension record' }, null];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
clearHandlerCache();
|
|
211
|
-
await triggerHook('extension:updated', { name, oldVersion, newVersion: pkg.version });
|
|
212
|
-
|
|
213
|
-
return [null, { name, oldVersion, newVersion: pkg.version }];
|
|
75
|
+
return upgradeExtension({ name, record: oldRecord, extRoot });
|
|
214
76
|
};
|
|
215
|
-
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import db from '../../db/index.js';
|
|
2
|
+
import { extension, hook, permission, group, groupPermission } from '../../db/schema.js';
|
|
3
|
+
import { eq, and } from 'drizzle-orm';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
8
|
+
import createHook from '../hooks/createHook.js';
|
|
9
|
+
import registerDashboardWidget from '../dashboard/registerDashboardWidget.js';
|
|
10
|
+
import unregisterDashboardWidget from '../dashboard/unregisterDashboardWidget.js';
|
|
11
|
+
import registerJobs from '../scheduler/registerJobs.js';
|
|
12
|
+
import setSetting from '../settings/setSetting.js';
|
|
13
|
+
import declaredValue from '../settings/declaredValue.js';
|
|
14
|
+
import syncCoreSchema from '../setup/syncCoreSchema.js';
|
|
15
|
+
import readKempoConfig from './readKempoConfig.js';
|
|
16
|
+
import { invalidateScopeCache } from './scopeCache.js';
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
Brings the database in step with a newer version of an extension that is already on disk: its tables (additions only: new
|
|
20
|
+
tables, columns and indexes), its own `update.js` (given the old and new version and what each declared), and whatever the
|
|
21
|
+
new version declares that the old one did not (permissions, settings, groups, hooks, dashboard widgets and scheduled jobs).
|
|
22
|
+
It only adds: a permission, setting or group an extension no longer declares stays, because data may depend on it, and a
|
|
23
|
+
setting the owner changed keeps their value. Hooks, widgets and jobs follow what is declared now.
|
|
24
|
+
|
|
25
|
+
`record` is the extension's row as it was before. Resolves to [error, { name, oldVersion, newVersion }]. Run by the startup
|
|
26
|
+
step (reconcileExtensions) and by updateExtension, never on a version that is older than the row's.
|
|
27
|
+
*/
|
|
28
|
+
export default async ({ name, record, extRoot = join(process.cwd(), 'node_modules', name) }) => {
|
|
29
|
+
const oldKempo = record.kempo || {};
|
|
30
|
+
const oldVersion = record.version;
|
|
31
|
+
|
|
32
|
+
let pkg, newKempo;
|
|
33
|
+
try {
|
|
34
|
+
({ pkg, kempo: newKempo } = await readKempoConfig(extRoot));
|
|
35
|
+
} catch {
|
|
36
|
+
return [{ code: 404, msg: `Package "${name}" is not installed` }, null];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if(newKempo.schema){
|
|
40
|
+
try {
|
|
41
|
+
const schemaModule = await import(`${pathToFileURL(join(extRoot, newKempo.schema)).href}?t=${Date.now()}`);
|
|
42
|
+
const [schemaError, synced] = await syncCoreSchema({ schema: schemaModule });
|
|
43
|
+
if(schemaError) return [schemaError, null];
|
|
44
|
+
for(const { change, reason } of synced.failed) console.warn(`[${name}] database change not applied: ${change}: ${reason}`);
|
|
45
|
+
} catch(error) {
|
|
46
|
+
return [{ code: 500, msg: `Failed to load extension schema: ${error.message}` }, null];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const updateModule = await import(`${pathToFileURL(join(extRoot, 'update.js')).href}?t=${Date.now()}`).catch(() => null);
|
|
52
|
+
if(updateModule?.default){
|
|
53
|
+
await updateModule.default({ oldVersion, newVersion: pkg.version, oldKempo, newKempo });
|
|
54
|
+
}
|
|
55
|
+
} catch(error) {
|
|
56
|
+
return [{ code: 500, msg: `Extension update script failed: ${error.message}` }, null];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const oldPermNames = new Set((oldKempo.permissions || []).map(p => p.name));
|
|
60
|
+
for(const { name: permName, description } of newKempo.permissions || []){
|
|
61
|
+
if(!oldPermNames.has(permName)){
|
|
62
|
+
await db.insert(permission).values({ name: permName, description: description || null, owner: name, createdAt: new Date() }).onConflictDoNothing();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const oldSettingNames = new Set((oldKempo.settings || []).map(s => s.name));
|
|
67
|
+
for(const { name: settingName, value, type, description } of newKempo.settings || []){
|
|
68
|
+
if(!oldSettingNames.has(settingName)){
|
|
69
|
+
await setSetting(name, settingName, declaredValue(value, type), type || null, false, description || null);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const oldGroupNames = new Set((oldKempo.groups || []).map(g => g.name));
|
|
74
|
+
for(const { name: groupName, description, permissions: groupPerms = [] } of newKempo.groups || []){
|
|
75
|
+
if(!oldGroupNames.has(groupName)){
|
|
76
|
+
await db.insert(group).values({ name: groupName, description: description || null, owner: name, createdAt: new Date() }).onConflictDoNothing();
|
|
77
|
+
for(const permName of groupPerms){
|
|
78
|
+
await db.insert(groupPermission).values({ id: randomUUID(), groupName, permissionName: permName, createdAt: new Date() }).onConflictDoNothing();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const oldHooks = oldKempo.hooks || {};
|
|
84
|
+
const newHooks = newKempo.hooks || {};
|
|
85
|
+
for(const [event, callback] of Object.entries(newHooks)){
|
|
86
|
+
if(!oldHooks[event]) await createHook({ owner: name, event, callback });
|
|
87
|
+
}
|
|
88
|
+
for(const event of Object.keys(oldHooks)){
|
|
89
|
+
if(!newHooks[event]) await db.delete(hook).where(and(eq(hook.owner, name), eq(hook.event, event))).catch(() => {});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const newWidgetKeys = new Set((newKempo.widgets || []).map(w => w.key));
|
|
93
|
+
for(const widget of newKempo.widgets || []){
|
|
94
|
+
const [widgetError] = await registerDashboardWidget({ ...widget, owner: name });
|
|
95
|
+
if(widgetError) console.warn(`[${name}] dashboard widget "${widget.key}" was not registered: ${widgetError.msg}`);
|
|
96
|
+
}
|
|
97
|
+
for(const { key } of oldKempo.widgets || []){
|
|
98
|
+
if(!newWidgetKeys.has(key)) await unregisterDashboardWidget({ owner: name, key });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if((newKempo.jobs || []).length || (oldKempo.jobs || []).length){
|
|
102
|
+
const [jobsError] = await registerJobs({ owner: name, jobs: newKempo.jobs || [] });
|
|
103
|
+
if(jobsError) console.warn(`[${name}] scheduled jobs were not registered: ${jobsError.msg}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await db.update(extension).set({ version: pkg.version || null, kempo: newKempo, updatedAt: new Date() }).where(eq(extension.name, name));
|
|
108
|
+
} catch {
|
|
109
|
+
return [{ code: 500, msg: 'Failed to update extension record' }, null];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
clearHandlerCache();
|
|
113
|
+
invalidateScopeCache();
|
|
114
|
+
await triggerHook('extension:updated', { name, oldVersion, newVersion: pkg.version });
|
|
115
|
+
|
|
116
|
+
return [null, { name, oldVersion, newVersion: pkg.version }];
|
|
117
|
+
};
|