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
package/API.md CHANGED
@@ -1,4 +1,4 @@
1
- # includio-cms — Public API v0.21.0
1
+ # includio-cms — Public API v0.22.0
2
2
 
3
3
  > Auto-generated by `scripts/generate-api-md.ts`. Do not edit by hand.
4
4
 
@@ -13,19 +13,19 @@ Tags:
13
13
 
14
14
  ### `includio-cms`
15
15
 
16
- - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS instance and a user.
16
+ - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS
17
17
  - `getAuth(): <inferred>` — Returns the underlying `better-auth` instance from the initialized CMS.
18
- - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()` initializes the CMS in `hooks.server.ts`.
19
- - `interface ResolvedMedia`
20
- - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins resolving media references (e.g. photo-grid).
18
+ - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()`
19
+ - `interface ResolvedMedia` — Resolved media file plus its image styles + blur placeholder. Returned by
20
+ - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins or
21
21
 
22
22
  ### `includio-cms/core`
23
23
 
24
- - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS instance and a user.
24
+ - `createEntityAPI(cms: CMS, opts?: EntityAPIOptions): <inferred>` — Creates a high-level Entity API (CRUD + publish/archive) bound to a CMS
25
25
  - `getAuth(): <inferred>` — Returns the underlying `better-auth` instance from the initialized CMS.
