includio-cms 0.5.3 → 0.5.7

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/ROADMAP.md +33 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +48 -48
  18. package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
  19. package/dist/admin/client/admin/admin-layout.svelte +0 -2
  20. package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
  22. package/dist/admin/client/collection/collection-entries.svelte +334 -166
  23. package/dist/admin/client/collection/collection-page.svelte +6 -3
  24. package/dist/admin/client/collection/data-table.svelte +127 -18
  25. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  26. package/dist/admin/client/collection/row-actions.svelte +3 -1
  27. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
  28. package/dist/admin/client/entry/entry-page.svelte +5 -4
  29. package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
  30. package/dist/admin/client/form/form-page.svelte +5 -5
  31. package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
  32. package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
  33. package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
  34. package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
  35. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
  36. package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
  37. package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
  38. package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
  39. package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
  40. package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
  41. package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
  42. package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
  43. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
  44. package/dist/admin/components/fields/image-field.svelte +21 -20
  45. package/dist/admin/components/fields/media-field.svelte +45 -44
  46. package/dist/admin/components/fields/relation-field.svelte +428 -149
  47. package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
  48. package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
  49. package/dist/admin/components/fields/seo-field.svelte +66 -10
  50. package/dist/admin/components/icons/icon-map.d.ts +4 -0
  51. package/dist/admin/components/icons/icon-map.js +143 -0
  52. package/dist/admin/components/layout/header-actions.svelte +0 -10
  53. package/dist/admin/components/layout/nav-collections.svelte +55 -57
  54. package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
  55. package/dist/admin/components/layout/nav-forms.svelte +49 -49
  56. package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
  57. package/dist/admin/components/layout/nav-search.svelte +15 -22
  58. package/dist/admin/components/layout/nav-singletons.svelte +35 -38
  59. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
  60. package/dist/admin/components/media/file/file-preview.svelte +92 -91
  61. package/dist/admin/components/media/file-preview.svelte +17 -18
  62. package/dist/admin/components/media/media-library.svelte +39 -41
  63. package/dist/admin/components/media/media-selector.svelte +8 -8
  64. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
  65. package/dist/admin/components/tiptap/content-editor.svelte +2 -2
  66. package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
  67. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  68. package/dist/admin/components/tiptap/inline-block-node.js +12 -1
  69. package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
  70. package/dist/admin/remote/entry.remote.d.ts +20 -1
  71. package/dist/admin/remote/entry.remote.js +24 -3
  72. package/dist/admin/remote/invite.d.ts +2 -2
  73. package/dist/cli/scaffold/admin.js +8 -0
  74. package/dist/cms/runtime/remote.d.ts +8 -0
  75. package/dist/cms/runtime/remote.js +7 -0
  76. package/dist/cms/runtime/types.d.ts +1 -1
  77. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  78. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  79. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  80. package/dist/components/ui/sonner/sonner.svelte +1 -2
  81. package/dist/core/cms.d.ts +2 -1
  82. package/dist/core/cms.js +2 -0
  83. package/dist/core/server/entries/operations/get.d.ts +11 -0
  84. package/dist/core/server/entries/operations/get.js +63 -1
  85. package/dist/core/server/entries/operations/update.d.ts +1 -0
  86. package/dist/core/server/entries/operations/update.js +5 -1
  87. package/dist/core/server/fields/resolveUrlFields.js +49 -18
  88. package/dist/db-postgres/schema/entry.d.ts +17 -0
  89. package/dist/db-postgres/schema/entry.js +4 -2
  90. package/dist/server/auth.d.ts +4 -4
  91. package/dist/sveltekit/components/structured-content.svelte +18 -5
  92. package/dist/sveltekit/server/handle.js +1 -0
  93. package/dist/types/cms.d.ts +7 -0
  94. package/dist/types/collections.d.ts +2 -0
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/entries.d.ts +7 -1
  97. package/dist/types/forms.d.ts +2 -0
  98. package/dist/types/index.d.ts +2 -1
  99. package/dist/types/index.js +1 -0
  100. package/dist/updates/0.5.4/index.d.ts +2 -0
  101. package/dist/updates/0.5.4/index.js +15 -0
  102. package/dist/updates/0.5.5/index.d.ts +2 -0
  103. package/dist/updates/0.5.5/index.js +20 -0
  104. package/dist/updates/0.5.6/index.d.ts +2 -0
  105. package/dist/updates/0.5.6/index.js +17 -0
  106. package/dist/updates/0.5.7/index.d.ts +2 -0
  107. package/dist/updates/0.5.7/index.js +14 -0
  108. package/dist/updates/index.js +5 -1
  109. package/package.json +6 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,77 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.5.7 — 2026-03-06
