includio-cms 0.7.2 → 0.13.1

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 (185) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/ROADMAP.md +54 -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 +35 -13
  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/client/users/users-page.svelte +5 -6
  35. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  36. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  37. package/dist/admin/components/fields/blocks-field.svelte +40 -19
  38. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  39. package/dist/admin/components/fields/object-field.svelte +7 -12
  40. package/dist/admin/components/fields/select-field.svelte +8 -2
  41. package/dist/admin/components/fields/seo-field.svelte +40 -93
  42. package/dist/admin/components/fields/simple-array-field.svelte +27 -16
  43. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  44. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  45. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  46. package/dist/admin/components/fields/url-field.svelte +61 -72
  47. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  48. package/dist/admin/components/media/file-preview.svelte +10 -1
  49. package/dist/admin/components/media/file-upload.svelte +5 -1
  50. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  51. package/dist/admin/components/media/files-list.svelte +12 -3
  52. package/dist/admin/components/media/media-library.svelte +109 -37
  53. package/dist/admin/components/media/media-selector.svelte +90 -16
  54. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  55. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  56. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  57. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
  58. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  59. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  60. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  61. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  62. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  63. package/dist/admin/components/tiptap/lang.js +170 -0
  64. package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
  65. package/dist/admin/components/tiptap/slash-command.js +27 -23
  66. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  67. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  68. package/dist/admin/remote/email.remote.d.ts +1 -0
  69. package/dist/admin/remote/email.remote.js +5 -0
  70. package/dist/admin/remote/entry.remote.d.ts +2 -5
  71. package/dist/admin/remote/entry.remote.js +23 -28
  72. package/dist/admin/remote/index.d.ts +1 -0
  73. package/dist/admin/remote/index.js +1 -0
  74. package/dist/admin/remote/media.remote.d.ts +15 -0
  75. package/dist/admin/remote/media.remote.js +18 -2
  76. package/dist/admin/remote/preview.remote.js +3 -1
  77. package/dist/admin/utils/entryLabel.js +9 -6
  78. package/dist/admin/utils/translationStatus.js +1 -2
  79. package/dist/cli/scaffold/admin.js +34 -2
  80. package/dist/cms/runtime/api.d.ts +16 -12
  81. package/dist/cms/runtime/api.js +7 -6
  82. package/dist/cms/runtime/remote.js +2 -2
  83. package/dist/cms/runtime/schemas.d.ts +1 -1
  84. package/dist/cms/runtime/schemas.js +1 -1
  85. package/dist/cms/runtime/types.d.ts +118 -112
  86. package/dist/cms/runtime/types.js +0 -12
  87. package/dist/core/cms.d.ts +3 -1
  88. package/dist/core/cms.js +30 -0
  89. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  90. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  91. package/dist/core/server/entries/operations/create.js +10 -4
  92. package/dist/core/server/entries/operations/get.d.ts +1 -0
  93. package/dist/core/server/entries/operations/get.js +186 -191
  94. package/dist/core/server/entries/operations/update.d.ts +6 -7
  95. package/dist/core/server/entries/operations/update.js +20 -38
  96. package/dist/core/server/fields/populateEntry.js +16 -52
  97. package/dist/core/server/fields/resolveImageFields.js +69 -120
  98. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  99. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  100. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  101. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  102. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  103. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  104. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  105. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  106. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  107. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  108. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  109. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  110. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  111. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  112. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  113. package/dist/core/server/generator/fields.d.ts +6 -0
  114. package/dist/core/server/generator/fields.js +43 -5
  115. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  116. package/dist/core/server/generator/formFields.js +1 -0
  117. package/dist/core/server/generator/generator.js +98 -30
  118. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  119. package/dist/core/server/media/operations/getFiles.js +6 -0
  120. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  121. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  122. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  123. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  124. package/dist/db-postgres/index.js +303 -37
  125. package/dist/db-postgres/schema/entry.d.ts +0 -94
  126. package/dist/db-postgres/schema/entry.js +0 -6
  127. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  128. package/dist/db-postgres/schema/entryVersion.js +1 -0
  129. package/dist/entity/index.d.ts +9 -4
  130. package/dist/entity/index.js +24 -24
  131. package/dist/files-local/index.js +43 -0
  132. package/dist/paraglide/messages/_index.d.ts +36 -3
  133. package/dist/paraglide/messages/_index.js +71 -3
  134. package/dist/paraglide/messages/en.d.ts +5 -0
  135. package/dist/paraglide/messages/en.js +14 -0
  136. package/dist/paraglide/messages/pl.d.ts +5 -0
  137. package/dist/paraglide/messages/pl.js +14 -0
  138. package/dist/sveltekit/components/preview.svelte +2 -326
  139. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  140. package/dist/sveltekit/server/index.d.ts +2 -1
  141. package/dist/sveltekit/server/index.js +2 -1
  142. package/dist/sveltekit/server/preview.js +4 -7
  143. package/dist/types/adapters/db.d.ts +15 -1
  144. package/dist/types/adapters/files.d.ts +6 -0
  145. package/dist/types/cms.d.ts +5 -0
  146. package/dist/types/entries.d.ts +54 -18
  147. package/dist/types/fields.d.ts +14 -24
  148. package/dist/types/formFields.d.ts +7 -2
  149. package/dist/types/index.d.ts +2 -2
  150. package/dist/types/layout.d.ts +0 -1
  151. package/dist/types/structured-content.d.ts +5 -0
  152. package/dist/updates/0.10.0/index.d.ts +2 -0
  153. package/dist/updates/0.10.0/index.js +15 -0
  154. package/dist/updates/0.11.0/index.d.ts +2 -0
  155. package/dist/updates/0.11.0/index.js +12 -0
  156. package/dist/updates/0.12.0/index.d.ts +2 -0
  157. package/dist/updates/0.12.0/index.js +12 -0
  158. package/dist/updates/0.13.0/index.d.ts +2 -0
  159. package/dist/updates/0.13.0/index.js +10 -0
  160. package/dist/updates/0.13.1/index.d.ts +2 -0
  161. package/dist/updates/0.13.1/index.js +20 -0
  162. package/dist/updates/0.7.3/index.d.ts +2 -0
  163. package/dist/updates/0.7.3/index.js +10 -0
  164. package/dist/updates/0.8.0/index.d.ts +2 -0
  165. package/dist/updates/0.8.0/index.js +18 -0
  166. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  167. package/dist/updates/0.8.0/migrate.js +101 -0
  168. package/dist/updates/0.9.0/index.d.ts +2 -0
  169. package/dist/updates/0.9.0/index.js +38 -0
  170. package/dist/updates/index.js +9 -1
  171. package/package.json +7 -6
  172. package/dist/admin/components/fields/image-field.svelte +0 -198
  173. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  174. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  175. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  176. package/dist/admin/components/tiptap.svelte +0 -11
  177. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  178. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  179. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
  180. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  181. package/dist/paraglide/messages/hello_world.js +0 -33
  182. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  183. package/dist/paraglide/messages/login_hello.js +0 -34
  184. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  185. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -66,37 +66,53 @@ export async function getImageStyles(field, val) {
66
66
  const stylesArr = expandStyleFormats(rawStyles, originalFormat);
67
67
  const [styles, blurDataUrl] = await Promise.all([
68
68
  Promise.all(stylesArr.map(async (style) => {
69
- const styleDbData = await getImageStyle(val.id, style);
70
- const result = {
71
- url: styleDbData.url,
72
- media: styleDbData.media,
73
- mimeType: styleDbData.mimeType
74
- };
75
- // srcset expansion: generate width variants
76
- if (style.srcset && style.srcset.length > 0 && val.width) {
77
- const widths = style.srcset.filter((w) => w <= (val.width ?? 0));
78
- if (widths.length > 0) {
79
- const variants = await Promise.all(widths.map(async (w) => {
80
- const variantStyle = {
81
- ...style,
82
- name: `${style.name}_${w}w`,
83
- width: w,
84
- srcset: undefined,
85
- sizes: undefined
86
- };
87
- const variantData = await getImageStyle(val.id, variantStyle);
88
- return `${variantData.url} ${w}w`;
89
- }));
90
- result.srcset = variants.join(', ');
91
- result.sizes = style.sizes;
69
+ try {
70
+ const styleDbData = await getImageStyle(val.id, style);
71
+ const result = {
72
+ url: styleDbData.url,
73
+ media: styleDbData.media,
74
+ mimeType: styleDbData.mimeType
75
+ };
76
+ // srcset expansion: generate width variants
77
+ if (style.srcset && style.srcset.length > 0 && val.width) {
78
+ const widths = style.srcset.filter((w) => w <= (val.width ?? 0));
79
+ if (widths.length > 0) {
80
+ const variants = await Promise.all(widths.map(async (w) => {
81
+ try {
82
+ const variantStyle = {
83
+ ...style,
84
+ name: `${style.name}_${w}w`,
85
+ width: w,
86
+ srcset: undefined,
87
+ sizes: undefined
88
+ };
89
+ const variantData = await getImageStyle(val.id, variantStyle);
90
+ return `${variantData.url} ${w}w`;
91
+ }
92
+ catch (e) {
93
+ console.warn(`[CMS] Failed to generate srcset variant ${w}w for style "${style.name}" of media ${val.id}:`, e);
94
+ return null;
95
+ }
96
+ }));
97
+ const validVariants = variants.filter((v) => v !== null);
98
+ if (validVariants.length > 0) {
99
+ result.srcset = validVariants.join(', ');
100
+ result.sizes = style.sizes;
101
+ }
102
+ }
92
103
  }
104
+ return [style.name, result];
105
+ }
106
+ catch (e) {
107
+ console.warn(`[CMS] Failed to generate image style "${style.name}" for media ${val.id}:`, e);
108
+ return null;
93
109
  }
94
- return [style.name, result];
95
110
  })),
96
111
  ensureBlurDataUrl(val)
97
112
  ]);
113
+ const validStyles = styles.filter((s) => s !== null);
98
114
  return {
99
- styles: Object.fromEntries(styles),
115
+ styles: Object.fromEntries(validStyles),
100
116
  blurDataUrl
101
117
  };
102
118
  }
@@ -9,12 +9,7 @@ export async function resolveMediaWithStyles(mediaIds, styles) {
9
9
  return {};
10
10
  const cms = getCMS();
11
11
  const files = await cms.databaseAdapter.getMediaFiles({ data: { ids: mediaIds } });
12
- // Synthetic ImageField to pass to getImageStyles
13
- const syntheticField = {
14
- type: 'image',
15
- slug: '_resolved',
16
- styles: styles ?? []
17
- };
12
+ const syntheticField = { styles: styles ?? [] };
18
13
  const entries = await Promise.all(files.map(async (file) => {
19
14
  const { styles: resolvedStyles, blurDataUrl } = await getImageStyles(syntheticField, file);
20
15
  return [file.id, { data: file, styles: resolvedStyles, blurDataUrl }];
@@ -1,9 +1,33 @@
1
1
  import { getCMS } from '../../../../cms.js';
2
+ const PRIVATE_FILE_PREFIX = '/admin/api/media/private/';
3
+ async function cleanupPrivateFiles(data) {
4
+ const adapter = getCMS().filesAdapter;
5
+ if (!adapter.deletePrivateFile)
6
+ return;
7
+ for (const value of Object.values(data)) {
8
+ if (typeof value === 'string' && value.startsWith(PRIVATE_FILE_PREFIX)) {
9
+ const filename = value.slice(PRIVATE_FILE_PREFIX.length);
10
+ if (filename && !filename.includes('/') && !filename.includes('..')) {
11
+ try {
12
+ await adapter.deletePrivateFile(filename);
13
+ }
14
+ catch {
15
+ // best-effort cleanup
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
2
21
  export const deleteFormSubmission = async (id) => {
3
- return getCMS().databaseAdapter.deleteFormSubmission(id);
22
+ const cms = getCMS();
23
+ const submission = await cms.databaseAdapter.getFormSubmission(id);
24
+ if (submission?.data) {
25
+ await cleanupPrivateFiles(submission.data);
26
+ }
27
+ return cms.databaseAdapter.deleteFormSubmission(id);
4
28
  };
5
29
  export const deleteFormSubmissions = async (ids) => {
6
30
  for (const id of ids) {
7
- await getCMS().databaseAdapter.deleteFormSubmission(id);
31
+ await deleteFormSubmission(id);
8
32
  }
9
33
  };
@@ -0,0 +1,2 @@
1
+ import type { FormField } from '../../../../../types/formFields.js';
2
+ export declare function parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>;
@@ -0,0 +1,75 @@
1
+ import { uploadPrivateFile } from '../../../media/operations/uploadPrivateFile.js';
2
+ const SAFE_MIME_ALLOWLIST = new Set([
3
+ 'image/jpeg',
4
+ 'image/png',
5
+ 'image/gif',
6
+ 'image/webp',
7
+ 'application/pdf'
8
+ ]);
9
+ const MAGIC_BYTES = [
10
+ ['image/jpeg', [0xff, 0xd8, 0xff]],
11
+ ['image/png', [0x89, 0x50, 0x4e, 0x47]],
12
+ ['image/gif', [0x47, 0x49, 0x46]],
13
+ ['image/webp', [0x52, 0x49, 0x46, 0x46]], // RIFF
14
+ ['application/pdf', [0x25, 0x50, 0x44, 0x46]] // %PDF
15
+ ];
16
+ const DEFAULT_MAX_SIZE = 10 * 1024 * 1024; // 10MB
17
+ const HARD_MAX_SIZE = 25 * 1024 * 1024; // 25MB
18
+ function validateMagicBytes(buffer, declaredMime) {
19
+ for (const [mime, bytes] of MAGIC_BYTES) {
20
+ if (mime !== declaredMime)
21
+ continue;
22
+ if (buffer.length < bytes.length)
23
+ return false;
24
+ return bytes.every((b, i) => buffer[i] === b);
25
+ }
26
+ return false;
27
+ }
28
+ export async function parseFormDataForSubmission(formData, fields) {
29
+ const result = {};
30
+ const fileFields = new Map(fields.filter((f) => f.type === 'file').map((f) => [f.slug, f]));
31
+ for (const field of fields) {
32
+ const raw = formData.get(field.slug);
33
+ if (fileFields.has(field.slug)) {
34
+ if (!raw || !(raw instanceof File) || raw.size === 0) {
35
+ result[field.slug] = '';
36
+ continue;
37
+ }
38
+ const file = raw;
39
+ const fieldConfig = fileFields.get(field.slug);
40
+ const maxSize = Math.min(fieldConfig.maxSize ?? DEFAULT_MAX_SIZE, HARD_MAX_SIZE);
41
+ if (file.size > maxSize) {
42
+ throw new Error(`File "${field.slug}" exceeds max size (${Math.round(maxSize / 1024 / 1024)}MB)`);
43
+ }
44
+ if (!SAFE_MIME_ALLOWLIST.has(file.type)) {
45
+ throw new Error(`File "${field.slug}" has disallowed MIME type: ${file.type}`);
46
+ }
47
+ if (fieldConfig.accept) {
48
+ const accepted = fieldConfig.accept.split(',').map((s) => s.trim());
49
+ const mimeOk = accepted.some((pattern) => {
50
+ if (pattern.endsWith('/*')) {
51
+ return file.type.startsWith(pattern.replace('/*', '/'));
52
+ }
53
+ return file.type === pattern;
54
+ });
55
+ if (!mimeOk) {
56
+ throw new Error(`File "${field.slug}" type ${file.type} not in accept list: ${fieldConfig.accept}`);
57
+ }
58
+ }
59
+ const buffer = new Uint8Array(await file.arrayBuffer());
60
+ if (!validateMagicBytes(buffer, file.type)) {
61
+ throw new Error(`File "${field.slug}" content does not match declared MIME type ${file.type}`);
62
+ }
63
+ const privateFile = await uploadPrivateFile(file);
64
+ result[field.slug] = privateFile.url;
65
+ continue;
66
+ }
67
+ if (field.type === 'checkbox') {
68
+ result[field.slug] = raw === 'true' || raw === '1';
69
+ }
70
+ else {
71
+ result[field.slug] = typeof raw === 'string' ? raw : '';
72
+ }
73
+ }
74
+ return result;
75
+ }
@@ -2,3 +2,9 @@ import type { Field } from '../../../types/fields.js';
2
2
  import type { CustomFieldDefinition } from '../../../types/plugins.js';
3
3
  export declare function setGeneratorCustomFields(customFields: Map<string, CustomFieldDefinition>): void;
4
4
  export declare function generateTsTypeFromFields(fields: Field[]): string;
5
+ export declare function generateFlatTsTypeFromFields(fields: Field[]): string;
6
+ export interface InlineBlockTypeDef {
7
+ slug: string;
8
+ fields: Field[];
9
+ }
10
+ export declare function generateInlineBlockTypeString(block: InlineBlockTypeDef): string;
@@ -6,7 +6,6 @@ export function setGeneratorCustomFields(customFields) {
6
6
  function getFieldTypeAsString(field) {
7
7
  switch (field.type) {
8
8
  case 'text':
9
- case 'richtext':
10
9
  case 'date':
11
10
  case 'datetime':
12
11
  return 'string';
@@ -14,10 +13,6 @@ function getFieldTypeAsString(field) {
14
13
  return 'StructuredContentDoc';
15
14
  case 'radio':
16
15
  return field.options.map((opt) => `'${opt.value.trim()}'`).join(' | ');
17
- case 'image': {
18
- const base = field.multiple ? 'ImageFieldData[]' : 'ImageFieldData';
19
- return base + (field.required ? '' : ' | null');
20
- }
21
16
  case 'media': {
22
17
  const base = field.multiple
23
18
  ? '(ImageFieldData | VideoFieldData)[]'
@@ -99,3 +94,46 @@ export function generateTsTypeFromFields(fields) {
99
94
  ${fields.map((f) => ` ${f.slug}${f.required || f.type === 'seo' ? '' : '?'}: ${getFieldTypeAsString(f)}`).join(';\n')};
100
95
  }`;
101
96
  }
97
+ export function generateFlatTsTypeFromFields(fields) {
98
+ return `{
99
+ ${fields.map((f) => ` ${f.slug}${f.required || f.type === 'seo' ? '' : '?'}: ${getFlatFieldTypeAsString(f)}`).join(';\n')};
100
+ }`;
101
+ }
102
+ function getFlatFieldTypeAsString(field) {
103
+ switch (field.type) {
104
+ case 'media': {
105
+ const base = field.multiple
106
+ ? '(ImageFieldData | VideoFieldData)[]'
107
+ : 'ImageFieldData | VideoFieldData';
108
+ return base + (field.required ? '' : ' | null');
109
+ }
110
+ case 'relation': {
111
+ const name = toPascalCase(field.collection);
112
+ return field.multiple ? `${name}[]` : name;
113
+ }
114
+ case 'object':
115
+ return `{
116
+ _slug: '${field.slug}';
117
+ ${field.fields.map((f) => `${f.slug}${f.required ? '' : '?'}: ${getFlatFieldTypeAsString(f)}`).join(';\n')};
118
+ }`;
119
+ case 'blocks':
120
+ return `
121
+ (
122
+ ${field.of
123
+ .map((f) => `
124
+ {
125
+ _slug: '${f.slug}';
126
+ ${f.fields.map((f) => `${f.slug}${f.required ? '' : '?'}: ${getFlatFieldTypeAsString(f)}`).join(';\n')};
127
+ }
128
+ `)
129
+ .join(' | ')}
130
+ )[]`;
131
+ default:
132
+ return getFieldTypeAsString(field);
133
+ }
134
+ }
135
+ export function generateInlineBlockTypeString(block) {
136
+ return `{
137
+ ${block.fields.map((f) => ` ${f.slug}${f.required ? '' : '?'}: ${getFlatFieldTypeAsString(f)}`).join(';\n')};
138
+ }`;
139
+ }
@@ -50,6 +50,16 @@ export function generateZodSchemaFromFormFieldAsString(field) {
50
50
  code = `z.enum([${values.join(', ')}])`;
51
51
  break;
52
52
  }
53
+ case 'file': {
54
+ if (field.required) {
55
+ const msg = getErrorMsg(field);
56
+ code = 'z.string()' + (msg ? `.min(1, ${msg})` : '.min(1)');
57
+ }
58
+ else {
59
+ code = "z.string().optional().default('')";
60
+ }
61
+ break;
62
+ }
53
63
  default:
54
64
  code = 'z.any()';
55
65
  }
@@ -3,6 +3,7 @@ function getFormFieldTypeAsString(field) {
3
3
  case 'text':
4
4
  case 'email':
5
5
  case 'textarea':
6
+ case 'file':
6
7
  return 'string';
7
8
  case 'checkbox':
8
9
  return 'boolean';
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { generateTsTypeFromFields, setGeneratorCustomFields } from './fields.js';
3
+ import { generateTsTypeFromFields, generateFlatTsTypeFromFields, generateInlineBlockTypeString, setGeneratorCustomFields } from './fields.js';
4
4
  import { generateTsTypeFromFormFields } from './formFields.js';
5
5
  import { generateZodSchemaStringFromFormFieldsAsString } from './formFieldSchemaToString.js';
6
6
  import { toPascalCase } from './utils.js';
@@ -16,13 +16,18 @@ function generateTypesStringForRecords(type, records) {
16
16
  code += `
17
17
  ${records
18
18
  .map((single) => {
19
+ const fieldsType = generateFlatTsTypeFromFields(getFieldsFromConfig(single));
20
+ // Strip outer braces to inline fields into the interface
21
+ const innerFields = fieldsType.replace(/^\s*\{/, '').replace(/\}\s*$/, '');
19
22
  return `
20
23
  export interface ${toPascalCase(single.slug)} {
21
- id: string;
22
- slug: string;
23
- data: ${generateTsTypeFromFields(getFieldsFromConfig(single))}
24
- publishedAt: Date | null;
25
- };
24
+ _id: string;
25
+ _slug: string;
26
+ _type: string;
27
+ _publishedAt: Date | null;
28
+ _url?: string;
29
+ ${innerFields}
30
+ }
26
31
  `;
27
32
  })
28
33
  .join('\n')}
@@ -67,11 +72,43 @@ function generateTypesStringForForms(records) {
67
72
  `;
68
73
  return code;
69
74
  }
75
+ function collectInlineBlocks(fields) {
76
+ const blocks = new Map();
77
+ for (const field of fields) {
78
+ if (field.type === 'content') {
79
+ const cf = field;
80
+ if (cf.inlineBlocks) {
81
+ for (const block of cf.inlineBlocks) {
82
+ if (!blocks.has(block.slug)) {
83
+ blocks.set(block.slug, block.fields);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ if (field.type === 'object') {
89
+ const nested = collectInlineBlocks(field.fields);
90
+ for (const [slug, fields] of nested) {
91
+ if (!blocks.has(slug))
92
+ blocks.set(slug, fields);
93
+ }
94
+ }
95
+ if (field.type === 'blocks') {
96
+ for (const obj of field.of) {
97
+ const nested = collectInlineBlocks(obj.fields);
98
+ for (const [slug, fields] of nested) {
99
+ if (!blocks.has(slug))
100
+ blocks.set(slug, fields);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ return blocks;
106
+ }
70
107
  function generateTypes(config) {
71
108
  const cmsDir = join(process.cwd(), 'src/lib/cms/runtime');
72
109
  const filePath = join(cmsDir, 'types.ts');
73
110
  let code = `// This file is auto-generated. Do not edit directly.\n\n`;
74
- code += `import type { MediaFile, ImageFieldData, VideoFieldData, StructuredContentDoc } from 'includio-cms/types';\n\n`;
111
+ code += `import type { MediaFile, ImageFieldData, VideoFieldData, StructuredContentDoc, SCTypedInlineBlock } from 'includio-cms/types';\n\n`;
75
112
  if (config.singles && config.singles.length > 0) {
76
113
  code += generateTypesStringForRecords('single', Object.values(config.singles));
77
114
  }
@@ -81,6 +118,41 @@ function generateTypes(config) {
81
118
  if (config.forms && config.forms.length > 0) {
82
119
  code += generateTypesStringForForms(Object.values(config.forms));
83
120
  }
121
+ // Inline block types
122
+ const allInlineBlocks = new Map();
123
+ const allRecords = [...(config.singles ?? []), ...(config.collections ?? [])];
124
+ for (const record of allRecords) {
125
+ const blocks = collectInlineBlocks(getFieldsFromConfig(record));
126
+ for (const [slug, fields] of blocks) {
127
+ if (!allInlineBlocks.has(slug))
128
+ allInlineBlocks.set(slug, fields);
129
+ }
130
+ }
131
+ if (allInlineBlocks.size > 0) {
132
+ code += `\n// Inline block data types\n`;
133
+ for (const [slug, fields] of allInlineBlocks) {
134
+ const typeName = toPascalCase(slug) + 'BlockData';
135
+ code += `export type ${typeName} = ${generateInlineBlockTypeString({ slug, fields })};\n\n`;
136
+ }
137
+ }
138
+ // BlockDataMap per content field
139
+ for (const record of allRecords) {
140
+ const fields = getFieldsFromConfig(record);
141
+ for (const field of fields) {
142
+ if (field.type === 'content') {
143
+ const cf = field;
144
+ if (cf.inlineBlocks && cf.inlineBlocks.length > 0) {
145
+ const mapName = toPascalCase(record.slug) + toPascalCase(field.slug) + 'BlockDataMap';
146
+ code += `\nexport type ${mapName} = {\n`;
147
+ for (const block of cf.inlineBlocks) {
148
+ const key = block.slug.includes('-') ? `'${block.slug}'` : block.slug;
149
+ code += ` ${key}: ${toPascalCase(block.slug)}BlockData;\n`;
150
+ }
151
+ code += `};\n`;
152
+ }
153
+ }
154
+ }
155
+ }
84
156
  code += `\n export type SiteLanguage = ${config.languages.map((lang) => JSON.stringify(lang)).join(' | ')};\n\n`;
85
157
  writeFileSync(filePath, code);
86
158
  }
@@ -91,61 +163,57 @@ function generateAPI(config) {
91
163
  code += `
92
164
 
93
165
  import type { SingleEntryMap, SingleSlug, CollectionEntryMap, CollectionSlug, FormEntryMap, SiteLanguage } from './types';
94
- import { getEntry, getEntries, createFormSubmission } from 'includio-cms/sveltekit/server';
166
+ import { getEntry, getEntries, countEntries, createFormSubmission } from 'includio-cms/sveltekit/server';
95
167
 
96
168
  `;
97
169
  code += `
98
170
 
99
- interface GetEntryQueryOptions {
171
+ interface GetEntryOptions {
100
172
  id?: string;
101
173
  status?: 'draft' | 'published' | 'scheduled' | 'archived';
102
174
  dataValues?: Record<string, unknown>;
103
- }
104
-
105
- interface GetEntryOptions {
106
-
107
175
  `;
108
176
  if (config.languages && config.languages.length > 0) {
109
177
  code += `
110
- /**
111
- * Language code to fetch the entry in. Defaults to the first language in the CMS config.
112
- */
113
178
  language?: SiteLanguage;
114
179
  `;
115
180
  }
116
181
  code += `
117
-
118
182
  }
119
-
183
+
184
+ interface GetEntriesOptions extends GetEntryOptions {
185
+ ids?: string[];
186
+ dataLike?: Record<string, unknown>;
187
+ dataILikeOr?: Record<string, unknown>;
188
+ orderBy?: { column: 'createdAt' | 'updatedAt' | 'sortOrder'; direction: 'asc' | 'desc' };
189
+ dataOrderBy?: { field: string; direction: 'asc' | 'desc' };
190
+ limit?: number;
191
+ offset?: number;
192
+ }
193
+
120
194
  `;
121
195
  code += `
122
196
 
123
- export async function getSingleEntry<K extends SingleSlug>(slug: K, data: GetEntryQueryOptions, options: GetEntryOptions): Promise<SingleEntryMap[K] | null> {
197
+ export async function getSingleEntry<K extends SingleSlug>(slug: K, options: GetEntryOptions = {}): Promise<SingleEntryMap[K] | null> {
124
198
  return (await getEntry({
125
- ...data,
126
199
  slug,
127
200
  ...options
128
201
  })) as unknown as SingleEntryMap[K] | null;
129
202
  }
130
203
 
131
- export async function getCollectionEntry<K extends CollectionSlug>(slug: K, data: GetEntryQueryOptions, options: GetEntryOptions): Promise<CollectionEntryMap[K] | null> {
204
+ export async function getCollectionEntry<K extends CollectionSlug>(slug: K, options: GetEntryOptions = {}): Promise<CollectionEntryMap[K] | null> {
132
205
  return (await getEntry({
133
- ...data,
134
206
  slug,
135
207
  ...options
136
208
  })) as unknown as CollectionEntryMap[K] | null;
137
209
  }
138
210
 
139
- interface GetEntriesQueryOptions {
140
- ids?: string[];
141
- status?: 'draft' | 'published' | 'scheduled' | 'archived';
142
- dataValues?: Record<string, unknown>;
143
- dataLike?: Record<string, unknown>;
144
- orderBy?: { column: 'createdAt' | 'updatedAt' | 'sortOrder'; direction: 'asc' | 'desc' };
211
+ export async function getCollectionEntries<K extends CollectionSlug>(slug: K, options: GetEntriesOptions = {}): Promise<CollectionEntryMap[K][]> {
212
+ return (await getEntries({ slug, ...options })) as unknown as CollectionEntryMap[K][];
145
213
  }
146
214
 
147
- export async function getCollectionEntries<K extends CollectionSlug>(slug: K, options: GetEntryOptions & GetEntriesQueryOptions = {}): Promise<CollectionEntryMap[K][]> {
148
- return (await getEntries({ slug, ...options })) as unknown as CollectionEntryMap[K][];
215
+ export async function countCollectionEntries<K extends CollectionSlug>(slug: K, options: Omit<GetEntriesOptions, 'limit' | 'offset' | 'orderBy'> = {}): Promise<number> {
216
+ return countEntries({ slug, ...options });
149
217
  }
150
218
 
151
219
  ${config.forms && config.forms.length > 0
@@ -1,4 +1,9 @@
1
1
  import type { GetMediaFilesOptions } from '../../../../types/adapters/db.js';
2
2
  import type { MediaFile } from '../../../../types/media.js';
3
3
  export declare function getFiles(options: GetMediaFilesOptions): Promise<MediaFile[]>;
4
+ export declare function countFiles(options: GetMediaFilesOptions): Promise<number>;
5
+ export declare function getMediaTagsWithCounts(): Promise<{
6
+ tag: import("../../../../types/media.js").MediaTag;
7
+ count: number;
8
+ }[]>;
4
9
  export declare function getFile(fileId: string): Promise<MediaFile | null>;
@@ -3,6 +3,12 @@ import { z } from 'zod';
3
3
  export async function getFiles(options) {
4
4
  return getCMS().databaseAdapter.getMediaFiles(options);
5
5
  }
6
+ export async function countFiles(options) {
7
+ return getCMS().databaseAdapter.countMediaFiles(options);
8
+ }
9
+ export async function getMediaTagsWithCounts() {
10
+ return getCMS().databaseAdapter.getMediaTagsWithCounts();
11
+ }
6
12
  const fileSchema = z.string().uuid();
7
13
  export async function getFile(fileId) {
8
14
  const parsedFileId = fileSchema.safeParse(fileId);
@@ -0,0 +1,4 @@
1
+ export declare function uploadPrivateFile(file: File): Promise<{
2
+ filename: string;
3
+ url: string;
4
+ }>;
@@ -0,0 +1,8 @@
1
+ import { getCMS } from '../../../cms.js';
2
+ export async function uploadPrivateFile(file) {
3
+ const adapter = getCMS().filesAdapter;
4
+ if (!adapter.uploadPrivateFile) {
5
+ throw new Error('Private file uploads not supported by current files adapter');
6
+ }
7
+ return adapter.uploadPrivateFile(file);
8
+ }
@@ -0,0 +1,16 @@
1
+ export type BatchProgress = {
2
+ type: 'progress' | 'error' | 'done';
3
+ total: number;
4
+ processed: number;
5
+ created: number;
6
+ skipped: number;
7
+ currentFile?: string;
8
+ error?: string;
9
+ };
10
+ export declare function batchGenerateAllStyles(signal?: AbortSignal): AsyncGenerator<BatchProgress>;
11
+ export declare function getStylesStatus(): Promise<{
12
+ processableImages: number;
13
+ expectedStyles: number;
14
+ existingStyles: number;
15
+ missingStyles: number;
16
+ }>;