includio-cms 0.21.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 +20 -20
- package/CHANGELOG.md +90 -0
- package/DOCS.md +1 -1
- package/README.md +138 -32
- package/ROADMAP.md +4 -0
- 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/generateApiKey.d.ts +9 -0
- package/dist/admin/api/rest/middleware/generateApiKey.js +9 -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 +2 -0
- 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/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/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 +15 -1
- 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.schema.d.ts +452 -0
- package/dist/types/cms.schema.js +629 -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 +2 -1
- package/package.json +4 -1
- 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
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
bind:value={confirmInput}
|
|
98
98
|
placeholder={lang.deleteConfirmWord}
|
|
99
99
|
autocomplete="off"
|
|
100
|
+
data-testid="delete-confirm-input"
|
|
100
101
|
/>
|
|
101
102
|
</div>
|
|
102
103
|
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
variant="destructive"
|
|
109
110
|
disabled={!confirmed || loading}
|
|
110
111
|
onclick={handleDelete}
|
|
112
|
+
data-testid="delete-user-submit"
|
|
111
113
|
>
|
|
112
114
|
{lang.deleteUser}
|
|
113
115
|
</Button>
|
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
{lang.invite.inviteUser}
|
|
361
361
|
</Button>
|
|
362
362
|
{/if}
|
|
363
|
-
<Button variant="default" size="sm" onclick={() => (createOpen = true)}>
|
|
363
|
+
<Button variant="default" size="sm" onclick={() => (createOpen = true)} data-testid="create-user-button">
|
|
364
364
|
<Plus class="size-4" />
|
|
365
365
|
{lang.createUser}
|
|
366
366
|
</Button>
|
|
@@ -384,7 +384,7 @@
|
|
|
384
384
|
<p class="mb-5 max-w-sm text-sm" style="color: var(--muted-foreground);">
|
|
385
385
|
{lang.emptyDescription}
|
|
386
386
|
</p>
|
|
387
|
-
<Button onclick={() => (createOpen = true)}>
|
|
387
|
+
<Button onclick={() => (createOpen = true)} data-testid="create-user-button">
|
|
388
388
|
<Plus class="size-4" />
|
|
389
389
|
{lang.addUser}
|
|
390
390
|
</Button>
|
|
@@ -518,6 +518,7 @@
|
|
|
518
518
|
class="text-destructive h-8 w-8"
|
|
519
519
|
onclick={() => openDelete(user)}
|
|
520
520
|
title={lang.deleteUser}
|
|
521
|
+
data-testid="delete-user-row-button"
|
|
521
522
|
>
|
|
522
523
|
<Trash class="size-4" />
|
|
523
524
|
</Button>
|
|
@@ -218,6 +218,7 @@
|
|
|
218
218
|
type="button"
|
|
219
219
|
class="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-1.5 text-[13px] font-semibold text-primary-foreground transition-colors hover:bg-plum-dark whitespace-nowrap"
|
|
220
220
|
onclick={() => inputElement.click()}
|
|
221
|
+
data-testid="file-upload-button"
|
|
221
222
|
>
|
|
222
223
|
<Upload class="h-4 w-4" />
|
|
223
224
|
{lang[interfaceLanguage.current].addFiles}
|
|
@@ -229,6 +230,7 @@
|
|
|
229
230
|
type="file"
|
|
230
231
|
{accept}
|
|
231
232
|
onchange={handleUpload}
|
|
233
|
+
data-testid="file-input"
|
|
232
234
|
/>
|
|
233
235
|
|
|
234
236
|
<!-- Drag overlay -->
|
|
@@ -4,8 +4,16 @@ import type { AIAdapter, AIConfig } from '../types/adapters/ai.js';
|
|
|
4
4
|
*
|
|
5
5
|
* `@anthropic-ai/sdk` is an **optional peer dependency** — install it in your
|
|
6
6
|
* project (`pnpm add @anthropic-ai/sdk`) when using this adapter. SDK loads
|
|
7
|
-
* lazily on first call; missing peer throws a clear error.
|
|
7
|
+
* lazily on first call; a missing peer throws a clear error.
|
|
8
8
|
*
|
|
9
|
+
* @param config - `apiKey` is required.
|
|
10
|
+
* @returns An `AIAdapter` ready to use in `defineConfig({ ai })`.
|
|
9
11
|
* @public
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { claudeAdapter } from 'includio-cms/ai-claude';
|
|
15
|
+
*
|
|
16
|
+
* const ai = claudeAdapter({ apiKey: process.env.ANTHROPIC_API_KEY! });
|
|
17
|
+
* ```
|
|
10
18
|
*/
|
|
11
19
|
export declare function claudeAdapter(config: AIConfig): AIAdapter;
|
package/dist/ai-claude/index.js
CHANGED
|
@@ -5,9 +5,17 @@ import sharp from 'sharp';
|
|
|
5
5
|
*
|
|
6
6
|
* `@anthropic-ai/sdk` is an **optional peer dependency** — install it in your
|
|
7
7
|
* project (`pnpm add @anthropic-ai/sdk`) when using this adapter. SDK loads
|
|
8
|
-
* lazily on first call; missing peer throws a clear error.
|
|
8
|
+
* lazily on first call; a missing peer throws a clear error.
|
|
9
9
|
*
|
|
10
|
+
* @param config - `apiKey` is required.
|
|
11
|
+
* @returns An `AIAdapter` ready to use in `defineConfig({ ai })`.
|
|
10
12
|
* @public
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { claudeAdapter } from 'includio-cms/ai-claude';
|
|
16
|
+
*
|
|
17
|
+
* const ai = claudeAdapter({ apiKey: process.env.ANTHROPIC_API_KEY! });
|
|
18
|
+
* ```
|
|
11
19
|
*/
|
|
12
20
|
export function claudeAdapter(config) {
|
|
13
21
|
let client = null;
|
|
@@ -4,8 +4,16 @@ import type { AIAdapter, AIConfig } from '../types/adapters/ai.js';
|
|
|
4
4
|
*
|
|
5
5
|
* `openai` is an **optional peer dependency** — install it in your project
|
|
6
6
|
* (`pnpm add openai`) when using this adapter. SDK loads lazily on first call;
|
|
7
|
-
* missing peer throws a clear error.
|
|
7
|
+
* a missing peer throws a clear error.
|
|
8
8
|
*
|
|
9
|
+
* @param config - `apiKey` is required.
|
|
10
|
+
* @returns An `AIAdapter` ready to use in `defineConfig({ ai })`.
|
|
9
11
|
* @public
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { openAIAdapter } from 'includio-cms/ai-openai';
|
|
15
|
+
*
|
|
16
|
+
* const ai = openAIAdapter({ apiKey: process.env.OPENAI_API_KEY! });
|
|
17
|
+
* ```
|
|
10
18
|
*/
|
|
11
19
|
export declare function openAIAdapter(config: AIConfig): AIAdapter;
|
package/dist/ai-openai/index.js
CHANGED
|
@@ -6,9 +6,17 @@ import sharp from 'sharp';
|
|
|
6
6
|
*
|
|
7
7
|
* `openai` is an **optional peer dependency** — install it in your project
|
|
8
8
|
* (`pnpm add openai`) when using this adapter. SDK loads lazily on first call;
|
|
9
|
-
* missing peer throws a clear error.
|
|
9
|
+
* a missing peer throws a clear error.
|
|
10
10
|
*
|
|
11
|
+
* @param config - `apiKey` is required.
|
|
12
|
+
* @returns An `AIAdapter` ready to use in `defineConfig({ ai })`.
|
|
11
13
|
* @public
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { openAIAdapter } from 'includio-cms/ai-openai';
|
|
17
|
+
*
|
|
18
|
+
* const ai = openAIAdapter({ apiKey: process.env.OPENAI_API_KEY! });
|
|
19
|
+
* ```
|
|
12
20
|
*/
|
|
13
21
|
export function openAIAdapter(config) {
|
|
14
22
|
let openai = null;
|
package/dist/cli/index.js
CHANGED
|
@@ -4,30 +4,111 @@ import { installPeers } from './install-peers.js';
|
|
|
4
4
|
import { createUser } from './create-user.js';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
7
8
|
const args = process.argv.slice(2);
|
|
8
9
|
const command = args[0];
|
|
9
10
|
const subcommand = args[1];
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const HELP_FLAGS = new Set(['--help', '-h']);
|
|
12
|
+
const VERSION_FLAGS = new Set(['--version', '-v']);
|
|
13
|
+
function readPackageVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
// In dist: dist/cli/index.js → ../../package.json
|
|
17
|
+
// In src dev: src/lib/cli/index.ts → ../../../package.json
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.resolve(here, '../../package.json'),
|
|
20
|
+
path.resolve(here, '../../../package.json')
|
|
21
|
+
];
|
|
22
|
+
for (const c of candidates) {
|
|
23
|
+
if (fs.existsSync(c)) {
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(c, 'utf-8'));
|
|
25
|
+
if (pkg?.name === 'includio-cms' && typeof pkg.version === 'string') {
|
|
26
|
+
return pkg.version;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// fall through
|
|
33
|
+
}
|
|
34
|
+
return '0.0.0';
|
|
35
|
+
}
|
|
36
|
+
function printTopLevelHelp() {
|
|
37
|
+
console.log(`includio — CLI for includio-cms
|
|
38
|
+
|
|
39
|
+
Usage: includio <command> [options]
|
|
12
40
|
|
|
13
41
|
Commands:
|
|
14
|
-
scaffold admin Generate admin route files
|
|
15
|
-
install-peers Install missing peer dependencies
|
|
16
|
-
create-user Create a new admin/user account
|
|
42
|
+
scaffold admin Generate admin route files (/admin and /api/admin)
|
|
43
|
+
install-peers Install missing optional peer dependencies
|
|
44
|
+
create-user Create a new admin/user account interactively
|
|
45
|
+
|
|
46
|
+
Global options:
|
|
47
|
+
--help, -h Show this help
|
|
48
|
+
--version, -v Print the package version
|
|
49
|
+
|
|
50
|
+
Run \`includio <command> --help\` for command-specific options.
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
function printScaffoldAdminHelp() {
|
|
54
|
+
console.log(`includio scaffold admin — generate admin route files
|
|
55
|
+
|
|
56
|
+
Usage: includio scaffold admin [options]
|
|
17
57
|
|
|
18
58
|
Options:
|
|
19
|
-
--force
|
|
20
|
-
--routes-dir Path to routes
|
|
21
|
-
--cms-config
|
|
22
|
-
--shop
|
|
23
|
-
--no-shop
|
|
24
|
-
--
|
|
59
|
+
--force Overwrite existing files
|
|
60
|
+
--routes-dir <DIR> Path to SvelteKit routes (default: src/routes)
|
|
61
|
+
--cms-config <FILE> Path to CMS config (default: src/lib/cms/cms.config.ts)
|
|
62
|
+
--shop Force shop routes ON (overrides auto-detect)
|
|
63
|
+
--no-shop Force shop routes OFF
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
includio scaffold admin
|
|
68
|
+
includio scaffold admin --force --shop
|
|
69
|
+
includio scaffold admin --routes-dir apps/web/src/routes
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
function printInstallPeersHelp() {
|
|
73
|
+
console.log(`includio install-peers — install missing optional peer deps
|
|
74
|
+
|
|
75
|
+
Reads your project's installed packages and offers to install any optional
|
|
76
|
+
peers used by the parts of includio-cms you have configured (e.g. \`openai\`,
|
|
77
|
+
\`@anthropic-ai/sdk\`, \`nodemailer\`).
|
|
78
|
+
|
|
79
|
+
Usage: includio install-peers [options]
|
|
80
|
+
|
|
81
|
+
Options:
|
|
82
|
+
--dry-run List what would be installed, don't run the package manager
|
|
83
|
+
--help, -h Show this help
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
includio install-peers --dry-run
|
|
87
|
+
includio install-peers
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
function printCreateUserHelp() {
|
|
91
|
+
console.log(`includio create-user — create an admin/user account
|
|
92
|
+
|
|
93
|
+
Interactive prompt — asks for email, password, name, and role. Requires
|
|
94
|
+
\`DATABASE_URL\` in the environment (or .env loaded by the project).
|
|
95
|
+
|
|
96
|
+
Usage: includio create-user [options]
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--help, -h Show this help
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
includio create-user
|
|
25
103
|
`);
|
|
26
104
|
}
|
|
27
105
|
function flagValue(name) {
|
|
28
106
|
const idx = args.indexOf(name);
|
|
29
107
|
return idx !== -1 ? args[idx + 1] : undefined;
|
|
30
108
|
}
|
|
109
|
+
function hasHelpFlag() {
|
|
110
|
+
return args.some((a) => HELP_FLAGS.has(a));
|
|
111
|
+
}
|
|
31
112
|
/**
|
|
32
113
|
* Best-effort detection: scan cms config text for an active `shop: defineShop(`
|
|
33
114
|
* or `shop: <var>` property inside `defineCMS({ ... })`. Returns null if the
|
|
@@ -43,7 +124,20 @@ function detectShopUsage(cmsConfigPath) {
|
|
|
43
124
|
.replace(/\/\/[^\n]*/g, '');
|
|
44
125
|
return /\bshop\s*:\s*\S/.test(stripped);
|
|
45
126
|
}
|
|
127
|
+
// Top-level: no command, --help, or --version
|
|
128
|
+
if (!command || HELP_FLAGS.has(command)) {
|
|
129
|
+
printTopLevelHelp();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
if (VERSION_FLAGS.has(command)) {
|
|
133
|
+
console.log(readPackageVersion());
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
46
136
|
if (command === 'scaffold' && subcommand === 'admin') {
|
|
137
|
+
if (hasHelpFlag()) {
|
|
138
|
+
printScaffoldAdminHelp();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
47
141
|
const force = args.includes('--force');
|
|
48
142
|
const cwd = process.cwd();
|
|
49
143
|
const routesDir = flagValue('--routes-dir') ?? path.join(cwd, 'src', 'routes');
|
|
@@ -73,13 +167,21 @@ if (command === 'scaffold' && subcommand === 'admin') {
|
|
|
73
167
|
scaffoldAdmin({ routesDir, force, shop });
|
|
74
168
|
}
|
|
75
169
|
else if (command === 'install-peers') {
|
|
170
|
+
if (hasHelpFlag()) {
|
|
171
|
+
printInstallPeersHelp();
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
76
174
|
const dryRun = args.includes('--dry-run');
|
|
77
175
|
installPeers({ dryRun });
|
|
78
176
|
}
|
|
79
177
|
else if (command === 'create-user') {
|
|
178
|
+
if (hasHelpFlag()) {
|
|
179
|
+
printCreateUserHelp();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
80
182
|
createUser();
|
|
81
183
|
}
|
|
82
184
|
else {
|
|
83
|
-
|
|
84
|
-
process.exit(
|
|
185
|
+
printTopLevelHelp();
|
|
186
|
+
process.exit(1);
|
|
85
187
|
}
|
|
@@ -44,7 +44,7 @@ export interface BlogPost {
|
|
|
44
44
|
slug?: string;
|
|
45
45
|
cover?: ImageFieldData | VideoFieldData | null;
|
|
46
46
|
rating: number;
|
|
47
|
-
category
|
|
47
|
+
category: 'technology' | 'design' | 'business' | 'tutorial';
|
|
48
48
|
publishedAt?: string;
|
|
49
49
|
thumbnail?: ImageFieldData | VideoFieldData | null;
|
|
50
50
|
content?: StructuredContentDoc;
|
package/dist/core/cms.d.ts
CHANGED
|
@@ -39,7 +39,19 @@ export declare class CMS implements ICMS {
|
|
|
39
39
|
}
|
|
40
40
|
export declare function initCMS(config: CMSConfig): CMS;
|
|
41
41
|
/**
|
|
42
|
-
* Returns the singleton CMS instance. Must be called after `includioCMS()`
|
|
42
|
+
* Returns the singleton CMS instance. Must be called after `includioCMS()`
|
|
43
|
+
* initializes the CMS in `hooks.server.ts`.
|
|
44
|
+
*
|
|
45
|
+
* @returns The active `CMS` instance (collections, adapters, plugins, ...).
|
|
46
|
+
* @throws {Error} when called before `includioCMS()` has run (e.g. in plain
|
|
47
|
+
* client code that imports `$lib/...` without going through SvelteKit hooks).
|
|
43
48
|
* @public
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { getCMS } from 'includio-cms';
|
|
52
|
+
*
|
|
53
|
+
* const cms = getCMS();
|
|
54
|
+
* await cms.databaseAdapter.getEntries({ slug: 'posts' });
|
|
55
|
+
* ```
|
|
44
56
|
*/
|
|
45
57
|
export declare function getCMS(): CMS;
|
package/dist/core/cms.js
CHANGED
|
@@ -160,8 +160,20 @@ export function initCMS(config) {
|
|
|
160
160
|
return cms;
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
|
-
* Returns the singleton CMS instance. Must be called after `includioCMS()`
|
|
163
|
+
* Returns the singleton CMS instance. Must be called after `includioCMS()`
|
|
164
|
+
* initializes the CMS in `hooks.server.ts`.
|
|
165
|
+
*
|
|
166
|
+
* @returns The active `CMS` instance (collections, adapters, plugins, ...).
|
|
167
|
+
* @throws {Error} when called before `includioCMS()` has run (e.g. in plain
|
|
168
|
+
* client code that imports `$lib/...` without going through SvelteKit hooks).
|
|
164
169
|
* @public
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* import { getCMS } from 'includio-cms';
|
|
173
|
+
*
|
|
174
|
+
* const cms = getCMS();
|
|
175
|
+
* await cms.databaseAdapter.getEntries({ slug: 'posts' });
|
|
176
|
+
* ```
|
|
165
177
|
*/
|
|
166
178
|
export function getCMS() {
|
|
167
179
|
if (!cms) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ZodError } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Base error class for CMS runtime failures. Carries a stable `code` and a
|
|
4
|
+
* `context` bag so callers (and clients) can branch on the failure mode without
|
|
5
|
+
* string-matching on `message`.
|
|
6
|
+
*
|
|
7
|
+
* `code` is a SCREAMING_SNAKE_CASE constant; see callers for the canonical list
|
|
8
|
+
* (`ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`,
|
|
9
|
+
* `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`).
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* try { await resolveEntry({ id }); }
|
|
15
|
+
* catch (e) {
|
|
16
|
+
* if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { ... }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class CmsError extends Error {
|
|
21
|
+
readonly code: string;
|
|
22
|
+
readonly context: Record<string, unknown>;
|
|
23
|
+
constructor(code: string, message: string, context?: Record<string, unknown>, options?: {
|
|
24
|
+
cause?: unknown;
|
|
25
|
+
});
|
|
26
|
+
toString(): string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One concrete validation issue extracted from a Zod error, with a friendly
|
|
30
|
+
* hint when the path matches a known config shape.
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export interface ConfigValidationIssue {
|
|
34
|
+
/** Dot/bracket-formatted path, e.g. `languages[0].code` or `collections[2].fields[0].slug`. */
|
|
35
|
+
path: string;
|
|
36
|
+
/** The validator's message, lightly normalized. */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Optional hint describing how to fix this specific issue. */
|
|
39
|
+
hint?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by {@link defineConfig} when the runtime config fails Zod validation.
|
|
43
|
+
* Aggregates ALL issues (not just the first) so the user can fix everything in
|
|
44
|
+
* one pass.
|
|
45
|
+
*
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export declare class ConfigValidationError extends CmsError {
|
|
49
|
+
readonly issues: ConfigValidationIssue[];
|
|
50
|
+
constructor(issues: ConfigValidationIssue[]);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Render a Zod issue path (`(string | number)[]`) into JS-style notation:
|
|
54
|
+
* - string segments → `.foo`
|
|
55
|
+
* - numeric segments → `[3]`
|
|
56
|
+
* - leading string segment has no leading dot.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatIssuePath(path: ReadonlyArray<string | number>): string;
|
|
59
|
+
/**
|
|
60
|
+
* Render a `ZodError` from entry/form data validation as a list of
|
|
61
|
+
* `path: message` lines (one per issue). Used by data-write operations to give
|
|
62
|
+
* callers a readable dump instead of `JSON.stringify(error.flatten())`.
|
|
63
|
+
*
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatZodDataIssues(error: ZodError): string;
|
|
67
|
+
/**
|
|
68
|
+
* Convert a `ZodError` into a {@link ConfigValidationError} with friendly,
|
|
69
|
+
* path-prefixed messages and per-issue hints.
|
|
70
|
+
*/
|
|
71
|
+
export declare function formatConfigError(error: ZodError): ConfigValidationError;
|
|
@@ -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);
|