kempo 4.5.8 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/components/tableControls/ExtensionDetails.js +2 -0
- package/dist/admin/components/tableControls/ExtensionDetails.js.map +1 -0
- package/dist/admin/components/tableControls/InstallExtension.js.map +1 -1
- package/dist/admin/components/tableControls/ToggleExtension.js +2 -0
- package/dist/admin/components/tableControls/ToggleExtension.js.map +1 -0
- package/dist/admin/components/tableControls/UninstallExtension.js +1 -1
- package/dist/admin/components/tableControls/UninstallExtension.js.map +1 -1
- package/dist/admin/extensions/browse/index.page.html +118 -0
- package/dist/admin/extensions/details/index.page.html +254 -0
- package/dist/admin/extensions/index.page.html +8 -2
- package/dist/kempo/api/extensions/DELETE.js +1 -1
- package/dist/kempo/api/extensions/DELETE.js.map +1 -1
- package/dist/kempo/api/extensions/POST.js +1 -1
- package/dist/kempo/api/extensions/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/dependents/GET.js +2 -0
- package/dist/kempo/api/extensions/[name]/dependents/GET.js.map +1 -0
- package/dist/kempo/api/extensions/[name]/disable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/disable/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/enable/POST.js +1 -1
- package/dist/kempo/api/extensions/[name]/enable/POST.js.map +1 -1
- package/dist/kempo/api/extensions/[name]/requirements/GET.js +2 -0
- package/dist/kempo/api/extensions/[name]/requirements/GET.js.map +1 -0
- package/dist/kempo/api/extensions/available/GET.js +1 -1
- package/dist/kempo/api/extensions/available/GET.js.map +1 -1
- package/dist/kempo/api/extensions/known/GET.js +1 -1
- package/dist/kempo/api/extensions/known/GET.js.map +1 -1
- package/dist/kempo/api/extensions/rating/PUT.js +2 -0
- package/dist/kempo/api/extensions/rating/PUT.js.map +1 -0
- package/dist/kempo/icons/payments.svg +1 -0
- package/dist/kempo/icons/toll.svg +1 -0
- package/dist/kempo/icons/wallet.svg +1 -0
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/package.json +1 -1
- package/server/utils/extensions/catalogClient.js +203 -0
- package/server/utils/extensions/disableExtension.js +2 -32
- package/server/utils/extensions/disableExtensionTree.js +46 -0
- package/server/utils/extensions/enableExtension.js +2 -33
- package/server/utils/extensions/enableExtensionTree.js +54 -0
- package/server/utils/extensions/getDependentTree.js +47 -0
- package/server/utils/extensions/getMarketplaceClient.js +20 -0
- package/server/utils/extensions/getRequirementTree.js +43 -0
- package/server/utils/extensions/getSiteInstallId.js +21 -0
- package/server/utils/extensions/installFromCatalog.js +127 -0
- package/server/utils/extensions/installPackage.js +36 -0
- package/server/utils/extensions/scopeCache.js +14 -5
- package/server/utils/extensions/uninstallExtension.js +2 -124
- package/server/utils/extensions/uninstallExtensionTree.js +165 -0
- package/server/utils/hooks/triggerHook.js +8 -0
- package/dist/admin/known-extensions.json +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import db from '../../db/index.js';
|
|
2
|
+
import { extension, hook, permission, setting, group, groupPermission, userGroup, dashboardWidget } from '../../db/schema.js';
|
|
3
|
+
import { eq, inArray, like } from 'drizzle-orm';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { sql } from 'drizzle-orm';
|
|
7
|
+
import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
|
|
8
|
+
import { invalidateScopeCache } from './scopeCache.js';
|
|
9
|
+
import getDependentTree from './getDependentTree.js';
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
Uninstall an extension.
|
|
13
|
+
|
|
14
|
+
`purgeData` false (the default) deregisters the extension but leaves everything it created in
|
|
15
|
+
place — its tables and their rows, group memberships, permissions, groups and settings — so
|
|
16
|
+
reinstalling restores the site exactly as it was. This is the right mode for reinstalling to pick
|
|
17
|
+
up a fix.
|
|
18
|
+
|
|
19
|
+
`purgeData` true additionally destroys all of that. For an extension like kempo-blog that means
|
|
20
|
+
every post and comment, so it must always be a deliberate choice rather than a default.
|
|
21
|
+
|
|
22
|
+
Hooks and the extension row are removed either way. A registered hook whose module is no longer
|
|
23
|
+
installed cannot be tolerated: `middleware:before_page` is triggered with `bail: true`, so a
|
|
24
|
+
handler that fails to import now aborts the render and every page 404s.
|
|
25
|
+
*/
|
|
26
|
+
const uninstallOne = async ({ name, purgeData = false }) => {
|
|
27
|
+
if(!name){
|
|
28
|
+
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let kempoConfig;
|
|
32
|
+
try {
|
|
33
|
+
const [existing] = await db.select().from(extension).where(eq(extension.name, name)).limit(1);
|
|
34
|
+
if(!existing){
|
|
35
|
+
return [{ code: 404, msg: 'Extension not found' }, null];
|
|
36
|
+
}
|
|
37
|
+
kempoConfig = existing.kempo || {};
|
|
38
|
+
} catch(error) {
|
|
39
|
+
return [{ code: 500, msg: 'Failed to find extension' }, null];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const extRoot = join(process.cwd(), 'node_modules', name);
|
|
44
|
+
const uninstallModule = await import(pathToFileURL(join(extRoot, 'uninstall.js')).href + `?t=${Date.now()}`).catch(() => null);
|
|
45
|
+
if(uninstallModule?.default){
|
|
46
|
+
await uninstallModule.default();
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
Drop declarative schema tables. Only when purging — these hold the extension's actual content.
|
|
52
|
+
*/
|
|
53
|
+
if(purgeData && kempoConfig.schema){
|
|
54
|
+
try {
|
|
55
|
+
const extRoot = join(process.cwd(), 'node_modules', name);
|
|
56
|
+
const schemaModule = await import(pathToFileURL(join(extRoot, kempoConfig.schema)).href);
|
|
57
|
+
const { getTableConfig } = await import('drizzle-orm/pg-core');
|
|
58
|
+
for(const exported of Object.values(schemaModule)){
|
|
59
|
+
if(typeof exported !== 'object' || !exported?.[Symbol.for('drizzle:Name')]) continue;
|
|
60
|
+
const { name: tableName } = getTableConfig(exported);
|
|
61
|
+
await db.execute(sql.raw(`DROP TABLE IF EXISTS "${tableName}" CASCADE`));
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
Remove declarative groups, permissions and settings. Only when purging: these are what a
|
|
68
|
+
reinstall would otherwise restore, and group memberships in particular represent real user
|
|
69
|
+
state — every registered user of kempo-blog belongs to kempo-blog:member.
|
|
70
|
+
|
|
71
|
+
userGroup and groupPermission both reference group.name, and groupPermission also references
|
|
72
|
+
permission.name, none of them cascading. The membership and grant rows therefore have to go
|
|
73
|
+
first or these deletes fail the foreign key — which they used to do silently, leaving every
|
|
74
|
+
uninstalled extension's groups and permissions behind forever.
|
|
75
|
+
*/
|
|
76
|
+
if(purgeData){
|
|
77
|
+
const groupNames = (kempoConfig.groups || []).map(g => g.name);
|
|
78
|
+
const permNames = (kempoConfig.permissions || []).map(p => p.name);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if(groupNames.length){
|
|
82
|
+
await db.delete(userGroup).where(inArray(userGroup.groupName, groupNames));
|
|
83
|
+
await db.delete(groupPermission).where(inArray(groupPermission.groupName, groupNames));
|
|
84
|
+
}
|
|
85
|
+
if(permNames.length){
|
|
86
|
+
await db.delete(groupPermission).where(inArray(groupPermission.permissionName, permNames));
|
|
87
|
+
}
|
|
88
|
+
if(groupNames.length){
|
|
89
|
+
await db.delete(group).where(inArray(group.name, groupNames));
|
|
90
|
+
}
|
|
91
|
+
if(permNames.length){
|
|
92
|
+
await db.delete(permission).where(inArray(permission.name, permNames));
|
|
93
|
+
}
|
|
94
|
+
} catch(error) {
|
|
95
|
+
return [{ code: 500, msg: `Failed to remove extension groups and permissions: ${error.message}` }, null];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
Ownership is encoded in the setting name as "extname:settingname" — the setting table has no
|
|
100
|
+
owner column, so these have to be matched by prefix.
|
|
101
|
+
*/
|
|
102
|
+
await db.delete(setting).where(like(setting.name, `${name}:%`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await db.delete(hook).where(eq(hook.owner, name));
|
|
107
|
+
await db.delete(dashboardWidget).where(eq(dashboardWidget.owner, name));
|
|
108
|
+
await db.delete(extension).where(eq(extension.name, name));
|
|
109
|
+
} catch(error) {
|
|
110
|
+
return [{ code: 500, msg: 'Failed to remove extension' }, null];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clearHandlerCache();
|
|
114
|
+
invalidateScopeCache();
|
|
115
|
+
await triggerHook('extension:uninstalled', { name });
|
|
116
|
+
|
|
117
|
+
return [null, { success: true }];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/*
|
|
121
|
+
Uninstalls an extension, and what can no longer exist without it.
|
|
122
|
+
|
|
123
|
+
Every extension installed that requires this one, directly or through another, would be left
|
|
124
|
+
installed without its requirement, so without `cascade` this refuses and returns that whole tree as
|
|
125
|
+
`dependents` (disabled ones included: installed-but-off is still installed) for the caller to show.
|
|
126
|
+
With `cascade` they are uninstalled first, deepest first, then this one, and `purgeData` applies to
|
|
127
|
+
all of them.
|
|
128
|
+
|
|
129
|
+
Each is a complete uninstall of its own, so a failure part way leaves the ones before it
|
|
130
|
+
uninstalled and says which (`uninstalled`) rather than pretending nothing happened.
|
|
131
|
+
*/
|
|
132
|
+
export default async ({ name, purgeData = false, cascade = false }) => {
|
|
133
|
+
if(!name){
|
|
134
|
+
return [{ code: 400, msg: 'Extension name is required' }, null];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let existing;
|
|
138
|
+
try {
|
|
139
|
+
[existing] = await db.select({ name: extension.name }).from(extension).where(eq(extension.name, name)).limit(1);
|
|
140
|
+
} catch(error) {
|
|
141
|
+
return [{ code: 500, msg: 'Failed to find extension' }, null];
|
|
142
|
+
}
|
|
143
|
+
if(!existing){
|
|
144
|
+
return [{ code: 404, msg: 'Extension not found' }, null];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dependents = await getDependentTree(name, { includeDisabled: true });
|
|
148
|
+
|
|
149
|
+
if(dependents.length && !cascade){
|
|
150
|
+
return [{
|
|
151
|
+
code: 409,
|
|
152
|
+
msg: `${dependents.map(dependent => dependent.name).join(', ')} depends on this extension and is still installed`,
|
|
153
|
+
dependents,
|
|
154
|
+
}, null];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const uninstalled = [];
|
|
158
|
+
for(const target of [...dependents.map(dependent => dependent.name), name]){
|
|
159
|
+
const [error] = await uninstallOne({ name: target, purgeData });
|
|
160
|
+
if(error) return [{ ...error, uninstalled }, null];
|
|
161
|
+
uninstalled.push(target);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return [null, { success: true, uninstalled }];
|
|
165
|
+
};
|
|
@@ -2,6 +2,7 @@ import db from '../../db/index.js';
|
|
|
2
2
|
import { hook } from '../../db/schema.js';
|
|
3
3
|
import { eq, asc } from 'drizzle-orm';
|
|
4
4
|
import { join } from 'path';
|
|
5
|
+
import { getDisabledExtensionNames } from '../extensions/scopeCache.js';
|
|
5
6
|
import { pathToFileURL } from 'url';
|
|
6
7
|
|
|
7
8
|
const handlerCache = new Map();
|
|
@@ -46,8 +47,15 @@ export default async (event, data = {}, { bail = false } = {}) => {
|
|
|
46
47
|
.where(eq(hook.event, event))
|
|
47
48
|
.orderBy(asc(hook.createdAt), asc(hook.id));
|
|
48
49
|
const results = [];
|
|
50
|
+
const disabled = await getDisabledExtensionNames();
|
|
49
51
|
|
|
50
52
|
for(const h of hooks){
|
|
53
|
+
/*
|
|
54
|
+
A disabled extension is not used by the CMS in any way, and its hooks are part of that: they stay
|
|
55
|
+
registered (so enabling it again needs no re-registration) but never run.
|
|
56
|
+
*/
|
|
57
|
+
if(disabled.has(h.owner)) continue;
|
|
58
|
+
|
|
51
59
|
const handlerPath = resolveHandler(h.owner, h.callback);
|
|
52
60
|
if(!handlerPath) continue;
|
|
53
61
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[{"name":"kempo-blog","description":"A blog extension for Kempo CMS","author":"Dustin Poissant"}]
|