includio-cms 0.7.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/ROADMAP.md +44 -2
  3. package/dist/admin/api/generate-styles.d.ts +2 -0
  4. package/dist/admin/api/generate-styles.js +32 -0
  5. package/dist/admin/api/handler.js +33 -0
  6. package/dist/admin/api/media-gc.js +10 -4
  7. package/dist/admin/api/rest/handler.js +17 -0
  8. package/dist/admin/api/rest/routes/collections.js +25 -13
  9. package/dist/admin/api/rest/routes/entries.d.ts +1 -1
  10. package/dist/admin/api/rest/routes/entries.js +10 -10
  11. package/dist/admin/api/rest/routes/media.d.ts +2 -0
  12. package/dist/admin/api/rest/routes/media.js +9 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +5 -0
  14. package/dist/admin/api/rest/routes/schema.js +152 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
  16. package/dist/admin/api/rest/routes/singletons.js +8 -7
  17. package/dist/admin/api/rest/routes/upload.d.ts +2 -0
  18. package/dist/admin/api/rest/routes/upload.js +28 -0
  19. package/dist/admin/api/upload.js +13 -0
  20. package/dist/admin/client/collection/collection-entries.svelte +19 -6
  21. package/dist/admin/client/entry/entry.svelte +21 -23
  22. package/dist/admin/client/entry/header/a11y-validator.js +2 -2
  23. package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
  24. package/dist/admin/client/entry/header/status-badge.svelte +2 -2
  25. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
  26. package/dist/admin/client/entry/header/visibility.svelte +16 -10
  27. package/dist/admin/client/entry/utils.d.ts +3 -0
  28. package/dist/admin/client/entry/utils.js +22 -4
  29. package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
  30. package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
  31. package/dist/admin/client/index.d.ts +1 -0
  32. package/dist/admin/client/index.js +1 -0
  33. package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
  34. package/dist/admin/components/fields/blocks-field.svelte +9 -10
  35. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  36. package/dist/admin/components/fields/object-field.svelte +7 -12
  37. package/dist/admin/components/fields/select-field.svelte +8 -2
  38. package/dist/admin/components/fields/seo-field.svelte +40 -93
  39. package/dist/admin/components/fields/simple-array-field.svelte +5 -5
  40. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  41. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  42. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  43. package/dist/admin/components/fields/url-field.svelte +61 -72
  44. package/dist/admin/components/media/file-upload.svelte +5 -1
  45. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  46. package/dist/admin/components/media/media-library.svelte +109 -37
  47. package/dist/admin/components/media/media-selector.svelte +79 -11
  48. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  49. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  50. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +21 -93
  51. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  52. package/dist/admin/components/tiptap/link-dialog.svelte +10 -11
  53. package/dist/admin/components/tiptap/slash-command.js +1 -1
  54. package/dist/admin/remote/entry.remote.d.ts +2 -5
  55. package/dist/admin/remote/entry.remote.js +22 -27
  56. package/dist/admin/remote/media.remote.d.ts +15 -0
  57. package/dist/admin/remote/media.remote.js +18 -2
  58. package/dist/admin/remote/preview.remote.js +3 -1
  59. package/dist/admin/utils/entryLabel.js +9 -6
  60. package/dist/admin/utils/translationStatus.js +1 -2
  61. package/dist/cli/create-user.d.ts +2 -0
  62. package/dist/cli/create-user.js +81 -0
  63. package/dist/cli/index.js +5 -0
  64. package/dist/cli/scaffold/admin.js +34 -2
  65. package/dist/cms/runtime/api.d.ts +16 -12
  66. package/dist/cms/runtime/api.js +7 -6
  67. package/dist/cms/runtime/remote.js +2 -2
  68. package/dist/cms/runtime/schemas.d.ts +1 -1
  69. package/dist/cms/runtime/schemas.js +1 -1
  70. package/dist/cms/runtime/types.d.ts +118 -112
  71. package/dist/cms/runtime/types.js +0 -12
  72. package/dist/core/cms.d.ts +3 -1
  73. package/dist/core/cms.js +30 -0
  74. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  75. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  76. package/dist/core/server/entries/operations/create.js +10 -4
  77. package/dist/core/server/entries/operations/get.d.ts +1 -0
  78. package/dist/core/server/entries/operations/get.js +186 -191
  79. package/dist/core/server/entries/operations/update.d.ts +6 -7
  80. package/dist/core/server/entries/operations/update.js +20 -38
  81. package/dist/core/server/fields/populateEntry.js +16 -52
  82. package/dist/core/server/fields/resolveImageFields.js +69 -120
  83. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  84. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  85. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  86. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  87. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  88. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  89. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  90. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  91. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  92. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  93. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  94. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  95. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  96. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  97. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  98. package/dist/core/server/generator/fields.d.ts +6 -0
  99. package/dist/core/server/generator/fields.js +43 -5
  100. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  101. package/dist/core/server/generator/formFields.js +1 -0
  102. package/dist/core/server/generator/generator.js +98 -30
  103. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  104. package/dist/core/server/media/operations/getFiles.js +6 -0
  105. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  106. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  107. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  108. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  109. package/dist/db-postgres/index.js +303 -37
  110. package/dist/db-postgres/schema/entry.d.ts +0 -94
  111. package/dist/db-postgres/schema/entry.js +0 -6
  112. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  113. package/dist/db-postgres/schema/entryVersion.js +1 -0
  114. package/dist/entity/index.d.ts +9 -4
  115. package/dist/entity/index.js +24 -24
  116. package/dist/files-local/index.js +43 -0
  117. package/dist/sveltekit/components/preview.svelte +2 -326
  118. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  119. package/dist/sveltekit/server/index.d.ts +2 -1
  120. package/dist/sveltekit/server/index.js +2 -1
  121. package/dist/sveltekit/server/preview.js +4 -7
  122. package/dist/types/adapters/db.d.ts +15 -1
  123. package/dist/types/adapters/files.d.ts +6 -0
  124. package/dist/types/cms.d.ts +5 -0
  125. package/dist/types/entries.d.ts +54 -18
  126. package/dist/types/fields.d.ts +14 -24
  127. package/dist/types/formFields.d.ts +7 -2
  128. package/dist/types/index.d.ts +2 -2
  129. package/dist/types/structured-content.d.ts +5 -0
  130. package/dist/updates/0.10.0/index.d.ts +2 -0
  131. package/dist/updates/0.10.0/index.js +15 -0
  132. package/dist/updates/0.11.0/index.d.ts +2 -0
  133. package/dist/updates/0.11.0/index.js +12 -0
  134. package/dist/updates/0.12.0/index.d.ts +2 -0
  135. package/dist/updates/0.12.0/index.js +12 -0
  136. package/dist/updates/0.13.0/index.d.ts +2 -0
  137. package/dist/updates/0.13.0/index.js +10 -0
  138. package/dist/updates/0.7.2/index.d.ts +2 -0
  139. package/dist/updates/0.7.2/index.js +10 -0
  140. package/dist/updates/0.7.3/index.d.ts +2 -0
  141. package/dist/updates/0.7.3/index.js +10 -0
  142. package/dist/updates/0.8.0/index.d.ts +2 -0
  143. package/dist/updates/0.8.0/index.js +18 -0
  144. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  145. package/dist/updates/0.8.0/migrate.js +101 -0
  146. package/dist/updates/0.9.0/index.d.ts +2 -0
  147. package/dist/updates/0.9.0/index.js +38 -0
  148. package/dist/updates/index.js +9 -1
  149. package/package.json +7 -6
  150. package/dist/admin/components/fields/image-field.svelte +0 -198
  151. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  152. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  153. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  154. package/dist/admin/components/tiptap.svelte +0 -11
  155. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  156. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  157. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
