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.
- package/CHANGELOG.md +71 -0
- package/ROADMAP.md +33 -0
- package/dist/admin/api/rest/handler.d.ts +7 -0
- package/dist/admin/api/rest/handler.js +116 -0
- package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
- package/dist/admin/api/rest/middleware/apiKey.js +45 -0
- package/dist/admin/api/rest/routes/collections.d.ts +5 -0
- package/dist/admin/api/rest/routes/collections.js +104 -0
- package/dist/admin/api/rest/routes/entries.d.ts +2 -0
- package/dist/admin/api/rest/routes/entries.js +37 -0
- package/dist/admin/api/rest/routes/languages.d.ts +1 -0
- package/dist/admin/api/rest/routes/languages.js +5 -0
- package/dist/admin/api/rest/routes/schema.d.ts +2 -0
- package/dist/admin/api/rest/routes/schema.js +78 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
- package/dist/admin/api/rest/routes/singletons.js +60 -0
- package/dist/admin/auth-client.d.ts +48 -48
- package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
- package/dist/admin/client/admin/admin-layout.svelte +0 -2
- package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
- package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
- package/dist/admin/client/collection/collection-entries.svelte +334 -166
- package/dist/admin/client/collection/collection-page.svelte +6 -3
- package/dist/admin/client/collection/data-table.svelte +127 -18
- package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
- package/dist/admin/client/collection/row-actions.svelte +3 -1
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
- package/dist/admin/client/entry/entry-page.svelte +5 -4
- package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
- package/dist/admin/client/form/form-page.svelte +5 -5
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
- package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
- package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
- package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
- package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
- package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
- package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
- package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
- package/dist/admin/components/fields/image-field.svelte +21 -20
- package/dist/admin/components/fields/media-field.svelte +45 -44
- package/dist/admin/components/fields/relation-field.svelte +428 -149
- package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
- package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
- package/dist/admin/components/fields/seo-field.svelte +66 -10
- package/dist/admin/components/icons/icon-map.d.ts +4 -0
- package/dist/admin/components/icons/icon-map.js +143 -0
- package/dist/admin/components/layout/header-actions.svelte +0 -10
- package/dist/admin/components/layout/nav-collections.svelte +55 -57
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
- package/dist/admin/components/layout/nav-forms.svelte +49 -49
- package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
- package/dist/admin/components/layout/nav-search.svelte +15 -22
- package/dist/admin/components/layout/nav-singletons.svelte +35 -38
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
- package/dist/admin/components/media/file/file-preview.svelte +92 -91
- package/dist/admin/components/media/file-preview.svelte +17 -18
- package/dist/admin/components/media/media-library.svelte +39 -41
- package/dist/admin/components/media/media-selector.svelte +8 -8
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
- package/dist/admin/components/tiptap/content-editor.svelte +2 -2
- package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +12 -1
- package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
- package/dist/admin/remote/entry.remote.d.ts +20 -1
- package/dist/admin/remote/entry.remote.js +24 -3
- package/dist/admin/remote/invite.d.ts +2 -2
- package/dist/cli/scaffold/admin.js +8 -0
- package/dist/cms/runtime/remote.d.ts +8 -0
- package/dist/cms/runtime/remote.js +7 -0
- package/dist/cms/runtime/types.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sonner/sonner.svelte +1 -2
- package/dist/core/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -0
- package/dist/core/server/entries/operations/get.d.ts +11 -0
- package/dist/core/server/entries/operations/get.js +63 -1
- package/dist/core/server/entries/operations/update.d.ts +1 -0
- package/dist/core/server/entries/operations/update.js +5 -1
- package/dist/core/server/fields/resolveUrlFields.js +49 -18
- package/dist/db-postgres/schema/entry.d.ts +17 -0
- package/dist/db-postgres/schema/entry.js +4 -2
- package/dist/server/auth.d.ts +4 -4
- package/dist/sveltekit/components/structured-content.svelte +18 -5
- package/dist/sveltekit/server/handle.js +1 -0
- package/dist/types/cms.d.ts +7 -0
- package/dist/types/collections.d.ts +2 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/entries.d.ts +7 -1
- package/dist/types/forms.d.ts +2 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.4/index.d.ts +2 -0
- package/dist/updates/0.5.4/index.js +15 -0
- package/dist/updates/0.5.5/index.d.ts +2 -0
- package/dist/updates/0.5.5/index.js +20 -0
- package/dist/updates/0.5.6/index.d.ts +2 -0
- package/dist/updates/0.5.6/index.js +17 -0
- package/dist/updates/0.5.7/index.d.ts +2 -0
- package/dist/updates/0.5.7/index.js +14 -0
- package/dist/updates/index.js +5 -1
- 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,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,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,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,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
|
+
}
|