@beechcms/core 0.6.0-preview.1 → 0.6.0-preview.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/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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +1 -1
|
@@ -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) {
|
package/dist/index.d.ts
CHANGED
|
@@ -51,6 +51,10 @@ export * from './demo-data.repository.js';
|
|
|
51
51
|
export * from './seed-layout.js';
|
|
52
52
|
export * from './seed-layout.repository.js';
|
|
53
53
|
export * from './layout-permissions.js';
|
|
54
|
+
export * from './dashboard-layout.js';
|
|
55
|
+
export * from './dashboard-scopes.js';
|
|
56
|
+
export * from './dashboard-permissions.js';
|
|
57
|
+
export * from './dashboard-layout.repository.js';
|
|
54
58
|
export * from './seed.repository.js';
|
|
55
59
|
export * from './seed-validation.js';
|
|
56
60
|
export * from './seed-ddl.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kCAAkC,CAAA;AAChD,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,10 @@ export * from './demo-data.repository.js';
|
|
|
53
53
|
export * from './seed-layout.js';
|
|
54
54
|
export * from './seed-layout.repository.js';
|
|
55
55
|
export * from './layout-permissions.js';
|
|
56
|
+
export * from './dashboard-layout.js';
|
|
57
|
+
export * from './dashboard-scopes.js';
|
|
58
|
+
export * from './dashboard-permissions.js';
|
|
59
|
+
export * from './dashboard-layout.repository.js';
|
|
56
60
|
export * from './seed.repository.js';
|
|
57
61
|
export * from './seed-validation.js';
|
|
58
62
|
export * from './seed-ddl.js';
|
package/dist/policies.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAgBzF"}
|
package/dist/policies.js
CHANGED
|
@@ -19,12 +19,15 @@ export function resolvePolicies(branch) {
|
|
|
19
19
|
// Non-plain privacy implies hidden by default: the CMS hashes/encrypts on write,
|
|
20
20
|
// so returning the stored value would leak the digest to readers.
|
|
21
21
|
const defaultVisibility = privacy !== 'plain' ? 'hidden' : 'full';
|
|
22
|
+
// Repeaters live in a single JSON column — never filterable, sortable, or
|
|
23
|
+
// searchable/facetable in v1, regardless of what's set on the branch.
|
|
24
|
+
const isRepeater = branch.type === 'repeater';
|
|
22
25
|
return {
|
|
23
26
|
privacy,
|
|
24
27
|
visibility: branch.policies?.visibility ?? defaultVisibility,
|
|
25
|
-
search: branch.policies?.search ?? true,
|
|
26
|
-
filter: branch.policies?.filter ?? true,
|
|
27
|
-
sort: branch.policies?.sort ?? true,
|
|
28
|
+
search: isRepeater ? false : branch.policies?.search ?? true,
|
|
29
|
+
filter: isRepeater ? false : branch.policies?.filter ?? true,
|
|
30
|
+
sort: isRepeater ? false : branch.policies?.sort ?? true,
|
|
28
31
|
public: branch.policies?.public ?? true,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Seed, BranchType } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Ordered DROP TABLE statements for a full hard-delete of a seed's tables.
|
|
4
|
+
* Order: junction drafts → junctions → FTS triggers → FTS table → drafts → main table.
|
|
5
|
+
* All statements use IF EXISTS (idempotent).
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateDropTable(seed: Seed): string[];
|
|
8
|
+
/**
|
|
9
|
+
* DROP COLUMN statements for a single branch (by alias).
|
|
10
|
+
* For multi-relation branches: drops the junction tables (no column on parent).
|
|
11
|
+
* For regular branches: drops from main table + drafts table (if allowDrafts).
|
|
12
|
+
* Does NOT include FTS handling — callers must prepend ftsTriggerDrops() and
|
|
13
|
+
* append planFtsRebuild() when the branch is searchable.
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateDropColumn(seed: Seed, alias: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* RENAME COLUMN statements for a branch alias change.
|
|
18
|
+
* Updates main table + drafts (if allowDrafts).
|
|
19
|
+
* Does NOT handle FTS — callers must rebuild FTS separately (SQLite auto-updates
|
|
20
|
+
* trigger bodies on RENAME but the FTS virtual-table columns keep the old name).
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateRenameColumn(seed: Seed, fromAlias: string, toAlias: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Full FTS rebuild: drop triggers + FTS table, then recreate them, then backfill.
|
|
25
|
+
* Returns an empty array if the seed has no searchable branches.
|
|
26
|
+
* The drop steps use IF EXISTS so this is safe to call even if FTS was already dropped.
|
|
27
|
+
*/
|
|
28
|
+
export declare function planFtsRebuild(seed: Seed): string[];
|
|
29
|
+
export interface RetypeColumnPlan {
|
|
30
|
+
statements: string[];
|
|
31
|
+
ftsRebuildNeeded: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generates statements to change a branch's SQL storage type using the
|
|
35
|
+
* add-new-column / copy / drop-old / rename approach.
|
|
36
|
+
* Includes FTS rebuild statements when the branch is (or becomes) searchable.
|
|
37
|
+
* `updatedSeed` must already reflect the new type on the renamed branch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function planRetypeColumn(seed: Seed, alias: string, newType: BranchType, updatedSeed: Seed): RetypeColumnPlan;
|
|
40
|
+
//# sourceMappingURL=seed-ddl-destructive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-ddl-destructive.d.ts","sourceRoot":"","sources":["../src/seed-ddl-destructive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAU,UAAU,EAAE,MAAM,YAAY,CAAA;AAkC1D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgBtD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBtE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ7F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAiBnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,IAAI,GAChB,gBAAgB,CAyClB"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { junctionTableName, generateFtsTable, generateFtsTriggers } from './engine.js';
|
|
4
|
+
const BRANCH_SQL_TYPE = {
|
|
5
|
+
text: 'TEXT',
|
|
6
|
+
number: 'REAL',
|
|
7
|
+
boolean: 'INTEGER',
|
|
8
|
+
date: 'INTEGER',
|
|
9
|
+
json: 'TEXT',
|
|
10
|
+
richtext: 'TEXT',
|
|
11
|
+
file: 'TEXT',
|
|
12
|
+
tags: 'TEXT',
|
|
13
|
+
relation: 'TEXT',
|
|
14
|
+
repeater: 'TEXT',
|
|
15
|
+
};
|
|
16
|
+
/** Branch types that receive a B-tree index in generateIndexes (used by retype to drop/recreate). */
|
|
17
|
+
const INDEXABLE_TYPES = new Set(['text', 'number', 'date', 'boolean', 'relation']);
|
|
18
|
+
function searchBranches(seed) {
|
|
19
|
+
return seed.branches.filter(b => (b.type === 'text' || b.type === 'richtext') && b.policies?.search !== false);
|
|
20
|
+
}
|
|
21
|
+
function ftsTriggerDrops(slug) {
|
|
22
|
+
return [
|
|
23
|
+
`DROP TRIGGER IF EXISTS fts_${slug}_insert;`,
|
|
24
|
+
`DROP TRIGGER IF EXISTS fts_${slug}_update;`,
|
|
25
|
+
`DROP TRIGGER IF EXISTS fts_${slug}_delete;`,
|
|
26
|
+
`DROP TABLE IF EXISTS fts_${slug};`,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ordered DROP TABLE statements for a full hard-delete of a seed's tables.
|
|
31
|
+
* Order: junction drafts → junctions → FTS triggers → FTS table → drafts → main table.
|
|
32
|
+
* All statements use IF EXISTS (idempotent).
|
|
33
|
+
*/
|
|
34
|
+
export function generateDropTable(seed) {
|
|
35
|
+
const stmts = [];
|
|
36
|
+
const { slug } = seed;
|
|
37
|
+
for (const branch of seed.branches) {
|
|
38
|
+
if (branch.type !== 'relation' || branch.multiple !== true)
|
|
39
|
+
continue;
|
|
40
|
+
const jt = junctionTableName(slug, branch.alias);
|
|
41
|
+
stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
|
|
42
|
+
stmts.push(`DROP TABLE IF EXISTS ${jt};`);
|
|
43
|
+
}
|
|
44
|
+
stmts.push(...ftsTriggerDrops(slug));
|
|
45
|
+
stmts.push(`DROP TABLE IF EXISTS content_${slug}_drafts;`);
|
|
46
|
+
stmts.push(`DROP TABLE IF EXISTS content_${slug};`);
|
|
47
|
+
return stmts;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* DROP COLUMN statements for a single branch (by alias).
|
|
51
|
+
* For multi-relation branches: drops the junction tables (no column on parent).
|
|
52
|
+
* For regular branches: drops from main table + drafts table (if allowDrafts).
|
|
53
|
+
* Does NOT include FTS handling — callers must prepend ftsTriggerDrops() and
|
|
54
|
+
* append planFtsRebuild() when the branch is searchable.
|
|
55
|
+
*/
|
|
56
|
+
export function generateDropColumn(seed, alias) {
|
|
57
|
+
const stmts = [];
|
|
58
|
+
const { slug } = seed;
|
|
59
|
+
const branch = seed.branches.find(b => b.alias === alias);
|
|
60
|
+
if (!branch)
|
|
61
|
+
throw new Error(`Branch '${alias}' not found in seed '${slug}'`);
|
|
62
|
+
if (branch.type === 'relation' && branch.multiple === true) {
|
|
63
|
+
const jt = junctionTableName(slug, alias);
|
|
64
|
+
if (seed.allowDrafts)
|
|
65
|
+
stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
|
|
66
|
+
stmts.push(`DROP TABLE IF EXISTS ${jt};`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
stmts.push(`ALTER TABLE content_${slug} DROP COLUMN ${alias};`);
|
|
70
|
+
if (seed.allowDrafts)
|
|
71
|
+
stmts.push(`ALTER TABLE content_${slug}_drafts DROP COLUMN ${alias};`);
|
|
72
|
+
}
|
|
73
|
+
return stmts;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* RENAME COLUMN statements for a branch alias change.
|
|
77
|
+
* Updates main table + drafts (if allowDrafts).
|
|
78
|
+
* Does NOT handle FTS — callers must rebuild FTS separately (SQLite auto-updates
|
|
79
|
+
* trigger bodies on RENAME but the FTS virtual-table columns keep the old name).
|
|
80
|
+
*/
|
|
81
|
+
export function generateRenameColumn(seed, fromAlias, toAlias) {
|
|
82
|
+
const stmts = [];
|
|
83
|
+
const { slug } = seed;
|
|
84
|
+
stmts.push(`ALTER TABLE content_${slug} RENAME COLUMN ${fromAlias} TO ${toAlias};`);
|
|
85
|
+
if (seed.allowDrafts) {
|
|
86
|
+
stmts.push(`ALTER TABLE content_${slug}_drafts RENAME COLUMN ${fromAlias} TO ${toAlias};`);
|
|
87
|
+
}
|
|
88
|
+
return stmts;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Full FTS rebuild: drop triggers + FTS table, then recreate them, then backfill.
|
|
92
|
+
* Returns an empty array if the seed has no searchable branches.
|
|
93
|
+
* The drop steps use IF EXISTS so this is safe to call even if FTS was already dropped.
|
|
94
|
+
*/
|
|
95
|
+
export function planFtsRebuild(seed) {
|
|
96
|
+
const rtBranches = searchBranches(seed);
|
|
97
|
+
if (rtBranches.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const { slug } = seed;
|
|
100
|
+
const stmts = [...ftsTriggerDrops(slug)];
|
|
101
|
+
const ftsTable = generateFtsTable(seed);
|
|
102
|
+
if (ftsTable)
|
|
103
|
+
stmts.push(ftsTable);
|
|
104
|
+
stmts.push(...generateFtsTriggers(seed));
|
|
105
|
+
const cols = rtBranches.map(b => b.alias);
|
|
106
|
+
stmts.push(`INSERT INTO fts_${slug}(entry_id, ${cols.join(', ')}) SELECT id, ${cols.join(', ')} FROM content_${slug};`);
|
|
107
|
+
return stmts;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generates statements to change a branch's SQL storage type using the
|
|
111
|
+
* add-new-column / copy / drop-old / rename approach.
|
|
112
|
+
* Includes FTS rebuild statements when the branch is (or becomes) searchable.
|
|
113
|
+
* `updatedSeed` must already reflect the new type on the renamed branch.
|
|
114
|
+
*/
|
|
115
|
+
export function planRetypeColumn(seed, alias, newType, updatedSeed) {
|
|
116
|
+
const branch = seed.branches.find(b => b.alias === alias);
|
|
117
|
+
if (!branch)
|
|
118
|
+
throw new Error(`Branch '${alias}' not found in seed '${seed.slug}'`);
|
|
119
|
+
const { slug } = seed;
|
|
120
|
+
const newSqlType = BRANCH_SQL_TYPE[newType];
|
|
121
|
+
const tmpAlias = `${alias}_migrate`;
|
|
122
|
+
const stmts = [];
|
|
123
|
+
// Always drop FTS first to avoid "column referenced in trigger" errors on DROP COLUMN.
|
|
124
|
+
stmts.push(...ftsTriggerDrops(slug));
|
|
125
|
+
// Drop the branch index if it exists on the old column (required before DROP COLUMN).
|
|
126
|
+
if (INDEXABLE_TYPES.has(branch.type) && branch.policies?.filter !== false) {
|
|
127
|
+
stmts.push(`DROP INDEX IF EXISTS idx_${slug}_${alias};`);
|
|
128
|
+
}
|
|
129
|
+
// Main table: add migration column, copy data, drop old, rename.
|
|
130
|
+
stmts.push(`ALTER TABLE content_${slug} ADD COLUMN ${tmpAlias} ${newSqlType};`);
|
|
131
|
+
stmts.push(`UPDATE content_${slug} SET ${tmpAlias} = CAST(${alias} AS ${newSqlType});`);
|
|
132
|
+
stmts.push(`ALTER TABLE content_${slug} DROP COLUMN ${alias};`);
|
|
133
|
+
stmts.push(`ALTER TABLE content_${slug} RENAME COLUMN ${tmpAlias} TO ${alias};`);
|
|
134
|
+
// Recreate index for the renamed column if the new type warrants it.
|
|
135
|
+
if (INDEXABLE_TYPES.has(newType) && branch.policies?.filter !== false) {
|
|
136
|
+
stmts.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_${alias} ON content_${slug}(${alias});`);
|
|
137
|
+
}
|
|
138
|
+
// Drafts table (no indexes to worry about).
|
|
139
|
+
if (seed.allowDrafts) {
|
|
140
|
+
stmts.push(`ALTER TABLE content_${slug}_drafts ADD COLUMN ${tmpAlias} ${newSqlType};`);
|
|
141
|
+
stmts.push(`UPDATE content_${slug}_drafts SET ${tmpAlias} = CAST(${alias} AS ${newSqlType});`);
|
|
142
|
+
stmts.push(`ALTER TABLE content_${slug}_drafts DROP COLUMN ${alias};`);
|
|
143
|
+
stmts.push(`ALTER TABLE content_${slug}_drafts RENAME COLUMN ${tmpAlias} TO ${alias};`);
|
|
144
|
+
}
|
|
145
|
+
// Rebuild FTS if the seed has searchable branches in the updated definition.
|
|
146
|
+
const rebuild = planFtsRebuild(updatedSeed);
|
|
147
|
+
stmts.push(...rebuild);
|
|
148
|
+
return { statements: stmts, ftsRebuildNeeded: rebuild.length > 0 };
|
|
149
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../src/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,
|
|
1
|
+
{"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../src/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAErD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,mBAAmB,EAAE,CA0N5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
|
package/dist/seed-validation.js
CHANGED
|
@@ -10,7 +10,7 @@ const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
|
|
|
10
10
|
* prevent DDL/query injection. Exported so the seeds API rename route reuses the
|
|
11
11
|
* exact same guard — do not inline a divergent copy.
|
|
12
12
|
*/
|
|
13
|
-
export const BRANCH_ALIAS_RE = /^[a-z][a-
|
|
13
|
+
export const BRANCH_ALIAS_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
14
14
|
/**
|
|
15
15
|
* Pure, console-free, throw-free validation of a seed set.
|
|
16
16
|
* The single seed being created/edited should be validated in the context of
|
|
@@ -93,7 +93,7 @@ export function validateSeedDefinitions(seeds) {
|
|
|
93
93
|
// identifier charset before it can reach DDL/query string interpolation.
|
|
94
94
|
if (typeof branch.alias !== 'string' || !BRANCH_ALIAS_RE.test(branch.alias)) {
|
|
95
95
|
messages.push(`branch alias '${branch.alias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
|
|
96
|
-
`(lowercase letter followed by
|
|
96
|
+
`(lowercase letter followed by alphanumeric characters or underscores).`);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
if (messages.length > 0)
|
|
@@ -111,6 +111,44 @@ export function validateSeedDefinitions(seeds) {
|
|
|
111
111
|
if (messages.length > 0)
|
|
112
112
|
result.push({ slug: seed.slug, messages, fatal: true });
|
|
113
113
|
}
|
|
114
|
+
// ── Fatal 10: repeater sub-field constraints ─────────────────────────────
|
|
115
|
+
{
|
|
116
|
+
const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
|
|
117
|
+
for (const seed of seeds) {
|
|
118
|
+
const messages = [];
|
|
119
|
+
for (const branch of seed.branches) {
|
|
120
|
+
if (branch.type !== 'repeater')
|
|
121
|
+
continue;
|
|
122
|
+
const subIds = new Set();
|
|
123
|
+
const subAliases = new Set();
|
|
124
|
+
for (const sub of branch.fields ?? []) {
|
|
125
|
+
if (REPEATER_DISALLOWED_SUBTYPES.has(sub.type)) {
|
|
126
|
+
messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has disallowed type '${sub.type}'. ` +
|
|
127
|
+
`Repeater sub-fields cannot be 'repeater', 'relation', or 'file' in v1.`);
|
|
128
|
+
}
|
|
129
|
+
if (!sub.id || !BRANCH_ID_RE.test(sub.id)) {
|
|
130
|
+
messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has invalid id '${sub.id}'. ` +
|
|
131
|
+
`Expected format ^br_[A-Za-z0-9]+$.`);
|
|
132
|
+
}
|
|
133
|
+
else if (subIds.has(sub.id)) {
|
|
134
|
+
messages.push(`branch '${branch.alias}': duplicate sub-field id '${sub.id}'`);
|
|
135
|
+
}
|
|
136
|
+
if (sub.id)
|
|
137
|
+
subIds.add(sub.id);
|
|
138
|
+
if (typeof sub.alias !== 'string' || !BRANCH_ALIAS_RE.test(sub.alias)) {
|
|
139
|
+
messages.push(`branch '${branch.alias}': sub-field alias '${sub.alias}' is invalid. ` +
|
|
140
|
+
`Expected format ${BRANCH_ALIAS_RE.source}.`);
|
|
141
|
+
}
|
|
142
|
+
else if (subAliases.has(sub.alias)) {
|
|
143
|
+
messages.push(`branch '${branch.alias}': duplicate sub-field alias '${sub.alias}'`);
|
|
144
|
+
}
|
|
145
|
+
subAliases.add(sub.alias);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (messages.length > 0)
|
|
149
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
114
152
|
// ── Warning 7: duplicate slug ─────────────────────────────────────────────
|
|
115
153
|
{
|
|
116
154
|
const slugsSeen = new Set();
|
|
@@ -148,6 +186,36 @@ export function validateSeedDefinitions(seeds) {
|
|
|
148
186
|
});
|
|
149
187
|
}
|
|
150
188
|
}
|
|
189
|
+
// ── Fatal 11 / Warning 10: repeater cardinality bounds ───────────────────
|
|
190
|
+
{
|
|
191
|
+
for (const seed of seeds) {
|
|
192
|
+
const fatals = [];
|
|
193
|
+
const warnings = [];
|
|
194
|
+
for (const branch of seed.branches) {
|
|
195
|
+
const hasBounds = branch.minItems !== undefined || branch.maxItems !== undefined;
|
|
196
|
+
if (!hasBounds)
|
|
197
|
+
continue;
|
|
198
|
+
if (branch.type !== 'repeater') {
|
|
199
|
+
warnings.push(`branch '${branch.alias}': minItems/maxItems are ignored on type ` +
|
|
200
|
+
`'${branch.type}' (repeater-only).`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const [key, val] of [['minItems', branch.minItems], ['maxItems', branch.maxItems]]) {
|
|
204
|
+
if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
|
|
205
|
+
fatals.push(`branch '${branch.alias}': ${key} must be a non-negative integer (got ${val}).`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (Number.isInteger(branch.minItems) && Number.isInteger(branch.maxItems) &&
|
|
209
|
+
branch.minItems > branch.maxItems) {
|
|
210
|
+
fatals.push(`branch '${branch.alias}': minItems (${branch.minItems}) must be <= maxItems (${branch.maxItems}).`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (fatals.length > 0)
|
|
214
|
+
result.push({ slug: seed.slug, messages: fatals, fatal: true });
|
|
215
|
+
if (warnings.length > 0)
|
|
216
|
+
result.push({ slug: seed.slug, messages: warnings, fatal: false });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
151
219
|
return result;
|
|
152
220
|
}
|
|
153
221
|
/** Convenience: true iff no fatal issues. */
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FileAccept } from './file-types.js';
|
|
2
|
-
export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation';
|
|
2
|
+
export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation' | 'repeater';
|
|
3
3
|
/** Configurazioni specializzate per il tipo di branch 'number' */
|
|
4
4
|
export interface NumberFieldOptions {
|
|
5
5
|
/** Stile di presentazione visiva. Default: 'decimal' */
|
|
@@ -120,6 +120,32 @@ export interface Branch {
|
|
|
120
120
|
* rule applies to the FK from the junction table to the target table.
|
|
121
121
|
*/
|
|
122
122
|
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
123
|
+
/**
|
|
124
|
+
* Sub-schema for `type === 'repeater'`. Each item of the repeater's array
|
|
125
|
+
* value is a record keyed by sub-branch alias, validated against this list.
|
|
126
|
+
* Sub-branches are restricted to leaf/scalar types (no nested `repeater`,
|
|
127
|
+
* `relation`, or `file`) — enforced by validation.ts and seed-validation.ts.
|
|
128
|
+
* Ignored for any other branch type.
|
|
129
|
+
*/
|
|
130
|
+
fields?: Branch[];
|
|
131
|
+
/**
|
|
132
|
+
* Minimum number of items a `repeater` value must contain when a value is
|
|
133
|
+
* provided. Repeater-only — ignored for every other branch type.
|
|
134
|
+
*
|
|
135
|
+
* NOTE: this constrains array *length when the field is present*. It does NOT by
|
|
136
|
+
* itself make the field mandatory — an absent/null payload is still allowed unless
|
|
137
|
+
* `requiredOnCreate` / `requiredOnUpdate` is also set. To model "exactly one
|
|
138
|
+
* required object", combine `minItems: 1, maxItems: 1, requiredOnCreate: true`.
|
|
139
|
+
* Must be a non-negative integer and `<= maxItems` when both are set
|
|
140
|
+
* (enforced at boot by seed-validation.ts).
|
|
141
|
+
*/
|
|
142
|
+
minItems?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Maximum number of items a `repeater` value may contain. Repeater-only — ignored
|
|
145
|
+
* for every other branch type. `maxItems: 1` models a single "object" column.
|
|
146
|
+
* Must be a non-negative integer and `>= minItems` when both are set.
|
|
147
|
+
*/
|
|
148
|
+
maxItems?: number;
|
|
123
149
|
}
|
|
124
150
|
/** Dashboard-specific config embedded in a Seed. All fields optional — defaults applied by the dashboard. */
|
|
125
151
|
export interface DashboardSeedConfig {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAc,IAAI,EAAE,MAAM,YAAY,CAAA;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAuBD,QAAA,MAAM,aAAa,2CAA4C,CAAA;AAmD/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAM1F;AA2lBD;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,GAAE,0BAA+B,GACvC,yBAAyB,CAqD3B;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
|
package/dist/validation.js
CHANGED
|
@@ -348,6 +348,33 @@ function fileSchema(branch, allowNull) {
|
|
|
348
348
|
.pipe(z.string().url());
|
|
349
349
|
return withNullable(withEmptyPreprocessing(inner, allowNull), allowNull);
|
|
350
350
|
}
|
|
351
|
+
// Sub-branches of a `repeater` are restricted to leaf/scalar types — no nested
|
|
352
|
+
// `repeater`, `relation`, or `file` (v1 restriction, see types.ts Branch.fields).
|
|
353
|
+
const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
|
|
354
|
+
function repeaterSchema(branch, options) {
|
|
355
|
+
const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
|
|
356
|
+
const subBranches = (branch.fields ?? []).filter((sub) => !REPEATER_DISALLOWED_SUBTYPES.has(sub.type));
|
|
357
|
+
const shape = {};
|
|
358
|
+
for (const sub of subBranches) {
|
|
359
|
+
const subSchema = schemaForBranch(sub, options);
|
|
360
|
+
shape[sub.alias] = sub[requiredFlag] ? subSchema : subSchema.optional();
|
|
361
|
+
}
|
|
362
|
+
// z.object() strips unknown keys by default — old item shapes from a renamed/
|
|
363
|
+
// removed sub-field are dropped rather than rejected (sprint 10 §5.1).
|
|
364
|
+
const itemSchema = z.object(shape);
|
|
365
|
+
let arraySchema = z.array(itemSchema);
|
|
366
|
+
if (Number.isInteger(branch.minItems) && branch.minItems >= 0) {
|
|
367
|
+
arraySchema = arraySchema.min(branch.minItems, {
|
|
368
|
+
message: `Expected array(min:${branch.minItems})`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (Number.isInteger(branch.maxItems) && branch.maxItems >= 0) {
|
|
372
|
+
arraySchema = arraySchema.max(branch.maxItems, {
|
|
373
|
+
message: `Expected array(max:${branch.maxItems})`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return withNullable(withEmptyPreprocessing(arraySchema, options.allowNull), options.allowNull);
|
|
377
|
+
}
|
|
351
378
|
const BRANCH_SCHEMA_BUILDERS = {
|
|
352
379
|
text: (_branch, options) => textSchema(options, options.allowNull),
|
|
353
380
|
richtext: (_branch, options) => richtextSchema(options, options.allowNull),
|
|
@@ -358,6 +385,7 @@ const BRANCH_SCHEMA_BUILDERS = {
|
|
|
358
385
|
tags: (_branch, options) => jsonOrTagsSchema(options.allowNull),
|
|
359
386
|
file: (branch, options) => fileSchema(branch, options.allowNull),
|
|
360
387
|
relation: (branch, options) => relationSchema(branch, options),
|
|
388
|
+
repeater: (branch, options) => repeaterSchema(branch, options),
|
|
361
389
|
};
|
|
362
390
|
function schemaForBranch(branch, options) {
|
|
363
391
|
const builder = BRANCH_SCHEMA_BUILDERS[branch.type];
|
|
@@ -380,6 +408,14 @@ function buildSeedFingerprint(seed) {
|
|
|
380
408
|
ru: branch.requiredOnUpdate === true,
|
|
381
409
|
n: branch.numberOptions ?? null,
|
|
382
410
|
fi: branch.fileOptions ?? null,
|
|
411
|
+
mi: branch.minItems ?? null,
|
|
412
|
+
ma: branch.maxItems ?? null,
|
|
413
|
+
sub: branch.fields?.map((sub) => ({
|
|
414
|
+
a: sub.alias,
|
|
415
|
+
t: sub.type,
|
|
416
|
+
rc: sub.requiredOnCreate === true,
|
|
417
|
+
ru: sub.requiredOnUpdate === true,
|
|
418
|
+
})) ?? null,
|
|
383
419
|
}));
|
|
384
420
|
return JSON.stringify({ s: seed.slug, b: parts });
|
|
385
421
|
}
|
|
@@ -500,11 +536,32 @@ function splitUnknownAliases(seed, payload) {
|
|
|
500
536
|
}
|
|
501
537
|
return { filtered, unknown, details };
|
|
502
538
|
}
|
|
539
|
+
function flattenZodIssues(issues, parentPath = []) {
|
|
540
|
+
const result = [];
|
|
541
|
+
for (const issue of issues) {
|
|
542
|
+
const issuePath = issue.path;
|
|
543
|
+
const currentPath = [...parentPath, ...issuePath];
|
|
544
|
+
if (issue.code === 'invalid_union' && 'errors' in issue) {
|
|
545
|
+
const unionErrors = issue.errors;
|
|
546
|
+
for (const subIssues of unionErrors) {
|
|
547
|
+
result.push(...flattenZodIssues(subIssues, currentPath));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
result.push({
|
|
552
|
+
...issue,
|
|
553
|
+
path: currentPath,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return result;
|
|
558
|
+
}
|
|
503
559
|
function processZodIssues(seed, issues, filtered, options) {
|
|
504
560
|
const details = [];
|
|
505
561
|
const unknown = [];
|
|
506
562
|
const dangerous = [];
|
|
507
|
-
|
|
563
|
+
const flatIssues = flattenZodIssues(issues);
|
|
564
|
+
for (const issue of flatIssues) {
|
|
508
565
|
if (issue.code === 'unrecognized_keys') {
|
|
509
566
|
for (const alias of issue.keys) {
|
|
510
567
|
unknown.push(alias);
|
|
@@ -36,6 +36,23 @@ export type AggregateFormula = {
|
|
|
36
36
|
* 7 days, 1 month, and 1 year respectively, anchored on `created_at`.
|
|
37
37
|
*/
|
|
38
38
|
export type TimeWindow = 'week' | 'month' | 'year' | 'all';
|
|
39
|
+
/**
|
|
40
|
+
* Explicit time range applied as a WHERE filter, anchored on `created_at`.
|
|
41
|
+
*
|
|
42
|
+
* Both bounds are unix seconds (inclusive). Used when the dashboard sets a
|
|
43
|
+
* custom date reference instead of one of the relative {@link TimeWindow} presets.
|
|
44
|
+
*/
|
|
45
|
+
export interface DateRange {
|
|
46
|
+
from: number;
|
|
47
|
+
to: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Time filter accepted by widget queries: either a relative preset
|
|
51
|
+
* ({@link TimeWindow}) or an explicit {@link DateRange}.
|
|
52
|
+
*/
|
|
53
|
+
export type WidgetWindow = TimeWindow | DateRange;
|
|
54
|
+
/** Narrows a {@link WidgetWindow} to a {@link DateRange}. */
|
|
55
|
+
export declare function isDateRange(window: WidgetWindow): window is DateRange;
|
|
39
56
|
export interface LeaderboardEntry {
|
|
40
57
|
id: string;
|
|
41
58
|
label: string;
|
|
@@ -71,6 +88,10 @@ export interface GrowthResult {
|
|
|
71
88
|
currentValue: number;
|
|
72
89
|
previousValue: number;
|
|
73
90
|
}
|
|
91
|
+
export interface DistributionSlice {
|
|
92
|
+
label: string;
|
|
93
|
+
value: number;
|
|
94
|
+
}
|
|
74
95
|
/**
|
|
75
96
|
* Read-only data access contract for widget routes.
|
|
76
97
|
*
|
|
@@ -84,14 +105,14 @@ export interface IWidgetRepository {
|
|
|
84
105
|
* Returns the formula result for the given time window. Always returns a
|
|
85
106
|
* number; implementations must return 0 when the query produces no rows.
|
|
86
107
|
*/
|
|
87
|
-
aggregate(seed: Seed, formula: AggregateFormula, window:
|
|
108
|
+
aggregate(seed: Seed, formula: AggregateFormula, window: WidgetWindow): Promise<number>;
|
|
88
109
|
/**
|
|
89
110
|
* Evaluates the formula twice — once for the current window period and once
|
|
90
111
|
* for the equivalent previous period — to support trend calculations.
|
|
91
112
|
* Implementations must return { currentValue: 0, previousValue: 0 } on
|
|
92
113
|
* empty results.
|
|
93
114
|
*/
|
|
94
|
-
growth(seed: Seed, formula: AggregateFormula, window:
|
|
115
|
+
growth(seed: Seed, formula: AggregateFormula, window: WidgetWindow): Promise<GrowthResult>;
|
|
95
116
|
/**
|
|
96
117
|
* Returns entries sorted by scoreColumn, excluding nulls. label resolves
|
|
97
118
|
* from seed.displayNameAlias; falls back to id when not set.
|
|
@@ -108,6 +129,14 @@ export interface IWidgetRepository {
|
|
|
108
129
|
* the formula. Days with no entries are omitted (no zero-fill). Points are
|
|
109
130
|
* ordered ascending by label.
|
|
110
131
|
*/
|
|
111
|
-
timeseries(seed: Seed, formula: AggregateFormula, window:
|
|
132
|
+
timeseries(seed: Seed, formula: AggregateFormula, window: WidgetWindow, groupColumn: string): Promise<TimeseriesPoint[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Counts entries grouped by the values of `column` within the window,
|
|
135
|
+
* descending by count, capped at `limit` slices. Implementations must
|
|
136
|
+
* validate `column` against the seed (UNSAFE_COLUMN on failure) and must
|
|
137
|
+
* return [] on empty results. Values beyond `limit` are NOT merged into
|
|
138
|
+
* an 'other' bucket — the client decides how to present truncation.
|
|
139
|
+
*/
|
|
140
|
+
distribution(seed: Seed, column: string, window: WidgetWindow, limit: number): Promise<DistributionSlice[]>;
|
|
112
141
|
}
|
|
113
142
|
//# sourceMappingURL=widget.repository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widget.repository.d.ts","sourceRoot":"","sources":["../../src/widget/widget.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,OAAO,CAAA;CAAE,GACf;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACpD;IAAE,EAAE,EAAE,cAAc,CAAC;IAAC,eAAe,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;AAE1D,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,cAAc,EAAE,KAAK,GAAG,MAAM,CAAA;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"widget.repository.d.ts","sourceRoot":"","sources":["../../src/widget/widget.repository.ts"],"names":[],"mappings":"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"}
|