package/CHANGELOG.md CHANGED
@@ -3,6 +3,123 @@
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.13.0 — 2026-03-19
7
+
8
+ Private file uploads for form submissions
9
+
10
+ ### Added
11
+ - Private file uploads — form submissions can upload files to a non-public directory with access control
12
+
13
+ ## 0.12.0 — 2026-03-18
14
+
15
+ File field type, dataOrderBy, and _url population
16
+
17
+ ### Added
18
+ - File field type with multipart upload, file validation, and rate limiting
19
+ - `dataOrderBy` — sort entries by JSON data fields in queries
20
+ - Auto-populate `_url` field on entries from slug resolver and `pathTemplate`
21
+
22
+ ## 0.11.0 — 2026-03-18
23
+
24
+ Codegen overhaul and REST API consolidation
25
+
26
+ ### Added
27
+ - Codegen: flat entry types, inline block types, `countEntries` query helper, simplified API surface
28
+ - REST API: catch-all route handler, file upload endpoint, media endpoint, schema template endpoints
29
+ - Admin scaffold updated for new REST route structure
30
+
31
+ ## 0.10.0 — 2026-03-18
32
+
33
+ Remove image, video, richtext field types — consolidate to media and content
34
+
35
+ ### Breaking
36
+ - Removed `image` field type — use `media` with `accept: "image/*"` instead
37
+ - Removed `richtext` field type — use `content` (structured ProseMirror JSON) instead
38
+ - Removed `ImageField` and `RichtextField` TypeScript interfaces
39
+ - `image-field.svelte` and `richtext-field.svelte` components removed
40
+ - `tiptap.svelte` (HTML richtext wrapper) removed
41
+
42
+ ### Notes
43
+
44
+ No automatic data migration. Replace `type: "image"` with `type: "media", accept: "image/*"` in your collection/singleton configs. Replace `type: "richtext"` with `type: "content"`. Existing stored data (media UUIDs, ProseMirror docs) remains compatible.
45
+
46
+ ## 0.9.0 — 2026-03-17
47
+
48
+ Per-language entry versions — separate entry_version per language
49
+
50
+ ### Added
51
+ - Per-language entry versions: each language gets its own entry_version with flat data
52
+ - Per-language publication: publish/unpublish each language independently
53
+ - Simplified data format: no more nested `{ pl: "X", en: "Y" }` — values are flat strings
54
+ - Language switching in admin loads different version data
55
+
56
+ ### Breaking
57
+ - `entry_version` table: new `lang` column (NOT NULL)
58
+ - `entry` table: removed `published_at`, `published_version_id`, `published_by`, `available_locales` columns
59
+ - `RawEntry`: `publishedVersion`/`draftVersion`/`scheduledVersion` replaced with per-lang `publishedVersions`/`draftVersions`/`scheduledVersions` records
60
+ - `DbEntryVersion`/`DbEntryVersionInsert`: new required `lang` field
61
+ - `translateObject` removed — data is single-language per version
62
+ - Resolve functions (relation, media, url, richtext) expect flat content/richtext values, not `Record<lang, value>`
63
+ - `upsertDraftVersion` and `pruneOldDraftVersions` now require `lang` parameter
64
+ - `unpublishEntry` renamed to `unpublishEntryLang` with required `lang` parameter
65
+
66
+ ### Migration
67
+
68
+ ```sql
69
+ -- Step 1: Add lang column as nullable
70
+ ALTER TABLE entry_version ADD COLUMN lang TEXT;
71
+
72
+ -- Step 2: Run migration script to split multi-lang versions (scripts/migrate-lang-versions.ts)
73
+ -- After migration, set NOT NULL:
74
+ ALTER TABLE entry_version ALTER COLUMN lang SET NOT NULL;
75
+
76
+ -- Step 3: Create index for performance
77
+ CREATE INDEX idx_entry_version_publish ON entry_version(entry_id, lang, published_at);
78
+
79
+ -- Step 4: Remove entry-level publish columns
80
+ ALTER TABLE entry DROP COLUMN IF EXISTS published_at;
81
+ ALTER TABLE entry DROP COLUMN IF EXISTS published_version_id;
82
+ ALTER TABLE entry DROP COLUMN IF EXISTS published_by;
83
+ ALTER TABLE entry DROP COLUMN IF EXISTS available_locales;
84
+ ```
85
+
86
+ ### Notes
87
+
88
+ Run the migration script BEFORE setting lang to NOT NULL. See scripts/migrate-lang-versions.ts for data migration.
89
+
90
+ ## 0.8.0 — 2026-03-17
91
+
92
+ Flat entry format — remove data wrapper from objects/blocks
93
+
94
+ ### Added
95
+ - Flat entry format: objects use `_slug` + spread fields instead of `slug` + `data` wrapper
96
+ - Blocks use `_slug` instead of `slug` with flat fields
97
+ - Entry type unified: `FlatEntry` renamed to `Entry`, old `Entry` removed
98
+
99
+ ### Breaking
100
+ - Object field data format: `{ slug, data: {...} }` → `{ _slug, ...fields }`
101
+ - Blocks field data format: `{ _id, slug, data: {...} }` → `{ _id, _slug, ...fields }`
102
+ - `FlatEntry` type removed — use `Entry` instead
103
+ - `flattenEntry`/`flattenFields` removed — data is flat from resolve functions
104
+
105
+ ### Notes
106
+
107
+ Run `includio update` to migrate existing entry data in the database.
108
+
109
+ ## 0.7.3 — 2026-03-17
110
+
111
+ Typography orphan fix for Polish conjunctions
112
+
113
+ ### Added
114
+ - Read-time orphan fix — replace space after single-letter Polish conjunctions (i, w, z, o, u, a) with non-breaking space `\u00A0`
115
+
116
+ ## 0.7.2 — 2026-03-16
117
+
118
+ CLI create-user command
119
+
120
+ ### Added
121
+ - CLI `create-user` command — interactive user creation with email, password (auto-generate option), name, and role via better-auth
122
+
6
123
  ## 0.7.1 — 2026-03-13
