includio-cms 0.20.0 → 0.22.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.
- package/API.md +22 -21
- package/CHANGELOG.md +147 -0
- package/DOCS.md +1 -1
- package/README.md +138 -32
- package/ROADMAP.md +11 -4
- package/dist/admin/api/rest/handler.d.ts +13 -1
- package/dist/admin/api/rest/handler.js +13 -1
- package/dist/admin/api/rest/middleware/apiKey.js +9 -1
- package/dist/admin/api/rest/middleware/generateApiKey.d.ts +16 -0
- package/dist/admin/api/rest/middleware/generateApiKey.js +19 -0
- package/dist/admin/client/collection/collection-entries.svelte +1 -1
- package/dist/admin/client/collection/empty-state.svelte +1 -1
- package/dist/admin/client/collection/row-actions.svelte +3 -3
- package/dist/admin/client/collection/table-toolbar.svelte +3 -1
- package/dist/admin/client/entry/entry-header.svelte +3 -1
- package/dist/admin/client/users/create-user-dialog.svelte +4 -4
- package/dist/admin/client/users/delete-user-dialog.svelte +4 -2
- package/dist/admin/client/users/lang.d.ts +10 -2
- package/dist/admin/client/users/lang.js +10 -4
- package/dist/admin/client/users/users-page.svelte +3 -2
- package/dist/admin/components/media/file-upload.svelte +2 -0
- package/dist/ai-claude/index.d.ts +9 -1
- package/dist/ai-claude/index.js +9 -1
- package/dist/ai-openai/index.d.ts +9 -1
- package/dist/ai-openai/index.js +9 -1
- package/dist/cli/index.js +115 -13
- package/dist/cms/runtime/schema.d.ts +2 -0
- package/dist/cms/runtime/schema.js +4 -0
- package/dist/cms/runtime/types.d.ts +1 -1
- package/dist/core/cms.d.ts +13 -1
- package/dist/core/cms.js +13 -1
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.js +179 -0
- package/dist/core/server/consentLogs/operations/create.d.ts +13 -1
- package/dist/core/server/consentLogs/operations/create.js +13 -1
- package/dist/core/server/entries/operations/create.js +6 -1
- package/dist/core/server/entries/operations/get.js +14 -3
- package/dist/core/server/entries/operations/resolveEntry.d.ts +32 -1
- package/dist/core/server/entries/operations/resolveEntry.js +36 -4
- package/dist/core/server/entries/operations/update.js +5 -1
- package/dist/core/server/fields/utils/resolveMedia.d.ts +18 -1
- package/dist/core/server/fields/utils/resolveMedia.js +13 -1
- package/dist/core/server/forms/submissions/operations/create.d.ts +21 -1
- package/dist/core/server/forms/submissions/operations/create.js +18 -2
- package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +15 -1
- package/dist/core/server/forms/submissions/utils/parseMultipart.js +15 -1
- package/dist/core/server/media/operations/uploadFile.js +4 -3
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +3 -2
- package/dist/core/server/media/utils/generateAdminThumbnail.js +3 -2
- package/dist/core/server/media/utils/generateBlurDataUrl.js +2 -1
- package/dist/db-postgres/index.d.ts +10 -0
- package/dist/db-postgres/index.js +10 -0
- package/dist/email-nodemailer/index.d.ts +13 -1
- package/dist/email-nodemailer/index.js +13 -1
- package/dist/entity/index.d.ts +16 -1
- package/dist/entity/index.js +16 -1
- package/dist/files-local/index.d.ts +12 -1
- package/dist/files-local/index.js +12 -1
- package/dist/paraglide/messages/_index.d.ts +3 -36
- package/dist/paraglide/messages/_index.js +3 -71
- package/dist/paraglide/messages/hello_world.d.ts +5 -0
- package/dist/paraglide/messages/hello_world.js +33 -0
- package/dist/paraglide/messages/login_hello.d.ts +16 -0
- package/dist/paraglide/messages/login_hello.js +34 -0
- package/dist/paraglide/messages/login_please_login.d.ts +16 -0
- package/dist/paraglide/messages/login_please_login.js +34 -0
- package/dist/server/auth.d.ts +11 -0
- package/dist/server/auth.js +11 -0
- package/dist/server/security/csp.d.ts +16 -0
- package/dist/server/security/csp.js +33 -0
- package/dist/server/security/csrf.d.ts +13 -0
- package/dist/server/security/csrf.js +49 -0
- package/dist/server/security/index.d.ts +3 -0
- package/dist/server/security/index.js +3 -0
- package/dist/server/security/rate-limit.d.ts +44 -0
- package/dist/server/security/rate-limit.js +97 -0
- package/dist/server/utils/withTimeout.d.ts +21 -0
- package/dist/server/utils/withTimeout.js +37 -0
- package/dist/sveltekit/config.d.ts +67 -4
- package/dist/sveltekit/config.js +73 -4
- package/dist/sveltekit/server/handle.d.ts +15 -1
- package/dist/sveltekit/server/handle.js +22 -1
- package/dist/sveltekit/server/index.d.ts +1 -0
- package/dist/sveltekit/server/index.js +1 -0
- package/dist/sveltekit/server/layout.d.ts +12 -1
- package/dist/sveltekit/server/layout.js +12 -1
- package/dist/sveltekit/server/preview.d.ts +21 -1
- package/dist/sveltekit/server/preview.js +21 -1
- package/dist/types/cms.d.ts +4 -0
- package/dist/types/cms.schema.d.ts +452 -0
- package/dist/types/cms.schema.js +629 -0
- package/dist/updates/0.21.0/index.d.ts +2 -0
- package/dist/updates/0.21.0/index.js +55 -0
- package/dist/updates/0.22.0/index.d.ts +2 -0
- package/dist/updates/0.22.0/index.js +75 -0
- package/dist/updates/index.js +3 -1
- package/package.json +12 -2
- package/dist/paraglide/messages/en.d.ts +0 -5
- package/dist/paraglide/messages/en.js +0 -14
- package/dist/paraglide/messages/pl.d.ts +0 -5
- package/dist/paraglide/messages/pl.js +0 -14
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for CMS runtime failures. Carries a stable `code` and a
|
|
3
|
+
* `context` bag so callers (and clients) can branch on the failure mode without
|
|
4
|
+
* string-matching on `message`.
|
|
5
|
+
*
|
|
6
|
+
* `code` is a SCREAMING_SNAKE_CASE constant; see callers for the canonical list
|
|
7
|
+
* (`ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`,
|
|
8
|
+
* `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`).
|
|
9
|
+
*
|
|
10
|
+
* @public
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* try { await resolveEntry({ id }); }
|
|
14
|
+
* catch (e) {
|
|
15
|
+
* if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { ... }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class CmsError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
context;
|
|
22
|
+
constructor(code, message, context = {}, options) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
this.name = 'CmsError';
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.context = context;
|
|
27
|
+
}
|
|
28
|
+
toString() {
|
|
29
|
+
const ctx = Object.entries(this.context)
|
|
30
|
+
.filter(([, v]) => v !== undefined)
|
|
31
|
+
.map(([k, v]) => `${k}=${formatContextValue(v)}`)
|
|
32
|
+
.join(', ');
|
|
33
|
+
return ctx
|
|
34
|
+
? `[${this.code}] ${this.message} (${ctx})`
|
|
35
|
+
: `[${this.code}] ${this.message}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function formatContextValue(v) {
|
|
39
|
+
if (v === null)
|
|
40
|
+
return 'null';
|
|
41
|
+
if (typeof v === 'string')
|
|
42
|
+
return v;
|
|
43
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
44
|
+
return String(v);
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(v);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return '[unserializable]';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Thrown by {@link defineConfig} when the runtime config fails Zod validation.
|
|
54
|
+
* Aggregates ALL issues (not just the first) so the user can fix everything in
|
|
55
|
+
* one pass.
|
|
56
|
+
*
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export class ConfigValidationError extends CmsError {
|
|
60
|
+
issues;
|
|
61
|
+
constructor(issues) {
|
|
62
|
+
const header = `CMSConfig validation failed (${issues.length} issue${issues.length === 1 ? '' : 's'}):`;
|
|
63
|
+
const body = issues
|
|
64
|
+
.map((i) => {
|
|
65
|
+
const hint = i.hint ? ` — Hint: ${i.hint}` : '';
|
|
66
|
+
return ` - ${i.path}: ${i.message}${hint}`;
|
|
67
|
+
})
|
|
68
|
+
.join('\n');
|
|
69
|
+
super('CONFIG_VALIDATION_FAILED', `${header}\n${body}`, { issueCount: issues.length });
|
|
70
|
+
this.name = 'ConfigValidationError';
|
|
71
|
+
this.issues = issues;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render a Zod issue path (`(string | number)[]`) into JS-style notation:
|
|
76
|
+
* - string segments → `.foo`
|
|
77
|
+
* - numeric segments → `[3]`
|
|
78
|
+
* - leading string segment has no leading dot.
|
|
79
|
+
*/
|
|
80
|
+
export function formatIssuePath(path) {
|
|
81
|
+
if (path.length === 0)
|
|
82
|
+
return '<root>';
|
|
83
|
+
let out = '';
|
|
84
|
+
for (let i = 0; i < path.length; i++) {
|
|
85
|
+
const seg = path[i];
|
|
86
|
+
if (typeof seg === 'number') {
|
|
87
|
+
out += `[${seg}]`;
|
|
88
|
+
}
|
|
89
|
+
else if (i === 0) {
|
|
90
|
+
out += seg;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out += `.${seg}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Maps a config validation issue to a human-readable hint based on its path
|
|
100
|
+
* prefix and Zod issue `code`. Returns `undefined` when no specific hint
|
|
101
|
+
* applies (the bare message is good enough).
|
|
102
|
+
*/
|
|
103
|
+
function hintFor(path, code, message) {
|
|
104
|
+
const root = path[0];
|
|
105
|
+
const last = path[path.length - 1];
|
|
106
|
+
// languages[i].code — must be ISO-639-1 (with optional region)
|
|
107
|
+
if (root === 'languages' && last === 'code') {
|
|
108
|
+
return "use a 2-letter ISO code, optionally with region — e.g. 'en' or 'pl-PL'";
|
|
109
|
+
}
|
|
110
|
+
if (root === 'languages' && code === 'too_small') {
|
|
111
|
+
return 'CMSConfig.languages must contain at least one language';
|
|
112
|
+
}
|
|
113
|
+
if (root === 'languages' && code === 'invalid_type') {
|
|
114
|
+
return 'CMSConfig.languages must be an array of { code, label, default? }';
|
|
115
|
+
}
|
|
116
|
+
// adapters
|
|
117
|
+
if (root === 'db') {
|
|
118
|
+
return 'pass a DatabaseAdapter (e.g. import { postgresAdapter } from "includio-cms/db-postgres")';
|
|
119
|
+
}
|
|
120
|
+
if (root === 'files') {
|
|
121
|
+
return 'pass a FilesAdapter (e.g. import { localFilesAdapter } from "includio-cms/files-local")';
|
|
122
|
+
}
|
|
123
|
+
if (root === 'email') {
|
|
124
|
+
return 'pass an EmailAdapter or omit `email` (notifications will be skipped)';
|
|
125
|
+
}
|
|
126
|
+
if (root === 'ai') {
|
|
127
|
+
return 'pass an AIAdapter or omit `ai` (AI features will be disabled)';
|
|
128
|
+
}
|
|
129
|
+
// duplicate slugs / cross-field invariants surface as `custom` issues
|
|
130
|
+
if (code === 'custom' && /duplicate/i.test(message)) {
|
|
131
|
+
return 'each collection/single/form must have a unique `slug`';
|
|
132
|
+
}
|
|
133
|
+
if (code === 'custom' && /default locale/i.test(message)) {
|
|
134
|
+
return 'mark exactly one language with `default: true`, or rely on languages[0]';
|
|
135
|
+
}
|
|
136
|
+
if (code === 'custom' && /relation target/i.test(message)) {
|
|
137
|
+
return 'the relation field references a collection slug that is not declared in `collections`';
|
|
138
|
+
}
|
|
139
|
+
if (code === 'custom' && /apiKeys/i.test(message)) {
|
|
140
|
+
return 'apiKeys[].permissions must reference declared collection slugs';
|
|
141
|
+
}
|
|
142
|
+
// Field-level issues — recurse hint for known shapes
|
|
143
|
+
if (last === 'slug' && code === 'invalid_string') {
|
|
144
|
+
return 'slug must be a non-empty string of [a-z0-9-]';
|
|
145
|
+
}
|
|
146
|
+
if (last === 'type' && code === 'invalid_enum_value') {
|
|
147
|
+
return 'check field.type against the supported list (text, content, number, boolean, date, datetime, file, media, select, radio, checkboxes, relation, object, array, blocks, slug, seo, shop, url, custom)';
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Render a `ZodError` from entry/form data validation as a list of
|
|
153
|
+
* `path: message` lines (one per issue). Used by data-write operations to give
|
|
154
|
+
* callers a readable dump instead of `JSON.stringify(error.flatten())`.
|
|
155
|
+
*
|
|
156
|
+
* @public
|
|
157
|
+
*/
|
|
158
|
+
export function formatZodDataIssues(error) {
|
|
159
|
+
if (error.issues.length === 0)
|
|
160
|
+
return '<no issues>';
|
|
161
|
+
return error.issues
|
|
162
|
+
.map((i) => {
|
|
163
|
+
const path = formatIssuePath(i.path);
|
|
164
|
+
return `${path}: ${i.message}`;
|
|
165
|
+
})
|
|
166
|
+
.join('\n');
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Convert a `ZodError` into a {@link ConfigValidationError} with friendly,
|
|
170
|
+
* path-prefixed messages and per-issue hints.
|
|
171
|
+
*/
|
|
172
|
+
export function formatConfigError(error) {
|
|
173
|
+
const issues = error.issues.map((issue) => ({
|
|
174
|
+
path: formatIssuePath(issue.path),
|
|
175
|
+
message: issue.message,
|
|
176
|
+
hint: hintFor(issue.path, issue.code, issue.message)
|
|
177
|
+
}));
|
|
178
|
+
return new ConfigValidationError(issues);
|
|
179
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { ConsentLogData } from '../../../../types/consent.js';
|
|
2
2
|
/**
|
|
3
|
-
* Persists a CMP consent log entry via the database adapter.
|
|
3
|
+
* Persists a CMP consent log entry via the database adapter. Used by the CMP
|
|
4
|
+
* banner to record user consent decisions.
|
|
5
|
+
*
|
|
6
|
+
* @param data - The consent payload (categories accepted/rejected + audit metadata).
|
|
7
|
+
* @returns A `Promise` that resolves once the row is written.
|
|
4
8
|
* @public
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* await createConsentLog({
|
|
12
|
+
* sessionId: 'abc',
|
|
13
|
+
* categories: { necessary: true, analytics: false },
|
|
14
|
+
* ip: '203.0.113.42'
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
5
17
|
*/
|
|
6
18
|
export declare const createConsentLog: (data: ConsentLogData) => Promise<void>;
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
2
|
/**
|
|
3
|
-
* Persists a CMP consent log entry via the database adapter.
|
|
3
|
+
* Persists a CMP consent log entry via the database adapter. Used by the CMP
|
|
4
|
+
* banner to record user consent decisions.
|
|
5
|
+
*
|
|
6
|
+
* @param data - The consent payload (categories accepted/rejected + audit metadata).
|
|
7
|
+
* @returns A `Promise` that resolves once the row is written.
|
|
4
8
|
* @public
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* await createConsentLog({
|
|
12
|
+
* sessionId: 'abc',
|
|
13
|
+
* categories: { necessary: true, analytics: false },
|
|
14
|
+
* ip: '203.0.113.42'
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
5
17
|
*/
|
|
6
18
|
export const createConsentLog = async (data) => {
|
|
7
19
|
await getCMS().databaseAdapter.createConsentLog(data);
|
|
@@ -5,6 +5,7 @@ import z from 'zod';
|
|
|
5
5
|
import { _getDbEntryOrThrow as getDbEntryOrThrow } from './get.js';
|
|
6
6
|
import { generateZodSchemaFromFields } from '../../../fields/fieldSchemaToTs.js';
|
|
7
7
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
8
|
+
import { CmsError, formatZodDataIssues } from '../../../errors.js';
|
|
8
9
|
export const createEntrySchema = z.object({
|
|
9
10
|
slug: z.string(),
|
|
10
11
|
type: z.enum(entryTypes)
|
|
@@ -42,7 +43,11 @@ export const createEntryVersion = async (data, options) => {
|
|
|
42
43
|
});
|
|
43
44
|
const parsedData = schema.safeParse(data.data);
|
|
44
45
|
if (!parsedData.success) {
|
|
45
|
-
throw
|
|
46
|
+
throw new CmsError('INVALID_DATA', `Invalid data for entry version:\n${formatZodDataIssues(parsedData.error)}`, {
|
|
47
|
+
collection: entry.slug,
|
|
48
|
+
entryId: data.entryId,
|
|
49
|
+
lang: data.lang
|
|
50
|
+
});
|
|
46
51
|
}
|
|
47
52
|
validatedData = parsedData.data;
|
|
48
53
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
2
|
import { getAtPath } from '../../../../admin/utils/objectPath.js';
|
|
3
|
+
import { CmsError } from '../../../errors.js';
|
|
3
4
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
4
5
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
|
|
5
6
|
export const _getDbEntries = async (options) => {
|
|
@@ -19,7 +20,10 @@ export const _getDbEntry = async (options) => {
|
|
|
19
20
|
export const _getDbEntryOrThrow = async (options) => {
|
|
20
21
|
const entry = await _getDbEntry(options);
|
|
21
22
|
if (!entry) {
|
|
22
|
-
throw new
|
|
23
|
+
throw new CmsError('ENTRY_NOT_FOUND', 'Entry not found', {
|
|
24
|
+
collection: options.slug,
|
|
25
|
+
id: options.id
|
|
26
|
+
});
|
|
23
27
|
}
|
|
24
28
|
return entry;
|
|
25
29
|
};
|
|
@@ -84,7 +88,10 @@ export const _getRawEntry = async (options) => {
|
|
|
84
88
|
export const _getRawEntryOrThrow = async (options) => {
|
|
85
89
|
const entry = await _getRawEntry(options);
|
|
86
90
|
if (!entry) {
|
|
87
|
-
throw new
|
|
91
|
+
throw new CmsError('ENTRY_NOT_FOUND', 'Entry not found', {
|
|
92
|
+
collection: options.slug,
|
|
93
|
+
id: options.id
|
|
94
|
+
});
|
|
88
95
|
}
|
|
89
96
|
return entry;
|
|
90
97
|
};
|
|
@@ -156,7 +163,11 @@ export const _getDbEntryVersion = async (options) => {
|
|
|
156
163
|
export const _getDbEntryVersionOrThrow = async (options) => {
|
|
157
164
|
const version = await _getDbEntryVersion(options);
|
|
158
165
|
if (!version) {
|
|
159
|
-
throw new
|
|
166
|
+
throw new CmsError('ENTRY_VERSION_NOT_FOUND', 'Entry version not found', {
|
|
167
|
+
versionId: options.id,
|
|
168
|
+
entryId: options.entryId,
|
|
169
|
+
lang: options.lang
|
|
170
|
+
});
|
|
160
171
|
}
|
|
161
172
|
return version;
|
|
162
173
|
};
|
|
@@ -73,7 +73,15 @@ export type CountEntriesOptions = Omit<ResolveEntriesOptions, 'limit' | 'offset'
|
|
|
73
73
|
* - `status` defaults to `'published'`. See {@link ResolveStatus}.
|
|
74
74
|
* - `populate` controls relation depth + per-field opt-out. See {@link PopulateConfig}.
|
|
75
75
|
*
|
|
76
|
+
* @param opts - At minimum, an `id` or a `collection` slug.
|
|
77
|
+
* @returns The populated `Entry`, or `null` when nothing matches.
|
|
78
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when neither `id`
|
|
79
|
+
* nor `collection` is provided.
|
|
76
80
|
* @public
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* const post = await resolveEntry({ collection: 'posts', locale: 'en' });
|
|
84
|
+
* ```
|
|
77
85
|
*/
|
|
78
86
|
export declare function resolveEntry(opts: ResolveEntryOptions): Promise<Entry | null>;
|
|
79
87
|
/**
|
|
@@ -84,11 +92,34 @@ export declare function resolveEntry(opts: ResolveEntryOptions): Promise<Entry |
|
|
|
84
92
|
* - `filter.{dataValues, dataLike, dataILikeOr}` map onto adapter `getEntryVersions` filters.
|
|
85
93
|
* - `limit`/`offset` applied after status pick.
|
|
86
94
|
*
|
|
95
|
+
* @param opts - Must include `collection`. See {@link ResolveEntriesOptions}.
|
|
96
|
+
* @returns Array of populated `Entry` objects in the requested order.
|
|
97
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when `collection`
|
|
98
|
+
* is omitted.
|
|
87
99
|
* @public
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* const posts = await resolveEntries({
|
|
103
|
+
* collection: 'posts',
|
|
104
|
+
* locale: 'en',
|
|
105
|
+
* limit: 10,
|
|
106
|
+
* orderBy: { column: 'createdAt', direction: 'desc' }
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
88
109
|
*/
|
|
89
110
|
export declare function resolveEntries(opts: ResolveEntriesOptions): Promise<Entry[]>;
|
|
90
111
|
/**
|
|
91
|
-
* Count entries matching the same filters as `resolveEntries`, without
|
|
112
|
+
* Count entries matching the same filters as `resolveEntries`, without
|
|
113
|
+
* fetching or populating their data.
|
|
114
|
+
*
|
|
115
|
+
* @param opts - Must include `collection`. See {@link CountEntriesOptions}.
|
|
116
|
+
* @returns The number of entries matching the filters.
|
|
117
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when `collection`
|
|
118
|
+
* is omitted.
|
|
92
119
|
* @public
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* const total = await countEntries({ collection: 'posts', locale: 'en' });
|
|
123
|
+
* ```
|
|
93
124
|
*/
|
|
94
125
|
export declare function countEntries(opts: CountEntriesOptions): Promise<number>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
2
|
import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
3
3
|
import { getEntrySlugPath, getSlugFromEntryData, getEntryPath } from '../../fields/slugResolver.js';
|
|
4
|
+
import { CmsError } from '../../../errors.js';
|
|
4
5
|
function pickVersion(versions, status) {
|
|
5
6
|
const sorted = versions.slice().sort((a, b) => b.versionNumber - a.versionNumber);
|
|
6
7
|
const now = new Date();
|
|
@@ -51,11 +52,19 @@ async function buildEntry(dbEntry, version, ctx) {
|
|
|
51
52
|
* - `status` defaults to `'published'`. See {@link ResolveStatus}.
|
|
52
53
|
* - `populate` controls relation depth + per-field opt-out. See {@link PopulateConfig}.
|
|
53
54
|
*
|
|
55
|
+
* @param opts - At minimum, an `id` or a `collection` slug.
|
|
56
|
+
* @returns The populated `Entry`, or `null` when nothing matches.
|
|
57
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when neither `id`
|
|
58
|
+
* nor `collection` is provided.
|
|
54
59
|
* @public
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const post = await resolveEntry({ collection: 'posts', locale: 'en' });
|
|
63
|
+
* ```
|
|
55
64
|
*/
|
|
56
65
|
export async function resolveEntry(opts) {
|
|
57
66
|
if (!opts.id && !opts.collection) {
|
|
58
|
-
throw new
|
|
67
|
+
throw new CmsError('MISSING_REQUIRED_PARAM', 'resolveEntry: must provide id or collection', { op: 'resolveEntry', missing: 'id|collection' });
|
|
59
68
|
}
|
|
60
69
|
const cms = getCMS();
|
|
61
70
|
const locale = opts.locale ?? cms.languages[0];
|
|
@@ -99,11 +108,24 @@ export async function resolveEntry(opts) {
|
|
|
99
108
|
* - `filter.{dataValues, dataLike, dataILikeOr}` map onto adapter `getEntryVersions` filters.
|
|
100
109
|
* - `limit`/`offset` applied after status pick.
|
|
101
110
|
*
|
|
111
|
+
* @param opts - Must include `collection`. See {@link ResolveEntriesOptions}.
|
|
112
|
+
* @returns Array of populated `Entry` objects in the requested order.
|
|
113
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when `collection`
|
|
114
|
+
* is omitted.
|
|
102
115
|
* @public
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* const posts = await resolveEntries({
|
|
119
|
+
* collection: 'posts',
|
|
120
|
+
* locale: 'en',
|
|
121
|
+
* limit: 10,
|
|
122
|
+
* orderBy: { column: 'createdAt', direction: 'desc' }
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
103
125
|
*/
|
|
104
126
|
export async function resolveEntries(opts) {
|
|
105
127
|
if (!opts.collection) {
|
|
106
|
-
throw new
|
|
128
|
+
throw new CmsError('MISSING_REQUIRED_PARAM', 'resolveEntries: collection is required', { op: 'resolveEntries', missing: 'collection' });
|
|
107
129
|
}
|
|
108
130
|
const cms = getCMS();
|
|
109
131
|
const locale = opts.locale ?? cms.languages[0];
|
|
@@ -169,12 +191,22 @@ export async function resolveEntries(opts) {
|
|
|
169
191
|
return results;
|
|
170
192
|
}
|
|
171
193
|
/**
|
|
172
|
-
* Count entries matching the same filters as `resolveEntries`, without
|
|
194
|
+
* Count entries matching the same filters as `resolveEntries`, without
|
|
195
|
+
* fetching or populating their data.
|
|
196
|
+
*
|
|
197
|
+
* @param opts - Must include `collection`. See {@link CountEntriesOptions}.
|
|
198
|
+
* @returns The number of entries matching the filters.
|
|
199
|
+
* @throws {CmsError} with `code: 'MISSING_REQUIRED_PARAM'` when `collection`
|
|
200
|
+
* is omitted.
|
|
173
201
|
* @public
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* const total = await countEntries({ collection: 'posts', locale: 'en' });
|
|
205
|
+
* ```
|
|
174
206
|
*/
|
|
175
207
|
export async function countEntries(opts) {
|
|
176
208
|
if (!opts.collection) {
|
|
177
|
-
throw new
|
|
209
|
+
throw new CmsError('MISSING_REQUIRED_PARAM', 'countEntries: collection is required', { op: 'countEntries', missing: 'collection' });
|
|
178
210
|
}
|
|
179
211
|
const cms = getCMS();
|
|
180
212
|
const locale = opts.locale ?? cms.languages[0];
|
|
@@ -4,6 +4,7 @@ import { getFieldsFromConfig } from '../../../fields/layoutUtils.js';
|
|
|
4
4
|
import z from 'zod';
|
|
5
5
|
import { _getDbEntryOrThrow as getDbEntryOrThrow, _getDbEntryVersionOrThrow as getDbEntryVersionOrThrow, _getDbEntryVersions as getDbEntryVersions } from './get.js';
|
|
6
6
|
import { createEntryVersion } from './create.js';
|
|
7
|
+
import { CmsError, formatZodDataIssues } from '../../../errors.js';
|
|
7
8
|
export const updateEntrySchema = z.object({
|
|
8
9
|
archivedAt: z.date().nullable().optional(),
|
|
9
10
|
sortOrder: z.number().int().nullable().optional()
|
|
@@ -53,7 +54,10 @@ export const updateEntryVersion = async (id, data) => {
|
|
|
53
54
|
});
|
|
54
55
|
const parsedData = schema.safeParse(filteredData.data);
|
|
55
56
|
if (!parsedData.success) {
|
|
56
|
-
throw
|
|
57
|
+
throw new CmsError('INVALID_DATA', `Invalid data for entry version:\n${formatZodDataIssues(parsedData.error)}`, {
|
|
58
|
+
collection: entry.slug,
|
|
59
|
+
versionId: id
|
|
60
|
+
});
|
|
57
61
|
}
|
|
58
62
|
dataToUpdate.data = parsedData.data;
|
|
59
63
|
}
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
import type { ImageFieldStyle } from '../../../../types/fields.js';
|
|
2
2
|
import type { ImageStyle, MediaFile } from '../../../../types/media.js';
|
|
3
3
|
/**
|
|
4
|
+
* Resolved media file plus its image styles + blur placeholder. Returned by
|
|
5
|
+
* {@link resolveMediaWithStyles}.
|
|
4
6
|
* @public
|
|
5
7
|
*/
|
|
6
8
|
export interface ResolvedMedia {
|
|
9
|
+
/** The original media file row (URL, MIME, dimensions, focal point, ...). */
|
|
7
10
|
data: MediaFile;
|
|
11
|
+
/** Generated image styles keyed by style name. */
|
|
8
12
|
styles: Record<string, ImageStyle>;
|
|
13
|
+
/** Tiny base64 blur placeholder, or `null` for non-image media. */
|
|
9
14
|
blurDataUrl: string | null;
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
|
-
* Resolve media files by IDs and generate image styles. Useful for plugins
|
|
17
|
+
* Resolve media files by IDs and generate image styles. Useful for plugins or
|
|
18
|
+
* custom resolvers that hold raw media references (e.g. photo-grid blocks).
|
|
19
|
+
*
|
|
20
|
+
* @param mediaIds - The media file UUIDs to resolve.
|
|
21
|
+
* @param styles - Optional list of `ImageFieldStyle` to generate for each file.
|
|
22
|
+
* @returns A map keyed by media id, each entry is a {@link ResolvedMedia}.
|
|
13
23
|
* @public
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const resolved = await resolveMediaWithStyles(
|
|
27
|
+
* ['uuid-1', 'uuid-2'],
|
|
28
|
+
* [{ name: 'thumb', width: 400 }]
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
14
31
|
*/
|
|
15
32
|
export declare function resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>;
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { getCMS } from '../../../cms.js';
|
|
2
2
|
import { getImageStyles } from './imageStyles.js';
|
|
3
3
|
/**
|
|
4
|
-
* Resolve media files by IDs and generate image styles. Useful for plugins
|
|
4
|
+
* Resolve media files by IDs and generate image styles. Useful for plugins or
|
|
5
|
+
* custom resolvers that hold raw media references (e.g. photo-grid blocks).
|
|
6
|
+
*
|
|
7
|
+
* @param mediaIds - The media file UUIDs to resolve.
|
|
8
|
+
* @param styles - Optional list of `ImageFieldStyle` to generate for each file.
|
|
9
|
+
* @returns A map keyed by media id, each entry is a {@link ResolvedMedia}.
|
|
5
10
|
* @public
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const resolved = await resolveMediaWithStyles(
|
|
14
|
+
* ['uuid-1', 'uuid-2'],
|
|
15
|
+
* [{ name: 'thumb', width: 400 }]
|
|
16
|
+
* );
|
|
17
|
+
* ```
|
|
6
18
|
*/
|
|
7
19
|
export async function resolveMediaWithStyles(mediaIds, styles) {
|
|
8
20
|
if (!mediaIds.length)
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Options for {@link createFormSubmission}.
|
|
2
3
|
* @public
|
|
3
4
|
*/
|
|
4
5
|
export interface CreateFormSubmissionOptions {
|
|
6
|
+
/** Form slug as declared in `defineConfig({ forms })`. */
|
|
5
7
|
slug: string;
|
|
8
|
+
/** Submission payload. Validated against the form's field Zod schema. */
|
|
6
9
|
data: Record<string, unknown>;
|
|
10
|
+
/** Optional remote IP for audit / rate-limit attribution. */
|
|
7
11
|
ip?: string;
|
|
12
|
+
/** Optional user-agent string. */
|
|
8
13
|
userAgent?: string;
|
|
9
14
|
}
|
|
10
15
|
/**
|
|
11
|
-
* Persists a form submission and triggers best-effort notification email.
|
|
16
|
+
* Persists a form submission and triggers a best-effort notification email.
|
|
17
|
+
*
|
|
18
|
+
* Validates `data` against the form's field schema and throws a `CmsError`
|
|
19
|
+
* with `code: 'INVALID_DATA'` listing every issue when invalid. Email failures
|
|
20
|
+
* are logged but do not affect the return value.
|
|
21
|
+
*
|
|
22
|
+
* @param options - See {@link CreateFormSubmissionOptions}.
|
|
23
|
+
* @returns `true` if the row was written, `false` if the DB insert failed.
|
|
24
|
+
* @throws {CmsError} with `code: 'INVALID_DATA'` when validation fails.
|
|
12
25
|
* @public
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* await createFormSubmission({
|
|
29
|
+
* slug: 'contact',
|
|
30
|
+
* data: { email: 'user@example.com', message: 'Hi' }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
13
33
|
*/
|
|
14
34
|
export declare const createFormSubmission: (options: CreateFormSubmissionOptions) => Promise<boolean>;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { getLocalizedLabel } from '../../../../../admin/utils/collectionLabel.js';
|
|
2
2
|
import { getCMS } from '../../../../cms.js';
|
|
3
3
|
import { generateZodSchemaFromFormFields } from '../../../../fields/formFieldSchemaToTs.js';
|
|
4
|
+
import { CmsError, formatZodDataIssues } from '../../../../errors.js';
|
|
4
5
|
/**
|
|
5
|
-
* Persists a form submission and triggers best-effort notification email.
|
|
6
|
+
* Persists a form submission and triggers a best-effort notification email.
|
|
7
|
+
*
|
|
8
|
+
* Validates `data` against the form's field schema and throws a `CmsError`
|
|
9
|
+
* with `code: 'INVALID_DATA'` listing every issue when invalid. Email failures
|
|
10
|
+
* are logged but do not affect the return value.
|
|
11
|
+
*
|
|
12
|
+
* @param options - See {@link CreateFormSubmissionOptions}.
|
|
13
|
+
* @returns `true` if the row was written, `false` if the DB insert failed.
|
|
14
|
+
* @throws {CmsError} with `code: 'INVALID_DATA'` when validation fails.
|
|
6
15
|
* @public
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* await createFormSubmission({
|
|
19
|
+
* slug: 'contact',
|
|
20
|
+
* data: { email: 'user@example.com', message: 'Hi' }
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
7
23
|
*/
|
|
8
24
|
export const createFormSubmission = async (options) => {
|
|
9
25
|
const { slug, data, ip, userAgent } = options;
|
|
@@ -11,7 +27,7 @@ export const createFormSubmission = async (options) => {
|
|
|
11
27
|
const schema = await generateZodSchemaFromFormFields(config.fields);
|
|
12
28
|
const parsedData = schema.safeParse(data);
|
|
13
29
|
if (!parsedData.success) {
|
|
14
|
-
throw new
|
|
30
|
+
throw new CmsError('INVALID_DATA', `Invalid data for form submission:\n${formatZodDataIssues(parsedData.error)}`, { formSlug: slug });
|
|
15
31
|
}
|
|
16
32
|
try {
|
|
17
33
|
await getCMS().databaseAdapter.createFormSubmission({
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { FormField } from '../../../../../types/formFields.js';
|
|
2
2
|
/**
|
|
3
|
-
* Parses multipart `FormData` into a typed record of field values, handling
|
|
3
|
+
* Parses multipart `FormData` into a typed record of field values, handling
|
|
4
|
+
* file uploads via the configured `FilesAdapter`.
|
|
5
|
+
*
|
|
6
|
+
* @param formData - The request's `FormData` (e.g. `await request.formData()`).
|
|
7
|
+
* @param fields - The form's `FormField[]` definitions.
|
|
8
|
+
* @returns A plain `Record<string, unknown>` keyed by field slug, with files
|
|
9
|
+
* replaced by their persisted URL/identifier.
|
|
4
10
|
* @public
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const data = await parseFormDataForSubmission(
|
|
14
|
+
* await request.formData(),
|
|
15
|
+
* formConfig.fields
|
|
16
|
+
* );
|
|
17
|
+
* await createFormSubmission({ slug: formConfig.slug, data });
|
|
18
|
+
* ```
|
|
5
19
|
*/
|
|
6
20
|
export declare function parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>;
|
|
@@ -26,8 +26,22 @@ function validateMagicBytes(buffer, declaredMime) {
|
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Parses multipart `FormData` into a typed record of field values, handling
|
|
29
|
+
* Parses multipart `FormData` into a typed record of field values, handling
|
|
30
|
+
* file uploads via the configured `FilesAdapter`.
|
|
31
|
+
*
|
|
32
|
+
* @param formData - The request's `FormData` (e.g. `await request.formData()`).
|
|
33
|
+
* @param fields - The form's `FormField[]` definitions.
|
|
34
|
+
* @returns A plain `Record<string, unknown>` keyed by field slug, with files
|
|
35
|
+
* replaced by their persisted URL/identifier.
|
|
30
36
|
* @public
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const data = await parseFormDataForSubmission(
|
|
40
|
+
* await request.formData(),
|
|
41
|
+
* formConfig.fields
|
|
42
|
+
* );
|
|
43
|
+
* await createFormSubmission({ slug: formConfig.slug, data });
|
|
44
|
+
* ```
|
|
31
45
|
*/
|
|
32
46
|
export async function parseFormDataForSubmission(formData, fields) {
|
|
33
47
|
const result = {};
|
|
@@ -4,6 +4,7 @@ import { generateAdminThumbnail } from '../utils/generateAdminThumbnail.js';
|
|
|
4
4
|
import { generateDefaultStylesInBackground } from '../styles/operations/generateDefaultStyles.js';
|
|
5
5
|
import { generateDefaultVideoStylesInBackground } from '../styles/operations/generateDefaultVideoStyles.js';
|
|
6
6
|
import sharp from 'sharp';
|
|
7
|
+
import { withTimeout, sharpTimeoutMs } from '../../../../server/utils/withTimeout.js';
|
|
7
8
|
async function maybeDownscale(file) {
|
|
8
9
|
const cms = getCMS();
|
|
9
10
|
const { maxOriginalWidth, maxOriginalHeight } = cms.mediaConfig;
|
|
@@ -14,19 +15,19 @@ async function maybeDownscale(file) {
|
|
|
14
15
|
return file;
|
|
15
16
|
try {
|
|
16
17
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
17
|
-
const metadata = await sharp(buffer).metadata();
|
|
18
|
+
const metadata = await withTimeout(sharp(buffer).metadata(), sharpTimeoutMs(), 'sharp.metadata');
|
|
18
19
|
const w = metadata.width || 0;
|
|
19
20
|
const h = metadata.height || 0;
|
|
20
21
|
const exceedsWidth = maxOriginalWidth && w > maxOriginalWidth;
|
|
21
22
|
const exceedsHeight = maxOriginalHeight && h > maxOriginalHeight;
|
|
22
23
|
if (!exceedsWidth && !exceedsHeight)
|
|
23
24
|
return file;
|
|
24
|
-
const resized = await sharp(buffer)
|
|
25
|
+
const resized = await withTimeout(sharp(buffer)
|
|
25
26
|
.resize(maxOriginalWidth, maxOriginalHeight, {
|
|
26
27
|
fit: 'inside',
|
|
27
28
|
withoutEnlargement: true
|
|
28
29
|
})
|
|
29
|
-
.toBuffer();
|
|
30
|
+
.toBuffer(), sharpTimeoutMs(), 'sharp.resize');
|
|
30
31
|
return new File([new Uint8Array(resized)], file.name, { type: file.type });
|
|
31
32
|
}
|
|
32
33
|
catch (e) {
|