@commonpub/layer 0.10.0 → 0.11.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.
@@ -9,8 +9,11 @@ export default defineEventHandler(async (event) => {
9
9
  const db = useDB();
10
10
  const { id } = parseParams(event, { id: 'uuid' });
11
11
 
12
- const deleted = await deleteContentCategory(db, id);
13
- if (!deleted) {
12
+ const result = await deleteContentCategory(db, id);
13
+ if (!result.deleted) {
14
+ if (result.error === 'system_category') {
15
+ throw createError({ statusCode: 403, statusMessage: 'System categories cannot be deleted' });
16
+ }
14
17
  throw createError({ statusCode: 404, statusMessage: 'Category not found' });
15
18
  }
16
19
  return { success: true };
@@ -0,0 +1,32 @@
1
+ import { getInstanceSetting } from '@commonpub/server';
2
+ import type { FeatureFlags } from '@commonpub/config';
3
+
4
+ /**
5
+ * GET /api/admin/features
6
+ * Returns current feature flags with metadata about defaults vs overrides.
7
+ */
8
+ export default defineEventHandler(async (event) => {
9
+ requireAdmin(event);
10
+
11
+ const db = useDB();
12
+ const config = useConfig();
13
+
14
+ // Get DB overrides (may be null if never set)
15
+ const raw = await getInstanceSetting(db, 'features.overrides');
16
+ const overrides: Partial<FeatureFlags> = (raw && typeof raw === 'object' && !Array.isArray(raw))
17
+ ? raw as Partial<FeatureFlags>
18
+ : {};
19
+
20
+ // Build response with default + effective values for each flag
21
+ const flags = config.features as unknown as Record<string, boolean>;
22
+ const result: Record<string, { enabled: boolean; isOverridden: boolean }> = {};
23
+
24
+ for (const [key, value] of Object.entries(flags)) {
25
+ result[key] = {
26
+ enabled: value,
27
+ isOverridden: key in overrides,
28
+ };
29
+ }
30
+
31
+ return { flags: result, overrides };
32
+ });
@@ -0,0 +1,56 @@
1
+ import { setInstanceSetting, getInstanceSetting } from '@commonpub/server';
2
+ import type { FeatureFlags } from '@commonpub/config';
3
+ import { z } from 'zod';
4
+
5
+ const updateFeaturesSchema = z.object({
6
+ overrides: z.record(z.string(), z.boolean()).refine(
7
+ (obj) => Object.keys(obj).length <= 20,
8
+ 'Too many overrides',
9
+ ),
10
+ });
11
+
12
+ /**
13
+ * PUT /api/admin/features
14
+ * Set feature flag overrides. Pass { overrides: { flagName: true/false } }.
15
+ * To remove an override, omit the key from overrides.
16
+ */
17
+ export default defineEventHandler(async (event) => {
18
+ const user = requireAdmin(event);
19
+
20
+ const body = await parseBody(event, updateFeaturesSchema);
21
+ const db = useDB();
22
+
23
+ // Validate that all keys are known feature flags
24
+ const config = useConfig();
25
+ const knownFlags = Object.keys(config.features);
26
+ for (const key of Object.keys(body.overrides)) {
27
+ if (!knownFlags.includes(key)) {
28
+ throw createError({ statusCode: 400, statusMessage: `Unknown feature flag: ${key}` });
29
+ }
30
+ }
31
+
32
+ // Merge with existing overrides (so partial updates work)
33
+ const raw = await getInstanceSetting(db, 'features.overrides');
34
+ const existing: Partial<FeatureFlags> = (raw && typeof raw === 'object' && !Array.isArray(raw))
35
+ ? raw as Partial<FeatureFlags>
36
+ : {};
37
+
38
+ const merged = { ...existing, ...body.overrides };
39
+
40
+ // Remove overrides that match the base config default (no point overriding to same value)
41
+ const base = config.features as unknown as Record<string, boolean>;
42
+ for (const [key, value] of Object.entries(merged)) {
43
+ if (base[key] === value) {
44
+ delete (merged as Record<string, unknown>)[key];
45
+ }
46
+ }
47
+
48
+ await setInstanceSetting(db, 'features.overrides', merged, user.id, getRequestIP(event) ?? undefined);
49
+
50
+ // Invalidate config cache so the change takes effect immediately
51
+ if (typeof invalidateConfigCache === 'function') {
52
+ invalidateConfigCache();
53
+ }
54
+
55
+ return { overrides: merged, message: 'Feature flags updated' };
56
+ });
@@ -0,0 +1,11 @@
1
+ import { getHomepageSections } from '@commonpub/server';
2
+
3
+ /**
4
+ * GET /api/admin/homepage/sections
5
+ * Returns homepage sections for admin editing.
6
+ */
7
+ export default defineEventHandler(async (event) => {
8
+ requireAdmin(event);
9
+ const db = useDB();
10
+ return getHomepageSections(db);
11
+ });
@@ -0,0 +1,52 @@
1
+ import { setHomepageSections } from '@commonpub/server';
2
+ import { z } from 'zod';
3
+
4
+ const sectionConfigSchema = z.object({
5
+ contentType: z.string().max(64).optional(),
6
+ sort: z.enum(['popular', 'recent', 'featured', 'editorial']).optional(),
7
+ limit: z.number().int().min(1).max(50).optional(),
8
+ columns: z.union([z.literal(2), z.literal(3), z.literal(4)]).optional(),
9
+ showEditorial: z.boolean().optional(),
10
+ categorySlug: z.string().max(64).optional(),
11
+ featureGate: z.string().max(64).optional(),
12
+ variant: z.string().max(64).optional(),
13
+ customTitle: z.string().max(255).optional(),
14
+ customSubtitle: z.string().max(500).optional(),
15
+ html: z.string().max(10000).optional(),
16
+ });
17
+
18
+ const sectionSchema = z.object({
19
+ id: z.string().min(1).max(64),
20
+ type: z.enum(['hero', 'editorial', 'content-grid', 'content-carousel', 'contests', 'hubs', 'learning', 'stats', 'custom-html']),
21
+ title: z.string().max(255).optional(),
22
+ enabled: z.boolean(),
23
+ order: z.number().int().min(0),
24
+ config: sectionConfigSchema,
25
+ });
26
+
27
+ const updateSectionsSchema = z.object({
28
+ sections: z.array(sectionSchema).min(1).max(20),
29
+ });
30
+
31
+ /**
32
+ * PUT /api/admin/homepage/sections
33
+ * Save homepage section configuration.
34
+ */
35
+ export default defineEventHandler(async (event) => {
36
+ const user = requireAdmin(event);
37
+ const db = useDB();
38
+ const body = await parseBody(event, updateSectionsSchema);
39
+
40
+ // Validate unique IDs
41
+ const ids = new Set<string>();
42
+ for (const section of body.sections) {
43
+ if (ids.has(section.id)) {
44
+ throw createError({ statusCode: 400, statusMessage: `Duplicate section ID: ${section.id}` });
45
+ }
46
+ ids.add(section.id);
47
+ }
48
+
49
+ await setHomepageSections(db, body.sections, user.id, getRequestIP(event) ?? undefined);
50
+
51
+ return { sections: body.sections, message: 'Homepage updated' };
52
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GET /api/features
3
+ * Returns the current merged feature flags (build-time + env + DB overrides).
4
+ * Public endpoint — used by client-side useFeatures() for runtime reactivity.
5
+ */
6
+ export default defineEventHandler(() => {
7
+ const config = useConfig();
8
+ return config.features;
9
+ });
@@ -0,0 +1,10 @@
1
+ import { getHomepageSections } from '@commonpub/server';
2
+
3
+ /**
4
+ * GET /api/homepage/sections
5
+ * Returns the homepage section configuration (public).
6
+ */
7
+ export default defineEventHandler(async () => {
8
+ const db = useDB();
9
+ return getHomepageSections(db);
10
+ });