7
124
 
8
125
  Custom fields plugin system, pathTemplate, admin public API
package/ROADMAP.md CHANGED
@@ -163,14 +163,56 @@
163
163
  - [x] `[feature]` `[P2]` resolveMediaWithStyles utility & core svelte export
164
164
  - [x] `[fix]` `[P1]` Schema serialization type cast (stricter TS compatibility)
165
165
 
166
- ## 0.8.0SEO module
166
+ ## 0.7.2CLI create-user
167
+
168
+ - [x] `[feature]` `[P1]` CLI `create-user` command — interactive user creation with role assignment <!-- files: src/lib/cli/create-user.ts, src/lib/cli/index.ts -->
169
+
170
+ ## 0.7.3 — Typography
171
+
172
+ - [x] `[feature]` `[P1]` Read-time orphan fix — replace space after single-letter Polish conjunctions with `\u00A0` <!-- files: src/lib/core/server/fields/resolveTypographyOrphans.ts, src/lib/core/server/fields/utils/fixOrphans.ts -->
173
+
174
+ ## 0.8.0 — Flat entry format
175
+
176
+ - [x] `[breaking]` `[P0]` Flat entry format: objects use `_slug` + spread fields instead of `slug` + `data` wrapper
177
+ - [x] `[breaking]` `[P0]` Blocks use `_slug` instead of `slug` with flat fields
178
+ - [x] `[breaking]` `[P0]` `FlatEntry` type removed — use `Entry` instead
179
+
180
+ ## 0.9.0 — Per-language entry versions
181
+
182
+ - [x] `[breaking]` `[P0]` Per-language entry versions: each language gets its own `entry_version` with flat data
183
+ - [x] `[feature]` `[P0]` Per-language publication: publish/unpublish each language independently
184
+ - [x] `[breaking]` `[P0]` Simplified data format: no more nested `{ pl: "X", en: "Y" }` — values are flat strings
185
+
186
+ ## 0.10.0 — Field type consolidation
187
+
188
+ - [x] `[breaking]` `[P1]` Remove `image` field type — use `media` with `accept: "image/*"`
189
+ - [x] `[breaking]` `[P1]` Remove `richtext` field type — use `content` (structured JSON)
190
+ - [x] `[chore]` `[P1]` Remove `ImageField`, `RichtextField` interfaces + components
191
+
192
+ ## 0.11.0 — Codegen & REST consolidation
193
+
194
+ - [x] `[feature]` `[P1]` Codegen: flat types, inline block types, `countEntries`, simplified API
195
+ - [x] `[feature]` `[P1]` REST API: catch-all route, upload endpoint, media endpoint, schema templates
196
+ - [x] `[chore]` `[P1]` Admin scaffold updated for new REST route structure
197
+
198
+ ## 0.12.0 — File field, dataOrderBy, _url
199
+
200
+ - [x] `[feature]` `[P1]` File field type with multipart upload, validation, rate limiting
201
+ - [x] `[feature]` `[P1]` `dataOrderBy` — sort entries by JSON data fields
202
+ - [x] `[feature]` `[P1]` Auto-populate `_url` field on entries from slug resolver
203
+
204
+ ## 0.13.0 — Private file uploads
205
+
206
+ - [x] `[feature]` `[P1]` Private file uploads — form submissions can upload files to non-public directory
207
+
208
+ ## 0.14.0 — SEO module
167
209
 