7
+
8
+ SEO field with slug toggle, structured-content & URL field fixes
9
+
10
+ ### Added
11
+ - SEO field: auto/manual slug toggle with Switch component
12
+
13
+ ### Fixed
14
+ - Structured content: skip empty paragraphs, wrap inline blocks in `<div>`
15
+ - `resolveUrlFields`: normalize flat string url/text values, use raw DB calls for performance
16
+ - Admin UI: migrate `{#await}` blocks to reactive query pattern for consistent loading states
17
+
18
+ ## 0.5.6 — 2026-03-05
19
+
20
+ Relation field UX overhaul, collection list improvements, icon map refactor
21
+
22
+ ### Added
23
+ - Relation field: dialog picker with search, reorder, multi-select UX
24
+ - `getEntryLabels` — lightweight labels endpoint for relation field options
25
+ - Collection list: relation labels displayed in columns, delete restricted to archived entries
26
+ - Icon map: string-based sidebar icons replace component references
27
+
28
+ ### Fixed
29
+ - TipTap: auto-expand new inline blocks, fix editor overflow, toolbar background
30
+ - Runtime types: FormEntryMap rename, add remote.ts typings
31
+
32
+ ### Notes
33
+
34
+ Removed `mode-watcher` dependency — light theme is now hardcoded.
35
+
36
+ ## 0.5.5 — 2026-03-04
37
+
38
+ REST API with API key auth, orderable collection fixes
39
+
40
+ ### Added
41
+ - REST API with API key authentication — CRUD for collections, singletons, entries, schema introspection, language listing
42
+ - API key configuration in CMS config (`apiKeys` array)
43
+ - getEntries supports `orderBy` parameter (column + direction)
44
+ - Admin scaffold generates REST API catch-all route
45
+
46
+ ### Fixed
47
+ - Orderable collections: DnD rows use proper `<tr>` element instead of `<div>` wrapper
48
+ - Reorder buttons always visible (not sr-only) for better discoverability
49
+ - Selection enabled for orderable collections
50
+ - Relation field respects orderable collection sort order
51
+ - Nav active state: exact match prevents false highlights on similarly-named routes
52
+
53
+ ### Notes
54
+
55
+ Add `apiKeys: [{ key: "your-secret", name: "My App" }]` to CMS config. REST API available at `/admin/api/rest/`.
56
+
57
+ ## 0.5.4 — 2026-03-04
58
+
59
+ Collection ordering (DnD), custom list columns
60
+
61
+ ### Added
62
+ - Orderable collections: DnD reordering of entries in admin list with keyboard alternative (WCAG 2.1)
63
+ - Custom list columns: display arbitrary entry fields as columns in collection list
64
+ - sortOrder column on entries table for persistent manual ordering
65
+ - reorderEntriesCommand remote for bulk sort order updates
66
+
67
+ ### Migration
68
+
69
+ ```sql
70
+ ALTER TABLE entry ADD COLUMN sort_order INTEGER;
71
+ ```
72
+
73
+ ### Notes
74
+
75
+ Add `orderable: true` to collections that need manual ordering. Add `listColumns: ["fieldSlug"]` to show fields in the entry list.
76
+
6
77
  ## 0.5.3 — 2026-03-04
7
78
 
8
79
  Form fields, inline blocks, hybrid preview, field components refactor
