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.
- package/CHANGELOG.md +128 -0
- package/ROADMAP.md +54 -2
- package/dist/admin/api/generate-styles.d.ts +2 -0
- package/dist/admin/api/generate-styles.js +32 -0
- package/dist/admin/api/handler.js +33 -0
- package/dist/admin/api/media-gc.js +10 -4
- package/dist/admin/api/rest/handler.js +17 -0
- package/dist/admin/api/rest/routes/collections.js +25 -13
- package/dist/admin/api/rest/routes/entries.d.ts +1 -1
- package/dist/admin/api/rest/routes/entries.js +10 -10
- package/dist/admin/api/rest/routes/media.d.ts +2 -0
- package/dist/admin/api/rest/routes/media.js +9 -0
- package/dist/admin/api/rest/routes/schema.d.ts +5 -0
- package/dist/admin/api/rest/routes/schema.js +152 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
- package/dist/admin/api/rest/routes/singletons.js +8 -7
- package/dist/admin/api/rest/routes/upload.d.ts +2 -0
- package/dist/admin/api/rest/routes/upload.js +28 -0
- package/dist/admin/api/upload.js +13 -0
- package/dist/admin/client/collection/collection-entries.svelte +35 -13
- package/dist/admin/client/entry/entry.svelte +21 -23
- package/dist/admin/client/entry/header/a11y-validator.js +2 -2
- package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
- package/dist/admin/client/entry/header/status-badge.svelte +2 -2
- package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
- package/dist/admin/client/entry/header/visibility.svelte +16 -10
- package/dist/admin/client/entry/utils.d.ts +3 -0
- package/dist/admin/client/entry/utils.js +22 -4
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
- package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
- package/dist/admin/client/index.d.ts +1 -0
- package/dist/admin/client/index.js +1 -0
- package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
- package/dist/admin/client/users/users-page.svelte +5 -6
- package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
- package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
- package/dist/admin/components/fields/blocks-field.svelte +40 -19
- package/dist/admin/components/fields/field-renderer.svelte +4 -8
- package/dist/admin/components/fields/object-field.svelte +7 -12
- package/dist/admin/components/fields/select-field.svelte +8 -2
- package/dist/admin/components/fields/seo-field.svelte +40 -93
- package/dist/admin/components/fields/simple-array-field.svelte +27 -16
- package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
- package/dist/admin/components/fields/url-field.svelte +61 -72
- package/dist/admin/components/layout/layout-renderer.svelte +10 -4
- package/dist/admin/components/media/file-preview.svelte +10 -1
- package/dist/admin/components/media/file-upload.svelte +5 -1
- package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
- package/dist/admin/components/media/files-list.svelte +12 -3
- package/dist/admin/components/media/media-library.svelte +109 -37
- package/dist/admin/components/media/media-selector.svelte +90 -16
- package/dist/admin/components/media/tag-sidebar.svelte +10 -6
- package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
- package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
- package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
- package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
- package/dist/admin/components/tiptap/inline-block-node.js +6 -5
- package/dist/admin/components/tiptap/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
- package/dist/admin/components/tiptap/slash-command.js +27 -23
- package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
- package/dist/admin/remote/email.remote.d.ts +1 -0
- package/dist/admin/remote/email.remote.js +5 -0
- package/dist/admin/remote/entry.remote.d.ts +2 -5
- package/dist/admin/remote/entry.remote.js +23 -28
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/media.remote.d.ts +15 -0
- package/dist/admin/remote/media.remote.js +18 -2
- package/dist/admin/remote/preview.remote.js +3 -1
- package/dist/admin/utils/entryLabel.js +9 -6
- package/dist/admin/utils/translationStatus.js +1 -2
- package/dist/cli/scaffold/admin.js +34 -2
- package/dist/cms/runtime/api.d.ts +16 -12
- package/dist/cms/runtime/api.js +7 -6
- package/dist/cms/runtime/remote.js +2 -2
- package/dist/cms/runtime/schemas.d.ts +1 -1
- package/dist/cms/runtime/schemas.js +1 -1
- package/dist/cms/runtime/types.d.ts +118 -112
- package/dist/cms/runtime/types.js +0 -12
- package/dist/core/cms.d.ts +3 -1
- package/dist/core/cms.js +30 -0
- package/dist/core/fields/fieldSchemaToTs.js +9 -15
- package/dist/core/fields/formFieldSchemaToTs.js +7 -0
- package/dist/core/server/entries/operations/create.js +10 -4
- package/dist/core/server/entries/operations/get.d.ts +1 -0
- package/dist/core/server/entries/operations/get.js +186 -191
- package/dist/core/server/entries/operations/update.d.ts +6 -7
- package/dist/core/server/entries/operations/update.js +20 -38
- package/dist/core/server/fields/populateEntry.js +16 -52
- package/dist/core/server/fields/resolveImageFields.js +69 -120
- package/dist/core/server/fields/resolveRelationFields.js +30 -51
- package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
- package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
- package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
- package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
- package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
- package/dist/core/server/fields/resolveUrlFields.js +47 -56
- package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
- package/dist/core/server/fields/utils/fixOrphans.js +12 -0
- package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
- package/dist/core/server/fields/utils/imageStyles.js +41 -25
- package/dist/core/server/fields/utils/resolveMedia.js +1 -6
- package/dist/core/server/forms/submissions/operations/delete.js +26 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
- package/dist/core/server/generator/fields.d.ts +6 -0
- package/dist/core/server/generator/fields.js +43 -5
- package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
- package/dist/core/server/generator/formFields.js +1 -0
- package/dist/core/server/generator/generator.js +98 -30
- package/dist/core/server/media/operations/getFiles.d.ts +5 -0
- package/dist/core/server/media/operations/getFiles.js +6 -0
- package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
- package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
- package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
- package/dist/db-postgres/index.js +303 -37
- package/dist/db-postgres/schema/entry.d.ts +0 -94
- package/dist/db-postgres/schema/entry.js +0 -6
- package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
- package/dist/db-postgres/schema/entryVersion.js +1 -0
- package/dist/entity/index.d.ts +9 -4
- package/dist/entity/index.js +24 -24
- package/dist/files-local/index.js +43 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/sveltekit/components/preview.svelte +2 -326
- package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
- package/dist/sveltekit/server/index.d.ts +2 -1
- package/dist/sveltekit/server/index.js +2 -1
- package/dist/sveltekit/server/preview.js +4 -7
- package/dist/types/adapters/db.d.ts +15 -1
- package/dist/types/adapters/files.d.ts +6 -0
- package/dist/types/cms.d.ts +5 -0
- package/dist/types/entries.d.ts +54 -18
- package/dist/types/fields.d.ts +14 -24
- package/dist/types/formFields.d.ts +7 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/layout.d.ts +0 -1
- package/dist/types/structured-content.d.ts +5 -0
- package/dist/updates/0.10.0/index.d.ts +2 -0
- package/dist/updates/0.10.0/index.js +15 -0
- package/dist/updates/0.11.0/index.d.ts +2 -0
- package/dist/updates/0.11.0/index.js +12 -0
- package/dist/updates/0.12.0/index.d.ts +2 -0
- package/dist/updates/0.12.0/index.js +12 -0
- package/dist/updates/0.13.0/index.d.ts +2 -0
- package/dist/updates/0.13.0/index.js +10 -0
- package/dist/updates/0.13.1/index.d.ts +2 -0
- package/dist/updates/0.13.1/index.js +20 -0
- package/dist/updates/0.7.3/index.d.ts +2 -0
- package/dist/updates/0.7.3/index.js +10 -0
- package/dist/updates/0.8.0/index.d.ts +2 -0
- package/dist/updates/0.8.0/index.js +18 -0
- package/dist/updates/0.8.0/migrate.d.ts +2 -0
- package/dist/updates/0.8.0/migrate.js +101 -0
- package/dist/updates/0.9.0/index.d.ts +2 -0
- package/dist/updates/0.9.0/index.js +38 -0
- package/dist/updates/index.js +9 -1
- package/package.json +7 -6
- package/dist/admin/components/fields/image-field.svelte +0 -198
- package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
- package/dist/admin/components/fields/richtext-field.svelte +0 -13
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
- package/dist/admin/components/tiptap.svelte +0 -11
- package/dist/admin/components/tiptap.svelte.d.ts +0 -6
- package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
- package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- 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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
31
|
+
await deleteFormSubmission(id);
|
|
8
32
|
}
|
|
9
33
|
};
|
|
@@ -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
|
}
|
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
148
|
-
return (
|
|
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,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
|
+
}>;
|