168
210
  - [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
169
211
  - [ ] `[feature]` `[P1]` Global SEO settings
170
212
  - [ ] `[feature]` `[P1]` Dedicated frontend SEO components <!-- files: src/lib/sveltekit/components/seo.svelte -->
171
213
  - [ ] `[feature]` `[P2]` Sitemap generation
172
214
 
173
- ## 0.9.0 — WCAG/ATAG compliance
215
+ ## 0.15.0 — WCAG/ATAG compliance
174
216
 
175
217
  - [ ] `[chore]` `[P0]` Full WCAG/ATAG audit
176
218
  - [ ] `[feature]` `[P0]` Accessibility rework based on audit findings
@@ -0,0 +1,2 @@
1
+ import type { RequestHandler } from '@sveltejs/kit';
2
+ export declare const POST: RequestHandler;
@@ -0,0 +1,32 @@
1
+ import { requireRole } from '../remote/middleware/auth.js';
2
+ import { batchGenerateAllStyles } from '../../core/server/media/styles/operations/batchGenerateStyles.js';
3
+ export const POST = async () => {
4
+ requireRole('admin');
5
+ const abort = new AbortController();
6
+ const encoder = new TextEncoder();
7
+ const stream = new ReadableStream({
8
+ async start(controller) {
9
+ try {
10
+ for await (const event of batchGenerateAllStyles(abort.signal)) {
11
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
12
+ }
13
+ }
14
+ catch (e) {
15
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'error', error: e instanceof Error ? e.message : String(e) })}\n\n`));
16
+ }
17
+ finally {
18
+ controller.close();
19
+ }
20
+ },
21
+ cancel() {
22
+ abort.abort();
23
+ }
24
+ });
25
+ return new Response(stream, {
26
+ headers: {
27
+ 'Content-Type': 'text/event-stream',
28
+ 'Cache-Control': 'no-cache',
29
+ Connection: 'keep-alive'
30
+ }
31
+ });
32
+ };
@@ -5,6 +5,10 @@ import * as replaceHandlers from './replace.js';
5
5
  import * as inviteHandlers from './invite.js';
6
6
  import * as acceptInviteHandlers from './accept-invite.js';
7
7
  import * as mediaGcHandlers from './media-gc.js';
8
+ import * as generateStylesHandlers from './generate-styles.js';
9
+ import { requireAuth } from '../remote/middleware/auth.js';
10
+ import { getCMS } from '../../core/cms.js';
11
+ import { lookup } from 'mrmime';
8
12
  export function createAdminApiHandler(options) {
9
13
  const routes = {
10
14
  upload: uploadHandlers,
@@ -13,14 +17,43 @@ export function createAdminApiHandler(options) {
13
17
  invite: inviteHandlers,
14
18
  'accept-invite': acceptInviteHandlers,
15
19
  'media-gc': mediaGcHandlers,
20
+ 'generate-styles': generateStylesHandlers,
16
21
  ...options?.extraRoutes
17
22
  };
23
+ const privateMediaGet = async (event) => {
24
+ requireAuth();
25
+ const path = event.params.path;
26
+ const filename = path.replace('media/private/', '');
27
+ if (!filename || filename.includes('/') || filename.includes('..')) {
28
+ return json({ error: 'Invalid filename' }, { status: 400 });
29
+ }
30
+ const adapter = getCMS().filesAdapter;
31
+ if (!adapter.downloadPrivateFile) {
32
+ return json({ error: 'Not supported' }, { status: 501 });
33
+ }
34
+ const file = await adapter.downloadPrivateFile(filename);
35
+ if (!file) {
36
+ return json({ error: 'Not found' }, { status: 404 });
37
+ }
38
+ const contentType = lookup(filename) || 'application/octet-stream';
39
+ return new Response(await file.arrayBuffer(), {
40
+ headers: {
41
+ 'Content-Type': contentType,
42
+ 'Content-Disposition': `inline; filename="${filename}"`,
43
+ 'Cache-Control': 'private, no-store'
44
+ }
45
+ });
46
+ };
18
47
  function handle(method) {
19
48
  return (event) => {
20
49
  const path = event.params.path;
21
50
  if (!path) {
22
51
  return json({ error: 'Not found' }, { status: 404 });
23
52
  }
53
+ // Handle media/private/[filename] route
54
+ if (path.startsWith('media/private/') && method === 'GET') {
55
+ return privateMediaGet(event);
56
+ }
24
57
  const handler = routes[path]?.[method];
25
58
  if (!handler) {
26
59
  return json({ error: 'Not found' }, { status: 404 });
@@ -1,13 +1,19 @@
1
1
  import { requireRole } from '../remote/middleware/auth.js';
2
- import { getImageStylesStats, purgeAllImageStyles } from '../../core/server/media/operations/purgeImageStyles.js';
2
+ import { purgeAllImageStyles } from '../../core/server/media/operations/purgeImageStyles.js';
3
3
  import { getReconciliationReport, deleteOrphanedDiskFiles } from '../../core/server/media/operations/reconcileMedia.js';
4
+ import { getStylesStatus } from '../../core/server/media/styles/operations/batchGenerateStyles.js';
4
5
  import { json } from '@sveltejs/kit';
5
6
  export const GET = async ({ url }) => {
6
7
  requireRole('admin');
7
- const stats = await getImageStylesStats();
8
- const report = await getReconciliationReport();
8
+ const [stylesStatus, report] = await Promise.all([
9
+ getStylesStatus(),
10
+ getReconciliationReport()
11
+ ]);
9
12
  return json({
10
- imageStylesCount: stats.count,
13
+ imageStylesCount: stylesStatus.existingStyles,
14
+ processableImagesCount: stylesStatus.processableImages,
15
+ expectedStylesCount: stylesStatus.expectedStyles,
16
+ missingStylesCount: stylesStatus.missingStyles,
11
17
  orphanedDiskFiles: report.orphanedDisk,
12
18
  missingDiskRecords: report.missingDisk
13
19
  });
@@ -5,6 +5,8 @@ import * as languagesRoutes from './routes/languages.js';
5
5
  import * as collectionsRoutes from './routes/collections.js';
6
6
  import * as singletonsRoutes from './routes/singletons.js';
7
7
  import * as entriesRoutes from './routes/entries.js';
8
+ import * as uploadRoutes from './routes/upload.js';
9
+ import * as mediaRoutes from './routes/media.js';
8
10
  function matchRoute(method, path) {
9
11
  // Schema routes
10
12
  if (method === 'GET' && path === 'schema') {
@@ -17,6 +19,15 @@ function matchRoute(method, path) {
17
19
  if (method === 'GET' && path === 'languages') {
18
20
  return { handler: 'languages', params: [] };
19
21
  }
22
+ // Upload: POST /upload
23
+ if (method === 'POST' && path === 'upload') {
24
+ return { handler: 'upload', params: [] };
25
+ }
26
+ // Media: GET /media/:id
27
+ const mediaMatch = path.match(/^media\/([^/]+)$/);
28
+ if (method === 'GET' && mediaMatch) {
29
+ return { handler: 'media', params: [mediaMatch[1]] };
30
+ }
20
31
  // Entries lifecycle: POST /entries/:id/:action
21
32
  const entriesMatch = path.match(/^entries\/([^/]+)\/(publish|unpublish|archive|unarchive)$/);
22
33
  if (method === 'POST' && entriesMatch) {
@@ -53,6 +64,12 @@ export function createRestApiHandler() {
53
64
  }
54
65
  try {
55
66
  switch (route.handler) {
67
+ case 'upload':
68
+ return await uploadRoutes.POST(event);
69
+ case 'media': {
70
+ const [mediaId] = route.params;
71
+ return await mediaRoutes.GET(event, mediaId);
72
+ }
56
73
  case 'schema': {
57
74
  // Pass restPath minus 'schema/' prefix
58
75
  const schemaPath = restPath === 'schema' ? '' : restPath.slice(7);
@@ -2,13 +2,14 @@ import { json } from '@sveltejs/kit';
2
2
  import { getCMS } from '../../../../core/cms.js';
3
3
  import { getRawEntries, getRawEntry, countRawEntries } from '../../../../core/server/entries/operations/get.js';
4
4
  import { createEntry } from '../../../../core/server/entries/operations/create.js';
5
- import { upsertDraftVersion } from '../../../../core/server/entries/operations/update.js';
5
+ import { upsertDraftVersion, updateEntry, updateEntryVersion } from '../../../../core/server/entries/operations/update.js';
6
6
  import { deleteEntry } from '../../../../core/server/entries/operations/delete.js';
7
7
  export async function GET(event, slug, id) {
8
8
  const cms = getCMS();
9
9
  if (!cms.collections[slug]) {
10
10
  return json({ error: `Collection "${slug}" not found` }, { status: 404 });
11
11
  }
12
+ const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
12
13
  // GET /collections/:slug/:id — single entry
13
14
  if (id) {
14
15
  const entry = await getRawEntry({ id, slug, includeArchived: true });
@@ -21,14 +22,12 @@ export async function GET(event, slug, id) {
21
22
  type: entry.type,
22
23
  createdAt: entry.createdAt,
23
24
  updatedAt: entry.updatedAt,
24
- publishedAt: entry.publishedAt,
25
25
  archivedAt: entry.archivedAt,
26
- availableLocales: entry.availableLocales,
27
26
  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
27
+ draftData: entry.draftVersions[lang]?.data ?? null,
28
+ publishedData: entry.publishedVersions[lang]?.data ?? null,
29
+ draftVersionId: entry.draftVersions[lang]?.id ?? null,
30
+ publishedVersionId: entry.publishedVersions[lang]?.id ?? null
32
31
  });
33
32
  }
34
33
  // GET /collections/:slug — list
@@ -48,12 +47,10 @@ export async function GET(event, slug, id) {
48
47
  type: entry.type,
49
48
  createdAt: entry.createdAt,
50
49
  updatedAt: entry.updatedAt,
51
- publishedAt: entry.publishedAt,
52
50
  archivedAt: entry.archivedAt,
53
- availableLocales: entry.availableLocales,
54
51
  sortOrder: entry.sortOrder,
55
- draftData: entry.draftVersion?.data ?? null,
56
- publishedData: entry.publishedVersion?.data ?? null
52
+ draftData: entry.draftVersions[lang]?.data ?? null,
53
+ publishedData: entry.publishedVersions[lang]?.data ?? null
57
54
  }));
58
55
  return json({ items, total, limit, offset });
59
56
  }
@@ -62,11 +59,25 @@ export async function POST(event, slug) {
62
59
  if (!cms.collections[slug]) {
63
60
  return json({ error: `Collection "${slug}" not found` }, { status: 404 });
64
61
  }
62
+ const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
65
63
  const body = await event.request.json().catch(() => null);
66
64
  const entry = await createEntry({ slug, type: 'collection' });
67
65
  // If data provided, save as draft
68
66
  if (body?.data && typeof body.data === 'object') {
69
- await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
67
+ await upsertDraftVersion(entry.id, body.data, lang, { skipValidation: true });
68
+ }
69
+ // Auto-publish if requested
70
+ if (body?.publish) {
71
+ const raw = await getRawEntry({ id: entry.id, slug });
72
+ const draftVersion = raw?.draftVersions[lang];
73
+ if (draftVersion) {
74
+ const user = event.locals.user;
75
+ await updateEntryVersion(draftVersion.id, {
76
+ publishedAt: new Date(),
77
+ publishedBy: user?.id ?? 'api'
78
+ });
79
+ }
80
+ return json({ id: entry.id, slug: entry.slug, published: true }, { status: 201 });
70
81
  }
71
82
  return json({ id: entry.id, slug: entry.slug }, { status: 201 });
72
83
  }
@@ -79,11 +90,12 @@ export async function PUT(event, slug, id) {
79
90
  if (!entry) {
80
91
  return json({ error: 'Entry not found' }, { status: 404 });
81
92
  }
93
+ const lang = event.url.searchParams.get('lang') || cms.languages[0] || 'en';
82
94
  const body = await event.request.json().catch(() => null);
83
95
  if (!body?.data || typeof body.data !== 'object') {
84
96
  return json({ error: 'Request body must contain "data" object' }, { status: 400 });
85
97
  }
86
- const version = await upsertDraftVersion(entry.id, body.data, { skipValidation: true });
98
+ const version = await upsertDraftVersion(entry.id, body.data, lang, { skipValidation: true });
87
99
  return json({
88
100
  id: entry.id,
89
101
  versionId: version.id,
@@ -1,2 +1,2 @@
1
1
  import { type RequestEvent } from '@sveltejs/kit';
2
- export declare function POST(_event: RequestEvent, id: string, action: string): Promise<Response>;
2
+ export declare function POST(event: RequestEvent, id: string, action: string): Promise<Response>;
@@ -1,26 +1,26 @@
1
1
  import { json } from '@sveltejs/kit';
2
- import { requireAuth } from '../../../remote/middleware/auth.js';
2
+ import { getCMS } from '../../../../core/cms.js';
3
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();
4
+ import { updateEntry, updateEntryVersion, unpublishEntryLang } from '../../../../core/server/entries/operations/update.js';
5
+ export async function POST(event, id, action) {
6
+ const user = event.locals.user;
7
+ const lang = event.url.searchParams.get('lang') || getCMS().languages[0] || 'en';
7
8
  switch (action) {
8
9
  case 'publish': {
9
10
  const entry = await getRawEntryOrThrow({ id, includeArchived: false });
10
- const draftVersion = entry.draftVersion;
11
+ const draftVersion = entry.draftVersions[lang];
11
12
  if (!draftVersion) {
12
13
  return json({ error: 'No draft version to publish' }, { status: 400 });
13
14
  }
14
- await updateEntry(id, {
15
- publishedVersionId: draftVersion.id,
15
+ await updateEntryVersion(draftVersion.id, {
16
16
  publishedAt: new Date(),
17
- publishedBy: user.id,
18
- archivedAt: null
17
+ publishedBy: user.id
19
18
  });
19
+ await updateEntry(id, { archivedAt: null });
20
20
  return json({ success: true, publishedVersionId: draftVersion.id });
21
21
  }
22
22
  case 'unpublish': {
23
- await unpublishEntry(id);
23
+ await unpublishEntryLang(id, lang);
24
24
  return json({ success: true });
25
25
  }
26
26
  case 'archive': {
@@ -0,0 +1,2 @@
1
+ import { type RequestEvent } from '@sveltejs/kit';
2
+ export declare function GET(_event: RequestEvent, id: string): Promise<Response>;
@@ -0,0 +1,9 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { getFile } from '../../../../core/server/media/operations/getFiles.js';
3
+ export async function GET(_event, id) {
4
+ const file = await getFile(id);
5
+ if (!file) {
6
+ return json({ error: 'Media file not found' }, { status: 404 });
7
+ }
8
+ return json(file);
9
+ }
@@ -1,2 +1,7 @@
1
1
  import { type RequestEvent } from '@sveltejs/kit';
2
+ import type { Field } from '../../../../types/fields.js';
3
+ export declare function generateTemplate(fields: Field[], languages: string[]): {
4
+ template: Record<string, unknown>;
5
+ meta: Record<string, string>;
6
+ };
2
7
  export declare function GET(event: RequestEvent): Promise<Response>;
@@ -10,9 +10,161 @@ function serializeField(field) {
10
10
  function serializeFields(fields) {
11
11
  return fields.map(serializeField);
12
12
  }
13
+ function localize(value, languages) {
14
+ const obj = {};
15
+ for (const lang of languages) {
16
+ obj[lang] = value;
17
+ }
18
+ return obj;
19
+ }
20
+ function generateFieldTemplate(field, languages) {
21
+ const wrap = (v, hint) => {
22
+ if (field.localized) {
23
+ return { value: localize(v, languages), hint: `${hint} (localized)` };
24
+ }
25
+ return { value: v, hint };
26
+ };
27
+ switch (field.type) {
28
+ case 'text':
29
+ case 'slug':
30
+ return wrap('', 'string');
31
+ case 'content':
32
+ return wrap({ type: 'doc', content: [] }, 'ProseMirror JSON document');
33
+ case 'number':
34
+ return wrap(0, 'number');
35
+ case 'boolean':
36
+ return { value: false, hint: 'boolean' };
37
+ case 'date':
38
+ return wrap('2024-01-01', 'ISO date string');
39
+ case 'datetime':
40
+ return wrap('2024-01-01T00:00:00Z', 'ISO datetime string');
41
+ case 'file':
42
+ if (field.multiple) {
43
+ return { value: ['<media-uuid-from-upload>'], hint: 'array of media UUIDs from POST /upload' };
44
+ }
45
+ return { value: '<media-uuid-from-upload>', hint: 'media UUID from POST /upload' };
46
+ case 'media':
47
+ if (field.multiple) {
48
+ return { value: ['<media-uuid-from-upload>'], hint: 'array of media UUIDs (image or video)' };
49
+ }
50
+ return { value: '<media-uuid-from-upload>', hint: 'media UUID (image or video) from POST /upload' };
51
+ case 'select': {
52
+ const sf = field;
53
+ const opts = sf.options.map((o) => o.value);
54
+ if (sf.multiple) {
55
+ return { value: opts.length ? [opts[0]] : [], hint: `array of: ${opts.join(' | ')}` };
56
+ }
57
+ return { value: opts[0] ?? '', hint: `one of: ${opts.join(' | ')}` };
58
+ }
59
+ case 'radio': {
60
+ const rf = field;
61
+ const opts = rf.options.map((o) => o.value);
62
+ return { value: opts[0] ?? '', hint: `one of: ${opts.join(' | ')}` };
63
+ }
64
+ case 'checkboxes': {
65
+ const cf = field;
66
+ const opts = cf.options.map((o) => o.value);
67
+ return { value: opts.length ? [opts[0]] : [], hint: `array of: ${opts.join(' | ')}` };
68
+ }
69
+ case 'relation': {
70
+ const rf = field;
71
+ if (rf.multiple) {
72
+ return { value: ['<entry-uuid>'], hint: `array of ${rf.collection} entry UUIDs` };
73
+ }
74
+ return { value: '<entry-uuid>', hint: `${rf.collection} entry UUID` };
75
+ }
76
+ case 'object': {
77
+ const of = field;
78
+ const { template, meta } = generateTemplate(of.fields, languages);
79
+ return { value: template, hint: `object — ${JSON.stringify(meta)}` };
80
+ }
81
+ case 'blocks': {
82
+ const bf = field;
83
+ const blocks = bf.of.map((block) => {
84
+ const { template } = generateTemplate(block.fields, languages);
85
+ return { _slug: block.slug, ...template };
86
+ });
87
+ return { value: blocks, hint: 'array of block objects with _slug' };
88
+ }
89
+ case 'array': {
90
+ const af = field;
91
+ const examples = { text: '', number: 0, url: { url: { [languages[0]]: '' } } };
92
+ return { value: [examples[af.of] ?? ''], hint: `array of ${af.of}` };
93
+ }
94
+ case 'seo':
95
+ return {
96
+ value: {
97
+ slug: localize('', languages),
98
+ title: localize('', languages),
99
+ description: localize('', languages),
100
+ ogImage: '',
101
+ keywords: localize('', languages),
102
+ canonicalUrl: localize('', languages),
103
+ customCode: localize('', languages)
104
+ },
105
+ hint: 'SEO data — slug/title required, ogImage = media UUID or empty'
106
+ };
107
+ case 'url':
108
+ return {
109
+ value: {
110
+ url: localize('', languages),
111
+ text: localize('', languages),
112
+ newTab: false
113
+ },
114
+ hint: 'URL field with optional text and newTab'
115
+ };
116
+ case 'custom':
117
+ return { value: null, hint: 'custom field — check plugin docs' };
118
+ default:
119
+ return { value: null, hint: 'unknown field type' };
120
+ }
121
+ }
122
+ export function generateTemplate(fields, languages) {
123
+ const template = {};
124
+ const meta = {};
125
+ for (const field of fields) {
126
+ const { value, hint } = generateFieldTemplate(field, languages);
127
+ template[field.slug] = value;
128
+ meta[field.slug] = hint;
129
+ }
130
+ return { template, meta };
131
+ }
13
132
  export async function GET(event) {
14
133
  const cms = getCMS();
15
134
  const path = event.params.restPath || '';
135
+ // GET /schema/collections/:slug/template
136
+ const templateMatch = path.match(/^collections\/([^/]+)\/template$/);
137
+ if (templateMatch) {
138
+ const slug = templateMatch[1];
139
+ const collection = cms.collections[slug];
140
+ if (!collection) {
141
+ return json({ error: `Collection "${slug}" not found` }, { status: 404 });
142
+ }
143
+ const fields = getFieldsFromConfig(collection);
144
+ const { template, meta } = generateTemplate(fields, cms.languages);
145
+ return json({
146
+ data: template,
147
+ publish: true,
148
+ _meta: meta,
149
+ _notes: 'POST this to /collections/' + slug + ' — set publish:true to auto-publish'
150
+ });
151
+ }
152
+ // GET /schema/singletons/:slug/template
153
+ const singletonTemplateMatch = path.match(/^singletons\/([^/]+)\/template$/);
154
+ if (singletonTemplateMatch) {
155
+ const slug = singletonTemplateMatch[1];
156
+ const single = cms.singles[slug];
157
+ if (!single) {
158
+ return json({ error: `Singleton "${slug}" not found` }, { status: 404 });
159
+ }
160
+ const fields = getFieldsFromConfig(single);
161
+ const { template, meta } = generateTemplate(fields, cms.languages);
162
+ return json({
163
+ data: template,
164
+ _meta: meta,
165
+ _notes: 'PUT this to /singletons/' + slug
166
+ });
167
+ }
16
168
  // GET /schema/collections/:slug
17
169
  const collectionMatch = path.match(/^collections\/([^/]+)$/);
18
170
  if (collectionMatch) {
@@ -1,3 +1,3 @@
1
1
  import { type RequestEvent } from '@sveltejs/kit';
2
- export declare function GET(_event: RequestEvent, slug: string): Promise<Response>;
2
+ export declare function GET(event: RequestEvent, slug: string): Promise<Response>;
3
3
  export declare function PUT(event: RequestEvent, slug: string): Promise<Response>;