@beechcms/core 0.6.0-preview.1 → 0.6.0-preview.3
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/content.repository.d.ts +84 -3
- package/dist/content.repository.d.ts.map +1 -1
- package/dist/content.repository.js +12 -0
- package/dist/dashboard-layout.d.ts +154 -0
- package/dist/dashboard-layout.d.ts.map +1 -0
- package/dist/dashboard-layout.js +186 -0
- package/dist/dashboard-layout.repository.d.ts +18 -0
- package/dist/dashboard-layout.repository.d.ts.map +1 -0
- package/dist/dashboard-layout.repository.js +3 -0
- package/dist/dashboard-permissions.d.ts +3 -0
- package/dist/dashboard-permissions.d.ts.map +1 -0
- package/dist/dashboard-permissions.js +11 -0
- package/dist/dashboard-scopes.d.ts +12 -0
- package/dist/dashboard-scopes.d.ts.map +1 -0
- package/dist/dashboard-scopes.js +30 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +13 -0
- package/dist/hooks.d.ts +29 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +6 -3
- package/dist/seed-ddl-destructive.d.ts +40 -0
- package/dist/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/seed-ddl-destructive.js +149 -0
- package/dist/seed-validation.d.ts.map +1 -1
- package/dist/seed-validation.js +70 -2
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +58 -1
- package/dist/widget/widget.repository.d.ts +32 -3
- package/dist/widget/widget.repository.d.ts.map +1 -1
- package/dist/widget/widget.repository.js +4 -1
- package/package.json +11 -11
|
@@ -48,6 +48,20 @@ export declare class RelationTargetNotFoundError extends RepositoryError {
|
|
|
48
48
|
value: string;
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Thrown by lifecycle `before*` hooks to signal a business validation failure.
|
|
53
|
+
* Mapped to 422 Unprocessable Entity by the API problem-mapper.
|
|
54
|
+
*/
|
|
55
|
+
export declare class HookValidationError extends RepositoryError {
|
|
56
|
+
readonly fields?: Array<{
|
|
57
|
+
field: string;
|
|
58
|
+
message: string;
|
|
59
|
+
}>;
|
|
60
|
+
constructor(message: string, fields?: Array<{
|
|
61
|
+
field: string;
|
|
62
|
+
message: string;
|
|
63
|
+
}>);
|
|
64
|
+
}
|
|
51
65
|
export type BulkFieldUpdate = {
|
|
52
66
|
kind: 'set';
|
|
53
67
|
value: unknown;
|
|
@@ -61,6 +75,52 @@ export type BulkFieldUpdate = {
|
|
|
61
75
|
kind: 'array_remove';
|
|
62
76
|
value: string[];
|
|
63
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Options passed to write operations. Currently carries the acting user
|
|
80
|
+
* (extracted from the JWT) so lifecycle hooks can attribute changes.
|
|
81
|
+
*/
|
|
82
|
+
export interface RepositoryOptions {
|
|
83
|
+
actor?: {
|
|
84
|
+
id: string;
|
|
85
|
+
role?: string;
|
|
86
|
+
email?: string;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Declarative multi-write operation translated into a single `db.batch` call.
|
|
91
|
+
* Used for coordinated writes across one or more seeds when D1's lack of
|
|
92
|
+
* interactive transactions makes a callback-based API impossible.
|
|
93
|
+
*
|
|
94
|
+
* NOTE: document-level lifecycle hooks do NOT run for operations inside a
|
|
95
|
+
* `runBatch` call — they would be non-atomic side-effects.
|
|
96
|
+
*/
|
|
97
|
+
export type BatchWrite = {
|
|
98
|
+
kind: 'create';
|
|
99
|
+
seed: Seed;
|
|
100
|
+
id: string;
|
|
101
|
+
slug: string;
|
|
102
|
+
status: string;
|
|
103
|
+
data: Record<string, any>;
|
|
104
|
+
} | {
|
|
105
|
+
kind: 'update';
|
|
106
|
+
seed: Seed;
|
|
107
|
+
id: string;
|
|
108
|
+
data: Record<string, any>;
|
|
109
|
+
status?: string;
|
|
110
|
+
} | {
|
|
111
|
+
kind: 'mutateField';
|
|
112
|
+
seed: Seed;
|
|
113
|
+
id: string;
|
|
114
|
+
fieldName: string;
|
|
115
|
+
operation: {
|
|
116
|
+
type: 'increment' | 'decrement';
|
|
117
|
+
value: number;
|
|
118
|
+
};
|
|
119
|
+
options?: {
|
|
120
|
+
min?: number;
|
|
121
|
+
max?: number;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
64
124
|
/**
|
|
65
125
|
* Interface defining the standard operations for content persistence.
|
|
66
126
|
* This is platform-agnostic and should be implemented for specific databases (e.g., D1).
|
|
@@ -94,20 +154,41 @@ export interface ContentRepository {
|
|
|
94
154
|
* Creates a new content entry in the live table.
|
|
95
155
|
* Throws SlugConflictError if the slug is already taken.
|
|
96
156
|
*/
|
|
97
|
-
create(seed: Seed, id: string, slug: string, status: string, data: Record<string, any
|
|
157
|
+
create(seed: Seed, id: string, slug: string, status: string, data: Record<string, any>, options?: RepositoryOptions): Promise<void>;
|
|
98
158
|
/**
|
|
99
159
|
* Partially updates an existing entry in the live table.
|
|
100
160
|
* Throws EntryNotFoundError if the ID does not exist.
|
|
101
161
|
*/
|
|
102
|
-
update(seed: Seed, id: string, data: Record<string, any>, status?: string): Promise<void>;
|
|
162
|
+
update(seed: Seed, id: string, data: Record<string, any>, status?: string, options?: RepositoryOptions): Promise<void>;
|
|
103
163
|
/**
|
|
104
164
|
* Deletes an entry from the live table.
|
|
105
165
|
* Returns the deleted row data (useful for media cleanup).
|
|
106
166
|
* Throws EntryNotFoundError if the ID does not exist.
|
|
107
167
|
*/
|
|
108
|
-
delete(seed: Seed, id: string): Promise<{
|
|
168
|
+
delete(seed: Seed, id: string, options?: RepositoryOptions): Promise<{
|
|
109
169
|
row: Record<string, any>;
|
|
110
170
|
}>;
|
|
171
|
+
/**
|
|
172
|
+
* Atomically increments/decrements a numeric field with optional min/max guards.
|
|
173
|
+
* Bypasses document-level lifecycle hooks — it's a single UPDATE statement,
|
|
174
|
+
* used to prevent race conditions on counters (stock, balances).
|
|
175
|
+
* Throws RepositoryError if `fieldName` is not a numeric branch of `seed`, or
|
|
176
|
+
* if the guard conditions fail / the row does not exist.
|
|
177
|
+
*/
|
|
178
|
+
mutateField(seed: Seed, id: string, fieldName: string, operation: {
|
|
179
|
+
type: 'increment' | 'decrement';
|
|
180
|
+
value: number;
|
|
181
|
+
}, options?: {
|
|
182
|
+
min?: number;
|
|
183
|
+
max?: number;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
newValue: number;
|
|
186
|
+
}>;
|
|
187
|
+
/**
|
|
188
|
+
* Executes a list of write operations atomically via a single `db.batch`.
|
|
189
|
+
* Document-level lifecycle hooks do NOT run for operations inside this call.
|
|
190
|
+
*/
|
|
191
|
+
runBatch(operations: BatchWrite[]): Promise<void>;
|
|
111
192
|
/**
|
|
112
193
|
* Saves or updates a pending draft in the mirror table.
|
|
113
194
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content.repository.d.ts","sourceRoot":"","sources":["../src/content.repository.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAEhD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE;QACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACnB,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACJ,KAAK,CAAC,EAAE,OAAO;gBAAvC,OAAO,EAAE,MAAM,EAAS,KAAK,CAAC,EAAE,OAAO,YAAA;CAIpD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,eAAe;gBACzC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,eAAe;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;;GAGG;AACH,qBAAa,2BAA4B,SAAQ,eAAe;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;gBAEV,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;CASzE;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GACtC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAE7C;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAEtG;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IAE9D;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAA;IAElE;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC;QAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QAChC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;KACvC,CAAC,CAAA;IAEF;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"content.repository.d.ts","sourceRoot":"","sources":["../src/content.repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -45,3 +45,15 @@ export class RelationTargetNotFoundError extends RepositoryError {
|
|
|
45
45
|
this.value = params.value;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Thrown by lifecycle `before*` hooks to signal a business validation failure.
|
|
50
|
+
* Mapped to 422 Unprocessable Entity by the API problem-mapper.
|
|
51
|
+
*/
|
|
52
|
+
export class HookValidationError extends RepositoryError {
|
|
53
|
+
fields;
|
|
54
|
+
constructor(message, fields) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = 'HookValidationError';
|
|
57
|
+
this.fields = fields;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Seed } from './types.js';
|
|
3
|
+
/** A placed widget. `type` is namespaced ('core/stat', '@acme/weather').
|
|
4
|
+
* `config` is opaque to core/API except for the optional `seedSlug` key,
|
|
5
|
+
* which enables auto-cleanup when the referenced seed disappears. */
|
|
6
|
+
export interface DashboardWidgetInstance {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
config: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface DashboardColumn {
|
|
13
|
+
id: string;
|
|
14
|
+
widgets: DashboardWidgetInstance[];
|
|
15
|
+
}
|
|
16
|
+
export interface DashboardSection {
|
|
17
|
+
id: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
hideLabel?: boolean;
|
|
20
|
+
collapsible?: boolean;
|
|
21
|
+
/** 1–4 columns. Enforced by validator. */
|
|
22
|
+
columns: DashboardColumn[];
|
|
23
|
+
/** Optional spans on a 12-unit grid, parallel to `columns`, must sum to 12.
|
|
24
|
+
* Absent = equal split. */
|
|
25
|
+
columnSpans?: number[];
|
|
26
|
+
}
|
|
27
|
+
export interface DashboardPageLayout {
|
|
28
|
+
id: string;
|
|
29
|
+
/** URL identity (?page=<slug>). Unique within the layout. */
|
|
30
|
+
slug: string;
|
|
31
|
+
label: string;
|
|
32
|
+
/** Lucide icon name, same convention as DashboardSeedConfig.icon. */
|
|
33
|
+
icon?: string;
|
|
34
|
+
sections: DashboardSection[];
|
|
35
|
+
}
|
|
36
|
+
export interface DashboardLayout {
|
|
37
|
+
/** Format version — bump when introducing breaking changes. */
|
|
38
|
+
version: 1;
|
|
39
|
+
pages: DashboardPageLayout[];
|
|
40
|
+
}
|
|
41
|
+
/** Namespaced widget type — lowercase npm-name-compatible.
|
|
42
|
+
* Built-ins use the 'core/' prefix; custom widgets '@scope/name'. */
|
|
43
|
+
export declare const WIDGET_TYPE_REGEX: RegExp;
|
|
44
|
+
/** Max serialized size of a single widget `config`
|
|
45
|
+
* (`JSON.stringify(widget.config).length`) — guards D1 row bloat. */
|
|
46
|
+
export declare const MAX_WIDGET_CONFIG_BYTES = 8192;
|
|
47
|
+
export declare const dashboardWidgetInstanceSchema: z.ZodObject<{
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
type: z.ZodString;
|
|
50
|
+
title: z.ZodOptional<z.ZodString>;
|
|
51
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export declare const dashboardColumnSchema: z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
widgets: z.ZodArray<z.ZodObject<{
|
|
56
|
+
id: z.ZodString;
|
|
57
|
+
type: z.ZodString;
|
|
58
|
+
title: z.ZodOptional<z.ZodString>;
|
|
59
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
export declare const dashboardSectionSchema: z.ZodObject<{
|
|
63
|
+
id: z.ZodString;
|
|
64
|
+
label: z.ZodOptional<z.ZodString>;
|
|
65
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
66
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
67
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
68
|
+
id: z.ZodString;
|
|
69
|
+
widgets: z.ZodArray<z.ZodObject<{
|
|
70
|
+
id: z.ZodString;
|
|
71
|
+
type: z.ZodString;
|
|
72
|
+
title: z.ZodOptional<z.ZodString>;
|
|
73
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
}, z.core.$strip>>;
|
|
76
|
+
columnSpans: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
export declare const dashboardPageSchema: z.ZodObject<{
|
|
79
|
+
id: z.ZodString;
|
|
80
|
+
slug: z.ZodString;
|
|
81
|
+
label: z.ZodString;
|
|
82
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
83
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
84
|
+
id: z.ZodString;
|
|
85
|
+
label: z.ZodOptional<z.ZodString>;
|
|
86
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
87
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
88
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
89
|
+
id: z.ZodString;
|
|
90
|
+
widgets: z.ZodArray<z.ZodObject<{
|
|
91
|
+
id: z.ZodString;
|
|
92
|
+
type: z.ZodString;
|
|
93
|
+
title: z.ZodOptional<z.ZodString>;
|
|
94
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
95
|
+
}, z.core.$strip>>;
|
|
96
|
+
}, z.core.$strip>>;
|
|
97
|
+
columnSpans: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
98
|
+
}, z.core.$strip>>;
|
|
99
|
+
}, z.core.$strip>;
|
|
100
|
+
export declare const dashboardLayoutSchema: z.ZodObject<{
|
|
101
|
+
version: z.ZodLiteral<1>;
|
|
102
|
+
pages: z.ZodArray<z.ZodObject<{
|
|
103
|
+
id: z.ZodString;
|
|
104
|
+
slug: z.ZodString;
|
|
105
|
+
label: z.ZodString;
|
|
106
|
+
icon: z.ZodOptional<z.ZodString>;
|
|
107
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
108
|
+
id: z.ZodString;
|
|
109
|
+
label: z.ZodOptional<z.ZodString>;
|
|
110
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
111
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
112
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
113
|
+
id: z.ZodString;
|
|
114
|
+
widgets: z.ZodArray<z.ZodObject<{
|
|
115
|
+
id: z.ZodString;
|
|
116
|
+
type: z.ZodString;
|
|
117
|
+
title: z.ZodOptional<z.ZodString>;
|
|
118
|
+
config: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
119
|
+
}, z.core.$strip>>;
|
|
120
|
+
}, z.core.$strip>>;
|
|
121
|
+
columnSpans: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
122
|
+
}, z.core.$strip>>;
|
|
123
|
+
}, z.core.$strip>>;
|
|
124
|
+
}, z.core.$strip>;
|
|
125
|
+
/** Structural port of the legacy hardcoded dashboard config into a single
|
|
126
|
+
* 'Overview' page. Widgets fetch their own data: configs carry only the
|
|
127
|
+
* variant and (where relevant) the default seed binding. */
|
|
128
|
+
export declare function generateDefaultDashboardLayout(seeds: Seed[], opts?: {
|
|
129
|
+
newId: () => string;
|
|
130
|
+
}): DashboardLayout;
|
|
131
|
+
export interface DashboardLayoutContext {
|
|
132
|
+
/** Slugs of currently registered seeds (from ISeedRegistry.all()). */
|
|
133
|
+
seedSlugs: ReadonlySet<string>;
|
|
134
|
+
/** Optional: widget types known to the caller (frontend registry).
|
|
135
|
+
* When provided, unknown types produce WARNINGS, never strips. */
|
|
136
|
+
knownWidgetTypes?: ReadonlySet<string>;
|
|
137
|
+
}
|
|
138
|
+
export type ValidateDashboardLayoutResult = {
|
|
139
|
+
ok: true;
|
|
140
|
+
cleaned: DashboardLayout;
|
|
141
|
+
warnings: string[];
|
|
142
|
+
} | {
|
|
143
|
+
ok: false;
|
|
144
|
+
errors: string[];
|
|
145
|
+
cleaned: DashboardLayout;
|
|
146
|
+
warnings: string[];
|
|
147
|
+
};
|
|
148
|
+
/** Semantic validation on top of the Zod shape check (the caller's job).
|
|
149
|
+
* Widgets bound to a seed that no longer exists are silently stripped
|
|
150
|
+
* (auto-cleanup, recorded as a warning). Unknown widget types only warn —
|
|
151
|
+
* custom widgets are invisible to the Worker and must never be stripped.
|
|
152
|
+
* `cleaned` is always returned, errors or not. */
|
|
153
|
+
export declare function validateDashboardLayout(layout: DashboardLayout, ctx: DashboardLayoutContext): ValidateDashboardLayoutResult;
|
|
154
|
+
//# sourceMappingURL=dashboard-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-layout.d.ts","sourceRoot":"","sources":["../src/dashboard-layout.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAMtC;;sEAEsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,uBAAuB,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,0CAA0C;IAC1C,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B;gCAC4B;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,OAAO,EAAE,CAAC,CAAA;IACV,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAC7B;AAMD;sEACsE;AACtE,eAAO,MAAM,iBAAiB,QAA6B,CAAA;AAE3D;sEACsE;AACtE,eAAO,MAAM,uBAAuB,OAAO,CAAA;AAM3C,eAAO,MAAM,6BAA6B;;;;;iBAKxC,CAAA;AACF,eAAO,MAAM,qBAAqB;;;;;;;;iBAGhC,CAAA;AACF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;iBAOjC,CAAA;AACF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;iBAM9B,CAAA;AACF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;iBAGhC,CAAA;AAQF;;6DAE6D;AAC7D,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7B,eAAe,CA2EjB;AAMD,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAC9B;uEACmE;IACnE,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACvC;AAED,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEjF;;;;mDAImD;AACnD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,sBAAsB,GAC1B,6BAA6B,CA2E/B"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Shared rules used by both the Zod schemas and the semantic validator.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
/** Namespaced widget type — lowercase npm-name-compatible.
|
|
8
|
+
* Built-ins use the 'core/' prefix; custom widgets '@scope/name'. */
|
|
9
|
+
export const WIDGET_TYPE_REGEX = /^[a-z0-9@][a-z0-9@/_-]*$/;
|
|
10
|
+
/** Max serialized size of a single widget `config`
|
|
11
|
+
* (`JSON.stringify(widget.config).length`) — guards D1 row bloat. */
|
|
12
|
+
export const MAX_WIDGET_CONFIG_BYTES = 8192;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Zod schemas for shape validation
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
export const dashboardWidgetInstanceSchema = z.object({
|
|
17
|
+
id: z.string().min(1),
|
|
18
|
+
type: z.string().regex(WIDGET_TYPE_REGEX),
|
|
19
|
+
title: z.string().max(80).optional(),
|
|
20
|
+
config: z.record(z.string(), z.unknown()),
|
|
21
|
+
});
|
|
22
|
+
export const dashboardColumnSchema = z.object({
|
|
23
|
+
id: z.string().min(1),
|
|
24
|
+
widgets: z.array(dashboardWidgetInstanceSchema),
|
|
25
|
+
});
|
|
26
|
+
export const dashboardSectionSchema = z.object({
|
|
27
|
+
id: z.string().min(1),
|
|
28
|
+
label: z.string().max(60).optional(),
|
|
29
|
+
hideLabel: z.boolean().optional(),
|
|
30
|
+
collapsible: z.boolean().optional(),
|
|
31
|
+
columns: z.array(dashboardColumnSchema).min(1).max(4),
|
|
32
|
+
columnSpans: z.array(z.number().int().min(1).max(12)).optional(),
|
|
33
|
+
});
|
|
34
|
+
export const dashboardPageSchema = z.object({
|
|
35
|
+
id: z.string().min(1),
|
|
36
|
+
slug: z.string().regex(/^[a-z0-9][a-z0-9-]*$/).max(40),
|
|
37
|
+
label: z.string().min(1).max(60),
|
|
38
|
+
icon: z.string().max(40).optional(),
|
|
39
|
+
sections: z.array(dashboardSectionSchema).min(1),
|
|
40
|
+
});
|
|
41
|
+
export const dashboardLayoutSchema = z.object({
|
|
42
|
+
version: z.literal(1),
|
|
43
|
+
pages: z.array(dashboardPageSchema).min(1).max(8),
|
|
44
|
+
});
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Default dashboard layout generator
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
const defaultIdFactory = () => crypto.randomUUID();
|
|
49
|
+
/** Structural port of the legacy hardcoded dashboard config into a single
|
|
50
|
+
* 'Overview' page. Widgets fetch their own data: configs carry only the
|
|
51
|
+
* variant and (where relevant) the default seed binding. */
|
|
52
|
+
export function generateDefaultDashboardLayout(seeds, opts) {
|
|
53
|
+
const newId = opts?.newId ?? defaultIdFactory;
|
|
54
|
+
const widget = (type, config) => ({ id: newId(), type, config });
|
|
55
|
+
const singleWidgetColumn = (w) => ({
|
|
56
|
+
id: newId(),
|
|
57
|
+
widgets: [w],
|
|
58
|
+
});
|
|
59
|
+
const sections = [];
|
|
60
|
+
// Setup checklist, full width.
|
|
61
|
+
sections.push({
|
|
62
|
+
id: newId(),
|
|
63
|
+
hideLabel: true,
|
|
64
|
+
columns: [singleWidgetColumn(widget('core/setup-checklist', { variant: 'full' }))],
|
|
65
|
+
});
|
|
66
|
+
// Status row: equal columns; quick-draft needs at least one seed to target.
|
|
67
|
+
const statusWidgets = [
|
|
68
|
+
widget('core/site-status', { variant: 'badge' }),
|
|
69
|
+
widget('core/storage', { variant: 'gauge' }),
|
|
70
|
+
widget('core/publication-stats', { variant: 'trio' }),
|
|
71
|
+
];
|
|
72
|
+
if (seeds.length > 0) {
|
|
73
|
+
statusWidgets.push(widget('core/quick-draft', { variant: 'minimal' }));
|
|
74
|
+
}
|
|
75
|
+
sections.push({
|
|
76
|
+
id: newId(),
|
|
77
|
+
hideLabel: true,
|
|
78
|
+
columns: statusWidgets.map(singleWidgetColumn),
|
|
79
|
+
});
|
|
80
|
+
if (seeds.length > 0) {
|
|
81
|
+
const seedSlug = seeds.find((s) => s.slug === 'articoli')?.slug ?? seeds[0].slug;
|
|
82
|
+
// Content row: recent content | pending drafts.
|
|
83
|
+
sections.push({
|
|
84
|
+
id: newId(),
|
|
85
|
+
hideLabel: true,
|
|
86
|
+
columns: [
|
|
87
|
+
singleWidgetColumn(widget('core/recent-content', { seedSlug, variant: 'list' })),
|
|
88
|
+
singleWidgetColumn(widget('core/pending-drafts', { seedSlug, variant: 'list' })),
|
|
89
|
+
],
|
|
90
|
+
columnSpans: [6, 6],
|
|
91
|
+
});
|
|
92
|
+
// Media & activity row: media gallery | activity feed.
|
|
93
|
+
sections.push({
|
|
94
|
+
id: newId(),
|
|
95
|
+
hideLabel: true,
|
|
96
|
+
columns: [
|
|
97
|
+
singleWidgetColumn(widget('core/media-gallery', { seedSlug, variant: 'grid' })),
|
|
98
|
+
singleWidgetColumn(widget('core/activity-feed', { seedSlug, variant: 'feed' })),
|
|
99
|
+
],
|
|
100
|
+
columnSpans: [6, 6],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
version: 1,
|
|
105
|
+
pages: [
|
|
106
|
+
{
|
|
107
|
+
id: newId(),
|
|
108
|
+
slug: 'overview',
|
|
109
|
+
label: 'Overview',
|
|
110
|
+
icon: 'LayoutDashboard',
|
|
111
|
+
sections,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Semantic validation on top of the Zod shape check (the caller's job).
|
|
117
|
+
* Widgets bound to a seed that no longer exists are silently stripped
|
|
118
|
+
* (auto-cleanup, recorded as a warning). Unknown widget types only warn —
|
|
119
|
+
* custom widgets are invisible to the Worker and must never be stripped.
|
|
120
|
+
* `cleaned` is always returned, errors or not. */
|
|
121
|
+
export function validateDashboardLayout(layout, ctx) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
const warnings = [];
|
|
124
|
+
// Auto-cleanup: drop widgets whose config.seedSlug references a missing seed.
|
|
125
|
+
const cleanedPages = layout.pages.map((page) => ({
|
|
126
|
+
...page,
|
|
127
|
+
sections: page.sections.map((section) => ({
|
|
128
|
+
...section,
|
|
129
|
+
columns: section.columns.map((col) => ({
|
|
130
|
+
...col,
|
|
131
|
+
widgets: (col.widgets ?? []).filter((w) => {
|
|
132
|
+
const seedSlug = (w.config ?? {})['seedSlug'];
|
|
133
|
+
if (typeof seedSlug === 'string' && !ctx.seedSlugs.has(seedSlug)) {
|
|
134
|
+
warnings.push(`Widget '${w.type}' (id=${w.id}) removed: seed '${seedSlug}' is not registered.`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}),
|
|
139
|
+
})),
|
|
140
|
+
})),
|
|
141
|
+
}));
|
|
142
|
+
// Semantic checks on the cleaned layout
|
|
143
|
+
const seenWidgetIds = new Set();
|
|
144
|
+
const seenPageSlugs = new Set();
|
|
145
|
+
for (const page of cleanedPages) {
|
|
146
|
+
if (seenPageSlugs.has(page.slug)) {
|
|
147
|
+
errors.push(`Page slug '${page.slug}' appears more than once in the layout.`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
seenPageSlugs.add(page.slug);
|
|
151
|
+
}
|
|
152
|
+
for (const section of page.sections) {
|
|
153
|
+
if (section.columnSpans !== undefined) {
|
|
154
|
+
if (section.columnSpans.length !== section.columns.length) {
|
|
155
|
+
errors.push(`Section ${section.id}: columnSpans has ${section.columnSpans.length} entries for ${section.columns.length} columns.`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const sum = section.columnSpans.reduce((acc, span) => acc + span, 0);
|
|
159
|
+
if (sum !== 12) {
|
|
160
|
+
errors.push(`Section ${section.id}: columnSpans must sum to 12, got ${sum}.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const col of section.columns) {
|
|
165
|
+
for (const w of col.widgets) {
|
|
166
|
+
if (seenWidgetIds.has(w.id)) {
|
|
167
|
+
errors.push(`Widget id '${w.id}' appears more than once in the layout.`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
seenWidgetIds.add(w.id);
|
|
171
|
+
}
|
|
172
|
+
if (JSON.stringify(w.config ?? {}).length > MAX_WIDGET_CONFIG_BYTES) {
|
|
173
|
+
errors.push(`Widget '${w.type}' (id=${w.id}): config exceeds ${MAX_WIDGET_CONFIG_BYTES} bytes when serialized.`);
|
|
174
|
+
}
|
|
175
|
+
if (ctx.knownWidgetTypes !== undefined && !ctx.knownWidgetTypes.has(w.type)) {
|
|
176
|
+
warnings.push(`Widget '${w.type}' (id=${w.id}) is not a known widget type.`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const cleaned = { ...layout, pages: cleanedPages };
|
|
183
|
+
if (errors.length > 0)
|
|
184
|
+
return { ok: false, errors, cleaned, warnings };
|
|
185
|
+
return { ok: true, cleaned, warnings };
|
|
186
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DashboardLayout } from './dashboard-layout.js';
|
|
2
|
+
export interface DashboardLayoutRecord {
|
|
3
|
+
scope: string;
|
|
4
|
+
layout: DashboardLayout;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
updatedBy: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IDashboardLayoutRepository {
|
|
9
|
+
/** Stored layout for a scope, or null if none was ever saved. */
|
|
10
|
+
get(scope: string): Promise<DashboardLayoutRecord | null>;
|
|
11
|
+
/** Scopes that currently have a stored row — used by the Sprint 06 builder UI. */
|
|
12
|
+
listScopes(): Promise<string[]>;
|
|
13
|
+
/** Upsert. `updatedBy` is the writer's user id. */
|
|
14
|
+
upsert(scope: string, layout: DashboardLayout, updatedBy: string): Promise<void>;
|
|
15
|
+
/** Remove the stored row — the "Reset" action. */
|
|
16
|
+
remove(scope: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=dashboard-layout.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-layout.repository.d.ts","sourceRoot":"","sources":["../src/dashboard-layout.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAE5D,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,eAAe,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;IACzD,kFAAkF;IAClF,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC/B,mDAAmD;IACnD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChF,kDAAkD;IAClD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-permissions.d.ts","sourceRoot":"","sources":["../src/dashboard-permissions.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC,MAAM,CAAa,CAAA;AAE/E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAGzE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
// NOTE: change this list to extend write-access to other roles
|
|
4
|
+
// (e.g. add 'editor', or introduce a fine-grained 'dashboard:edit' permission).
|
|
5
|
+
// Single source of truth — used by both API guards and dashboard buttons.
|
|
6
|
+
export const ROLES_ALLOWED_TO_EDIT_DASHBOARD = ['admin'];
|
|
7
|
+
export function canEditDashboard(role) {
|
|
8
|
+
if (!role)
|
|
9
|
+
return false;
|
|
10
|
+
return ROLES_ALLOWED_TO_EDIT_DASHBOARD.includes(role);
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Roles that may have their own dashboard layout. Single source of truth —
|
|
2
|
+
* extend this list if a new role is introduced. */
|
|
3
|
+
export declare const KNOWN_DASHBOARD_ROLES: ReadonlyArray<string>;
|
|
4
|
+
export declare const DEFAULT_DASHBOARD_SCOPE = "default";
|
|
5
|
+
/** Builds the per-role scope string, e.g. `roleScope('editor') === 'role:editor'`. */
|
|
6
|
+
export declare function roleScope(role: string): string;
|
|
7
|
+
/** Whether `scope` is one of the closed set: `'default' | 'role:<known role>'`. */
|
|
8
|
+
export declare function isValidDashboardScope(scope: string): boolean;
|
|
9
|
+
/** Read-resolution order for a caller's role: `['role:<role>', 'default']`,
|
|
10
|
+
* or just `['default']` for unknown/missing roles. */
|
|
11
|
+
export declare function resolveDashboardScopeChain(role: string | undefined | null): string[];
|
|
12
|
+
//# sourceMappingURL=dashboard-scopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard-scopes.d.ts","sourceRoot":"","sources":["../src/dashboard-scopes.ts"],"names":[],"mappings":"AAOA;oDACoD;AACpD,eAAO,MAAM,qBAAqB,EAAE,aAAa,CAAC,MAAM,CAAuB,CAAA;AAE/E,eAAO,MAAM,uBAAuB,YAAY,CAAA;AAEhD,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED;uDACuD;AACvD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,EAAE,CAKpF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Dashboard layout scopes (Sprint 06: role-based dashboards)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Roles that may have their own dashboard layout. Single source of truth —
|
|
7
|
+
* extend this list if a new role is introduced. */
|
|
8
|
+
export const KNOWN_DASHBOARD_ROLES = ['admin', 'editor'];
|
|
9
|
+
export const DEFAULT_DASHBOARD_SCOPE = 'default';
|
|
10
|
+
/** Builds the per-role scope string, e.g. `roleScope('editor') === 'role:editor'`. */
|
|
11
|
+
export function roleScope(role) {
|
|
12
|
+
return `role:${role}`;
|
|
13
|
+
}
|
|
14
|
+
/** Whether `scope` is one of the closed set: `'default' | 'role:<known role>'`. */
|
|
15
|
+
export function isValidDashboardScope(scope) {
|
|
16
|
+
if (scope === DEFAULT_DASHBOARD_SCOPE)
|
|
17
|
+
return true;
|
|
18
|
+
const match = /^role:(.+)$/.exec(scope);
|
|
19
|
+
if (!match)
|
|
20
|
+
return false;
|
|
21
|
+
return KNOWN_DASHBOARD_ROLES.includes(match[1]);
|
|
22
|
+
}
|
|
23
|
+
/** Read-resolution order for a caller's role: `['role:<role>', 'default']`,
|
|
24
|
+
* or just `['default']` for unknown/missing roles. */
|
|
25
|
+
export function resolveDashboardScopeChain(role) {
|
|
26
|
+
if (role && KNOWN_DASHBOARD_ROLES.includes(role)) {
|
|
27
|
+
return [roleScope(role), DEFAULT_DASHBOARD_SCOPE];
|
|
28
|
+
}
|
|
29
|
+
return [DEFAULT_DASHBOARD_SCOPE];
|
|
30
|
+
}
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,MAAM,EAKN,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,MAAM,EAKN,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAsCnB,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAI5D;AAkED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CA0BtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAyB5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAuBpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAc1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgCxD;AAID;;;;GAIG;AAGH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAqE5F;AAgHD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,EAAE,CAiB7D;AAID;;;GAGG;AAEH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAwCrF;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAkBxE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM5E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAcD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CA0BtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBzE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAqCzE"}
|
package/dist/engine.js
CHANGED
|
@@ -10,6 +10,7 @@ const BRANCH_TYPE_SQL = {
|
|
|
10
10
|
file: { sqlType: 'TEXT' }, // URL singolo o JSON array di URL
|
|
11
11
|
tags: { sqlType: 'TEXT' }, // JSON array di stringhe
|
|
12
12
|
relation: { sqlType: 'TEXT' }, // FK reference stored as TEXT (id of the target row)
|
|
13
|
+
repeater: { sqlType: 'TEXT' }, // JSON array di record (sub-branch alias -> value)
|
|
13
14
|
};
|
|
14
15
|
const SYSTEM_COLUMNS = new Set(['id', 'slug', 'status', 'created_at', 'updated_at']);
|
|
15
16
|
// ---- Private helpers ----
|
|
@@ -469,6 +470,10 @@ export function serializeForDb(branch, value) {
|
|
|
469
470
|
return Array.isArray(value) ? JSON.stringify(value) : typeof value === 'string' ? value : null;
|
|
470
471
|
}
|
|
471
472
|
return typeof value === 'string' ? value : null;
|
|
473
|
+
case 'repeater':
|
|
474
|
+
// Non-array input is rejected by serializing as an empty list — validation.ts
|
|
475
|
+
// is responsible for ever letting a non-array repeater value reach here.
|
|
476
|
+
return JSON.stringify(Array.isArray(value) ? value : []);
|
|
472
477
|
default:
|
|
473
478
|
return typeof value === 'string' ? value : typeof value === 'number' ? value : null;
|
|
474
479
|
}
|
|
@@ -676,6 +681,14 @@ export function generateRetypeColumn(seed, branch) {
|
|
|
676
681
|
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
|
|
677
682
|
*/
|
|
678
683
|
export function deserializeFromDb(branch, value) {
|
|
684
|
+
// Repeater columns deserialize to [] (never null) — drafts and rows that
|
|
685
|
+
// predate the column being added carry NULL, which is an empty list of items.
|
|
686
|
+
if (branch.type === 'repeater') {
|
|
687
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
688
|
+
return [];
|
|
689
|
+
const parsed = parseJsonSafe(value);
|
|
690
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
691
|
+
}
|
|
679
692
|
if (value === null || value === undefined)
|
|
680
693
|
return null;
|
|
681
694
|
switch (branch.type) {
|