package/ROADMAP.md CHANGED
@@ -89,6 +89,39 @@
89
89
  - [x] `[chore]` `[P2]` Storybook stories for all field types
90
90
  - [x] `[chore]` `[P1]` Field components refactor: bindable value props
91
91
 
92
+ ## 0.5.4 — Collection ordering & list columns
93
+
94
+ - [x] `[feature]` `[P1]` Orderable collections: DnD reordering with keyboard alternative (WCAG 2.1) <!-- files: src/lib/admin/client/collection/data-table.svelte, src/lib/admin/client/collection/collection-entries.svelte -->
95
+ - [x] `[feature]` `[P1]` Custom list columns: display entry fields as columns in collection list <!-- files: src/lib/admin/client/collection/collection-entries.svelte -->
96
+ - [x] `[feature]` `[P1]` `sortOrder` column + `reorderEntriesCommand` remote <!-- files: src/lib/db-postgres/schema/entry.ts, src/lib/admin/remote/entry.remote.ts -->
97
+
98
+ ## 0.5.5 — REST API & orderable fixes
99
+
100
+ - [x] `[feature]` `[P1]` REST API with API key authentication — CRUD for collections, singletons, entries, schema, languages <!-- files: src/lib/admin/api/rest/ -->
101
+ - [x] `[feature]` `[P1]` API key configuration in CMS config (`apiKeys`) <!-- files: src/lib/types/cms.ts, src/lib/core/cms.ts -->
102
+ - [x] `[feature]` `[P2]` getEntries `orderBy` parameter support <!-- files: src/lib/types/entries.ts, src/lib/core/server/entries/operations/get.ts -->
103
+ - [x] `[fix]` `[P1]` Orderable collections: proper `<tr>` markup, visible reorder buttons, selection enabled <!-- files: src/lib/admin/client/collection/data-table.svelte -->
104
+ - [x] `[fix]` `[P1]` Relation field respects orderable collection sort order <!-- files: src/lib/admin/components/fields/relation-field.svelte -->
105
+ - [x] `[fix]` `[P1]` Nav active state exact match — prevents false highlights <!-- files: src/lib/admin/components/layout/nav-*.svelte -->
106
+ - [x] `[chore]` `[P2]` Admin scaffold generates REST API route <!-- files: src/lib/cli/scaffold/admin.ts -->
107
+
108
+ ## 0.5.6 — Relation field & collection improvements
109
+
110
+ - [x] `[feature]` `[P1]` Relation field: dialog picker, reorder, search, multi-select UX <!-- files: src/lib/admin/components/fields/relation-field.svelte -->
111
+ - [x] `[feature]` `[P1]` getEntryLabels — lightweight labels endpoint for relation fields <!-- files: src/lib/admin/remote/entry.remote.ts -->
112
+ - [x] `[feature]` `[P2]` Collection list: relation labels in columns, delete only for archived <!-- files: src/lib/admin/client/collection/collection-entries.svelte -->
113
+ - [x] `[feature]` `[P2]` Icon map — string-based sidebar icons replace component refs <!-- files: src/lib/admin/components/layout/nav-main.svelte -->
114
+ - [x] `[fix]` `[P1]` TipTap: auto-expand new inline blocks, editor overflow, toolbar bg <!-- files: src/lib/admin/components/fields/structured-content/ -->
115
+ - [x] `[chore]` `[P2]` Runtime types: FormEntryMap rename, add remote.ts
116
+ - [x] `[chore]` `[P2]` Remove mode-watcher, hardcode light theme
117
+
118
+ ## 0.5.7 — SEO field & content fixes
119
+
120
+ - [x] `[feature]` `[P1]` SEO field: auto/manual slug toggle with Switch <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
121
+ - [x] `[fix]` `[P1]` Structured content: skip empty paragraphs, wrap inline blocks in div <!-- files: src/lib/core/server/entries/operations/ -->
122
+ - [x] `[fix]` `[P1]` resolveUrlFields: normalize flat string url/text, use raw DB calls <!-- files: src/lib/core/server/entries/ -->
123
+ - [x] `[chore]` `[P1]` Migrate #await to reactive query pattern across admin UI
124
+
92
125
  ## 0.6.0 — Plugin system _(deferred from 0.2.0)_
