@setzkasten-cms/core 0.4.2

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/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ import {
2
+ defineCollection,
3
+ defineConfig,
4
+ defineSection,
5
+ f
6
+ } from "./chunk-IL2PWN4R.js";
7
+
8
+ // src/validation/schema-to-zod.ts
9
+ import { z } from "zod";
10
+ function fieldToZod(field) {
11
+ switch (field.type) {
12
+ case "text":
13
+ return textToZod(field);
14
+ case "number":
15
+ return numberToZod(field);
16
+ case "boolean":
17
+ return z.boolean();
18
+ case "select":
19
+ return selectToZod(field);
20
+ case "icon":
21
+ return field.required ? z.string().min(1, `${field.label} ist erforderlich`) : z.string();
22
+ case "image":
23
+ return imageToZod(field);
24
+ case "array":
25
+ return arrayToZod(field);
26
+ case "object":
27
+ return objectToZod(field);
28
+ case "color":
29
+ return z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color");
30
+ case "override":
31
+ return overrideToZod(field);
32
+ default:
33
+ return z.unknown();
34
+ }
35
+ }
36
+ function textToZod(field) {
37
+ let schema = z.string();
38
+ if (field.required) {
39
+ schema = schema.min(1, `${field.label} ist erforderlich`);
40
+ }
41
+ if (field.maxLength) {
42
+ schema = schema.max(field.maxLength, `${field.label} darf max. ${field.maxLength} Zeichen haben`);
43
+ }
44
+ if (field.pattern) {
45
+ schema = schema.regex(field.pattern, `${field.label} hat ein ung\xFCltiges Format`);
46
+ }
47
+ return schema;
48
+ }
49
+ function numberToZod(field) {
50
+ let schema = z.number();
51
+ if (field.min !== void 0) {
52
+ schema = schema.min(field.min);
53
+ }
54
+ if (field.max !== void 0) {
55
+ schema = schema.max(field.max);
56
+ }
57
+ return schema;
58
+ }
59
+ function selectToZod(field) {
60
+ const values = field.options.map((o) => o.value);
61
+ if (values.length === 0) return z.string();
62
+ const [first, ...rest] = values;
63
+ return z.enum([first, ...rest]);
64
+ }
65
+ function imageToZod(field) {
66
+ const schema = z.object({
67
+ path: z.string(),
68
+ alt: z.string().optional()
69
+ });
70
+ return field.required ? schema.refine((v) => v.path.length > 0, `${field.label} ist erforderlich`) : schema;
71
+ }
72
+ function arrayToZod(field) {
73
+ const itemSchema = fieldToZod(field.itemField);
74
+ let schema = z.array(itemSchema);
75
+ if (field.minItems !== void 0) {
76
+ schema = schema.min(field.minItems);
77
+ }
78
+ if (field.maxItems !== void 0) {
79
+ schema = schema.max(field.maxItems);
80
+ }
81
+ return schema;
82
+ }
83
+ function objectToZod(field) {
84
+ return schemaToZod(field.fields);
85
+ }
86
+ function overrideToZod(field) {
87
+ const innerSchema = schemaToZod(field.fields);
88
+ return z.object({
89
+ active: z.boolean()
90
+ }).and(innerSchema.partial());
91
+ }
92
+ function schemaToZod(fields) {
93
+ const shape = {};
94
+ for (const [key, field] of Object.entries(fields)) {
95
+ const zodField = fieldToZod(field);
96
+ shape[key] = field.required ? zodField : zodField.optional();
97
+ }
98
+ return z.object(shape);
99
+ }
100
+
101
+ // src/serialization/serializer.ts
102
+ function serializeEntry(fields, values, registry) {
103
+ const result = {};
104
+ for (const [key, field] of Object.entries(fields)) {
105
+ const serializer = registry[field.type];
106
+ const value = values[key];
107
+ if (serializer && value !== void 0) {
108
+ result[key] = serializer.serialize(value);
109
+ } else if (value !== void 0) {
110
+ result[key] = value;
111
+ }
112
+ }
113
+ return result;
114
+ }
115
+ function deserializeEntry(fields, raw, registry) {
116
+ const result = {};
117
+ for (const [key, field] of Object.entries(fields)) {
118
+ const serializer = registry[field.type];
119
+ const rawValue = raw[key];
120
+ if (serializer && rawValue !== void 0) {
121
+ result[key] = serializer.deserialize(rawValue);
122
+ } else if (rawValue !== void 0) {
123
+ result[key] = rawValue;
124
+ } else if (field.defaultValue !== void 0) {
125
+ result[key] = field.defaultValue;
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+
131
+ // src/serialization/json-serializer.ts
132
+ var textSerializer = {
133
+ serialize: (value) => value,
134
+ deserialize: (raw) => typeof raw === "string" ? raw : String(raw ?? "")
135
+ };
136
+ var numberSerializer = {
137
+ serialize: (value) => value,
138
+ deserialize: (raw) => typeof raw === "number" ? raw : Number(raw ?? 0)
139
+ };
140
+ var booleanSerializer = {
141
+ serialize: (value) => value,
142
+ deserialize: (raw) => Boolean(raw)
143
+ };
144
+ var selectSerializer = {
145
+ serialize: (value) => value,
146
+ deserialize: (raw) => String(raw ?? "")
147
+ };
148
+ var imageSerializer = {
149
+ serialize: (value) => {
150
+ if (typeof value === "string") return value;
151
+ if (value && typeof value === "object" && "path" in value) {
152
+ return value.path;
153
+ }
154
+ return "";
155
+ },
156
+ deserialize: (raw) => {
157
+ if (typeof raw === "string") return { path: raw, alt: "" };
158
+ if (raw && typeof raw === "object" && "path" in raw) return raw;
159
+ return { path: "", alt: "" };
160
+ }
161
+ };
162
+ var colorSerializer = {
163
+ serialize: (value) => value,
164
+ deserialize: (raw) => String(raw ?? "#000000")
165
+ };
166
+ var jsonSerializerRegistry = {
167
+ text: textSerializer,
168
+ number: numberSerializer,
169
+ boolean: booleanSerializer,
170
+ select: selectSerializer,
171
+ image: imageSerializer,
172
+ color: colorSerializer,
173
+ // array, object, override are handled recursively by serializeEntry/deserializeEntry
174
+ array: {
175
+ serialize: (value) => Array.isArray(value) ? value : [],
176
+ deserialize: (raw) => Array.isArray(raw) ? raw : []
177
+ },
178
+ object: {
179
+ serialize: (value) => value && typeof value === "object" ? value : {},
180
+ deserialize: (raw) => raw && typeof raw === "object" ? raw : {}
181
+ },
182
+ override: {
183
+ serialize: (value) => {
184
+ if (value && typeof value === "object") return value;
185
+ return { active: false };
186
+ },
187
+ deserialize: (raw) => {
188
+ if (raw && typeof raw === "object") return raw;
189
+ return { active: false };
190
+ }
191
+ }
192
+ };
193
+
194
+ // src/commands/command.ts
195
+ function createCommandHistory() {
196
+ const undoStack = [];
197
+ const redoStack = [];
198
+ return {
199
+ execute(command) {
200
+ command.execute();
201
+ undoStack.push(command);
202
+ redoStack.length = 0;
203
+ },
204
+ undo() {
205
+ const command = undoStack.pop();
206
+ if (command) {
207
+ command.undo();
208
+ redoStack.push(command);
209
+ }
210
+ },
211
+ redo() {
212
+ const command = redoStack.pop();
213
+ if (command) {
214
+ command.execute();
215
+ undoStack.push(command);
216
+ }
217
+ },
218
+ canUndo() {
219
+ return undoStack.length > 0;
220
+ },
221
+ canRedo() {
222
+ return redoStack.length > 0;
223
+ },
224
+ clear() {
225
+ undoStack.length = 0;
226
+ redoStack.length = 0;
227
+ }
228
+ };
229
+ }
230
+
231
+ // src/events/content-event-bus.ts
232
+ function createEventBus() {
233
+ const listeners = /* @__PURE__ */ new Map();
234
+ return {
235
+ emit(event) {
236
+ const handlers = listeners.get(event.type);
237
+ if (handlers) {
238
+ for (const handler of handlers) {
239
+ handler(event);
240
+ }
241
+ }
242
+ },
243
+ on(type, handler) {
244
+ if (!listeners.has(type)) {
245
+ listeners.set(type, /* @__PURE__ */ new Set());
246
+ }
247
+ const handlers = listeners.get(type);
248
+ handlers.add(handler);
249
+ return () => {
250
+ handlers.delete(handler);
251
+ if (handlers.size === 0) {
252
+ listeners.delete(type);
253
+ }
254
+ };
255
+ }
256
+ };
257
+ }
258
+
259
+ // src/errors/errors.ts
260
+ function ok(value) {
261
+ return { ok: true, value };
262
+ }
263
+ function err(error) {
264
+ return { ok: false, error };
265
+ }
266
+ function validationError(fieldPath, rule, message) {
267
+ return { type: "validation", fieldPath, rule, message, timestamp: Date.now() };
268
+ }
269
+ function conflictError(message) {
270
+ return { type: "conflict", message, timestamp: Date.now() };
271
+ }
272
+ function rateLimitError(retryAfter, remaining) {
273
+ return {
274
+ type: "rate-limit",
275
+ retryAfter,
276
+ remaining,
277
+ message: `Rate limit exceeded. Retry after ${retryAfter}s`,
278
+ timestamp: Date.now()
279
+ };
280
+ }
281
+ function authError(message) {
282
+ return { type: "auth", message, timestamp: Date.now() };
283
+ }
284
+ function networkError(message, cause) {
285
+ return { type: "network", message, cause, timestamp: Date.now() };
286
+ }
287
+ function notFoundError(path) {
288
+ return { type: "not-found", path, message: `Not found: ${path}`, timestamp: Date.now() };
289
+ }
290
+ function serializationError(message, path) {
291
+ return { type: "serialization", message, path, timestamp: Date.now() };
292
+ }
293
+ export {
294
+ authError,
295
+ conflictError,
296
+ createCommandHistory,
297
+ createEventBus,
298
+ defineCollection,
299
+ defineConfig,
300
+ defineSection,
301
+ deserializeEntry,
302
+ err,
303
+ f,
304
+ fieldToZod,
305
+ jsonSerializerRegistry,
306
+ networkError,
307
+ notFoundError,
308
+ ok,
309
+ rateLimitError,
310
+ schemaToZod,
311
+ serializationError,
312
+ serializeEntry,
313
+ validationError
314
+ };
@@ -0,0 +1,28 @@
1
+ import { y as SetzKastenConfig } from './define-config-bJ65Zaui.js';
2
+
3
+ /**
4
+ * @setzkasten-cms/core/testing – Test utilities for other packages
5
+ */
6
+ /**
7
+ * Creates a minimal test config for use in tests.
8
+ */
9
+ declare function createTestConfig(): SetzKastenConfig;
10
+ /**
11
+ * Creates sample content data matching the test config schema.
12
+ */
13
+ declare function createTestContent(): {
14
+ hero: {
15
+ heading: string;
16
+ subtitle: string;
17
+ badge: string;
18
+ };
19
+ benefits: {
20
+ items: {
21
+ title: string;
22
+ description: string;
23
+ icon: string;
24
+ }[];
25
+ };
26
+ };
27
+
28
+ export { createTestConfig, createTestContent };
@@ -0,0 +1,69 @@
1
+ import {
2
+ defineConfig,
3
+ defineSection,
4
+ f
5
+ } from "./chunk-IL2PWN4R.js";
6
+
7
+ // src/testing/index.ts
8
+ function createTestConfig() {
9
+ return defineConfig({
10
+ storage: { kind: "local" },
11
+ auth: { providers: ["email"] },
12
+ products: {
13
+ test: {
14
+ label: "Test Product",
15
+ sections: {
16
+ hero: defineSection({
17
+ label: "Hero",
18
+ fields: {
19
+ heading: f.text({ label: "Heading", required: true }),
20
+ subtitle: f.text({ label: "Subtitle", multiline: true }),
21
+ badge: f.text({ label: "Badge" })
22
+ }
23
+ }),
24
+ benefits: defineSection({
25
+ label: "Benefits",
26
+ fields: {
27
+ items: f.array(
28
+ f.object(
29
+ {
30
+ title: f.text({ label: "Title" }),
31
+ description: f.text({ label: "Description", multiline: true }),
32
+ icon: f.select({
33
+ label: "Icon",
34
+ options: [
35
+ { label: "Check", value: "check" },
36
+ { label: "Star", value: "star" }
37
+ ]
38
+ })
39
+ },
40
+ { label: "Benefit" }
41
+ ),
42
+ { label: "Benefits", minItems: 1 }
43
+ )
44
+ }
45
+ })
46
+ }
47
+ }
48
+ }
49
+ });
50
+ }
51
+ function createTestContent() {
52
+ return {
53
+ hero: {
54
+ heading: "Test Heading",
55
+ subtitle: "Test Subtitle",
56
+ badge: "New"
57
+ },
58
+ benefits: {
59
+ items: [
60
+ { title: "Benefit 1", description: "Description 1", icon: "check" },
61
+ { title: "Benefit 2", description: "Description 2", icon: "star" }
62
+ ]
63
+ }
64
+ };
65
+ }
66
+ export {
67
+ createTestConfig,
68
+ createTestContent
69
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@setzkasten-cms/core",
3
+ "version": "0.4.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./src/index.ts",
8
+ "types": "./src/index.ts"
9
+ },
10
+ "./testing": {
11
+ "import": "./src/testing.ts",
12
+ "types": "./src/testing.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "dependencies": {
20
+ "immer": "^10.2.0",
21
+ "zod": "^3.25.76"
22
+ },
23
+ "devDependencies": {
24
+ "vitest": "^3.2.4"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "typecheck": "tsc --noEmit"
31
+ }
32
+ }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Command } from './command'
3
+ import { createCommandHistory } from './command'
4
+
5
+ function createTestCommand(state: { value: string }, newValue: string): Command {
6
+ const previousValue = state.value
7
+ return {
8
+ execute() {
9
+ state.value = newValue
10
+ },
11
+ undo() {
12
+ state.value = previousValue
13
+ },
14
+ describe() {
15
+ return `Set value to "${newValue}"`
16
+ },
17
+ }
18
+ }
19
+
20
+ describe('CommandHistory', () => {
21
+ it('executes commands', () => {
22
+ const history = createCommandHistory()
23
+ const state = { value: 'initial' }
24
+
25
+ history.execute(createTestCommand(state, 'updated'))
26
+ expect(state.value).toBe('updated')
27
+ })
28
+
29
+ it('undoes commands', () => {
30
+ const history = createCommandHistory()
31
+ const state = { value: 'initial' }
32
+
33
+ history.execute(createTestCommand(state, 'updated'))
34
+ history.undo()
35
+ expect(state.value).toBe('initial')
36
+ })
37
+
38
+ it('redoes commands', () => {
39
+ const history = createCommandHistory()
40
+ const state = { value: 'initial' }
41
+
42
+ history.execute(createTestCommand(state, 'updated'))
43
+ history.undo()
44
+ history.redo()
45
+ expect(state.value).toBe('updated')
46
+ })
47
+
48
+ it('clears redo stack on new command', () => {
49
+ const history = createCommandHistory()
50
+ const state = { value: 'initial' }
51
+
52
+ history.execute(createTestCommand(state, 'first'))
53
+ history.undo()
54
+ expect(history.canRedo()).toBe(true)
55
+
56
+ history.execute(createTestCommand(state, 'second'))
57
+ expect(history.canRedo()).toBe(false)
58
+ })
59
+
60
+ it('reports canUndo/canRedo correctly', () => {
61
+ const history = createCommandHistory()
62
+ const state = { value: 'initial' }
63
+
64
+ expect(history.canUndo()).toBe(false)
65
+ expect(history.canRedo()).toBe(false)
66
+
67
+ history.execute(createTestCommand(state, 'updated'))
68
+ expect(history.canUndo()).toBe(true)
69
+ expect(history.canRedo()).toBe(false)
70
+
71
+ history.undo()
72
+ expect(history.canUndo()).toBe(false)
73
+ expect(history.canRedo()).toBe(true)
74
+ })
75
+
76
+ it('clears history', () => {
77
+ const history = createCommandHistory()
78
+ const state = { value: 'initial' }
79
+
80
+ history.execute(createTestCommand(state, 'updated'))
81
+ history.clear()
82
+ expect(history.canUndo()).toBe(false)
83
+ expect(history.canRedo()).toBe(false)
84
+ })
85
+ })
@@ -0,0 +1,65 @@
1
+ import type { FieldPath } from '../fields/field-definition'
2
+
3
+ /**
4
+ * Command pattern – all mutations go through commands.
5
+ * This gives undo/redo for free.
6
+ */
7
+ export interface Command {
8
+ execute(): void
9
+ undo(): void
10
+ describe(): string
11
+ }
12
+
13
+ /**
14
+ * Command history – manages undo/redo stacks.
15
+ */
16
+ export interface CommandHistory {
17
+ execute(command: Command): void
18
+ undo(): void
19
+ redo(): void
20
+ canUndo(): boolean
21
+ canRedo(): boolean
22
+ clear(): void
23
+ }
24
+
25
+ export function createCommandHistory(): CommandHistory {
26
+ const undoStack: Command[] = []
27
+ const redoStack: Command[] = []
28
+
29
+ return {
30
+ execute(command) {
31
+ command.execute()
32
+ undoStack.push(command)
33
+ redoStack.length = 0 // clear redo stack on new action
34
+ },
35
+
36
+ undo() {
37
+ const command = undoStack.pop()
38
+ if (command) {
39
+ command.undo()
40
+ redoStack.push(command)
41
+ }
42
+ },
43
+
44
+ redo() {
45
+ const command = redoStack.pop()
46
+ if (command) {
47
+ command.execute()
48
+ undoStack.push(command)
49
+ }
50
+ },
51
+
52
+ canUndo() {
53
+ return undoStack.length > 0
54
+ },
55
+
56
+ canRedo() {
57
+ return redoStack.length > 0
58
+ },
59
+
60
+ clear() {
61
+ undoStack.length = 0
62
+ redoStack.length = 0
63
+ },
64
+ }
65
+ }
@@ -0,0 +1,112 @@
1
+ import type { FieldPath } from '../fields/field-definition'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Discriminated error union
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type SetzKastenError =
8
+ | ValidationError
9
+ | ConflictError
10
+ | RateLimitError
11
+ | AuthError
12
+ | NetworkError
13
+ | NotFoundError
14
+ | SerializationError
15
+
16
+ interface BaseError {
17
+ readonly message: string
18
+ readonly cause?: unknown
19
+ readonly timestamp: number
20
+ }
21
+
22
+ export interface ValidationError extends BaseError {
23
+ readonly type: 'validation'
24
+ readonly fieldPath: FieldPath
25
+ readonly rule: string
26
+ }
27
+
28
+ export interface ConflictError extends BaseError {
29
+ readonly type: 'conflict'
30
+ }
31
+
32
+ export interface RateLimitError extends BaseError {
33
+ readonly type: 'rate-limit'
34
+ readonly retryAfter: number
35
+ readonly remaining: number
36
+ }
37
+
38
+ export interface AuthError extends BaseError {
39
+ readonly type: 'auth'
40
+ }
41
+
42
+ export interface NetworkError extends BaseError {
43
+ readonly type: 'network'
44
+ }
45
+
46
+ export interface NotFoundError extends BaseError {
47
+ readonly type: 'not-found'
48
+ readonly path: string
49
+ }
50
+
51
+ export interface SerializationError extends BaseError {
52
+ readonly type: 'serialization'
53
+ readonly path?: string
54
+ }
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Result type for predictable errors
58
+ // ---------------------------------------------------------------------------
59
+
60
+ export type Result<T, E = SetzKastenError> =
61
+ | { readonly ok: true; readonly value: T }
62
+ | { readonly ok: false; readonly error: E }
63
+
64
+ export function ok<T>(value: T): Result<T, never> {
65
+ return { ok: true, value }
66
+ }
67
+
68
+ export function err<E>(error: E): Result<never, E> {
69
+ return { ok: false, error }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Error factories
74
+ // ---------------------------------------------------------------------------
75
+
76
+ export function validationError(
77
+ fieldPath: FieldPath,
78
+ rule: string,
79
+ message: string,
80
+ ): ValidationError {
81
+ return { type: 'validation', fieldPath, rule, message, timestamp: Date.now() }
82
+ }
83
+
84
+ export function conflictError(message: string): ConflictError {
85
+ return { type: 'conflict', message, timestamp: Date.now() }
86
+ }
87
+
88
+ export function rateLimitError(retryAfter: number, remaining: number): RateLimitError {
89
+ return {
90
+ type: 'rate-limit',
91
+ retryAfter,
92
+ remaining,
93
+ message: `Rate limit exceeded. Retry after ${retryAfter}s`,
94
+ timestamp: Date.now(),
95
+ }
96
+ }
97
+
98
+ export function authError(message: string): AuthError {
99
+ return { type: 'auth', message, timestamp: Date.now() }
100
+ }
101
+
102
+ export function networkError(message: string, cause?: unknown): NetworkError {
103
+ return { type: 'network', message, cause, timestamp: Date.now() }
104
+ }
105
+
106
+ export function notFoundError(path: string): NotFoundError {
107
+ return { type: 'not-found', path, message: `Not found: ${path}`, timestamp: Date.now() }
108
+ }
109
+
110
+ export function serializationError(message: string, path?: string): SerializationError {
111
+ return { type: 'serialization', message, path, timestamp: Date.now() }
112
+ }