kempo 4.2.31 → 4.2.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo",
3
- "version": "4.2.31",
3
+ "version": "4.2.33",
4
4
  "type": "module",
5
5
  "description": "A fullstack CMS framework for creating websites with user authentication",
6
6
  "scripts": {
@@ -0,0 +1,26 @@
1
+ import db from '../../db/index.js';
2
+ import { extension } from '../../db/schema.js';
3
+ import { eq } from 'drizzle-orm';
4
+
5
+ /*
6
+ An extension declares the other extensions it requires via kempo-config.json's `dependencies`
7
+ array (a list of extension names). Enforced at install/enable time (a dependency must already be
8
+ enabled) and in reverse at disable/uninstall time (nothing enabled may be left depending on an
9
+ extension that is going away).
10
+ */
11
+
12
+ export const getUnmetDependencies = async (dependencies = []) => {
13
+ if(!dependencies.length) return [];
14
+ const rows = await db.select({ name: extension.name, enabled: extension.enabled }).from(extension);
15
+ const enabled = new Set(rows.filter(row => row.enabled).map(row => row.name));
16
+ return dependencies.filter(dep => !enabled.has(dep));
17
+ };
18
+
19
+ export const getEnabledDependents = async (name) => {
20
+ const rows = await db.select({ name: extension.name, kempo: extension.kempo })
21
+ .from(extension)
22
+ .where(eq(extension.enabled, true));
23
+ return rows
24
+ .filter(row => row.name !== name && (row.kempo?.dependencies || []).includes(name))
25
+ .map(row => row.name);
26
+ };
@@ -2,12 +2,18 @@ import db from '../../db/index.js';
2
2
  import { extension } from '../../db/schema.js';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { invalidateScopeCache } from './scopeCache.js';
5
+ import { getEnabledDependents } from './dependencies.js';
5
6
 
6
7
  export default async ({ name }) => {
7
8
  if(!name){
8
9
  return [{ code: 400, msg: 'Extension name is required' }, null];
9
10
  }
10
11
 
12
+ const dependents = await getEnabledDependents(name);
13
+ if(dependents.length){
14
+ return [{ code: 409, msg: `${dependents.join(', ')} depends on this extension and is still enabled` }, null];
15
+ }
16
+
11
17
  try {
12
18
  const updated = await db.update(extension)
13
19
  .set({ enabled: false, updatedAt: new Date() })
@@ -2,6 +2,7 @@ import db from '../../db/index.js';
2
2
  import { extension } from '../../db/schema.js';
3
3
  import { eq } from 'drizzle-orm';
4
4
  import { invalidateScopeCache } from './scopeCache.js';
5
+ import { getUnmetDependencies } from './dependencies.js';
5
6
 
6
7
  export default async ({ name }) => {
7
8
  if(!name){
@@ -9,17 +10,23 @@ export default async ({ name }) => {
9
10
  }
10
11
 
11
12
  try {
12
- const updated = await db.update(extension)
13
+ const [existing] = await db.select().from(extension).where(eq(extension.name, name)).limit(1);
14
+ if(!existing){
15
+ return [{ code: 404, msg: 'Extension not found' }, null];
16
+ }
17
+
18
+ const unmet = await getUnmetDependencies(existing.kempo?.dependencies || []);
19
+ if(unmet.length){
20
+ return [{ code: 409, msg: `Requires ${unmet.join(', ')} to be enabled first` }, null];
21
+ }
22
+
23
+ const [updated] = await db.update(extension)
13
24
  .set({ enabled: true, updatedAt: new Date() })
14
25
  .where(eq(extension.name, name))
15
26
  .returning();
16
27
 
17
- if(!updated.length){
18
- return [{ code: 404, msg: 'Extension not found' }, null];
19
- }
20
-
21
28
  invalidateScopeCache();
22
- return [null, updated[0]];
29
+ return [null, updated];
23
30
  } catch(error) {
24
31
  return [{ code: 500, msg: 'Failed to enable extension' }, null];
25
32
  }
@@ -11,6 +11,7 @@ import createHook from '../hooks/createHook.js';
11
11
  import setSetting from '../settings/setSetting.js';
12
12
  import createTablesFromSchema from './createTablesFromSchema.js';
13
13
  import { invalidateScopeCache } from './scopeCache.js';
14
+ import { getUnmetDependencies } from './dependencies.js';
14
15
 
15
16
  export default async ({ name }) => {
16
17
  if(!name){
@@ -44,6 +45,11 @@ export default async ({ name }) => {
44
45
  return [{ code: 500, msg: `Failed to read extension package files` }, null];
45
46
  }
46
47
 
48
+ const unmet = await getUnmetDependencies(kempoConfig.dependencies || []);
49
+ if(unmet.length){
50
+ return [{ code: 409, msg: `Requires ${unmet.join(', ')} to be installed and enabled first` }, null];
51
+ }
52
+
47
53
  /*
48
54
  Create tables from declarative schema
49
55
  */
@@ -6,6 +6,7 @@ import { pathToFileURL } from 'url';
6
6
  import { sql } from 'drizzle-orm';
7
7
  import triggerHook, { clearHandlerCache } from '../hooks/triggerHook.js';
8
8
  import { invalidateScopeCache } from './scopeCache.js';
9
+ import { getEnabledDependents } from './dependencies.js';
9
10
 
10
11
  /*
11
12
  Uninstall an extension.
@@ -38,6 +39,11 @@ export default async ({ name, purgeData = false }) => {
38
39
  return [{ code: 500, msg: 'Failed to find extension' }, null];
39
40
  }
40
41
 
42
+ const dependents = await getEnabledDependents(name);
43
+ if(dependents.length){
44
+ return [{ code: 409, msg: `${dependents.join(', ')} depends on this extension and is still enabled` }, null];
45
+ }
46
+
41
47
  try {
42
48
  const extRoot = join(process.cwd(), 'node_modules', name);
43
49
  const uninstallModule = await import(pathToFileURL(join(extRoot, 'uninstall.js')).href + `?t=${Date.now()}`).catch(() => null);
@@ -11,7 +11,7 @@ const slugify = name => name
11
11
  .replace(/-+/g, '-')
12
12
  .replace(/^-|-$/g, '');
13
13
 
14
- export default async ({ rootDir, directory, name, author, locked = false }) => {
14
+ export default async ({ rootDir, directory, name, author, owner = 'custom', locked = false }) => {
15
15
  if(!rootDir){
16
16
  return [{ code: 400, msg: 'Root directory is required' }, null];
17
17
  }
@@ -38,7 +38,7 @@ export default async ({ rootDir, directory, name, author, locked = false }) => {
38
38
  const now = new Date().toISOString();
39
39
  const frontmatter = [
40
40
  '<!--',
41
- ` owner: custom`,
41
+ ` owner: ${owner}`,
42
42
  ` name: ${name}`,
43
43
  ` author: ${author || ''}`,
44
44
  ` created: ${now}`,
@@ -20,7 +20,7 @@ const serializeEntries = entries => entries.map(e => {
20
20
  return `<content ${attrs}>\n${e.markup}\n</content>`;
21
21
  }).join('\n');
22
22
 
23
- export default async ({ rootDir, name, location, priority, author }) => {
23
+ export default async ({ rootDir, name, location, priority, author, owner = 'custom' }) => {
24
24
  if(!rootDir){
25
25
  return [{ code: 400, msg: 'Root directory is required' }, null];
26
26
  }
@@ -42,7 +42,7 @@ export default async ({ rootDir, name, location, priority, author }) => {
42
42
  const entry = {
43
43
  id: crypto.randomUUID(),
44
44
  name,
45
- owner: 'custom',
45
+ owner,
46
46
  author: author || '',
47
47
  locked: false,
48
48
  location,
@@ -13,7 +13,7 @@ const slugify = name => name
13
13
  .replace(/-+/g, '-')
14
14
  .replace(/^-|-$/g, '');
15
15
 
16
- export default async ({ rootDir, directory, name, template, author, locked = false, extraMetadata = {} }) => {
16
+ export default async ({ rootDir, directory, name, template, author, owner = 'custom', locked = false, extraMetadata = {} }) => {
17
17
  if(!rootDir){
18
18
  return [{ code: 400, msg: 'Root directory is required' }, null];
19
19
  }
@@ -46,7 +46,7 @@ export default async ({ rootDir, directory, name, template, author, locked = fal
46
46
 
47
47
  const now = new Date().toISOString();
48
48
  const defaultLines = [
49
- ` owner: custom`,
49
+ ` owner: ${owner}`,
50
50
  ` name: ${name}`,
51
51
  ` author: ${author || ''}`,
52
52
  ` created: ${now}`,
@@ -11,7 +11,7 @@ const slugify = name => name
11
11
  .replace(/-+/g, '-')
12
12
  .replace(/^-|-$/g, '');
13
13
 
14
- export default async ({ rootDir, directory, name, author, copyFrom, locked = false }) => {
14
+ export default async ({ rootDir, directory, name, author, owner = 'custom', copyFrom, locked = false }) => {
15
15
  if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
16
16
  if(!name) return [{ code: 400, msg: 'Template name is required' }, null];
17
17
 
@@ -30,7 +30,7 @@ export default async ({ rootDir, directory, name, author, copyFrom, locked = fal
30
30
  const now = new Date().toISOString();
31
31
  const frontmatter = [
32
32
  '<!--',
33
- ` owner: custom`,
33
+ ` owner: ${owner}`,
34
34
  ` author: ${author || ''}`,
35
35
  ` created: ${now}`,
36
36
  ` updated: ${now}`,