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.
Files changed (79) hide show
  1. package/API.md +20 -20
  2. package/CHANGELOG.md +90 -0
  3. package/DOCS.md +1 -1
  4. package/README.md +138 -32
  5. package/ROADMAP.md +4 -0
  6. package/dist/admin/api/rest/handler.d.ts +13 -1
  7. package/dist/admin/api/rest/handler.js +13 -1
  8. package/dist/admin/api/rest/middleware/generateApiKey.d.ts +9 -0
  9. package/dist/admin/api/rest/middleware/generateApiKey.js +9 -0
  10. package/dist/admin/client/collection/collection-entries.svelte +1 -1
  11. package/dist/admin/client/collection/empty-state.svelte +1 -1
  12. package/dist/admin/client/collection/row-actions.svelte +3 -3
  13. package/dist/admin/client/collection/table-toolbar.svelte +3 -1
  14. package/dist/admin/client/entry/entry-header.svelte +3 -1
  15. package/dist/admin/client/users/create-user-dialog.svelte +4 -4
  16. package/dist/admin/client/users/delete-user-dialog.svelte +2 -0
  17. package/dist/admin/client/users/users-page.svelte +3 -2
  18. package/dist/admin/components/media/file-upload.svelte +2 -0
  19. package/dist/ai-claude/index.d.ts +9 -1
  20. package/dist/ai-claude/index.js +9 -1
  21. package/dist/ai-openai/index.d.ts +9 -1
  22. package/dist/ai-openai/index.js +9 -1
  23. package/dist/cli/index.js +115 -13
  24. package/dist/cms/runtime/schema.d.ts +2 -0
  25. package/dist/cms/runtime/schema.js +4 -0
  26. package/dist/cms/runtime/types.d.ts +1 -1
  27. package/dist/core/cms.d.ts +13 -1
  28. package/dist/core/cms.js +13 -1
  29. package/dist/core/errors.d.ts +71 -0
  30. package/dist/core/errors.js +179 -0
  31. package/dist/core/server/consentLogs/operations/create.d.ts +13 -1
  32. package/dist/core/server/consentLogs/operations/create.js +13 -1
  33. package/dist/core/server/entries/operations/create.js +6 -1
  34. package/dist/core/server/entries/operations/get.js +14 -3
  35. package/dist/core/server/entries/operations/resolveEntry.d.ts +32 -1
  36. package/dist/core/server/entries/operations/resolveEntry.js +36 -4
  37. package/dist/core/server/entries/operations/update.js +5 -1
  38. package/dist/core/server/fields/utils/resolveMedia.d.ts +18 -1
  39. package/dist/core/server/fields/utils/resolveMedia.js +13 -1
  40. package/dist/core/server/forms/submissions/operations/create.d.ts +21 -1
  41. package/dist/core/server/forms/submissions/operations/create.js +18 -2
  42. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +15 -1
  43. package/dist/core/server/forms/submissions/utils/parseMultipart.js +15 -1
  44. package/dist/db-postgres/index.d.ts +10 -0
  45. package/dist/db-postgres/index.js +10 -0
  46. package/dist/email-nodemailer/index.d.ts +13 -1
  47. package/dist/email-nodemailer/index.js +13 -1
  48. package/dist/entity/index.d.ts +16 -1
  49. package/dist/entity/index.js +16 -1
  50. package/dist/files-local/index.d.ts +12 -1
  51. package/dist/files-local/index.js +12 -1
  52. package/dist/paraglide/messages/_index.d.ts +3 -36
  53. package/dist/paraglide/messages/_index.js +3 -71
  54. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  55. package/dist/paraglide/messages/hello_world.js +33 -0
  56. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  57. package/dist/paraglide/messages/login_hello.js +34 -0
  58. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  59. package/dist/paraglide/messages/login_please_login.js +34 -0
  60. package/dist/server/auth.d.ts +11 -0
  61. package/dist/server/auth.js +11 -0
  62. package/dist/sveltekit/config.d.ts +67 -4
  63. package/dist/sveltekit/config.js +73 -4
  64. package/dist/sveltekit/server/handle.d.ts +15 -1
  65. package/dist/sveltekit/server/handle.js +15 -1
  66. package/dist/sveltekit/server/layout.d.ts +12 -1
  67. package/dist/sveltekit/server/layout.js +12 -1
  68. package/dist/sveltekit/server/preview.d.ts +21 -1
  69. package/dist/sveltekit/server/preview.js +21 -1
  70. package/dist/types/cms.schema.d.ts +452 -0
  71. package/dist/types/cms.schema.js +629 -0
  72. package/dist/updates/0.22.0/index.d.ts +2 -0
  73. package/dist/updates/0.22.0/index.js +75 -0
  74. package/dist/updates/index.js +2 -1
  75. package/package.json +4 -1
  76. package/dist/paraglide/messages/en.d.ts +0 -5
  77. package/dist/paraglide/messages/en.js +0 -14
  78. package/dist/paraglide/messages/pl.d.ts +0 -5
  79. 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;
@@ -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;
@@ -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
- function printUsage() {
11
- console.log(`Usage: includio <command>
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 Overwrite existing files
20
- --routes-dir Path to routes directory (default: src/routes)
21
- --cms-config Path to cms config (default: src/lib/cms/cms.config.ts)
22
- --shop Force shop routes ON (default: auto-detect from cms config)
23
- --no-shop Force shop routes OFF
24
- --dry-run Show what would be installed (install-peers)
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
- printUsage();
84
- process.exit(command ? 1 : 0);
185
+ printTopLevelHelp();
186
+ process.exit(1);
85
187
  }
@@ -0,0 +1,2 @@
1
+ export * from 'includio-cms/db-postgres/schema-core';
2
+ export * from 'includio-cms/auth-schema';
@@ -0,0 +1,4 @@
1
+ // This file is auto-generated. Do not edit directly.
2
+ // Point your drizzle.config.ts schema field at this file.
3
+ export * from 'includio-cms/db-postgres/schema-core';
4
+ export * from 'includio-cms/auth-schema';
@@ -44,7 +44,7 @@ export interface BlogPost {
44
44
  slug?: string;
45
45
  cover?: ImageFieldData | VideoFieldData | null;
46
46
  rating: number;
47
- category?: 'technology' | 'design' | 'business' | 'tutorial';
47
+ category: 'technology' | 'design' | 'business' | 'tutorial';
48
48
  publishedAt?: string;
49
49
  thumbnail?: ImageFieldData | VideoFieldData | null;
50
50
  content?: StructuredContentDoc;
@@ -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()` initializes the CMS in `hooks.server.ts`.
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()` initializes the CMS in `hooks.server.ts`.
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);