26
- - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()` initializes the CMS in `hooks.server.ts`.
27
- - `interface ResolvedMedia`
28
- - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins resolving media references (e.g. photo-grid).
26
+ - `getCMS(): CMS` — Returns the singleton CMS instance. Must be called after `includioCMS()`
27
+ - `interface ResolvedMedia` — Resolved media file plus its image styles + blur placeholder. Returned by
28
+ - `resolveMediaWithStyles(mediaIds: string[], styles?: ImageFieldStyle[]): Promise<Record<string, ResolvedMedia>>` — Resolve media files by IDs and generate image styles. Useful for plugins or
29
29
 
30
30
  ### `includio-cms/types`
31
31
 
@@ -239,11 +239,11 @@ Tags:
239
239
  ### `includio-cms/sveltekit`
240
240
 
241
241
  - `const CmsProvider: LegacyComponentType`
242
- - `defineCollection(config: CollectionInput): CollectionConfig` — Defines a collection (multi-entry content type).
242
+ - `defineCollection(config: CollectionInput): CollectionConfig` — Defines a collection (multi-entry content type, e.g. blog posts, products).
243
243
  - `defineConfig(config: CMSConfig): CMSConfig` — Defines the root CMS configuration. Pass to `includioCMS()` in `hooks.server.ts`.
244
- - `defineForm(config: FormConfig): FormConfig` — Defines a public form (submitted via `/api/forms/[slug]/submit`).
245
- - `defineObject(config: Omit<ObjectField, 'type'>): ObjectField` — Defines a reusable object field (nested record). Use inside `fields[]`.
246
- - `defineSingle(config: SingleInput): SingleConfig` — Defines a singleton (single-entry content type, e.g. site settings).
244
+ - `defineForm(config: FormConfig): FormConfig` — Defines a public form (submitted via `POST /api/forms/[slug]/submit`).
245
+ - `defineObject(config: Omit<ObjectField, 'type'>): ObjectField` — Defines a reusable object field (nested record). Use inline inside `fields[]`
246
+ - `defineSingle(config: SingleInput): SingleConfig` — Defines a singleton (single-entry content type, e.g. site settings, homepage).
247
247
  - `enableHybridEditing(): <inferred>` — Call in a layout/component script to enable data-hybrid-path rendering for all descendant HybridTarget/Image/Video.
248
248
  - `extractBlocks(doc: StructuredContentDoc, type?: string): SCNode[]` — Extract block-level nodes, optionally filtered by type.
249
249
  - `extractInlineBlocks(doc: StructuredContentDoc, blockType?: string): SCInlineBlockAttrs[]` — Extract inline block nodes, optionally filtered by blockType.
@@ -267,16 +267,16 @@ Tags:
267
267
 
268
268
  ### `includio-cms/sveltekit/server`
269
269
 
270
- - `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout `load`. Drop into your `+layout.server.ts`.
271
- - `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without populating.
270
+ - `cmsLayoutLoad(event: RequestEvent): <inferred>` — Returns `cmsContext` from `event.locals` for use in a SvelteKit layout
271
+ - `countEntries(opts: CountEntriesOptions): Promise<number>` — Count entries matching the same filters as `resolveEntries`, without
272
272
  - `type CountEntriesOptions = Omit< ResolveEntriesOptions, 'limit' | 'offset' | 'populate' | 'orderBy' | 'dataOrderBy' >`
273
273
  - `const createConsentLog: <inferred>`
274
274
  - `const createFormSubmission: <inferred>`
275
- - `createRestApiHandler(): <inferred>` — REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
275
+ - `createRestApiHandler(): <inferred>` — REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
276
276
  - `generateApiKey(): string` — Generate a cryptographically random API key (32 bytes, base64url-encoded).
277
- - `getPreviewEntry(event: RequestEvent, options: { language: string }): Promise<Entry | null>` — Resolves the preview entry from a `?preview=<versionId>` query param. Requires an authenticated session — throws `Unauthorized` otherwise.
278
- - `includioCMS(cmsConfig: CMSConfig): Handle[]` — SvelteKit `Handle[]` array that initializes the CMS, generates runtime artifacts, wires auth/admin guards, and exposes `event.locals.cmsContext`. Compose with `sequence()` in `hooks.server.ts`.
279
- - `parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>` — Parses multipart `FormData` into a typed record of field values, handling file uploads via the configured files adapter.
277
+ - `getPreviewEntry(event: RequestEvent, options: { language: string }): Promise<Entry | null>` — Resolves the preview entry from a `?preview=<versionId>` query param.
278
+ - `includioCMS(cmsConfig: CMSConfig): Handle[]` — SvelteKit `Handle[]` array that initializes the CMS, generates runtime
279
+ - `parseFormDataForSubmission(formData: FormData, fields: FormField[]): Promise<Record<string, unknown>>` — Parses multipart `FormData` into a typed record of field values, handling
280
280
  - `type PopulateConfig = { /** Hard cap on relation depth. Default 5. Use `0` to keep all relations as raw IDs. */ maxDept...` — Recursion + per-field opt-out config for relation population.
281
281
  - `resolveEntries(opts: ResolveEntriesOptions): Promise<Entry[]>` — Fetch a list of populated Entries from a collection (or singleton with multiple instances).
282
282
  - `interface ResolveEntriesOptions`
@@ -387,7 +387,7 @@ Tags:
387
387
  ### `includio-cms/files-local`
388
388
 
389
389
  - `const fullDir: <inferred>`
390
- - `local(config?: LocalFilesConfig): FilesAdapter` — Local-disk files adapter. Stores uploads under `./static/uploads` (dev) or `/data/uploads` (prod).
390
+ - `local(config?: LocalFilesConfig): FilesAdapter` — Local-disk files adapter. Stores uploads under `./static/uploads` (dev) or
391
391
  - `interface LocalFilesConfig`
392
392
 
393
393
  ### `includio-cms/email-nodemailer`
package/CHANGELOG.md CHANGED
@@ -3,6 +3,96 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.22.0 — 2026-04-30
7
+
8
+ Faza 9 — DX & config validation pass. `defineConfig()` waliduje config strict Zodem z czytelnymi błędami (path + hint), resolvery / operacje throwują typowane `CmsError` z `code` + `context`, CLI ma `--help` per subcommand i `--version`, README przepisany pod nowych userów (system requirements + 5-min quickstart), `.env.example` rozszerzony o wszystkie `INCLUDIO_*` envy, JSDoc na każdym `@public` symbolu (opis + `@param` + `@returns` + `@example`).
9
+
10
+ ### Added
11
+ - `defineConfig()` (`includio-cms/sveltekit`) — runtime Zod validation z agregacją wszystkich błędów. Invalid config → `ConfigValidationError` (extends `CmsError`) z `issues[]` (path + message + hint), bullet-list message ready-to-paste do PR. `superRefine`: unique slugs (collections/singles/forms), default locale ≤ 1, relation targets w deklarowanych kolekcjach, `apiKeys[].permissions` referuje istniejące collection slugs.
12
+ - `CmsError` + `ConfigValidationError` (`includio-cms` → `core/errors`) — bazowa klasa z `code` (SCREAMING_SNAKE_CASE), `context: Record<string, unknown>`, `cause?`. Stabilne kody: `ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`, `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`. `toString()` = `[CODE] message (k=v, k=v)`.
13
+ - `formatZodDataIssues(error)` helper — render Zod errora z entry/form data jako lista `path: message`. Używany w `createEntryVersion`, `updateEntryVersion`, `createFormSubmission`.
14
+ - CLI: `includio --help`, `includio scaffold admin --help`, `includio install-peers --help`, `includio create-user --help` — exit 0 + Usage/Options/Example block. `includio --version` / `-v` z `package.json`. Unknown command → exit 1 + top-level help.
15
+ - `.env.example` rozszerzony — 11 envów z komentarzami i sekcjami (Required / Media & Uploads / Security & Rate Limits). Demo vars usunięte (były tylko dla `src/lib/demo/` skasowanego w Fazie 1).
16
+ - README rebuild: TOC, sekcja **System requirements** (Node 18+, PG 14+, ffmpeg optional), 5-step **Quick start** (install → .env → config → scaffold → create-user → dev), poprawiony link do `DOCS.md`, dodana tabelka adapterów, sekcja CLI z subcommandami.
17
+ - JSDoc bodies (opis + `@param` + `@returns` + `@example`) na każdym `@public` symbolu — `defineConfig`, `defineCollection`, `defineSingle`, `defineForm`, `defineObject`, `getCMS`, `resolveEntry`, `resolveEntries`, `countEntries`, `createFormSubmission`, `parseFormDataForSubmission`, `createConsentLog`, `resolveMediaWithStyles`, `createEntityAPI`, `createRestApiHandler`, `generateApiKey`, `getAuth`, `includioCMS`, `getPreviewEntry`, `cmsLayoutLoad`, factory adapterów (`pg`, `local`, `nodemailerAdapter`, `openAIAdapter`, `claudeAdapter`).
18
+
19
+ ### Fixed
20
+ - `MISSING_REQUIRED_PARAM` zamiast `throw new Error('... is required')` w `resolveEntry` / `resolveEntries` / `countEntries`. Context zawiera `op` + `missing`.
21
+ - `INVALID_DATA` zamiast `throw Error('Invalid data: ' + JSON.stringify(parsedData.error.flatten()))` w `createEntryVersion` / `updateEntryVersion` / `createFormSubmission`. Context zawiera `collection`/`entryId`/`lang` (entries) lub `formSlug` (forms). Body errora to czytelna lista `field.path: message`.
22
+ - `ENTRY_NOT_FOUND` z `{ collection, id }` w `_getDbEntryOrThrow` / `_getRawEntryOrThrow`; `ENTRY_VERSION_NOT_FOUND` z `{ versionId, entryId, lang }` w `_getDbEntryVersionOrThrow`.
23
+
24
+ ### Breaking
25
+ - **Strict config validation w `defineConfig`** — invalid configs które wcześniej "działały" w runtime (np. duplicate slug, brak default locale, relation do nieistniejącej kolekcji, brak metody w adapterze) **teraz throwują `ConfigValidationError` na starcie**. Najczęstsze przypadki + fix:
26
+ - **Duplicate slug**: dwie kolekcje/singles/forms z tym samym `slug` → unique slug per kategoria.
27
+ - **Invalid language code**: `code: 'ENGLISH'` → `code: 'en'` (ISO-639-1, opcjonalnie z regionem `'pl-PL'`).
28
+ - **Multiple default locales**: dwa `{ default: true }` → tylko jeden.
29
+ - **Missing adapter method**: stub adaptera bez `getEntries` etc. → zaimportuj pełen `pg()` / `local()` lub doimplementuj brakujące metody.
30
+ - **Invalid relation target**: `{ type: 'relation', collection: 'authors' }` przy braku kolekcji `authors` → dodaj kolekcję lub popraw target.
31
+ - **Resolver / operation error format** — błędy z `resolveEntry` / `resolveEntries` / `countEntries` / `createEntryVersion` / `updateEntryVersion` / `createFormSubmission` rzucają teraz `CmsError` (extends `Error`). `error.message` zachowuje stary opis (przy `toString()` poprzedzony `[CODE]`) — string-matching `error.message === 'Entry not found'` może się rozjechać. **Migracja:**
32
+ ```ts
33
+ import { CmsError } from 'includio-cms';
34
+
35
+ try { await resolveEntry({ id }); }
36
+ catch (e) {
37
+ if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { ... }
38
+ }
39
+ ```
40
+ - **`.env.example` reformatted** — `DEMO_USER_EMAIL`, `DEMO_USER_PASSWORD`, `DEMO_RESET_KEY` usunięte (były tylko dla wewnętrznego demo skasowanego w Fazie 1). Jeśli polegałeś na nich → trzymaj w lokalnym `.env`, nie w `.env.example`.
41
+
42
+ ### Notes
43
+
44
+ ## Czytelność błędów config
45
+
46
+ Po update — invalid config wyrzuci na starcie:
47
+
48
+ ```
49
+ ConfigValidationError: CMSConfig validation failed (2 issues):
50
+ - languages[0]: must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')
51
+ - collections[1].slug: duplicate collection slug 'posts' — Hint: each collection/single/form must have a unique `slug`
52
+ ```
53
+
54
+ Każdy issue ma `path` (JS-notation: `collections[1].slug`, `apiKeys[0].permissions[2]`) i `message`. Tam gdzie da się rozsądnie zgadnąć, dochodzi `Hint:` z fix-suggestion.
55
+
56
+ ## Pattern: branchowanie po error code
57
+
58
+ Stary kod string-matchujący po `error.message` musi się przepisać:
59
+
60
+ ```ts
61
+ // PRZED
62
+ catch (e) {
63
+ if ((e as Error).message === 'Entry not found') { /* 404 */ }
64
+ }
65
+
66
+ // PO
67
+ import { CmsError } from 'includio-cms';
68
+ catch (e) {
69
+ if (e instanceof CmsError && e.code === 'ENTRY_NOT_FOUND') { /* 404 */ }
70
+ }
71
+ ```
72
+
73
+ Dostępne kody w v0.22.0: `ENTRY_NOT_FOUND`, `ENTRY_VERSION_NOT_FOUND`, `INVALID_DATA`, `MISSING_REQUIRED_PARAM`, `CONFIG_VALIDATION_FAILED`.
74
+
75
+ ## Nowe envy (opt-in)
76
+
77
+ Skopiuj `node_modules/includio-cms/.env.example` do swojego `.env` — domyślne wartości działają OOTB:
78
+
79
+ - `INCLUDIO_SHARP_TIMEOUT_MS` (default 30000)
80
+ - `INCLUDIO_RATE_LIMIT_MAX` / `INCLUDIO_RATE_LIMIT_WINDOW_MS` (default 200/60000)
81
+ - `INCLUDIO_FORM_RATE_LIMIT_MAX` / `INCLUDIO_FORM_RATE_LIMIT_WINDOW_MS` (default 5/3600000)
82
+ - `INCLUDIO_CSRF_ALLOWED_ORIGINS` (default: same-origin only)
83
+
84
+ ## CLI helpy
85
+
86
+ ```bash
87
+ includio --help
88
+ includio scaffold admin --help
89
+ includio install-peers --help
90
+ includio create-user --help
91
+ includio --version
92
+ ```
93
+
94
+ Brak SQL migration.
95
+
6
96
  ## 0.21.0 — 2026-04-30
7
97
 
8
98
  Faza 5 część 2 — security finish: form rate-limit DRY refactor, API keys `expiresAt` / `rotatedAt`, sharp timeout 30s, `{@html}` audit close. `KNOWN-RISKS.md` w root jako single source of truth dla zaakceptowanych ryzyk v1.0. Plus: faza 6 setup — vitest coverage (info-only), docker test profile, integration test scaffolding (`tests/`).
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.21.0)
1
+ # Includio CMS Documentation (v0.22.0)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/README.md CHANGED
@@ -1,57 +1,149 @@
1
- # Includio CMS
1
+ # includio-cms
2
2
 
3
- A headless CMS built for SvelteKit. Type-safe, extensible, with a modern admin interface.
3
+ [![CI](https://github.com/includio/includio-cms/actions/workflows/ci.yml/badge.svg)](https://github.com/includio/includio-cms/actions/workflows/ci.yml)
4
+
5
+ A headless CMS built for SvelteKit. Type-safe, extensible, with a modern admin interface and pluggable adapters for database, files, email, and AI.
6
+
7
+ ## Contents
8
+
9
+ - [Features](#features)
10
+ - [System requirements](#system-requirements)
11
+ - [Quick start (5 minutes)](#quick-start-5-minutes)
12
+ - [Configuration](#configuration)
13
+ - [Adapters](#adapters)
14
+ - [Writing your own adapter](#writing-your-own-adapter)
15
+ - [Optional peer dependencies](#optional-peer-dependencies)
16
+ - [CLI](#cli)
17
+ - [Links](#links)
18
+ - [For AI assistants](#for-ai-assistants)
19
+ - [License](#license)
4
20
 
5
21
  ## Features
6
22
 
7
- - **19 field types** — text, structured content, media, relations, blocks, SEO, and more
23
+ - **20 field types** — text, structured content, media, relations, blocks, SEO, URL, custom plugins, and more
8
24
  - **Structured content** — ProseMirror-based rich text with inline blocks, tables, media embeds
9
25
  - **Media management** — image styles (Sharp), video transcoding (ffmpeg), focal points, blur placeholders
10
- - **Per-language versioning** — independent draft/published/scheduled per language
26
+ - **Per-language versioning** — independent draft / published / scheduled per language
11
27
  - **Layout DSL** — organize admin editor with sections, columns, cards, accordions
12
28
  - **Frontend components** — `<Image>`, `<Video>`, `<StructuredContent>`, `<Media>` for SvelteKit
13
29
  - **REST API** — API key auth, CRUD endpoints, schema introspection
14
30
  - **Entity API** — server-side programmatic CRUD for scripts, migrations, webhooks
15
31
  - **Code generation** — TypeScript types and Zod schemas from your content schema
16
- - **Pluggable adapters** — swap database, file storage, email, and AI providers
32
+ - **Pluggable adapters** — swap database, file storage, email, AI provider
17
33
  - **Plugin system** — lifecycle hooks and custom field types
18
- - **Video transcoding** — auto-transcode to mp4/webm with background processing
34
+ - **Strict config validation** — invalid configs throw a `ConfigValidationError` listing every issue with a path and a fix hint
35
+
36
+ ## System requirements
37
+
38
+ - **Node.js** 18+ (20 LTS recommended)
39
+ - **PostgreSQL** 14+ — required by the bundled `db-postgres` adapter; or write your own [`DatabaseAdapter`](#writing-your-own-adapter)
40
+ - **ffmpeg + ffprobe** — optional, only required for video transcoding (set `FFMPEG_PATH` / `FFPROBE_PATH` if not on `PATH`)
41
+ - A SvelteKit project (Kit ≥ 2.48, Svelte ≥ 5.43, Vite ≥ 7)
42
+
43
+ ## Quick start (5 minutes)
19
44
 
20
- ## Quick Start
45
+ The five steps below take you from `pnpm add` to a running admin at `/admin`.
46
+
47
+ **1. Install:**
21
48
 
22
49
  ```bash
23
50
  pnpm add includio-cms
51
+ pnpm dlx includio install-peers
52
+ ```
53
+
54
+ **2. Configure environment:**
55
+
56
+ ```bash
57
+ cp node_modules/includio-cms/.env.example .env
58
+ # edit .env — set DATABASE_URL and BETTER_AUTH_SECRET
24
59
  ```
25
60
 
26
- ```typescript
61
+ Generate a secret:
62
+
63
+ ```bash
64
+ openssl rand -hex 32
65
+ ```
66
+
67
+ **3. Create `src/lib/cms/cms.config.ts`:**
68
+
69
+ ```ts
27
70
  import { defineConfig } from 'includio-cms/sveltekit';
28
71
  import { pg } from 'includio-cms/db-postgres';
29
72
  import { local } from 'includio-cms/files-local';
30
73
 
31
74
  export default defineConfig({
32
75
  languages: ['en'],
33
- db: pg({ databaseUrl: process.env.DATABASE_URL }),
76
+ db: pg({ databaseUrl: process.env.DATABASE_URL! }),
34
77
  files: local(),
35
- collections: [/* ... */],
78
+ auth: { secret: process.env.BETTER_AUTH_SECRET! },
79
+ collections: []
36
80
  });
37
81
  ```
38
82
 
39
- See the [full documentation](/docs) for installation, configuration, and usage.
83
+ **4. Scaffold admin routes + create a user:**
40
84
 
41
- ## Writing your own adapter
85
+ ```bash
86
+ pnpm dlx includio scaffold admin
87
+ pnpm dlx includio create-user
88
+ ```
42
89
 
43
- `includio-cms` ships with default adapters (`db-postgres`, `files-local`, `email-nodemailer`, `ai-openai`, `ai-claude`). Each is a factory returning an object that implements a `@public` contract from `includio-cms/types`. Build your own by implementing the same interface — TypeScript will enforce the surface.
90
+ **5. Run the dev server:**
44
91
 
45
- Four adapter contracts:
92
+ ```bash
93
+ pnpm dev
94
+ ```
46
95
 
47
- - `DatabaseAdapter` entries, versions, media, tags, form submissions, consent logs.
48
- - `FilesAdapter` — file upload/download/list, optional private files (e.g. shop receipts).
49
- - `EmailAdapter` `sendMail`, default from address/name.
50
- - `AIAdapter` — currently `generateAltText(fileId)`.
96
+ Open `http://localhost:5173/admin` and sign in with the user you just created.
97
+
98
+ > Need more? Full reference in [`DOCS.md`](./DOCS.md). API surface in [`API.md`](./API.md).
99
+
100
+ ## Configuration
101
+
102
+ `defineConfig({ ... })` is your single source of truth. The shape (abridged):
103
+
104
+ ```ts
105
+ defineConfig({
106
+ languages: ['en', 'pl'], // first entry is the default locale
107
+ db, files, // required adapters
108
+ email, ai, // optional adapters
109
+ auth: { secret }, // required for /admin
110
+ collections: [postsCollection],
111
+ singles: [siteSettings],
112
+ forms: [contactForm],
113
+ apiKeys: [{ key: '...', role: 'admin' }],
114
+ media: { /* sharp / video / maintenance options */ },
115
+ typography: { fixOrphans: true },
116
+ shop, cmp // optional modules
117
+ });
118
+ ```
119
+
120
+ Invalid configs throw immediately at startup with every issue listed:
121
+
122
+ ```
123
+ ConfigValidationError: CMSConfig validation failed (2 issues):
124
+ - languages[0]: must be a 2-letter ISO code (e.g. 'en' or 'pl-PL')
125
+ - collections[1].slug: duplicate collection slug 'posts'
126
+ — Hint: each collection/single/form must have a unique `slug`
127
+ ```
128
+
129
+ See [`DOCS.md`](./DOCS.md) for every field type and option.
130
+
131
+ ## Adapters
132
+
133
+ `includio-cms` ships four pluggable contracts. Each has a default implementation; bring your own when you need to.
134
+
135
+ | Contract | Default | Lazy peer dep |
136
+ |--------------------|------------------------|------------------------------|
137
+ | `DatabaseAdapter` | `includio-cms/db-postgres` | `postgres`, `drizzle-orm` |
138
+ | `FilesAdapter` | `includio-cms/files-local` | — |
139
+ | `EmailAdapter` | `includio-cms/email-nodemailer` | `nodemailer` |
140
+ | `AIAdapter` | `includio-cms/ai-openai`, `includio-cms/ai-claude` | `openai`, `@anthropic-ai/sdk` |
51
141
 
52
142
  Source of truth: [`src/lib/types/adapters/`](src/lib/types/adapters/).
53
143
 
54
- ### Minimal custom DB adapter (sketch)
144
+ ### Writing your own adapter
145
+
146
+ Each contract is a TypeScript interface — implement every required method (optionals are marked `?`). TypeScript will enforce the surface.
55
147
 
56
148
  ```ts
57
149
  import type { DatabaseAdapter } from 'includio-cms/types';
@@ -68,7 +160,7 @@ export function myDbAdapter(config: { connection: string }): DatabaseAdapter {
68
160
  createEntryVersion: async (data) => { /* ... */ },
69
161
  updateEntryVersion: async (data) => { /* ... */ },
70
162
  getEntryVersions: async (data) => { /* ... */ },
71
- deleteEntryVersion: async (data) => { /* ... */ },
163
+ deleteEntryVersion: async (data) => { /* ... */ }
72
164
  // ...form submissions, media files, image/video styles, tags, consent logs...
73
165
  };
74
166
  }
@@ -90,25 +182,39 @@ Reference implementation: [`src/lib/db-postgres/index.ts`](src/lib/db-postgres/i
90
182
 
91
183
  ### Optional peer dependencies
92
184
 
93
- The default email + AI adapters depend on third-party SDKs that **are not installed by default**. If you use them, install the peer:
185
+ The default email + AI adapters depend on third-party SDKs that **are not installed by default**. Install the peer once you use the adapter:
186
+
187
+ | Adapter | Required peer | Install |
188
+ |--------------------|---------------------|-------------------------------|
189
+ | `email-nodemailer` | `nodemailer` | `pnpm add nodemailer` |
190
+ | `ai-openai` | `openai` | `pnpm add openai` |
191
+ | `ai-claude` | `@anthropic-ai/sdk` | `pnpm add @anthropic-ai/sdk` |
192
+
193
+ Each SDK is loaded lazily on first call. A missing peer throws a clear error at runtime — install it and the adapter wakes up. `pnpm dlx includio install-peers` automates this for the parts of the CMS you have configured.
194
+
195
+ ## CLI
196
+
197
+ The `includio` binary ships with three subcommands. Run any of them with `--help` for details.
94
198
 
95
- | Adapter | Required peer | Install |
96
- | ------------------------- | ------------------------ | -------------------------------------- |
97
- | `email-nodemailer` | `nodemailer` | `pnpm add nodemailer` |
98
- | `ai-openai` | `openai` | `pnpm add openai` |
99
- | `ai-claude` | `@anthropic-ai/sdk` | `pnpm add @anthropic-ai/sdk` |
199
+ | Command | What it does |
200
+ |--------------------------|-----------------------------------------------------------|
201
+ | `includio scaffold admin`| Generates `/admin` and `/api/admin` route files |
202
+ | `includio install-peers` | Installs missing optional peer SDKs based on your config |
203
+ | `includio create-user` | Creates an admin/user account interactively |
100
204
 
101
- Each SDK is loaded lazily on first call. Missing peer throws a clear error at runtime — install it and the adapter wakes up.
205
+ Global flags: `--help, -h` and `--version, -v`.
102
206
 
103
207
  ## Links
104
208
 
105
- - [Full Documentation](DOCS.md)
106
- - [Changelog](CHANGELOG.md)
107
- - [Roadmap](ROADMAP.md)
209
+ - [Full documentation](./DOCS.md)
210
+ - [Public API surface](./API.md)
211
+ - [Changelog](./CHANGELOG.md)
212
+ - [Roadmap](./ROADMAP.md)
213
+ - [Known risks](./KNOWN-RISKS.md)
108
214
 
109
- ## For AI Assistants
215
+ ## For AI assistants
110
216
 
111
- Full documentation is in [`DOCS.md`](DOCS.md) (shipped with the npm package). When working on a project using includio-cms, read `node_modules/includio-cms/DOCS.md` for the complete API reference, available components, and migration guides.
217
+ Full documentation is in [`DOCS.md`](./DOCS.md) (shipped with the npm package). When working on a project using includio-cms, read `node_modules/includio-cms/DOCS.md` for the complete API reference, available components, and migration guides.
112
218
 
113
219
  ## License
114
220
 
package/ROADMAP.md CHANGED
@@ -339,6 +339,10 @@
339
339
  > 13 faz w ~16 sesjach, droga 0.16 → 1.0.0. Lean scope: stabilizacja, security, testy, docs, audit + polish shopa. Bez nowych feature'ów.
340
340
 
341
341
  - [x] `[breaking]` `[P0]` API surface lock — exports trim 26→15, JSDoc tagi (@public/@internal/@experimental), `API.md` autogenerated (0.20.0) <!-- files: package.json, scripts/generate-api-md.ts -->
342
+ - [x] `[chore]` `[P1]` Faza 6 — Test foundation: docker postgres + integration project + 43 cases (30 REST + 6 shop checkout + 7 webhook signature/idempotency dla PayU/InPost), coverage report <!-- files: tests/integration/rest/, tests/helpers/{rest,shop}.ts, tests/integration/fixtures/shopConfig.ts, tests/setup.ts -->
343
+ - [x] `[chore]` `[P1]` Faza 7 — E2E foundation: Playwright config (storageState reuse, setup project), `db_test_e2e` na 5435, `e2e/setup/global-setup.ts` (drizzle push), `e2e/admin/auth.setup.ts` (sign-up + admin role + login), 4 specy zielone w ~23s: login, collection-crud (blog-post create→edit→archive→permanent-delete), media-upload, user-mgmt (create→delete); data-testid na critical buttons admin <!-- files: playwright.config.ts, e2e/admin/*, src/lib/admin/client/{collection,entry,users}/, src/lib/admin/components/media/file-upload.svelte -->
344
+ - [x] `[chore]` `[P1]` Faza 8 — CI na PR: `.github/workflows/ci.yml` z jobs lint/typecheck/unit/integration (postgres :5434)/e2e (postgres :5435 + playwright cache)/build, pnpm cache, trace artifacts on fail, README CI badge <!-- files: .github/workflows/ci.yml, README.md -->
345
+ - [x] `[breaking]` `[P0]` Faza 9 — DX & config validation (0.22.0): `defineConfig` Zod strict z field path + hint, `CmsError` + `ConfigValidationError` z code/context, resolver throw sites zmigrowane, CLI `--help` per subcommand + `--version`, `.env.example` rozszerzony o `INCLUDIO_*`, README rebuild (TOC + system req + 5-min quickstart), JSDoc body (opis + @param + @returns + @example) na każdym `@public` <!-- files: src/lib/core/errors.ts, src/lib/types/cms.schema.ts, src/lib/sveltekit/config.ts, src/lib/cli/index.ts, README.md, .env.example, src/lib/updates/0.22.0/ -->
342
346
  - [ ] `[fix]` `[P1]` Select field — `defaultValue` propagacja do zod schema (full repro: `ideas/post-v1/select-field-defaultvalue-bug.md`); fix planowany w Fazie 12 (RC)
343
347
 
344
348
  ## v1.x — Post-v1.0 deferred
@@ -1,7 +1,19 @@
1
1
  import { type RequestHandler } from '@sveltejs/kit';
2
2
  /**
3
- * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
3
+ * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
4
+ * `RequestHandler`s authenticated via the `x-api-key` header. Mount in
5
+ * `src/routes/admin/api/rest/[...restPath]/+server.ts`.
6
+ *
7
+ * @returns An object with `GET`, `POST`, `PUT`, `DELETE` SvelteKit
8
+ * `RequestHandler`s, ready to re-export from a `+server.ts` route.
4
9
  * @public
10
+ * @example
11
+ * ```ts
12
+ * // src/routes/admin/api/rest/[...restPath]/+server.ts
13
+ * import { createRestApiHandler } from 'includio-cms/admin/remote';
14
+ *
15
+ * export const { GET, POST, PUT, DELETE } = createRestApiHandler();
16
+ * ```
5
17
  */
6
18
  export declare function createRestApiHandler(): {
7
19
  GET: RequestHandler;
@@ -50,8 +50,20 @@ function matchRoute(method, path) {
50
50
  return null;
51
51
  }
52
52
  /**
53
- * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }` `RequestHandler`s authenticated via `x-api-key` header. Mount in `src/routes/admin/api/rest/[...restPath]/+server.ts`.
53
+ * REST API handler factory. Returns `{ GET, POST, PUT, DELETE }`
54
+ * `RequestHandler`s authenticated via the `x-api-key` header. Mount in
55
+ * `src/routes/admin/api/rest/[...restPath]/+server.ts`.
56
+ *
57
+ * @returns An object with `GET`, `POST`, `PUT`, `DELETE` SvelteKit
58
+ * `RequestHandler`s, ready to re-export from a `+server.ts` route.
54
59
  * @public
60
+ * @example
61
+ * ```ts
62
+ * // src/routes/admin/api/rest/[...restPath]/+server.ts
63
+ * import { createRestApiHandler } from 'includio-cms/admin/remote';
64
+ *
65
+ * export const { GET, POST, PUT, DELETE } = createRestApiHandler();
66
+ * ```
55
67
  */
56
68
  export function createRestApiHandler() {
57
69
  function handle(method) {
@@ -2,6 +2,15 @@
2
2
  * Generate a cryptographically random API key (32 bytes, base64url-encoded).
3
3
  * Use the result for `ApiKeyConfig.key`. Full rotation requires updating
4
4
  * `cms.config.ts` and redeploying — see `KNOWN-RISKS.md` §2.
5
+ *
6
+ * @returns A 43-character base64url string (no padding).
5
7
  * @public
8
+ * @example
9
+ * ```ts
10
+ * import { generateApiKey } from 'includio-cms/admin/remote';
11
+ *
12
+ * const key = generateApiKey();
13
+ * console.log(key); // 'A1b2C3...xYz'
14
+ * ```
6
15
  */
7
16
  export declare function generateApiKey(): string;
@@ -3,7 +3,16 @@ import { randomBytes } from 'node:crypto';
3
3
  * Generate a cryptographically random API key (32 bytes, base64url-encoded).
4
4
  * Use the result for `ApiKeyConfig.key`. Full rotation requires updating
5
5
  * `cms.config.ts` and redeploying — see `KNOWN-RISKS.md` §2.
6
+ *
7
+ * @returns A 43-character base64url string (no padding).
6
8
  * @public
9
+ * @example
10
+ * ```ts
11
+ * import { generateApiKey } from 'includio-cms/admin/remote';
12
+ *
13
+ * const key = generateApiKey();
14
+ * console.log(key); // 'A1b2C3...xYz'
15
+ * ```
7
16
  */
8
17
  export function generateApiKey() {
9
18
  return randomBytes(32).toString('base64url');
@@ -897,7 +897,7 @@
897
897
  >
898
898
  <AlertDialog.Footer>
899
899
  <AlertDialog.Cancel>{lang[interfaceLanguage.current].cancel}</AlertDialog.Cancel>
900
- <AlertDialog.Action onclick={confirmDelete}
900
+ <AlertDialog.Action onclick={confirmDelete} data-testid="confirm-delete-entry"
901
901
  >{lang[interfaceLanguage.current].delete}</AlertDialog.Action
902
902
  >
903
903
  </AlertDialog.Footer>
@@ -20,7 +20,7 @@
20
20
  <h3 class="text-lg font-bold text-foreground mb-2">{title}</h3>
21
21
  <p class="text-sm text-muted-foreground max-w-[360px] mb-6">{description}</p>
22
22
  {#if ctaLabel && onCta}
23
- <Button variant="default" onclick={onCta}>
23
+ <Button variant="default" onclick={onCta} data-testid="create-entry-button">
24
24
  <Plus class="size-4 mr-1.5" />
25
25
  {ctaLabel}
26
26
  </Button>
@@ -51,18 +51,18 @@
51
51
  </DropdownMenu.Item>
52
52
  <DropdownMenu.Separator />
53
53
  {#if onRestore}
54
- <DropdownMenu.Item onclick={onRestore}>
54
+ <DropdownMenu.Item onclick={onRestore} data-testid="row-action-restore">
55
55
  <ArchiveOff class="mr-2 size-4" />
56
56
  {t.restore}
57
57
  </DropdownMenu.Item>
58
58
  {:else}
59
- <DropdownMenu.Item onclick={onArchive}>
59
+ <DropdownMenu.Item onclick={onArchive} data-testid="row-action-archive">
60
60
  <Archive class="mr-2 size-4" />
61
61
  {t.archive}
62
62
  </DropdownMenu.Item>
63
63
  {/if}
64
64
  {#if onDelete}
65
- <DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={onDelete}>
65
+ <DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={onDelete} data-testid="row-action-delete">
66
66
  <Trash class="mr-2 size-4" />
67
67
  {t.delete}
68
68
  </DropdownMenu.Item>
@@ -131,6 +131,7 @@
131
131
  class="gap-1.5 {hasActiveFilter
132
132
  ? 'border-primary/30 bg-lavender-lighter text-primary'
133
133
  : ''}"
134
+ data-testid="status-filter-trigger"
134
135
  >
135
136
  <Filter class="size-3.5" />
136
137
  {t.status}{hasActiveFilter ? `: ${activeFilterLabel}` : ''}
@@ -148,6 +149,7 @@
148
149
  onStatusFilterChange(option.value);
149
150
  statusPopoverOpen = false;
150
151
  }}
152
+ data-testid="status-filter-option-{option.value ?? 'all'}"
151
153
  >
152
154
  {option.label()}
153
155
  </button>
@@ -228,7 +230,7 @@
228
230
 
229
231
  <div class="flex-1"></div>
230
232
 
231
- <Button variant="default" size="sm" onclick={onCreateEntry}>
233
+ <Button variant="default" size="sm" onclick={onCreateEntry} data-testid="create-entry-button">
232
234
  <Plus class="mr-1 size-4" />
233
235
  {createLabel}
234
236
  </Button>
@@ -166,6 +166,7 @@
166
166
  type="button"
167
167
  class="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center gap-1.5 rounded-l-lg px-4 py-1.5 text-[13px] font-semibold transition-colors"
168
168
  onclick={() => onSave('published-now')}
169
+ data-testid="publish-button"
169
170
  >
170
171
  <SendIcon class="size-3.5" />
171
172
  {primaryButtonLabel}
@@ -179,13 +180,14 @@
179
180
  type="button"
180
181
  class="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center rounded-r-lg border-l border-white/20 px-1.5 py-1.5 transition-colors"
181
182
  aria-label={lang[interfaceLanguage.current].saveDraft}
183
+ data-testid="save-draft-trigger"
182
184
  >
183
185
  <ChevronDownIcon class="size-3.5" />
184
186
  </button>
185
187
  {/snippet}
186
188
  </DropdownMenu.Trigger>
187
189
  <DropdownMenu.Content align="end" class="w-48">
188
- <DropdownMenu.Item onclick={onSaveDraft}>
190
+ <DropdownMenu.Item onclick={onSaveDraft} data-testid="save-draft-button">
189
191
  {lang[interfaceLanguage.current].saveDraft}
190
192
  <DropdownMenu.Shortcut>{shortcutLabel}</DropdownMenu.Shortcut>
191
193
  </DropdownMenu.Item>
@@ -141,12 +141,12 @@
141
141
  <div class="space-y-2">
142
142
  <Label>{lang.role}</Label>
143
143
  <Select.Root type="single" value={role} onValueChange={(v) => v && (role = v as UserRole)}>
144
- <Select.Trigger class="w-full" aria-describedby="create-role-hint">
144
+ <Select.Trigger class="w-full" aria-describedby="create-role-hint" data-testid="role-select">
145
145
  {role === 'admin' ? lang.roleAdmin : lang.roleUser}
146
146
  </Select.Trigger>
147
147
  <Select.Content>
148
- <Select.Item value="user">{lang.roleUser}</Select.Item>
149
- <Select.Item value="admin">{lang.roleAdmin}</Select.Item>
148
+ <Select.Item value="user" data-testid="role-option-user">{lang.roleUser}</Select.Item>
149
+ <Select.Item value="admin" data-testid="role-option-admin">{lang.roleAdmin}</Select.Item>
150
150
  </Select.Content>
151
151
  </Select.Root>
152
152
  <p id="create-role-hint" class="text-xs" style="color: var(--text-light);">
@@ -158,7 +158,7 @@
158
158
  {/if}
159
159
  <Dialog.Footer>
160
160
  <Button type="button" variant="outline" onclick={() => onOpenChange(false)}>{lang.cancel}</Button>
161
- <Button type="submit" disabled={loading}>{lang.createUser}</Button>
161
+ <Button type="submit" disabled={loading} data-testid="create-user-submit">{lang.createUser}</Button>
162
162
  </Dialog.Footer>
163
163
  </form>
164
164
  </Dialog.Content>