@beechcms/core 0.5.0 → 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/auth/hash-provider.d.ts.map +1 -1
- package/dist/auth/hash-provider.js +2 -0
- package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
- package/dist/auth/password-reset-token.repository.js +2 -0
- package/dist/auth/session.repository.d.ts.map +1 -1
- package/dist/auth/session.repository.js +2 -0
- package/dist/auth/token-service.d.ts +2 -0
- package/dist/auth/token-service.d.ts.map +1 -1
- package/dist/auth/token-service.js +2 -0
- package/dist/auth/user.repository.d.ts +4 -1
- package/dist/auth/user.repository.d.ts.map +1 -1
- package/dist/auth/user.repository.js +2 -0
- package/dist/automations-grammar-words.d.ts.map +1 -1
- package/dist/automations-grammar-words.js +2 -0
- package/dist/automations.repository.interface.d.ts.map +1 -1
- package/dist/automations.repository.interface.js +2 -0
- package/dist/automations.runner.interface.d.ts.map +1 -1
- package/dist/automations.runner.interface.js +2 -0
- package/dist/automations.runner.stub.d.ts.map +1 -1
- package/dist/automations.runner.stub.js +2 -0
- package/dist/automations.types.d.ts +2 -0
- package/dist/automations.types.d.ts.map +1 -1
- package/dist/automations.types.js +2 -0
- package/dist/clock.d.ts.map +1 -1
- package/dist/clock.js +2 -0
- package/dist/content-scan.repository.d.ts.map +1 -1
- package/dist/content-scan.repository.js +2 -0
- package/dist/content.repository.d.ts +62 -0
- package/dist/content.repository.d.ts.map +1 -1
- package/dist/content.repository.js +18 -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/define-seed.d.ts.map +1 -1
- package/dist/define-seed.js +2 -0
- package/dist/demo-data.repository.d.ts +5 -0
- package/dist/demo-data.repository.d.ts.map +1 -0
- package/dist/demo-data.repository.js +3 -0
- package/dist/engine.d.ts +76 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +259 -5
- package/dist/file-types.d.ts +13 -0
- package/dist/file-types.d.ts.map +1 -0
- package/dist/file-types.js +85 -0
- package/dist/id-generator.d.ts +7 -0
- package/dist/id-generator.d.ts.map +1 -1
- package/dist/id-generator.js +5 -0
- package/dist/idempotency.repository.d.ts.map +1 -1
- package/dist/idempotency.repository.js +2 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/layout-permissions.d.ts +3 -0
- package/dist/layout-permissions.d.ts.map +1 -0
- package/dist/layout-permissions.js +11 -0
- package/dist/media.repository.d.ts.map +1 -1
- package/dist/media.repository.js +2 -0
- package/dist/notifications/notification-service.d.ts.map +1 -1
- package/dist/notifications/notification-service.js +2 -0
- package/dist/notifications/notification.repository.d.ts.map +1 -1
- package/dist/notifications/notification.repository.js +2 -9
- package/dist/observability/activity-log.repository.d.ts.map +1 -1
- package/dist/observability/activity-log.repository.js +2 -0
- package/dist/observability/activity-logger.d.ts +2 -2
- package/dist/observability/activity-logger.d.ts.map +1 -1
- package/dist/observability/activity-logger.js +2 -11
- package/dist/observability/analytics.repository.d.ts.map +1 -1
- package/dist/observability/analytics.repository.js +2 -0
- package/dist/policies.d.ts.map +1 -1
- package/dist/policies.js +8 -3
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/rate-limiter.js +2 -0
- package/dist/relations.d.ts +28 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +25 -0
- package/dist/richtext-render.d.ts.map +1 -1
- package/dist/richtext-render.js +2 -0
- package/dist/richtext.d.ts.map +1 -1
- package/dist/richtext.js +2 -0
- package/dist/scheduler.interface.d.ts.map +1 -1
- package/dist/scheduler.interface.js +2 -0
- package/dist/scheduler.stub.d.ts.map +1 -1
- package/dist/scheduler.stub.js +2 -0
- package/dist/schema-mutator.d.ts +22 -0
- package/dist/schema-mutator.d.ts.map +1 -0
- package/dist/schema-mutator.js +3 -0
- package/dist/search/search.repository.d.ts.map +1 -1
- package/dist/search/search.repository.js +2 -0
- 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-ddl.d.ts +40 -0
- package/dist/seed-ddl.d.ts.map +1 -0
- package/dist/seed-ddl.js +100 -0
- package/dist/seed-layout.d.ts +117 -0
- package/dist/seed-layout.d.ts.map +1 -0
- package/dist/seed-layout.js +169 -0
- package/dist/seed-layout.repository.d.ts +18 -0
- package/dist/seed-layout.repository.d.ts.map +1 -0
- package/dist/seed-layout.repository.js +3 -0
- package/dist/seed-registry.d.ts +23 -1
- package/dist/seed-registry.d.ts.map +1 -1
- package/dist/seed-registry.js +105 -0
- package/dist/seed-validation.d.ts +23 -0
- package/dist/seed-validation.d.ts.map +1 -0
- package/dist/seed-validation.js +224 -0
- package/dist/seed.repository.d.ts +37 -0
- package/dist/seed.repository.d.ts.map +1 -0
- package/dist/seed.repository.js +3 -0
- package/dist/seeds.d.ts.map +1 -1
- package/dist/seeds.js +2 -0
- package/dist/site-settings.repository.d.ts +16 -0
- package/dist/site-settings.repository.d.ts.map +1 -0
- package/dist/site-settings.repository.js +3 -0
- package/dist/slug-utils.d.ts.map +1 -1
- package/dist/slug-utils.js +2 -0
- package/dist/storage.d.ts +12 -38
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -0
- package/dist/types.d.ts +81 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/validation.d.ts +23 -17
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +487 -381
- package/dist/webhook-validation.d.ts +3 -0
- package/dist/webhook-validation.d.ts.map +1 -0
- package/dist/webhook-validation.js +6 -0
- 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 +6 -1
- package/package.json +5 -4
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Seed, Branch } from './types.js';
|
|
3
|
+
/** Reference to a Seed branch by its stable id (e.g. 'br_03').
|
|
4
|
+
* Aliases can be renamed — ids cannot. When the id does not match any
|
|
5
|
+
* branch on the Seed at read time, the field is silently stripped (auto-cleanup). */
|
|
6
|
+
export interface LayoutField {
|
|
7
|
+
branchId: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LayoutColumn {
|
|
10
|
+
id: string;
|
|
11
|
+
fields: LayoutField[];
|
|
12
|
+
}
|
|
13
|
+
export interface LayoutSection {
|
|
14
|
+
id: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
hideLabel?: boolean;
|
|
17
|
+
hideBorder?: boolean;
|
|
18
|
+
collapsible?: boolean;
|
|
19
|
+
/** Max 4 columns. Enforced by validator. */
|
|
20
|
+
columns: LayoutColumn[];
|
|
21
|
+
}
|
|
22
|
+
export interface LayoutTab {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
sections: LayoutSection[];
|
|
26
|
+
}
|
|
27
|
+
export interface FormLayout {
|
|
28
|
+
/** Format version — bump when introducing breaking changes. */
|
|
29
|
+
version: 1;
|
|
30
|
+
tabs: LayoutTab[];
|
|
31
|
+
}
|
|
32
|
+
/** Branch types that must occupy a section alone, full width. */
|
|
33
|
+
export declare const FULL_WIDTH_BRANCH_TYPES: Set<import("./types.js").BranchType>;
|
|
34
|
+
/** Returns true when a branch is a gallery (file + multiple/asset-list). */
|
|
35
|
+
export declare function isGalleryBranch(branch: Branch): boolean;
|
|
36
|
+
/** Branch types currently unsupported in the Layout Builder. */
|
|
37
|
+
export declare const UNSUPPORTED_BRANCH_TYPES: Set<import("./types.js").BranchType>;
|
|
38
|
+
/** Aliases that are NEVER included in the editor form (handled by other UI). */
|
|
39
|
+
export declare const SYSTEM_ALIASES: Set<string>;
|
|
40
|
+
/** True when the branch is eligible to appear in the layout at all. */
|
|
41
|
+
export declare function isLayoutableBranch(branch: Branch): boolean;
|
|
42
|
+
/** True when the branch must occupy a dedicated full-width section. */
|
|
43
|
+
export declare function isFullWidthBranch(branch: Branch): boolean;
|
|
44
|
+
/** Branches considered SEO fields. Matches the existing rule in entry-editor.tsx. */
|
|
45
|
+
export declare function isSeoBranch(branch: Branch): boolean;
|
|
46
|
+
export declare const layoutFieldSchema: z.ZodObject<{
|
|
47
|
+
branchId: z.ZodString;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export declare const layoutColumnSchema: z.ZodObject<{
|
|
50
|
+
id: z.ZodString;
|
|
51
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
52
|
+
branchId: z.ZodString;
|
|
53
|
+
}, z.core.$strip>>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
export declare const layoutSectionSchema: z.ZodObject<{
|
|
56
|
+
id: z.ZodString;
|
|
57
|
+
label: z.ZodOptional<z.ZodString>;
|
|
58
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
59
|
+
hideBorder: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
62
|
+
id: z.ZodString;
|
|
63
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
64
|
+
branchId: z.ZodString;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
}, z.core.$strip>>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
export declare const layoutTabSchema: z.ZodObject<{
|
|
69
|
+
id: z.ZodString;
|
|
70
|
+
label: z.ZodString;
|
|
71
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
72
|
+
id: z.ZodString;
|
|
73
|
+
label: z.ZodOptional<z.ZodString>;
|
|
74
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
hideBorder: z.ZodOptional<z.ZodBoolean>;
|
|
76
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
77
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
78
|
+
id: z.ZodString;
|
|
79
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
80
|
+
branchId: z.ZodString;
|
|
81
|
+
}, z.core.$strip>>;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
export declare const formLayoutSchema: z.ZodObject<{
|
|
86
|
+
version: z.ZodLiteral<1>;
|
|
87
|
+
tabs: z.ZodArray<z.ZodObject<{
|
|
88
|
+
id: z.ZodString;
|
|
89
|
+
label: z.ZodString;
|
|
90
|
+
sections: z.ZodArray<z.ZodObject<{
|
|
91
|
+
id: z.ZodString;
|
|
92
|
+
label: z.ZodOptional<z.ZodString>;
|
|
93
|
+
hideLabel: z.ZodOptional<z.ZodBoolean>;
|
|
94
|
+
hideBorder: z.ZodOptional<z.ZodBoolean>;
|
|
95
|
+
collapsible: z.ZodOptional<z.ZodBoolean>;
|
|
96
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
97
|
+
id: z.ZodString;
|
|
98
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
99
|
+
branchId: z.ZodString;
|
|
100
|
+
}, z.core.$strip>>;
|
|
101
|
+
}, z.core.$strip>>;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
}, z.core.$strip>>;
|
|
104
|
+
}, z.core.$strip>;
|
|
105
|
+
export declare function generateDefaultLayout(seed: Seed, opts?: {
|
|
106
|
+
newId: () => string;
|
|
107
|
+
}): FormLayout;
|
|
108
|
+
export type ValidateLayoutResult = {
|
|
109
|
+
ok: true;
|
|
110
|
+
cleaned: FormLayout;
|
|
111
|
+
} | {
|
|
112
|
+
ok: false;
|
|
113
|
+
errors: string[];
|
|
114
|
+
cleaned: FormLayout;
|
|
115
|
+
};
|
|
116
|
+
export declare function validateLayoutAgainstSeed(layout: FormLayout, seed: Seed): ValidateLayoutResult;
|
|
117
|
+
//# sourceMappingURL=seed-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-layout.d.ts","sourceRoot":"","sources":["../src/seed-layout.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAO9C;;sFAEsF;AACtF,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4CAA4C;IAC5C,OAAO,EAAE,YAAY,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,EAAE,CAAA;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,+DAA+D;IAC/D,OAAO,EAAE,CAAC,CAAA;IACV,IAAI,EAAE,SAAS,EAAE,CAAA;CAClB;AAMD,iEAAiE;AACjE,eAAO,MAAM,uBAAuB,sCAAwC,CAAA;AAE5E,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,sCAAoC,CAAA;AAEzE,gFAAgF;AAChF,eAAO,MAAM,cAAc,aAEzB,CAAA;AAEF,uEAAuE;AACvE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAK1D;AAED,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnD;AAMD,eAAO,MAAM,iBAAiB;;iBAAgE,CAAA;AAC9F,eAAO,MAAM,kBAAkB;;;;;iBAG7B,CAAA;AACF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAO9B,CAAA;AACF,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;iBAI1B,CAAA;AACF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;iBAG3B,CAAA;AAiDF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,IAAI,EACV,IAAI,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,MAAM,CAAA;CAAE,GAC7B,UAAU,CAmBZ;AAMD,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAAA;AAExD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,IAAI,GACT,oBAAoB,CAsDtB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { findBranchById } from './seed-registry.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Branch-type rules used by both the default generator and the validator.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Branch types that must occupy a section alone, full width. */
|
|
9
|
+
export const FULL_WIDTH_BRANCH_TYPES = new Set(['richtext']);
|
|
10
|
+
/** Returns true when a branch is a gallery (file + multiple/asset-list). */
|
|
11
|
+
export function isGalleryBranch(branch) {
|
|
12
|
+
return branch.type === 'file'
|
|
13
|
+
&& (branch.multiple === true || branch.format === 'asset-list');
|
|
14
|
+
}
|
|
15
|
+
/** Branch types currently unsupported in the Layout Builder. */
|
|
16
|
+
export const UNSUPPORTED_BRANCH_TYPES = new Set(['json']);
|
|
17
|
+
/** Aliases that are NEVER included in the editor form (handled by other UI). */
|
|
18
|
+
export const SYSTEM_ALIASES = new Set([
|
|
19
|
+
'id', 'slug', 'status', 'created_at', 'updated_at',
|
|
20
|
+
]);
|
|
21
|
+
/** True when the branch is eligible to appear in the layout at all. */
|
|
22
|
+
export function isLayoutableBranch(branch) {
|
|
23
|
+
if (SYSTEM_ALIASES.has(branch.alias))
|
|
24
|
+
return false;
|
|
25
|
+
if (branch.policies?.visibility === 'hidden')
|
|
26
|
+
return false;
|
|
27
|
+
if (UNSUPPORTED_BRANCH_TYPES.has(branch.type))
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** True when the branch must occupy a dedicated full-width section. */
|
|
32
|
+
export function isFullWidthBranch(branch) {
|
|
33
|
+
return FULL_WIDTH_BRANCH_TYPES.has(branch.type) || isGalleryBranch(branch);
|
|
34
|
+
}
|
|
35
|
+
/** Branches considered SEO fields. Matches the existing rule in entry-editor.tsx. */
|
|
36
|
+
export function isSeoBranch(branch) {
|
|
37
|
+
return branch.alias.startsWith('meta');
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Zod schema for shape validation
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
export const layoutFieldSchema = z.object({ branchId: z.string().regex(/^br_[A-Za-z0-9]+$/) });
|
|
43
|
+
export const layoutColumnSchema = z.object({
|
|
44
|
+
id: z.string().min(1),
|
|
45
|
+
fields: z.array(layoutFieldSchema),
|
|
46
|
+
});
|
|
47
|
+
export const layoutSectionSchema = z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
label: z.string().optional(),
|
|
50
|
+
hideLabel: z.boolean().optional(),
|
|
51
|
+
hideBorder: z.boolean().optional(),
|
|
52
|
+
collapsible: z.boolean().optional(),
|
|
53
|
+
columns: z.array(layoutColumnSchema).min(1).max(4),
|
|
54
|
+
});
|
|
55
|
+
export const layoutTabSchema = z.object({
|
|
56
|
+
id: z.string().min(1),
|
|
57
|
+
label: z.string().min(1).max(60),
|
|
58
|
+
sections: z.array(layoutSectionSchema).min(1),
|
|
59
|
+
});
|
|
60
|
+
export const formLayoutSchema = z.object({
|
|
61
|
+
version: z.literal(1),
|
|
62
|
+
tabs: z.array(layoutTabSchema).min(1).max(8),
|
|
63
|
+
});
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Default layout generator
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
const DEFAULT_COLUMNS_PER_SECTION = 3;
|
|
68
|
+
const defaultIdFactory = () => crypto.randomUUID();
|
|
69
|
+
function buildSectionsForBranches(branches, newId) {
|
|
70
|
+
const sections = [];
|
|
71
|
+
let currentColumns = [];
|
|
72
|
+
function flushCompact() {
|
|
73
|
+
if (currentColumns.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
sections.push({ id: newId(), columns: currentColumns });
|
|
76
|
+
currentColumns = [];
|
|
77
|
+
}
|
|
78
|
+
for (const branch of branches) {
|
|
79
|
+
if (isFullWidthBranch(branch)) {
|
|
80
|
+
flushCompact();
|
|
81
|
+
sections.push({
|
|
82
|
+
id: newId(),
|
|
83
|
+
label: branch.label,
|
|
84
|
+
hideLabel: true,
|
|
85
|
+
columns: [{ id: newId(), fields: [{ branchId: branch.id }] }],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
currentColumns.push({ id: newId(), fields: [{ branchId: branch.id }] });
|
|
90
|
+
if (currentColumns.length >= DEFAULT_COLUMNS_PER_SECTION) {
|
|
91
|
+
flushCompact();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
flushCompact();
|
|
96
|
+
if (sections.length === 0) {
|
|
97
|
+
sections.push({ id: newId(), columns: [{ id: newId(), fields: [] }] });
|
|
98
|
+
}
|
|
99
|
+
return sections;
|
|
100
|
+
}
|
|
101
|
+
export function generateDefaultLayout(seed, opts) {
|
|
102
|
+
const newId = opts?.newId ?? defaultIdFactory;
|
|
103
|
+
const layoutable = seed.branches.filter(isLayoutableBranch);
|
|
104
|
+
const seo = layoutable.filter(isSeoBranch);
|
|
105
|
+
const main = layoutable.filter((b) => !isSeoBranch(b));
|
|
106
|
+
const dataTab = {
|
|
107
|
+
id: newId(),
|
|
108
|
+
label: 'Data',
|
|
109
|
+
sections: buildSectionsForBranches(main, newId),
|
|
110
|
+
};
|
|
111
|
+
const seoTab = {
|
|
112
|
+
id: newId(),
|
|
113
|
+
label: 'SEO',
|
|
114
|
+
sections: buildSectionsForBranches(seo, newId),
|
|
115
|
+
};
|
|
116
|
+
return { version: 1, tabs: [dataTab, seoTab] };
|
|
117
|
+
}
|
|
118
|
+
export function validateLayoutAgainstSeed(layout, seed) {
|
|
119
|
+
const errors = [];
|
|
120
|
+
const seenBranchIds = new Set();
|
|
121
|
+
const cleanedTabs = layout.tabs.map((tab) => ({
|
|
122
|
+
...tab,
|
|
123
|
+
sections: tab.sections.map((section) => ({
|
|
124
|
+
...section,
|
|
125
|
+
columns: section.columns.map((col) => ({
|
|
126
|
+
...col,
|
|
127
|
+
fields: (col.fields ?? []).filter((f) => {
|
|
128
|
+
const branch = findBranchById(seed, f.branchId);
|
|
129
|
+
if (!branch)
|
|
130
|
+
return false;
|
|
131
|
+
if (UNSUPPORTED_BRANCH_TYPES.has(branch.type) || SYSTEM_ALIASES.has(branch.alias))
|
|
132
|
+
return false;
|
|
133
|
+
return true;
|
|
134
|
+
}),
|
|
135
|
+
})),
|
|
136
|
+
})),
|
|
137
|
+
}));
|
|
138
|
+
// Duplicate and full-width constraint checks on cleaned layout
|
|
139
|
+
for (const tab of cleanedTabs) {
|
|
140
|
+
for (const section of tab.sections) {
|
|
141
|
+
const fieldsInSection = [];
|
|
142
|
+
for (const col of section.columns) {
|
|
143
|
+
for (const f of col.fields) {
|
|
144
|
+
const branch = findBranchById(seed, f.branchId);
|
|
145
|
+
if (!branch)
|
|
146
|
+
continue;
|
|
147
|
+
if (seenBranchIds.has(f.branchId)) {
|
|
148
|
+
errors.push(`Branch '${branch.alias}' (id=${f.branchId}) appears more than once in the layout.`);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
seenBranchIds.add(f.branchId);
|
|
152
|
+
fieldsInSection.push(branch);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Full-width branches must occupy a dedicated section
|
|
157
|
+
const fullWidthInSection = fieldsInSection.filter(isFullWidthBranch);
|
|
158
|
+
if (fullWidthInSection.length > 0 && fieldsInSection.length > fullWidthInSection.length) {
|
|
159
|
+
for (const fw of fullWidthInSection) {
|
|
160
|
+
errors.push(`Branch '${fw.alias}' (type=${fw.type}) must occupy a dedicated section and cannot share it with other fields.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const cleaned = { ...layout, tabs: cleanedTabs };
|
|
166
|
+
if (errors.length > 0)
|
|
167
|
+
return { ok: false, errors, cleaned };
|
|
168
|
+
return { ok: true, cleaned };
|
|
169
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FormLayout } from './seed-layout.js';
|
|
2
|
+
export interface SeedLayoutRecord {
|
|
3
|
+
slug: string;
|
|
4
|
+
layout: FormLayout;
|
|
5
|
+
updatedAt: number;
|
|
6
|
+
updatedBy: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ISeedLayoutRepository {
|
|
9
|
+
/** Return the stored layout for a seed, or null if none was ever saved. */
|
|
10
|
+
get(slug: string): Promise<SeedLayoutRecord | null>;
|
|
11
|
+
/** Return all stored layouts, keyed by slug — used by GET /api/schema to enrich. */
|
|
12
|
+
getAllAsMap(): Promise<Map<string, FormLayout>>;
|
|
13
|
+
/** Upsert. `updatedBy` is the writer's user id. */
|
|
14
|
+
upsert(slug: string, layout: FormLayout, updatedBy: string): Promise<void>;
|
|
15
|
+
/** Remove the stored row — used by the "Reset" action. */
|
|
16
|
+
remove(slug: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=seed-layout.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-layout.repository.d.ts","sourceRoot":"","sources":["../src/seed-layout.repository.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACnD,oFAAoF;IACpF,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAA;IAC/C,mDAAmD;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1E,0DAA0D;IAC1D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC"}
|
package/dist/seed-registry.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Seed } from './types.js';
|
|
1
|
+
import type { Seed, Branch } from './types.js';
|
|
2
2
|
export interface ISeedRegistry {
|
|
3
3
|
/**
|
|
4
4
|
* Returns all seeds as a flat array, preserving insertion order.
|
|
@@ -46,4 +46,26 @@ export declare class SeedRegistry implements ISeedRegistry {
|
|
|
46
46
|
*/
|
|
47
47
|
export declare class InMemorySeedRegistry extends SeedRegistry {
|
|
48
48
|
}
|
|
49
|
+
/** Finds a branch by its stable id within a Seed. Returns null if not found. */
|
|
50
|
+
export declare function findBranchById(seed: Seed, id: string): Branch | null;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
|
|
53
|
+
* growing as needed). Scans existing ids, never reuses a number already present —
|
|
54
|
+
* even if a branch was removed, its id is not recycled (ids must be globally stable
|
|
55
|
+
* for the life of the seed so layouts/automations/FTS triggers never collide).
|
|
56
|
+
*/
|
|
57
|
+
export declare function nextBranchId(seed: Pick<Seed, 'branches'>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the input seeds reordered so that every seed appears AFTER all the
|
|
60
|
+
* seeds it depends on (its `relation` branch targets). Pure function — does
|
|
61
|
+
* not read from any global registry.
|
|
62
|
+
*
|
|
63
|
+
* Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
|
|
64
|
+
* ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
|
|
65
|
+
* the correct order without disabling SQLite FK constraints.
|
|
66
|
+
*
|
|
67
|
+
* @throws {Error} When a relation targets an unknown slug not in `seeds`.
|
|
68
|
+
* @throws {Error} When a cyclic dependency is detected, listing involved slugs.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sortSeedsByDependencies(seeds: ReadonlyArray<Seed>): Seed[];
|
|
49
71
|
//# sourceMappingURL=seed-registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../src/seed-registry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../src/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAG9C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,KAAK,EAAE,IAAI,EAAE;IA4BzB,GAAG,IAAI,IAAI,EAAE;IAIb,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI9B,kBAAkB,IAAI,IAAI,EAAE;IAI5B,cAAc,IAAI,IAAI,EAAE;IAIxB,YAAY,IAAI,IAAI,EAAE;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAsE1E"}
|
package/dist/seed-registry.js
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
1
3
|
import { AUTOMATION_RESERVED_WORDS } from './automations-grammar-words.js';
|
|
4
|
+
const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
|
|
2
5
|
export class SeedRegistry {
|
|
3
6
|
seedMap;
|
|
4
7
|
orderedSeeds;
|
|
5
8
|
constructor(seeds) {
|
|
6
9
|
for (const seed of seeds) {
|
|
10
|
+
const idsInSeed = new Set();
|
|
7
11
|
for (const branch of seed.branches) {
|
|
8
12
|
if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
|
|
9
13
|
throw new Error(`Seed "${seed.slug}" uses reserved alias "${branch.alias}". `
|
|
10
14
|
+ `Pick a different alias — this word is used by the automation template grammar.`);
|
|
11
15
|
}
|
|
16
|
+
if (!branch.id || !BRANCH_ID_RE.test(branch.id)) {
|
|
17
|
+
throw new Error(`Seed "${seed.slug}" branch "${branch.alias}" has invalid id "${branch.id}". `
|
|
18
|
+
+ `Expected format ^br_[A-Za-z0-9]+$.`);
|
|
19
|
+
}
|
|
20
|
+
if (idsInSeed.has(branch.id)) {
|
|
21
|
+
throw new Error(`Seed "${seed.slug}" has duplicate branch id "${branch.id}".`);
|
|
22
|
+
}
|
|
23
|
+
idsInSeed.add(branch.id);
|
|
12
24
|
}
|
|
13
25
|
}
|
|
14
26
|
this.orderedSeeds = seeds;
|
|
@@ -36,3 +48,96 @@ export class SeedRegistry {
|
|
|
36
48
|
*/
|
|
37
49
|
export class InMemorySeedRegistry extends SeedRegistry {
|
|
38
50
|
}
|
|
51
|
+
/** Finds a branch by its stable id within a Seed. Returns null if not found. */
|
|
52
|
+
export function findBranchById(seed, id) {
|
|
53
|
+
return seed.branches.find(b => b.id === id) ?? null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
|
|
57
|
+
* growing as needed). Scans existing ids, never reuses a number already present —
|
|
58
|
+
* even if a branch was removed, its id is not recycled (ids must be globally stable
|
|
59
|
+
* for the life of the seed so layouts/automations/FTS triggers never collide).
|
|
60
|
+
*/
|
|
61
|
+
export function nextBranchId(seed) {
|
|
62
|
+
let max = 0;
|
|
63
|
+
for (const b of seed.branches) {
|
|
64
|
+
const m = /^br_0*([0-9]+)$/.exec(b.id);
|
|
65
|
+
if (m)
|
|
66
|
+
max = Math.max(max, parseInt(m[1], 10));
|
|
67
|
+
}
|
|
68
|
+
const n = max + 1;
|
|
69
|
+
return `br_${String(n).padStart(2, '0')}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns the input seeds reordered so that every seed appears AFTER all the
|
|
73
|
+
* seeds it depends on (its `relation` branch targets). Pure function — does
|
|
74
|
+
* not read from any global registry.
|
|
75
|
+
*
|
|
76
|
+
* Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
|
|
77
|
+
* ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
|
|
78
|
+
* the correct order without disabling SQLite FK constraints.
|
|
79
|
+
*
|
|
80
|
+
* @throws {Error} When a relation targets an unknown slug not in `seeds`.
|
|
81
|
+
* @throws {Error} When a cyclic dependency is detected, listing involved slugs.
|
|
82
|
+
*/
|
|
83
|
+
export function sortSeedsByDependencies(seeds) {
|
|
84
|
+
const slugSet = new Set(seeds.map(s => s.slug));
|
|
85
|
+
// Validate all relation targets exist in the input array
|
|
86
|
+
for (const seed of seeds) {
|
|
87
|
+
for (const branch of seed.branches) {
|
|
88
|
+
if (branch.type === 'relation' && branch.targetSeed) {
|
|
89
|
+
if (!slugSet.has(branch.targetSeed)) {
|
|
90
|
+
throw new Error(`Seed '${seed.slug}' relates to unknown target '${branch.targetSeed}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build adjacency: inDegree[slug] = number of dependencies
|
|
96
|
+
// dependents[slug] = slugs that depend on slug
|
|
97
|
+
const inDegree = new Map();
|
|
98
|
+
const dependents = new Map();
|
|
99
|
+
for (const seed of seeds) {
|
|
100
|
+
if (!inDegree.has(seed.slug))
|
|
101
|
+
inDegree.set(seed.slug, 0);
|
|
102
|
+
if (!dependents.has(seed.slug))
|
|
103
|
+
dependents.set(seed.slug, []);
|
|
104
|
+
}
|
|
105
|
+
for (const seed of seeds) {
|
|
106
|
+
// Deduplicate targets to avoid inflating inDegree on multi-branch refs to same seed
|
|
107
|
+
const targets = new Set(seed.branches
|
|
108
|
+
.filter(b => b.type === 'relation' && b.targetSeed)
|
|
109
|
+
.map(b => b.targetSeed));
|
|
110
|
+
for (const target of targets) {
|
|
111
|
+
// seed depends on target → increment seed's in-degree
|
|
112
|
+
inDegree.set(seed.slug, (inDegree.get(seed.slug) ?? 0) + 1);
|
|
113
|
+
dependents.get(target).push(seed.slug);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Kahn's BFS: start with seeds that have no dependencies
|
|
117
|
+
const seedBySlug = new Map(seeds.map(s => [s.slug, s]));
|
|
118
|
+
const queue = [];
|
|
119
|
+
for (const [slug, deg] of inDegree) {
|
|
120
|
+
if (deg === 0)
|
|
121
|
+
queue.push(slug);
|
|
122
|
+
}
|
|
123
|
+
const result = [];
|
|
124
|
+
while (queue.length > 0) {
|
|
125
|
+
const slug = queue.shift();
|
|
126
|
+
result.push(seedBySlug.get(slug));
|
|
127
|
+
for (const dependent of dependents.get(slug) ?? []) {
|
|
128
|
+
const newDeg = (inDegree.get(dependent) ?? 0) - 1;
|
|
129
|
+
inDegree.set(dependent, newDeg);
|
|
130
|
+
if (newDeg === 0)
|
|
131
|
+
queue.push(dependent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (result.length !== seeds.length) {
|
|
135
|
+
// Cycle exists — collect participating slugs (those with in-degree > 0 after BFS)
|
|
136
|
+
const cycle = [...inDegree.entries()]
|
|
137
|
+
.filter(([, deg]) => deg > 0)
|
|
138
|
+
.map(([slug]) => slug)
|
|
139
|
+
.sort();
|
|
140
|
+
throw new Error(`Cyclic dependency detected among seeds: ${cycle.join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Seed } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Allowed charset for a branch alias. An alias becomes a raw SQL column name in
|
|
4
|
+
* CREATE TABLE / ADD COLUMN / CREATE INDEX and in SELECT/WHERE/ORDER BY clauses
|
|
5
|
+
* (see engine.ts), so it MUST be restricted to a safe identifier charset to
|
|
6
|
+
* prevent DDL/query injection. Exported so the seeds API rename route reuses the
|
|
7
|
+
* exact same guard — do not inline a divergent copy.
|
|
8
|
+
*/
|
|
9
|
+
export declare const BRANCH_ALIAS_RE: RegExp;
|
|
10
|
+
export interface SeedValidationIssue {
|
|
11
|
+
slug: string;
|
|
12
|
+
messages: string[];
|
|
13
|
+
fatal: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Pure, console-free, throw-free validation of a seed set.
|
|
17
|
+
* The single seed being created/edited should be validated in the context of
|
|
18
|
+
* the full active set: call validateSeedDefinitions([...otherActiveSeeds, edited]).
|
|
19
|
+
*/
|
|
20
|
+
export declare function validateSeedDefinitions(seeds: Seed[]): SeedValidationIssue[];
|
|
21
|
+
/** Convenience: true iff no fatal issues. */
|
|
22
|
+
export declare function isSeedSetValid(seeds: Seed[]): boolean;
|
|
23
|
+
//# sourceMappingURL=seed-validation.d.ts.map
|
|
@@ -0,0 +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,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"}
|