includio-cms 0.5.2 → 0.5.5
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 +60 -0
- package/ROADMAP.md +29 -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 +7 -7
- package/dist/admin/client/collection/collection-entries.svelte +56 -5
- 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/entry/entry-form.svelte +1 -0
- package/dist/admin/client/entry/entry.svelte +130 -123
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
- package/dist/admin/components/fields/blocks-field.svelte +142 -112
- package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
- package/dist/admin/components/fields/boolean-field.svelte +28 -38
- package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
- package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/content-field.svelte +4 -17
- package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/date-field.svelte +8 -21
- package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/datetime-field.svelte +8 -21
- package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/field-renderer.svelte +32 -19
- package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
- package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
- package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
- package/dist/admin/components/fields/fields-form.svelte +13 -10
- package/dist/admin/components/fields/file-field.svelte +12 -27
- package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/image-field.svelte +13 -28
- package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/media-field.svelte +15 -30
- package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/number-field.svelte +6 -20
- package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/object-field.svelte +26 -29
- package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
- package/dist/admin/components/fields/radio-field.svelte +8 -20
- package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/relation-field.svelte +28 -40
- package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/richtext-field.svelte +4 -17
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/select-field.svelte +14 -28
- package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/seo-field.svelte +5 -12
- package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
- package/dist/admin/components/fields/simple-array-field.svelte +29 -42
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/slug-field.svelte +6 -11
- package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
- package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
- package/dist/admin/components/fields/text-field.svelte +7 -19
- package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
- package/dist/admin/components/fields/url-field.svelte +294 -128
- package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
- package/dist/admin/components/layout/layout-renderer.svelte +8 -6
- package/dist/admin/components/layout/nav-collections.svelte +2 -1
- package/dist/admin/components/layout/nav-forms.svelte +2 -1
- package/dist/admin/components/layout/nav-singletons.svelte +2 -1
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
- package/dist/admin/components/tiptap/content-editor.svelte +13 -2
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +18 -1
- package/dist/admin/components/tiptap/slash-command.js +2 -3
- package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
- package/dist/admin/components/tiptap/standalone-form.js +31 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
- package/dist/admin/remote/entry.remote.d.ts +9 -1
- package/dist/admin/remote/entry.remote.js +30 -2
- package/dist/admin/styles/admin.css +10 -0
- package/dist/admin/utils/fieldCondition.d.ts +6 -0
- package/dist/admin/utils/fieldCondition.js +20 -0
- package/dist/cli/scaffold/admin.js +8 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +26 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/core/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -0
- package/dist/core/fields/fieldSchemaToTs.js +15 -3
- package/dist/core/fields/formFieldSchemaToTs.js +22 -6
- package/dist/core/fields/urlUtils.d.ts +14 -0
- package/dist/core/fields/urlUtils.js +21 -0
- package/dist/core/server/entries/operations/get.js +2 -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/populateEntry.js +43 -0
- package/dist/core/server/fields/resolveImageFields.js +33 -1
- package/dist/core/server/fields/resolveRelationFields.js +46 -0
- package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
- package/dist/core/server/fields/resolveUrlFields.js +65 -0
- package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
- package/dist/core/server/generator/formFields.js +2 -0
- package/dist/core/server/generator/generator.js +25 -1
- package/dist/db-postgres/schema/entry.d.ts +17 -0
- package/dist/db-postgres/schema/entry.js +4 -2
- package/dist/schemas/field/url.d.ts +2 -0
- package/dist/schemas/field/url.js +4 -2
- package/dist/server/auth.d.ts +6 -6
- 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/entries.d.ts +7 -1
- package/dist/types/fields.d.ts +9 -0
- package/dist/types/formFields.d.ts +15 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.3/index.d.ts +2 -0
- package/dist/updates/0.5.3/index.js +19 -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/index.js +4 -1
- package/package.json +7 -1
- package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
- package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,66 @@
|
|
|
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.5 — 2026-03-04
|
|
7
|
+
|
|
8
|
+
REST API with API key auth, orderable collection fixes
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- REST API with API key authentication — CRUD for collections, singletons, entries, schema introspection, language listing
|
|
12
|
+
- API key configuration in CMS config (`apiKeys` array)
|
|
13
|
+
- getEntries supports `orderBy` parameter (column + direction)
|
|
14
|
+
- Admin scaffold generates REST API catch-all route
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Orderable collections: DnD rows use proper `<tr>` element instead of `<div>` wrapper
|
|
18
|
+
- Reorder buttons always visible (not sr-only) for better discoverability
|
|
19
|
+
- Selection enabled for orderable collections
|
|
20
|
+
- Relation field respects orderable collection sort order
|
|
21
|
+
- Nav active state: exact match prevents false highlights on similarly-named routes
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
Add `apiKeys: [{ key: "your-secret", name: "My App" }]` to CMS config. REST API available at `/admin/api/rest/`.
|
|
26
|
+
|
|
27
|
+
## 0.5.4 — 2026-03-04
|
|
28
|
+
|
|
29
|
+
Collection ordering (DnD), custom list columns
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
- Orderable collections: DnD reordering of entries in admin list with keyboard alternative (WCAG 2.1)
|
|
33
|
+
- Custom list columns: display arbitrary entry fields as columns in collection list
|
|
34
|
+
- sortOrder column on entries table for persistent manual ordering
|
|
35
|
+
- reorderEntriesCommand remote for bulk sort order updates
|
|
36
|
+
|
|
37
|
+
### Migration
|
|
38
|
+
|
|
39
|
+
```sql
|
|
40
|
+
ALTER TABLE entry ADD COLUMN sort_order INTEGER;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Notes
|
|
44
|
+
|
|
45
|
+
Add `orderable: true` to collections that need manual ordering. Add `listColumns: ["fieldSlug"]` to show fields in the entry list.
|
|
46
|
+
|
|
47
|
+
## 0.5.3 — 2026-03-04
|
|
48
|
+
|
|
49
|
+
Form fields, inline blocks, hybrid preview, field components refactor
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
- Form fields: select type, minLength/maxLength, errorMessage, remote commands
|
|
53
|
+
- Conditional field visibility (showWhen)
|
|
54
|
+
- Hybrid preview: device frames, container queries
|
|
55
|
+
- TipTap placeholder extension + Notion-style styles
|
|
56
|
+
- Server-side field resolution for inline blocks
|
|
57
|
+
- Inline blocks: standalone form, full field rendering, collapse UI
|
|
58
|
+
- URL field: rel attribute, external auto-detect, UI redesign
|
|
59
|
+
- Switch UI component (bits-ui)
|
|
60
|
+
- Boolean field: Switch toggle zamiast checkbox
|
|
61
|
+
- Storybook stories for all field types
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
- Hybrid preview layout fix
|
|
65
|
+
|
|
6
66
|
## 0.5.2 — 2026-02-25
|
|
7
67
|
|
|
8
68
|
Update system: split migration into sql + notes
|
package/ROADMAP.md
CHANGED
|
@@ -76,6 +76,35 @@
|
|
|
76
76
|
|
|
77
77
|
- [x] `[fix]` `[P1]` Split `migration` field into `sql` + `notes` — fixes CLI crash on text descriptions
|
|
78
78
|
|
|
79
|
+
## 0.5.3 — Form fields, inline blocks, hybrid preview
|
|
80
|
+
|
|
81
|
+
- [x] `[feature]` `[P1]` Form fields: select type, minLength/maxLength, errorMessage, remote commands
|
|
82
|
+
- [x] `[feature]` `[P1]` Conditional field visibility (showWhen)
|
|
83
|
+
- [x] `[feature]` `[P1]` Hybrid preview: device frames, container queries
|
|
84
|
+
- [x] `[feature]` `[P2]` TipTap placeholder extension + Notion-style styles
|
|
85
|
+
- [x] `[feature]` `[P1]` Server-side field resolution for inline blocks
|
|
86
|
+
- [x] `[feature]` `[P1]` Inline blocks: standalone form, field rendering, collapse UI
|
|
87
|
+
- [x] `[feature]` `[P2]` URL field: rel attribute, external auto-detect, UI redesign
|
|
88
|
+
- [x] `[feature]` `[P2]` Switch UI component + boolean field toggle
|
|
89
|
+
- [x] `[chore]` `[P2]` Storybook stories for all field types
|
|
90
|
+
- [x] `[chore]` `[P1]` Field components refactor: bindable value props
|
|
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
|
+
|
|
79
108
|
## 0.6.0 — Plugin system _(deferred from 0.2.0)_
|
|
80
109
|
|
|
81
110
|
- [ ] `[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
|
+
}
|
|
@@ -45,7 +45,7 @@ export declare const authClient: {
|
|
|
45
45
|
}) | undefined;
|
|
46
46
|
body?: (Partial<{
|
|
47
47
|
userId: string;
|
|
48
|
-
role: "
|
|
48
|
+
role: "admin" | "user" | ("admin" | "user")[];
|
|
49
49
|
}> & Record<string, any>) | undefined;
|
|
50
50
|
query?: (Partial<Record<string, any>> & Record<string, any>) | undefined;
|
|
51
51
|
params?: Record<string, any> | undefined;
|
|
@@ -58,7 +58,7 @@ export declare const authClient: {
|
|
|
58
58
|
disableValidation?: boolean | undefined;
|
|
59
59
|
}>(data_0: import("better-auth", { with: { "resolution-mode": "require" } }).Prettify<{
|
|
60
60
|
userId: string;
|
|
61
|
-
role: "
|
|
61
|
+
role: "admin" | "user" | ("admin" | "user")[];
|
|
62
62
|
} & {
|
|
63
63
|
fetchOptions?: FetchOptions | undefined;
|
|
64
64
|
}>, data_1?: FetchOptions | undefined) => Promise<import("better-auth/svelte", { with: { "resolution-mode": "require" } }).BetterFetchResponse<{
|
|
@@ -192,7 +192,7 @@ export declare const authClient: {
|
|
|
192
192
|
email: string;
|
|
193
193
|
password: string;
|
|
194
194
|
name: string;
|
|
195
|
-
role?: "
|
|
195
|
+
role?: "admin" | "user" | ("admin" | "user")[] | undefined;
|
|
196
196
|
data?: Record<string, any>;
|
|
197
197
|
}> & Record<string, any>) | undefined;
|
|
198
198
|
query?: (Partial<Record<string, any>> & Record<string, any>) | undefined;
|
|
@@ -208,7 +208,7 @@ export declare const authClient: {
|
|
|
208
208
|
email: string;
|
|
209
209
|
password: string;
|
|
210
210
|
name: string;
|
|
211
|
-
role?: "
|
|
211
|
+
role?: "admin" | "user" | ("admin" | "user")[] | undefined;
|
|
212
212
|
data?: Record<string, any>;
|
|
213
213
|
} & {
|
|
214
214
|
fetchOptions?: FetchOptions | undefined;
|
|
@@ -1073,7 +1073,7 @@ export declare const authClient: {
|
|
|
1073
1073
|
permission?: never;
|
|
1074
1074
|
}) & {
|
|
1075
1075
|
userId?: string;
|
|
1076
|
-
role?: "
|
|
1076
|
+
role?: "admin" | "user" | undefined;
|
|
1077
1077
|
}> & Record<string, any>) | undefined;
|
|
1078
1078
|
query?: (Partial<Record<string, any>> & Record<string, any>) | undefined;
|
|
1079
1079
|
params?: Record<string, any> | undefined;
|
|
@@ -1098,7 +1098,7 @@ export declare const authClient: {
|
|
|
1098
1098
|
permission?: never;
|
|
1099
1099
|
}) & {
|
|
1100
1100
|
userId?: string;
|
|
1101
|
-
role?: "
|
|
1101
|
+
role?: "admin" | "user" | undefined;
|
|
1102
1102
|
}) & {
|
|
1103
1103
|
fetchOptions?: FetchOptions | undefined;
|
|
1104
1104
|
}>, data_1?: FetchOptions | undefined) => Promise<import("better-auth/svelte", { with: { "resolution-mode": "require" } }).BetterFetchResponse<{
|
|
@@ -3105,7 +3105,7 @@ export declare const authClient: {
|
|
|
3105
3105
|
}, FetchOptions["throw"] extends true ? true : false>>;
|
|
3106
3106
|
} & {
|
|
3107
3107
|
admin: {
|
|
3108
|
-
checkRolePermission: <R extends "
|
|
3108
|
+
checkRolePermission: <R extends "admin" | "user">(data: ({
|
|
3109
3109
|
permission: {
|
|
3110
3110
|
readonly user?: ("create" | "list" | "set-role" | "ban" | "impersonate" | "delete" | "set-password" | "get" | "update")[] | undefined;
|
|
3111
3111
|
readonly session?: ("list" | "delete" | "revoke")[] | undefined;
|