93
126
 
94
127
  - [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
@@ -0,0 +1,7 @@
1
+ import { type RequestHandler } from '@sveltejs/kit';
2
+ export declare function createRestApiHandler(): {
3
+ GET: RequestHandler;
4
+ POST: RequestHandler;
5
+ PUT: RequestHandler;
6
+ DELETE: RequestHandler;
7
+ };
@@ -0,0 +1,116 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { authenticateApiKey } from './middleware/apiKey.js';
3
+ import * as schemaRoutes from './routes/schema.js';
4
+ import * as languagesRoutes from './routes/languages.js';
5
+ import * as collectionsRoutes from './routes/collections.js';
6
+ import * as singletonsRoutes from './routes/singletons.js';
7
+ import * as entriesRoutes from './routes/entries.js';
8
+ function matchRoute(method, path) {
9
+ // Schema routes
10
+ if (method === 'GET' && path === 'schema') {
11
+ return { handler: 'schema', params: [] };
12
+ }
13
+ if (method === 'GET' && path.startsWith('schema/')) {
14
+ return { handler: 'schema', params: [] };
15
+ }
16
+ // Languages
17
+ if (method === 'GET' && path === 'languages') {
18
+ return { handler: 'languages', params: [] };
19
+ }
20
+ // Entries lifecycle: POST /entries/:id/:action
21
+ const entriesMatch = path.match(/^entries\/([^/]+)\/(publish|unpublish|archive|unarchive)$/);
22
+ if (method === 'POST' && entriesMatch) {
23
+ return { handler: 'entries', params: [entriesMatch[1], entriesMatch[2]] };
24
+ }
25
+ // Collections: /collections/:slug/:id?
26
+ const collectionWithId = path.match(/^collections\/([^/]+)\/([^/]+)$/);
27
+ if (collectionWithId) {
28
+ return { handler: 'collections', params: [collectionWithId[1], collectionWithId[2]] };
29
+ }
30
+ const collectionOnly = path.match(/^collections\/([^/]+)$/);
31
+ if (collectionOnly) {
32
+ return { handler: 'collections', params: [collectionOnly[1]] };
33
+ }
34
+ // Singletons: /singletons/:slug
35
+ const singletonMatch = path.match(/^singletons\/([^/]+)$/);
36
+ if (singletonMatch) {
37
+ return { handler: 'singletons', params: [singletonMatch[1]] };
38
+ }
39
+ return null;
40
+ }
41
+ export function createRestApiHandler() {
42
+ function handle(method) {
43
+ return async (event) => {
44
+ // Authenticate
45
+ const apiKey = authenticateApiKey(event);
46
+ if (!apiKey) {
47
+ return json({ error: 'Unauthorized — invalid or missing API key' }, { status: 401 });
48
+ }
49
+ const restPath = event.params.restPath || '';
50
+ const route = matchRoute(method, restPath);
51
+ if (!route) {
52
+ return json({ error: 'Not found' }, { status: 404 });
53
+ }
54
+ try {
55
+ switch (route.handler) {
56
+ case 'schema': {
57
+ // Pass restPath minus 'schema/' prefix
58
+ const schemaPath = restPath === 'schema' ? '' : restPath.slice(7);
59
+ event.params.restPath = schemaPath;
60
+ return await schemaRoutes.GET(event);
61
+ }
62
+ case 'languages':
63
+ return await languagesRoutes.GET();
64
+ case 'collections': {
65
+ const [slug, id] = route.params;
66
+ switch (method) {
67
+ case 'GET':
68
+ return await collectionsRoutes.GET(event, slug, id);
69
+ case 'POST':
70
+ return await collectionsRoutes.POST(event, slug);
71
+ case 'PUT':
72
+ if (!id)
73
+ return json({ error: 'Entry ID required' }, { status: 400 });
74
+ return await collectionsRoutes.PUT(event, slug, id);
75
+ case 'DELETE':
76
+ if (!id)
77
+ return json({ error: 'Entry ID required' }, { status: 400 });
78
+ return await collectionsRoutes.DELETE(event, slug, id);
79
+ }
80
+ break;
81
+ }
82
+ case 'singletons': {
83
+ const [slug] = route.params;
84
+ switch (method) {
85
+ case 'GET':
86
+ return await singletonsRoutes.GET(event, slug);
87
+ case 'PUT':
88
+ return await singletonsRoutes.PUT(event, slug);
89
+ default:
90
+ return json({ error: 'Method not allowed' }, { status: 405 });
91
+ }
92
+ }
93
+ case 'entries': {
94
+ const [id, action] = route.params;
95
+ return await entriesRoutes.POST(event, id, action);
96
+ }
97
+ }
98
+ }
99
+ catch (err) {
100
+ const message = err instanceof Error ? err.message : 'Internal server error';
101
+ const status = message === 'Unauthorized' ? 401
102
+ : message === 'Forbidden' ? 403
103
+ : message.includes('not found') || message.includes('Not found') ? 404
104
+ : 500;
105
+ return json({ error: message }, { status });
106
+ }
107
+ return json({ error: 'Not found' }, { status: 404 });
108
+ };
109
+ }
110
+ return {
111
+ GET: handle('GET'),
112
+ POST: handle('POST'),
113
+ PUT: handle('PUT'),
114
+ DELETE: handle('DELETE')
115
+ };
116
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { ApiKeyConfig } from '../../../../types/cms.js';
3
+ export declare function extractApiKey(event: RequestEvent): string | null;
4
+ export declare function validateApiKey(key: string): ApiKeyConfig | null;
5
+ export declare function setSyntheticUser(event: RequestEvent, apiKey: ApiKeyConfig): void;
6
+ export declare function authenticateApiKey(event: RequestEvent): ApiKeyConfig | null;
@@ -0,0 +1,45 @@
1
+ import { getCMS } from '../../../../core/cms.js';
2
+ export function extractApiKey(event) {
3
+ const authHeader = event.request.headers.get('authorization');
4
+ if (authHeader?.startsWith('Bearer ')) {
5
+ return authHeader.slice(7);
6
+ }
7
+ return event.request.headers.get('x-api-key');
8
+ }
9
+ export function validateApiKey(key) {
10
+ const cms = getCMS();
11
+ return cms.apiKeys.find((k) => k.key === key) ?? null;
12
+ }
13
+ export function setSyntheticUser(event, apiKey) {
14
+ const name = apiKey.name || 'api';
15
+ event.locals.user = {
16
+ id: `api:${name}`,
17
+ name,
18
+ role: apiKey.role || 'admin',
19
+ email: `${name}@api.local`,
20
+ emailVerified: true,
21
+ createdAt: new Date(),
22
+ updatedAt: new Date(),
23
+ image: null
24
+ };
25
+ event.locals.session = {
26
+ id: 'api-session',
27
+ userId: `api:${name}`,
28
+ expiresAt: new Date(Date.now() + 86400000),
29
+ token: '',
30
+ createdAt: new Date(),
31
+ updatedAt: new Date(),
32
+ ipAddress: null,
33
+ userAgent: null
34
+ };
35
+ }
36
+ export function authenticateApiKey(event) {
37
+ const key = extractApiKey(event);
38
+ if (!key)
39
+ return null;
40
+ const apiKey = validateApiKey(key);
41
+ if (!apiKey)
42
+ return null;
43
+ setSyntheticUser(event, apiKey);
44
+ return apiKey;
45
+ }
@@ -0,0 +1,5 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function GET(event: RequestEvent, slug: string, id?: string): Promise<Response>;
3
+ export declare function POST(event: RequestEvent, slug: string): Promise<Response>;
4
+ export declare function PUT(event: RequestEvent, slug: string, id: string): Promise<Response>;
5
+ export declare function DELETE(_event: RequestEvent, slug: string, id: string): Promise<Response>;
@@ -0,0 +1,104 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../../core/cms.js';
3
+ import { getRawEntries, getRawEntry, countRawEntries } from '../../../../core/server/entries/operations/get.js';
4
+ import { createEntry } from '../../../../core/server/entries/operations/create.js';
5
+ import { upsertDraftVersion } from '../../../../core/server/entries/operations/update.js';
6
+ import { deleteEntry } from '../../../../core/server/entries/operations/delete.js';
7
+ export async function GET(event, slug, id) {
8
+ const cms = getCMS();
9
+ if (!cms.collections[slug]) {
10
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
11
+ }
12
+ // GET /collections/:slug/:id — single entry
13
+ if (id) {
14
+ const entry = await getRawEntry({ id, slug, includeArchived: true });
15
+ if (!entry) {
16
+ return json({ error: 'Entry not found' }, { status: 404 });
17
+ }
18
+ return json({
19
+ id: entry.id,
20
+ slug: entry.slug,
21
+ type: entry.type,
22
+ createdAt: entry.createdAt,
23
+ updatedAt: entry.updatedAt,
24
+ publishedAt: entry.publishedAt,
25
+ archivedAt: entry.archivedAt,
26
+ availableLocales: entry.availableLocales,
27
+ sortOrder: entry.sortOrder,
28
+ draftData: entry.draftVersion?.data ?? null,
29
+ publishedData: entry.publishedVersion?.data ?? null,
30
+ draftVersionId: entry.draftVersion?.id ?? null,
31
+ publishedVersionId: entry.publishedVersion?.id ?? null
32
+ });
33
+ }
34
+ // GET /collections/:slug — list
35
+ const url = event.url;
36
+ const status = url.searchParams.get('status') || undefined;
37
+ const limit = parseInt(url.searchParams.get('limit') || '50', 10);
38
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
39
+ const includeArchived = status === 'archived';
40
+ const onlyArchived = status === 'archived';
41
+ const [entries, total] = await Promise.all([
42
+ getRawEntries({ slug, limit, offset, includeArchived, onlyArchived }),
43
+ countRawEntries({ slug, includeArchived, onlyArchived })
44
+ ]);
45
+ const items = entries.map((entry) => ({
46
+ id: entry.id,
47
+ slug: entry.slug,
48
+ type: entry.type,
49
+ createdAt: entry.createdAt,
50
+ updatedAt: entry.updatedAt,
51
+ publishedAt: entry.publishedAt,
52
+ archivedAt: entry.archivedAt,
53
+ availableLocales: entry.availableLocales,
54
+ sortOrder: entry.sortOrder,
55
+ draftData: entry.draftVersion?.data ?? null,
56
+ publishedData: entry.publishedVersion?.data ?? null
57
+ }));
58
+ return json({ items, total, limit, offset });
59
+ }
60
+ export async function POST(event, slug) {
61
+ const cms = getCMS();
62
+ if (!cms.collections[slug]) {
63
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
64
+ }
65
+ const body = await event.request.json().catch(() => null);
66
+ const entry = await createEntry({ slug, type: 'collection' });
67
+ // If data provided, save as draft
68
+ if (body?.data && typeof body.data === 'object') {
69
+ await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
70
+ }
71
+ return json({ id: entry.id, slug: entry.slug }, { status: 201 });
72
+ }
73
+ export async function PUT(event, slug, id) {
74
+ const cms = getCMS();
75
+ if (!cms.collections[slug]) {
76
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
77
+ }
78
+ const entry = await getRawEntry({ id, slug });
79
+ if (!entry) {
80
+ return json({ error: 'Entry not found' }, { status: 404 });
81
+ }
82
+ const body = await event.request.json().catch(() => null);
83
+ if (!body?.data || typeof body.data !== 'object') {
84
+ return json({ error: 'Request body must contain "data" object' }, { status: 400 });
85
+ }
86
+ const version = await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
87
+ return json({
88
+ id: entry.id,
89
+ versionId: version.id,
90
+ versionNumber: version.versionNumber
91
+ });
92
+ }
93
+ export async function DELETE(_event, slug, id) {
94
+ const cms = getCMS();
95
+ if (!cms.collections[slug]) {
96
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
97
+ }
98
+ const entry = await getRawEntry({ id, slug, includeArchived: true });
99
+ if (!entry) {
100
+ return json({ error: 'Entry not found' }, { status: 404 });
101
+ }
102
+ await deleteEntry(entry.id);
103
+ return json({ success: true });
104
+ }
@@ -0,0 +1,2 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function POST(_event: RequestEvent, id: string, action: string): Promise<Response>;
@@ -0,0 +1,37 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { requireAuth } from '../../../remote/middleware/auth.js';
3
+ import { getRawEntryOrThrow } from '../../../../core/server/entries/operations/get.js';
4
+ import { updateEntry, unpublishEntry } from '../../../../core/server/entries/operations/update.js';
5
+ export async function POST(_event, id, action) {
6
+ const { user } = requireAuth();
7
+ switch (action) {
8
+ case 'publish': {
9
+ const entry = await getRawEntryOrThrow({ id, includeArchived: false });
10
+ const draftVersion = entry.draftVersion;
11
+ if (!draftVersion) {
12
+ return json({ error: 'No draft version to publish' }, { status: 400 });
13
+ }
14
+ await updateEntry(id, {
15
+ publishedVersionId: draftVersion.id,
16
+ publishedAt: new Date(),
17
+ publishedBy: user.id,
18
+ archivedAt: null
19
+ });
20
+ return json({ success: true, publishedVersionId: draftVersion.id });
21
+ }
22
+ case 'unpublish': {
23
+ await unpublishEntry(id);
24
+ return json({ success: true });
25
+ }
26
+ case 'archive': {
27
+ await updateEntry(id, { archivedAt: new Date() });
28
+ return json({ success: true });
29
+ }
30
+ case 'unarchive': {
31
+ await updateEntry(id, { archivedAt: null });
32
+ return json({ success: true });
33
+ }
34
+ default:
35
+ return json({ error: `Unknown action "${action}"` }, { status: 400 });
36
+ }
37
+ }
@@ -0,0 +1 @@
1
+ export declare function GET(): Promise<Response>;
@@ -0,0 +1,5 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../../core/cms.js';
3
+ export async function GET() {
4
+ return json({ languages: getCMS().languages });
5
+ }
@@ -0,0 +1,2 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function GET(event: RequestEvent): Promise<Response>;
@@ -0,0 +1,78 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../../core/cms.js';
3
+ import { getFieldsFromConfig } from '../../../../core/fields/layoutUtils.js';
4
+ function serializeField(field) {
5
+ const { ...rest } = field;
6
+ // Remove non-serializable properties
7
+ delete rest.icon;
8
+ return rest;
9
+ }
10
+ function serializeFields(fields) {
11
+ return fields.map(serializeField);
12
+ }
13
+ export async function GET(event) {
14
+ const cms = getCMS();
15
+ const path = event.params.restPath || '';
16
+ // GET /schema/collections/:slug
17
+ const collectionMatch = path.match(/^collections\/([^/]+)$/);
18
+ if (collectionMatch) {
19
+ const slug = collectionMatch[1];
20
+ const collection = cms.collections[slug];
21
+ if (!collection) {
22
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
23
+ }
24
+ return json({
25
+ slug: collection.slug,
26
+ type: collection.type,
27
+ labels: collection.labels,
28
+ fields: serializeFields(getFieldsFromConfig(collection)),
29
+ orderable: collection.orderable,
30
+ listColumns: collection.listColumns,
31
+ slugField: collection.slugField
32
+ });
33
+ }
34
+ // GET /schema/singletons/:slug
35
+ const singletonMatch = path.match(/^singletons\/([^/]+)$/);
36
+ if (singletonMatch) {
37
+ const slug = singletonMatch[1];
38
+ const single = cms.singles[slug];
39
+ if (!single) {
40
+ return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
41
+ }
42
+ return json({
43
+ slug: single.slug,
44
+ type: single.type,
45
+ label: single.label,
46
+ fields: serializeFields(getFieldsFromConfig(single))
47
+ });
48
+ }
49
+ // GET /schema — full schema overview
50
+ if (path === '') {
51
+ const collections = Object.values(cms.collections).map((c) => ({
52
+ slug: c.slug,
53
+ type: c.type,
54
+ labels: c.labels,
55
+ fields: serializeFields(getFieldsFromConfig(c)),
56
+ orderable: c.orderable,
57
+ listColumns: c.listColumns,
58
+ slugField: c.slugField
59
+ }));
60
+ const singletons = Object.values(cms.singles).map((s) => ({
61
+ slug: s.slug,
62
+ type: s.type,
63
+ label: s.label,
64
+ fields: serializeFields(getFieldsFromConfig(s))
65
+ }));
66
+ const forms = Object.values(cms.forms).map((f) => ({
67
+ slug: f.slug,
68
+ fields: f.fields
69
+ }));
70
+ return json({
71
+ collections,
72
+ singletons,
73
+ forms,
74
+ languages: cms.languages
75
+ });
76
+ }
77
+ return json({ error: 'Not found' }, { status: 404 });
78
+ }
@@ -0,0 +1,3 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function GET(_event: RequestEvent, slug: string): Promise<Response>;
3
+ export declare function PUT(event: RequestEvent, slug: string): Promise<Response>;
@@ -0,0 +1,60 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getCMS } from '../../../../core/cms.js';
3
+ import { getRawEntries } from '../../../../core/server/entries/operations/get.js';
4
+ import { createEntry } from '../../../../core/server/entries/operations/create.js';
5
+ import { upsertDraftVersion } from '../../../../core/server/entries/operations/update.js';
6
+ export async function GET(_event, slug) {
7
+ const cms = getCMS();
8
+ if (!cms.singles[slug]) {
9
+ return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
10
+ }
11
+ const entries = await getRawEntries({ slug });
12
+ const entry = entries[0];
13
+ if (!entry) {
14
+ return json({
15
+ slug,
16
+ type: 'single',
17
+ draftData: null,
18
+ publishedData: null
19
+ });
20
+ }
21
+ return json({
22
+ id: entry.id,
23
+ slug: entry.slug,
24
+ type: entry.type,
25
+ createdAt: entry.createdAt,
26
+ updatedAt: entry.updatedAt,
27
+ publishedAt: entry.publishedAt,
28
+ draftData: entry.draftVersion?.data ?? null,
29
+ publishedData: entry.publishedVersion?.data ?? null,
30
+ draftVersionId: entry.draftVersion?.id ?? null,
31
+ publishedVersionId: entry.publishedVersion?.id ?? null
32
+ });
33
+ }
34
+ export async function PUT(event, slug) {
35
+ const cms = getCMS();
36
+ if (!cms.singles[slug]) {
37
+ return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
38
+ }
39
+ const body = await event.request.json().catch(() => null);
40
+ if (!body?.data || typeof body.data !== 'object') {
41
+ return json({ error: 'Request body must contain "data" object' }, { status: 400 });
42
+ }
43
+ // Find or create singleton entry
44
+ const entries = await getRawEntries({ slug });
45
+ let entry = entries[0];
46
+ if (!entry) {
47
+ const dbEntry = await createEntry({ slug, type: 'single' });
48
+ const created = await getRawEntries({ slug });
49
+ entry = created[0];
50
+ if (!entry) {
51
+ return json({ error: 'Failed to create singleton entry' }, { status: 500 });
52
+ }
53
+ }
54
+ const version = await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
55
+ return json({
56
+ id: entry.id,
57
+ versionId: version.id,
58
+ versionNumber: version.versionNumber
59
+ });
60
+ }