@beechcms/core 0.6.0-preview.4 → 0.6.1
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/automations/automations-grammar-words.d.ts.map +1 -0
- package/dist/automations/automations.repository.interface.d.ts.map +1 -0
- package/dist/automations/automations.runner.interface.d.ts.map +1 -0
- package/dist/automations/automations.types.d.ts.map +1 -0
- package/dist/common/clock.d.ts.map +1 -0
- package/dist/common/email.provider.d.ts +32 -0
- package/dist/common/email.provider.d.ts.map +1 -0
- package/dist/common/email.provider.js +4 -0
- package/dist/{hooks.d.ts → common/hooks.d.ts} +2 -2
- package/dist/common/hooks.d.ts.map +1 -0
- package/dist/common/id-generator.d.ts.map +1 -0
- package/dist/common/storage.d.ts.map +1 -0
- package/dist/{content-scan.repository.d.ts → content/content-scan.repository.d.ts} +1 -1
- package/dist/content/content-scan.repository.d.ts.map +1 -0
- package/dist/{content.repository.d.ts → content/content.repository.d.ts} +13 -4
- package/dist/content/content.repository.d.ts.map +1 -0
- package/dist/content/demo-data.repository.d.ts.map +1 -0
- package/dist/content/idempotency.repository.d.ts.map +1 -0
- package/dist/content/richtext/richtext-render.d.ts +12 -0
- package/dist/content/richtext/richtext-render.d.ts.map +1 -0
- package/dist/{richtext-render.js → content/richtext/richtext-render.js} +7 -14
- package/dist/content/richtext/richtext.d.ts.map +1 -0
- package/dist/{seed.repository.d.ts → content/seed.repository.d.ts} +1 -1
- package/dist/content/seed.repository.d.ts.map +1 -0
- package/dist/content/slug-utils.d.ts.map +1 -0
- package/dist/{dashboard-layout.d.ts → dashboard-layout/dashboard-layout.d.ts} +1 -1
- package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -0
- package/dist/dashboard-layout/dashboard-layout.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/dashboard-permissions.d.ts.map +1 -0
- package/dist/dashboard-layout/dashboard-scopes.d.ts.map +1 -0
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts +19 -0
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/kanban/kanban.d.ts +75 -0
- package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -0
- package/dist/dashboard-layout/kanban/kanban.js +82 -0
- package/dist/dashboard-layout/layout-permissions.d.ts.map +1 -0
- package/dist/{seed-layout.d.ts → dashboard-layout/seed-layout.d.ts} +77 -3
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -0
- package/dist/{seed-layout.js → dashboard-layout/seed-layout.js} +101 -8
- package/dist/{seed-layout.repository.d.ts → dashboard-layout/seed-layout.repository.d.ts} +5 -1
- package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/view-authorization.d.ts +18 -0
- package/dist/dashboard-layout/view-authorization.d.ts.map +1 -0
- package/dist/dashboard-layout/view-authorization.js +28 -0
- package/dist/engine/ddl.d.ts +173 -0
- package/dist/engine/ddl.d.ts.map +1 -0
- package/dist/engine/ddl.js +454 -0
- package/dist/engine/define-seed.d.ts.map +1 -0
- package/dist/engine/engine.d.ts +4 -0
- package/dist/engine/engine.d.ts.map +1 -0
- package/dist/engine/engine.js +4 -0
- package/dist/engine/policies.d.ts.map +1 -0
- package/dist/engine/query.d.ts +23 -0
- package/dist/engine/query.d.ts.map +1 -0
- package/dist/engine/query.js +217 -0
- package/dist/engine/relations.d.ts.map +1 -0
- package/dist/{schema-mutator.d.ts → engine/schema-mutator.d.ts} +4 -0
- package/dist/engine/schema-mutator.d.ts.map +1 -0
- package/dist/engine/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/seed-ddl.d.ts.map +1 -0
- package/dist/engine/seed-registry.d.ts.map +1 -0
- package/dist/{seed-registry.js → engine/seed-registry.js} +1 -1
- package/dist/engine/seed-types-generator.d.ts +10 -0
- package/dist/engine/seed-types-generator.d.ts.map +1 -0
- package/dist/engine/seed-types-generator.js +86 -0
- package/dist/engine/seed-validation.d.ts.map +1 -0
- package/dist/{seed-validation.js → engine/seed-validation.js} +14 -1
- package/dist/engine/seeds.d.ts.map +1 -0
- package/dist/engine/serialize.d.ts +20 -0
- package/dist/engine/serialize.d.ts.map +1 -0
- package/dist/engine/serialize.js +160 -0
- package/dist/engine/types.d.ts +278 -0
- package/dist/engine/types.d.ts.map +1 -0
- package/dist/engine/validation/cache.d.ts +12 -0
- package/dist/engine/validation/cache.d.ts.map +1 -0
- package/dist/engine/validation/cache.js +136 -0
- package/dist/engine/validation/file-branch.d.ts +48 -0
- package/dist/engine/validation/file-branch.d.ts.map +1 -0
- package/dist/engine/validation/file-branch.js +133 -0
- package/dist/engine/validation/index.d.ts +115 -0
- package/dist/engine/validation/index.d.ts.map +1 -0
- package/dist/engine/validation/index.js +336 -0
- package/dist/engine/validation/primitives.d.ts +34 -0
- package/dist/engine/validation/primitives.d.ts.map +1 -0
- package/dist/engine/validation/primitives.js +47 -0
- package/dist/engine/validation/richtext-sanitizer.d.ts +30 -0
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -0
- package/dist/engine/validation/richtext-sanitizer.js +203 -0
- package/dist/engine/validation/schema-builders.d.ts +12 -0
- package/dist/engine/validation/schema-builders.d.ts.map +1 -0
- package/dist/engine/validation/schema-builders.js +413 -0
- package/dist/index.d.ts +48 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -44
- package/dist/media/file-types.d.ts +13 -0
- package/dist/media/file-types.d.ts.map +1 -0
- package/dist/media/media.repository.d.ts.map +1 -0
- package/dist/queue/queue.interface.d.ts +39 -0
- package/dist/queue/queue.interface.d.ts.map +1 -0
- package/dist/queue/queue.stub.d.ts +6 -0
- package/dist/queue/queue.stub.d.ts.map +1 -0
- package/dist/queue/queue.stub.js +8 -0
- package/dist/queue/scheduler.interface.d.ts.map +1 -0
- package/dist/queue/scheduler.interface.js +3 -0
- package/dist/queue/scheduler.stub.d.ts.map +1 -0
- package/dist/queue.interface.d.ts +4 -4
- package/dist/queue.interface.d.ts.map +1 -1
- package/dist/queue.stub.d.ts.map +1 -1
- package/dist/rate-limit/rate-limiter.d.ts +5 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
- package/dist/search/search.repository.d.ts +1 -1
- package/dist/search/search.repository.d.ts.map +1 -1
- package/dist/seed-types-generator.d.ts +1 -1
- package/dist/seed-types-generator.d.ts.map +1 -1
- package/dist/settings/site-settings.repository.d.ts.map +1 -0
- package/dist/settings/site-settings.repository.js +3 -0
- package/dist/webhooks/webhook-crypto.d.ts +8 -0
- package/dist/webhooks/webhook-crypto.d.ts.map +1 -0
- package/dist/webhooks/webhook-crypto.js +37 -0
- package/dist/webhooks/webhook-validation.d.ts.map +1 -0
- package/dist/widget/widget.repository.d.ts +1 -1
- package/dist/widget/widget.repository.d.ts.map +1 -1
- package/package.json +4 -3
- package/dist/automations-grammar-words.d.ts.map +0 -1
- package/dist/automations.repository.interface.d.ts.map +0 -1
- package/dist/automations.runner.interface.d.ts.map +0 -1
- package/dist/automations.runner.stub.d.ts +0 -5
- package/dist/automations.runner.stub.d.ts.map +0 -1
- package/dist/automations.runner.stub.js +0 -7
- package/dist/automations.types.d.ts.map +0 -1
- package/dist/clock.d.ts.map +0 -1
- package/dist/content-scan.repository.d.ts.map +0 -1
- package/dist/content.repository.d.ts.map +0 -1
- package/dist/dashboard-layout.d.ts.map +0 -1
- package/dist/dashboard-layout.repository.d.ts.map +0 -1
- package/dist/dashboard-permissions.d.ts.map +0 -1
- package/dist/dashboard-scopes.d.ts.map +0 -1
- package/dist/define-seed.d.ts.map +0 -1
- package/dist/demo-data.repository.d.ts.map +0 -1
- package/dist/engine.d.ts +0 -144
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -723
- package/dist/file-types.d.ts +0 -13
- package/dist/file-types.d.ts.map +0 -1
- package/dist/hooks.d.ts.map +0 -1
- package/dist/id-generator.d.ts.map +0 -1
- package/dist/idempotency.repository.d.ts.map +0 -1
- package/dist/layout-permissions.d.ts.map +0 -1
- package/dist/media.repository.d.ts.map +0 -1
- package/dist/policies.d.ts.map +0 -1
- package/dist/relations.d.ts.map +0 -1
- package/dist/richtext-render.d.ts +0 -11
- package/dist/richtext-render.d.ts.map +0 -1
- package/dist/richtext.d.ts.map +0 -1
- package/dist/scheduler.interface.d.ts.map +0 -1
- package/dist/scheduler.stub.d.ts.map +0 -1
- package/dist/schema-mutator.d.ts.map +0 -1
- package/dist/seed-ddl-destructive.d.ts.map +0 -1
- package/dist/seed-ddl.d.ts.map +0 -1
- package/dist/seed-layout.d.ts.map +0 -1
- package/dist/seed-layout.repository.d.ts.map +0 -1
- package/dist/seed-registry.d.ts.map +0 -1
- package/dist/seed-validation.d.ts.map +0 -1
- package/dist/seed.repository.d.ts.map +0 -1
- package/dist/seeds.d.ts.map +0 -1
- package/dist/site-settings.repository.d.ts.map +0 -1
- package/dist/slug-utils.d.ts.map +0 -1
- package/dist/storage.d.ts.map +0 -1
- package/dist/types.d.ts +0 -237
- package/dist/types.d.ts.map +0 -1
- package/dist/validation.d.ts +0 -50
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -659
- package/dist/webhook-validation.d.ts.map +0 -1
- /package/dist/{automations-grammar-words.d.ts → automations/automations-grammar-words.d.ts} +0 -0
- /package/dist/{automations-grammar-words.js → automations/automations-grammar-words.js} +0 -0
- /package/dist/{automations.repository.interface.d.ts → automations/automations.repository.interface.d.ts} +0 -0
- /package/dist/{automations.repository.interface.js → automations/automations.repository.interface.js} +0 -0
- /package/dist/{automations.runner.interface.d.ts → automations/automations.runner.interface.d.ts} +0 -0
- /package/dist/{automations.runner.interface.js → automations/automations.runner.interface.js} +0 -0
- /package/dist/{automations.types.d.ts → automations/automations.types.d.ts} +0 -0
- /package/dist/{automations.types.js → automations/automations.types.js} +0 -0
- /package/dist/{clock.d.ts → common/clock.d.ts} +0 -0
- /package/dist/{clock.js → common/clock.js} +0 -0
- /package/dist/{hooks.js → common/hooks.js} +0 -0
- /package/dist/{id-generator.d.ts → common/id-generator.d.ts} +0 -0
- /package/dist/{id-generator.js → common/id-generator.js} +0 -0
- /package/dist/{storage.d.ts → common/storage.d.ts} +0 -0
- /package/dist/{storage.js → common/storage.js} +0 -0
- /package/dist/{content-scan.repository.js → content/content-scan.repository.js} +0 -0
- /package/dist/{content.repository.js → content/content.repository.js} +0 -0
- /package/dist/{demo-data.repository.d.ts → content/demo-data.repository.d.ts} +0 -0
- /package/dist/{demo-data.repository.js → content/demo-data.repository.js} +0 -0
- /package/dist/{idempotency.repository.d.ts → content/idempotency.repository.d.ts} +0 -0
- /package/dist/{idempotency.repository.js → content/idempotency.repository.js} +0 -0
- /package/dist/{richtext.d.ts → content/richtext/richtext.d.ts} +0 -0
- /package/dist/{richtext.js → content/richtext/richtext.js} +0 -0
- /package/dist/{seed.repository.js → content/seed.repository.js} +0 -0
- /package/dist/{slug-utils.d.ts → content/slug-utils.d.ts} +0 -0
- /package/dist/{slug-utils.js → content/slug-utils.js} +0 -0
- /package/dist/{dashboard-layout.js → dashboard-layout/dashboard-layout.js} +0 -0
- /package/dist/{dashboard-layout.repository.d.ts → dashboard-layout/dashboard-layout.repository.d.ts} +0 -0
- /package/dist/{dashboard-layout.repository.js → dashboard-layout/dashboard-layout.repository.js} +0 -0
- /package/dist/{dashboard-permissions.d.ts → dashboard-layout/dashboard-permissions.d.ts} +0 -0
- /package/dist/{dashboard-permissions.js → dashboard-layout/dashboard-permissions.js} +0 -0
- /package/dist/{dashboard-scopes.d.ts → dashboard-layout/dashboard-scopes.d.ts} +0 -0
- /package/dist/{dashboard-scopes.js → dashboard-layout/dashboard-scopes.js} +0 -0
- /package/dist/{media.repository.js → dashboard-layout/kanban/kanban-position.repository.js} +0 -0
- /package/dist/{layout-permissions.d.ts → dashboard-layout/layout-permissions.d.ts} +0 -0
- /package/dist/{layout-permissions.js → dashboard-layout/layout-permissions.js} +0 -0
- /package/dist/{seed-layout.repository.js → dashboard-layout/seed-layout.repository.js} +0 -0
- /package/dist/{define-seed.d.ts → engine/define-seed.d.ts} +0 -0
- /package/dist/{define-seed.js → engine/define-seed.js} +0 -0
- /package/dist/{policies.d.ts → engine/policies.d.ts} +0 -0
- /package/dist/{policies.js → engine/policies.js} +0 -0
- /package/dist/{relations.d.ts → engine/relations.d.ts} +0 -0
- /package/dist/{relations.js → engine/relations.js} +0 -0
- /package/dist/{schema-mutator.js → engine/schema-mutator.js} +0 -0
- /package/dist/{seed-ddl-destructive.d.ts → engine/seed-ddl-destructive.d.ts} +0 -0
- /package/dist/{seed-ddl-destructive.js → engine/seed-ddl-destructive.js} +0 -0
- /package/dist/{seed-ddl.d.ts → engine/seed-ddl.d.ts} +0 -0
- /package/dist/{seed-ddl.js → engine/seed-ddl.js} +0 -0
- /package/dist/{seed-registry.d.ts → engine/seed-registry.d.ts} +0 -0
- /package/dist/{seed-validation.d.ts → engine/seed-validation.d.ts} +0 -0
- /package/dist/{seeds.d.ts → engine/seeds.d.ts} +0 -0
- /package/dist/{seeds.js → engine/seeds.js} +0 -0
- /package/dist/{types.js → engine/types.js} +0 -0
- /package/dist/{file-types.js → media/file-types.js} +0 -0
- /package/dist/{media.repository.d.ts → media/media.repository.d.ts} +0 -0
- /package/dist/{scheduler.interface.js → media/media.repository.js} +0 -0
- /package/dist/{site-settings.repository.js → queue/queue.interface.js} +0 -0
- /package/dist/{scheduler.interface.d.ts → queue/scheduler.interface.d.ts} +0 -0
- /package/dist/{scheduler.stub.d.ts → queue/scheduler.stub.d.ts} +0 -0
- /package/dist/{scheduler.stub.js → queue/scheduler.stub.js} +0 -0
- /package/dist/{site-settings.repository.d.ts → settings/site-settings.repository.d.ts} +0 -0
- /package/dist/{webhook-validation.d.ts → webhooks/webhook-validation.d.ts} +0 -0
- /package/dist/{webhook-validation.js → webhooks/webhook-validation.js} +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Types
|
|
3
|
+
* Core schema types for the Botanical Engine: `Seed` (content type definition),
|
|
4
|
+
* `Branch` (field definition), and the query types consumed by `buildSelectQuery`.
|
|
5
|
+
* Pure data shapes — no runtime logic lives here.
|
|
6
|
+
*/
|
|
7
|
+
import type { FileAccept } from '../media/file-types.js';
|
|
8
|
+
import type { DashboardView } from '../dashboard-layout/view-authorization.js';
|
|
9
|
+
/** All supported field value types for a Branch. */
|
|
10
|
+
export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation' | 'repeater';
|
|
11
|
+
/** Specialized configuration for a branch of type 'number'. */
|
|
12
|
+
export interface NumberFieldOptions {
|
|
13
|
+
/** Visual display style. Default: 'decimal'. */
|
|
14
|
+
format?: 'decimal' | 'currency' | 'percentage' | 'compact';
|
|
15
|
+
/** ISO 4217 currency code (required when format === 'currency'). E.g. 'EUR', 'USD'. */
|
|
16
|
+
currency?: string;
|
|
17
|
+
/** Custom text prepended to the formatted value. */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
/** Custom unit appended to the formatted value (e.g. 'kg', 'm²'). */
|
|
20
|
+
suffix?: string;
|
|
21
|
+
/** Explicit number of decimal digits to display and enforce. */
|
|
22
|
+
decimals?: number;
|
|
23
|
+
/** Enable/disable thousands separator grouping. Default: true. */
|
|
24
|
+
grouping?: boolean;
|
|
25
|
+
/** Alternative input mechanism for the entry editor. Default: 'input'. */
|
|
26
|
+
control?: 'input' | 'slider' | 'rating' | 'stepper';
|
|
27
|
+
/** Minimum allowed value. */
|
|
28
|
+
min?: number;
|
|
29
|
+
/** Maximum allowed value. */
|
|
30
|
+
max?: number;
|
|
31
|
+
/** Value increment (e.g. 1 for strict integers, 0.5 for half-steps). */
|
|
32
|
+
step?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Specialized configuration for a branch of type 'file'. */
|
|
35
|
+
export interface FileFieldOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Semantic type of file accepted.
|
|
38
|
+
* - 'image': renderable images with preview
|
|
39
|
+
* - 'document': PDF/Office/text
|
|
40
|
+
* - 'any': any file (default — UI shows a generic icon, no image render attempt)
|
|
41
|
+
* Default: 'any'.
|
|
42
|
+
*/
|
|
43
|
+
accept?: FileAccept;
|
|
44
|
+
/**
|
|
45
|
+
* Maximum size of a single file, in bytes.
|
|
46
|
+
* NOTE: the backend /upload endpoint enforces the global MAX_FILE_SIZE_BYTES (5MB) —
|
|
47
|
+
* this field is informational for the UI only; it is not enforced on upload.
|
|
48
|
+
* Default: 5_242_880.
|
|
49
|
+
*/
|
|
50
|
+
maxSize?: number;
|
|
51
|
+
}
|
|
52
|
+
/** Branch: definition of a single field. `alias` is the SQL column name. */
|
|
53
|
+
export interface Branch {
|
|
54
|
+
/**
|
|
55
|
+
* Stable logical id of this branch, e.g. 'br_01', 'br_title'.
|
|
56
|
+
* Format: ^br_[A-Za-z0-9]+$ — enforced by SeedRegistry at boot (sprint 04-pre).
|
|
57
|
+
*
|
|
58
|
+
* Used by every persistence layer that needs a reference that survives alias renames
|
|
59
|
+
* (FTS triggers, draft indexing, layout JSON, automations). NEVER use alias for that purpose.
|
|
60
|
+
*
|
|
61
|
+
* The Botanical Engine still emits alias as the SQL column name; id is a logical handle.
|
|
62
|
+
*/
|
|
63
|
+
id: string;
|
|
64
|
+
/** Human-readable alias, used in the API payload and as the SQL column name in the dedicated table. */
|
|
65
|
+
alias: string;
|
|
66
|
+
/** UI display label. */
|
|
67
|
+
label: string;
|
|
68
|
+
/** Optional help text shown as a tooltip next to the label in the form. UI-only, ignored by the engine. */
|
|
69
|
+
hint?: string;
|
|
70
|
+
/** Value type. */
|
|
71
|
+
type: BranchType;
|
|
72
|
+
/**
|
|
73
|
+
* Optional semantic variant of the field for UI/validation purposes.
|
|
74
|
+
* `asset-list` on a multiple `file` branch enables gallery management.
|
|
75
|
+
*/
|
|
76
|
+
format?: 'plain' | 'markdown' | 'html' | 'date' | 'datetime' | 'asset-list';
|
|
77
|
+
/**
|
|
78
|
+
* Optional cardinality for media fields:
|
|
79
|
+
* - false/undefined: single asset (string URL)
|
|
80
|
+
* - true: asset list (string[] URL)
|
|
81
|
+
*/
|
|
82
|
+
multiple?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Predefined vocabulary for tag/select/multiselect fields.
|
|
85
|
+
* Static list defined in the Seed (not persisted to the DB).
|
|
86
|
+
*/
|
|
87
|
+
options?: string[];
|
|
88
|
+
/** Required on create — generates NOT NULL in generateCreateTable. */
|
|
89
|
+
requiredOnCreate?: boolean;
|
|
90
|
+
/** Required on update. */
|
|
91
|
+
requiredOnUpdate?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Access and handling policy for the field.
|
|
94
|
+
* All values are optional — `resolvePolicies(branch)` supplies the defaults.
|
|
95
|
+
*/
|
|
96
|
+
policies?: {
|
|
97
|
+
/** How the value is stored. Default: 'plain'. */
|
|
98
|
+
privacy?: 'plain' | 'hash' | 'encrypt';
|
|
99
|
+
/** How the value is returned in API responses. Default: 'full'. */
|
|
100
|
+
visibility?: 'full' | 'masked' | 'hidden';
|
|
101
|
+
/** Whether the field is included in full-text search queries. Default: true. */
|
|
102
|
+
search?: boolean;
|
|
103
|
+
/** Whether the field is available as a filter column in the dashboard. Default: true. */
|
|
104
|
+
filter?: boolean;
|
|
105
|
+
/** Whether the field is available as a sort column in the dashboard. Default: true. */
|
|
106
|
+
sort?: boolean;
|
|
107
|
+
/** Whether the field is included in Public API responses. Default: true. */
|
|
108
|
+
public?: boolean;
|
|
109
|
+
};
|
|
110
|
+
/** Advanced options for number fields. Ignored if type !== 'number'. */
|
|
111
|
+
numberOptions?: NumberFieldOptions;
|
|
112
|
+
/** Advanced options for file fields. Ignored if type !== 'file'. */
|
|
113
|
+
fileOptions?: FileFieldOptions;
|
|
114
|
+
/**
|
|
115
|
+
* Slug of the referenced Seed (without the `content_` prefix).
|
|
116
|
+
* REQUIRED when `type === 'relation'`. Ignored otherwise.
|
|
117
|
+
* Example: 'team' → references table `content_team(id)`.
|
|
118
|
+
*/
|
|
119
|
+
targetSeed?: string;
|
|
120
|
+
/**
|
|
121
|
+
* SQLite ON DELETE rule applied to the foreign-key constraint.
|
|
122
|
+
* Defaults to 'SET NULL' when `type === 'relation'` and no value is provided.
|
|
123
|
+
* - CASCADE : delete dependent rows when the parent is deleted.
|
|
124
|
+
* - SET NULL : null out the column when the parent is deleted (default).
|
|
125
|
+
* - RESTRICT : block parent deletion while dependent rows exist.
|
|
126
|
+
*
|
|
127
|
+
* NOTE: When `multiple: true` (introduced in Sprint 5 for many-to-many), this
|
|
128
|
+
* rule applies to the FK from the junction table to the target table.
|
|
129
|
+
*/
|
|
130
|
+
onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
|
|
131
|
+
/**
|
|
132
|
+
* Sub-schema for `type === 'repeater'`. Each item of the repeater's array
|
|
133
|
+
* value is a record keyed by sub-branch alias, validated against this list.
|
|
134
|
+
* Sub-branches are restricted to leaf/scalar types (no nested `repeater`,
|
|
135
|
+
* `relation`, or `file`) — enforced by validation.ts and seed-validation.ts.
|
|
136
|
+
* Ignored for any other branch type.
|
|
137
|
+
*/
|
|
138
|
+
fields?: Branch[];
|
|
139
|
+
/**
|
|
140
|
+
* Minimum number of items a `repeater` value must contain when a value is
|
|
141
|
+
* provided. Repeater-only — ignored for every other branch type.
|
|
142
|
+
*
|
|
143
|
+
* NOTE: this constrains array *length when the field is present*. It does NOT by
|
|
144
|
+
* itself make the field mandatory — an absent/null payload is still allowed unless
|
|
145
|
+
* `requiredOnCreate` / `requiredOnUpdate` is also set. To model "exactly one
|
|
146
|
+
* required object", combine `minItems: 1, maxItems: 1, requiredOnCreate: true`.
|
|
147
|
+
* Must be a non-negative integer and `<= maxItems` when both are set
|
|
148
|
+
* (enforced at boot by seed-validation.ts).
|
|
149
|
+
*/
|
|
150
|
+
minItems?: number;
|
|
151
|
+
/**
|
|
152
|
+
* Maximum number of items a `repeater` value may contain. Repeater-only — ignored
|
|
153
|
+
* for every other branch type. `maxItems: 1` models a single "object" column.
|
|
154
|
+
* Must be a non-negative integer and `>= minItems` when both are set.
|
|
155
|
+
*/
|
|
156
|
+
maxItems?: number;
|
|
157
|
+
}
|
|
158
|
+
/** Dashboard-specific config embedded in a Seed. All fields optional — defaults applied by the dashboard. */
|
|
159
|
+
export interface DashboardSeedConfig {
|
|
160
|
+
/** Lucide icon name (string, resolved to component client-side). Default: 'Folder'. */
|
|
161
|
+
icon?: string;
|
|
162
|
+
/** Sidebar group label. Ungrouped seeds share a single 'Contents' section. */
|
|
163
|
+
group?: string;
|
|
164
|
+
/** Sort order within the group. Lower = higher. Default: 99. */
|
|
165
|
+
order?: number;
|
|
166
|
+
/** Hide from sidebar navigation. Default: false. */
|
|
167
|
+
hidden?: boolean;
|
|
168
|
+
/** Tooltip description shown in the sidebar. */
|
|
169
|
+
description?: string;
|
|
170
|
+
/** UI feature toggles. All default to true unless specified. */
|
|
171
|
+
features?: {
|
|
172
|
+
search?: boolean;
|
|
173
|
+
filter?: boolean;
|
|
174
|
+
export?: boolean;
|
|
175
|
+
bulkDelete?: boolean;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Views authorized for this seed in the content manager. When omitted,
|
|
179
|
+
* the dashboard falls back to DEFAULT_AUTHORIZED_VIEWS. 'table' is always
|
|
180
|
+
* guaranteed at read time by resolveAuthorizedViews (universal fallback).
|
|
181
|
+
*/
|
|
182
|
+
views?: DashboardView[];
|
|
183
|
+
}
|
|
184
|
+
/** Seed: schema definition of a content type. */
|
|
185
|
+
export interface Seed {
|
|
186
|
+
/** Identifying slug — also the table name: `content_{slug}`. */
|
|
187
|
+
slug: string;
|
|
188
|
+
/** Singular UI label. */
|
|
189
|
+
label: string;
|
|
190
|
+
/** Plural UI label. Falls back to `label` when absent. */
|
|
191
|
+
labelPlural?: string;
|
|
192
|
+
/** Enable reads from the Public API (`GET /api/v1/public/:seed`). Default: false. */
|
|
193
|
+
allowPublicRead?: boolean;
|
|
194
|
+
/** Enable creation from the Public API (`POST /api/v1/public/:seed/add`). Default: false. */
|
|
195
|
+
allowPublicPost?: boolean;
|
|
196
|
+
/** Enable edits from the Public API (`PUT /api/v1/public/:seed/edit/:id`). Default: false. */
|
|
197
|
+
allowPublicEdit?: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Enables the "pending draft" feature for this seed.
|
|
200
|
+
* When true, generates the `content_{slug}_drafts` table and enables the `/draft` endpoints.
|
|
201
|
+
* Default: false.
|
|
202
|
+
*/
|
|
203
|
+
allowDrafts?: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Alias of the branch used as the entry's human-readable name (e.g. "title", "name", "author").
|
|
206
|
+
* Required — UIs use it for display without heuristics.
|
|
207
|
+
*/
|
|
208
|
+
displayNameAlias: string;
|
|
209
|
+
/** List of fields (Branch). */
|
|
210
|
+
branches: Branch[];
|
|
211
|
+
/** Optional dashboard-specific UI config. Ignored by the Botanical Engine. */
|
|
212
|
+
dashboard?: DashboardSeedConfig;
|
|
213
|
+
/** Custom editor form layout. Absent when no override is stored.
|
|
214
|
+
* Populated server-side by GET /api/schema. Ignored by the Botanical Engine.
|
|
215
|
+
* Type matches FormLayout from seed-layout.ts — kept as unknown here to avoid circular imports. */
|
|
216
|
+
layout?: unknown;
|
|
217
|
+
}
|
|
218
|
+
/** Comparison/matching operator applied by a filter condition. */
|
|
219
|
+
export type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty' | 'in' | 'not_in' | 'has_tag' | 'has_any_tag' | 'has_all_tags';
|
|
220
|
+
/** Declared value type of a filtered column, used to normalize/coerce filter values before binding. */
|
|
221
|
+
export type FilterType = 'text' | 'number' | 'date' | 'boolean' | 'tags' | 'select' | 'system' | 'json';
|
|
222
|
+
/** A single operator + value pair applied to a FilterGroup's column. */
|
|
223
|
+
export interface FilterCondition {
|
|
224
|
+
/** Operator to apply. */
|
|
225
|
+
op: FilterOperator;
|
|
226
|
+
/** Comparison value(s). Arrays are only meaningful for `in`/`not_in`/`has_any_tag`/`has_all_tags`. */
|
|
227
|
+
value: string | number | boolean | null | string[] | number[];
|
|
228
|
+
}
|
|
229
|
+
export interface FilterGroup {
|
|
230
|
+
/** Column name: system column (id/slug/status/created_at/updated_at) or branch alias. */
|
|
231
|
+
column: string;
|
|
232
|
+
/** Declared type of the column, used to normalize condition values. */
|
|
233
|
+
type: FilterType;
|
|
234
|
+
/** Conditions applied to this column, ANDed together. */
|
|
235
|
+
conditions: FilterCondition[];
|
|
236
|
+
}
|
|
237
|
+
export interface SelectOptions {
|
|
238
|
+
/** Filter groups. Joined by `filterLogic` (default AND); conditions within a group are always ANDed. */
|
|
239
|
+
filters?: FilterGroup[];
|
|
240
|
+
/** How top-level `filters` groups are combined. Defaults to 'AND'. */
|
|
241
|
+
filterLogic?: 'AND' | 'OR';
|
|
242
|
+
/** Sort column and direction. Ignored when `kanbanOrder` is set. */
|
|
243
|
+
orderBy?: {
|
|
244
|
+
column: string;
|
|
245
|
+
dir: 'ASC' | 'DESC';
|
|
246
|
+
};
|
|
247
|
+
/** LIMIT/OFFSET pagination. */
|
|
248
|
+
pagination?: {
|
|
249
|
+
limit: number;
|
|
250
|
+
offset: number;
|
|
251
|
+
};
|
|
252
|
+
/** Filters by status. null = no status filter. */
|
|
253
|
+
status?: string | null;
|
|
254
|
+
/** Full-text search — uses FTS5 if the seed has indexable richtext/text branches. */
|
|
255
|
+
search?: string;
|
|
256
|
+
/** Column projection. Empty = SELECT *. */
|
|
257
|
+
fields?: string[];
|
|
258
|
+
/** When set, LEFT JOIN kanban_positions and order by fractional index (KB-S04c/S05).
|
|
259
|
+
* Mutually exclusive with `orderBy`; if both present, `kanbanOrder` wins. */
|
|
260
|
+
kanbanOrder?: {
|
|
261
|
+
seedSlug: string;
|
|
262
|
+
axisBranchId: string;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* When true, generates a COUNT(*) query instead of fetching rows.
|
|
266
|
+
* This omits projections, sorting/ordering clauses, and pagination limits/offsets,
|
|
267
|
+
* while keeping the join/where clauses intact for accurate counts.
|
|
268
|
+
*/
|
|
269
|
+
isCount?: boolean;
|
|
270
|
+
}
|
|
271
|
+
/** SQL string paired with its ordered parameter bindings, ready for a D1 `.bind(...)` call. */
|
|
272
|
+
export interface ParameterizedQuery {
|
|
273
|
+
/** Parameterized SQL statement, using `?` placeholders. */
|
|
274
|
+
sql: string;
|
|
275
|
+
/** Values bound to the `?` placeholders, in order. */
|
|
276
|
+
bindings: (string | number | boolean | null)[];
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Seed } from '../types.js';
|
|
3
|
+
import type { ResolvedOptions } from './index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Compiles or retrieves from cache the full Zod validation schema for a seed structure.
|
|
6
|
+
*
|
|
7
|
+
* @param seed - The seed definition.
|
|
8
|
+
* @param options - The resolved validation options.
|
|
9
|
+
* @returns The compiled strict Zod object schema.
|
|
10
|
+
*/
|
|
11
|
+
export declare function compileSeedSchema(seed: Seed, options: ResolvedOptions): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
12
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAsB,IAAI,EAAwC,MAAM,aAAa,CAAA;AAEjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAgIjD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAsCjH"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { schemaForBranch } from './schema-builders.js';
|
|
5
|
+
/** Max entries kept per cache map before evicting the least recently used. */
|
|
6
|
+
const SEED_SCHEMA_CACHE_MAX_SIZE = 150;
|
|
7
|
+
/** Cache storing compiled Zod schemas for relation-free seeds. */
|
|
8
|
+
const seedSchemaCache = new Map();
|
|
9
|
+
/**
|
|
10
|
+
* Per-`idGenerator`-instance caches for seeds with relation branches.
|
|
11
|
+
* Partitioning by instance identity keeps schemas compiled with different
|
|
12
|
+
* generators (and thus different `isValid()` semantics) from colliding,
|
|
13
|
+
* without needing a stable identifier on {@link IIdGenerator}.
|
|
14
|
+
*/
|
|
15
|
+
const relationSchemaCacheByGenerator = new WeakMap();
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves a cache entry and marks it as most recently used.
|
|
18
|
+
*
|
|
19
|
+
* @param cache - The cache map to read from.
|
|
20
|
+
* @param key - The cache key.
|
|
21
|
+
* @returns The cached schema, or undefined if absent.
|
|
22
|
+
*/
|
|
23
|
+
function getCachedSchema(cache, key) {
|
|
24
|
+
const cached = cache.get(key);
|
|
25
|
+
if (cached) {
|
|
26
|
+
cache.delete(key);
|
|
27
|
+
cache.set(key, cached);
|
|
28
|
+
}
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stores a cache entry, evicting the least recently used one if over capacity.
|
|
33
|
+
*
|
|
34
|
+
* @param cache - The cache map to write to.
|
|
35
|
+
* @param key - The cache key.
|
|
36
|
+
* @param value - The compiled schema to cache.
|
|
37
|
+
*/
|
|
38
|
+
function setCachedSchema(cache, key, value) {
|
|
39
|
+
if (cache.size >= SEED_SCHEMA_CACHE_MAX_SIZE) {
|
|
40
|
+
const oldestKey = cache.keys().next().value;
|
|
41
|
+
if (oldestKey !== undefined)
|
|
42
|
+
cache.delete(oldestKey);
|
|
43
|
+
}
|
|
44
|
+
cache.set(key, value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Recursively generates a typesafe fingerprint object for a branch and its sub-branches.
|
|
48
|
+
*
|
|
49
|
+
* @param branch - The branch definition.
|
|
50
|
+
* @returns The structured branch fingerprint object.
|
|
51
|
+
*/
|
|
52
|
+
function buildBranchFingerprint(branch) {
|
|
53
|
+
return {
|
|
54
|
+
a: branch.alias,
|
|
55
|
+
t: branch.type,
|
|
56
|
+
f: branch.format ?? null,
|
|
57
|
+
m: branch.multiple === true,
|
|
58
|
+
rc: branch.requiredOnCreate === true,
|
|
59
|
+
ru: branch.requiredOnUpdate === true,
|
|
60
|
+
n: branch.numberOptions ?? null,
|
|
61
|
+
fi: branch.fileOptions ?? null,
|
|
62
|
+
mi: branch.minItems ?? null,
|
|
63
|
+
ma: branch.maxItems ?? null,
|
|
64
|
+
sub: branch.fields?.map(buildBranchFingerprint) ?? null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Generates a unique JSON fingerprint string representing a seed's structure for caching.
|
|
69
|
+
*
|
|
70
|
+
* @param seed - The seed definition.
|
|
71
|
+
* @returns The fingerprint string.
|
|
72
|
+
*/
|
|
73
|
+
function buildSeedFingerprint(seed) {
|
|
74
|
+
const parts = seed.branches.map(buildBranchFingerprint);
|
|
75
|
+
return JSON.stringify({ s: seed.slug, b: parts });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds a cache key for a seed validation schema combining its fingerprint, operation, and options.
|
|
79
|
+
*
|
|
80
|
+
* @param seed - The seed definition.
|
|
81
|
+
* @param options - The resolved validation options.
|
|
82
|
+
* @returns The cache key string.
|
|
83
|
+
*/
|
|
84
|
+
function buildCacheKey(seed, options) {
|
|
85
|
+
return [
|
|
86
|
+
buildSeedFingerprint(seed),
|
|
87
|
+
options.operation,
|
|
88
|
+
options.allowNull ? '1' : '0',
|
|
89
|
+
options.enforceRequiredFields ? '1' : '0',
|
|
90
|
+
String(options.maxTextLength),
|
|
91
|
+
].join('|');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Compiles or retrieves from cache the full Zod validation schema for a seed structure.
|
|
95
|
+
*
|
|
96
|
+
* @param seed - The seed definition.
|
|
97
|
+
* @param options - The resolved validation options.
|
|
98
|
+
* @returns The compiled strict Zod object schema.
|
|
99
|
+
*/
|
|
100
|
+
export function compileSeedSchema(seed, options) {
|
|
101
|
+
// Seeds with relation branches capture the idGenerator by closure, so their
|
|
102
|
+
// schemas are cached per generator instance instead of in the shared cache
|
|
103
|
+
// (different generators, e.g. SystemIdGenerator vs a test double, have
|
|
104
|
+
// different isValid() semantics).
|
|
105
|
+
const hasRelation = seed.branches.some((b) => b.type === 'relation');
|
|
106
|
+
const key = buildCacheKey(seed, options);
|
|
107
|
+
let cache;
|
|
108
|
+
if (hasRelation) {
|
|
109
|
+
if (options.idGenerator) {
|
|
110
|
+
cache = relationSchemaCacheByGenerator.get(options.idGenerator);
|
|
111
|
+
if (!cache) {
|
|
112
|
+
cache = new Map();
|
|
113
|
+
relationSchemaCacheByGenerator.set(options.idGenerator, cache);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
cache = seedSchemaCache;
|
|
119
|
+
}
|
|
120
|
+
if (cache) {
|
|
121
|
+
const cached = getCachedSchema(cache, key);
|
|
122
|
+
if (cached)
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
|
|
126
|
+
const shape = {};
|
|
127
|
+
for (const branch of seed.branches) {
|
|
128
|
+
const branchSchema = schemaForBranch(branch, options);
|
|
129
|
+
const isRequired = branch[requiredFlag] && options.enforceRequiredFields;
|
|
130
|
+
shape[branch.alias] = isRequired ? branchSchema : branchSchema.optional();
|
|
131
|
+
}
|
|
132
|
+
const compiled = z.object(shape).strict();
|
|
133
|
+
if (cache)
|
|
134
|
+
setCachedSchema(cache, key, compiled);
|
|
135
|
+
return compiled;
|
|
136
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Branch } from '../types.js';
|
|
2
|
+
import type { FileAccept } from '../../media/file-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses a candidate value into a valid HTTP or HTTPS URL string, returning null if invalid.
|
|
5
|
+
*
|
|
6
|
+
* @param input - The value to validate.
|
|
7
|
+
* @returns The cleaned URL string, or null if invalid.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseHttpUrl(input: unknown): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the file validation options for a branch, applying default values.
|
|
12
|
+
*
|
|
13
|
+
* @param branch - The file branch containing option overrides.
|
|
14
|
+
* @returns An object with resolved file acceptance rules and maximum size limit.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveFileOptions(branch: Branch): {
|
|
17
|
+
accept: FileAccept;
|
|
18
|
+
maxSize: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a branch acts as an asset list (file type with multiple upload allowed or asset-list format).
|
|
22
|
+
*
|
|
23
|
+
* @param branch - The branch to check.
|
|
24
|
+
* @returns True if it is an asset list, false otherwise.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isAssetListBranch(branch: Branch): boolean;
|
|
27
|
+
/** A file URL paired with its byte size, when known from payload metadata. */
|
|
28
|
+
export interface FileCandidate {
|
|
29
|
+
url: string;
|
|
30
|
+
size: number | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Extracts a URL and, when available, a byte size from a candidate value
|
|
34
|
+
* (a string URL, or an object with `url`/`size` fields, e.g. uploader output).
|
|
35
|
+
*
|
|
36
|
+
* @param candidate - The candidate value.
|
|
37
|
+
* @returns The extracted file candidate, or null if no valid URL was found.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractFileCandidate(candidate: unknown): FileCandidate | null;
|
|
40
|
+
/**
|
|
41
|
+
* Collects and deduplicates valid file candidates (url + optional size) from a raw
|
|
42
|
+
* input representing an asset list.
|
|
43
|
+
*
|
|
44
|
+
* @param raw - The raw input data (can be array, JSON string, or single item).
|
|
45
|
+
* @returns The list of unique file candidates, or null if any item is invalid or the list exceeds the cap.
|
|
46
|
+
*/
|
|
47
|
+
export declare function collectAssetListItems(raw: unknown): FileCandidate[] | null;
|
|
48
|
+
//# sourceMappingURL=file-branch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-branch.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/file-branch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAoB3D;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAW1D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAM1F;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AA6BD,8EAA8E;AAC9E,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAI7E;AAKD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,EAAE,GAAG,IAAI,CAiB1E"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { cleanString, isPlainObject } from './primitives.js';
|
|
4
|
+
/** Default maximum file size in bytes for file branches. */
|
|
5
|
+
const DEFAULT_FILE_MAX_SIZE = 5 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Safely parses a JSON string, returning the parsed value, or the original string if parsing fails.
|
|
8
|
+
*
|
|
9
|
+
* @param raw - The string to attempt parsing.
|
|
10
|
+
* @returns The parsed object/array, or the original string.
|
|
11
|
+
*/
|
|
12
|
+
function tryParseJson(raw) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Parses a candidate value into a valid HTTP or HTTPS URL string, returning null if invalid.
|
|
22
|
+
*
|
|
23
|
+
* @param input - The value to validate.
|
|
24
|
+
* @returns The cleaned URL string, or null if invalid.
|
|
25
|
+
*/
|
|
26
|
+
export function parseHttpUrl(input) {
|
|
27
|
+
if (typeof input !== 'string')
|
|
28
|
+
return null;
|
|
29
|
+
const cleaned = cleanString(input);
|
|
30
|
+
if (cleaned.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = new URL(cleaned);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') ? cleaned : null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the file validation options for a branch, applying default values.
|
|
43
|
+
*
|
|
44
|
+
* @param branch - The file branch containing option overrides.
|
|
45
|
+
* @returns An object with resolved file acceptance rules and maximum size limit.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveFileOptions(branch) {
|
|
48
|
+
const opts = branch.fileOptions;
|
|
49
|
+
return {
|
|
50
|
+
accept: opts?.accept ?? 'any',
|
|
51
|
+
maxSize: opts?.maxSize ?? DEFAULT_FILE_MAX_SIZE,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a branch acts as an asset list (file type with multiple upload allowed or asset-list format).
|
|
56
|
+
*
|
|
57
|
+
* @param branch - The branch to check.
|
|
58
|
+
* @returns True if it is an asset list, false otherwise.
|
|
59
|
+
*/
|
|
60
|
+
export function isAssetListBranch(branch) {
|
|
61
|
+
return branch.type === 'file' && (branch.multiple === true || branch.format === 'asset-list');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extracts a URL string from a candidate value (which can be a string URL or an object containing a `url` field).
|
|
65
|
+
*
|
|
66
|
+
* @param candidate - The candidate value.
|
|
67
|
+
* @returns The extracted URL string, or null if not found.
|
|
68
|
+
*/
|
|
69
|
+
function extractUrlFromCandidate(candidate) {
|
|
70
|
+
const direct = parseHttpUrl(candidate);
|
|
71
|
+
if (direct)
|
|
72
|
+
return direct;
|
|
73
|
+
if (isPlainObject(candidate)) {
|
|
74
|
+
return parseHttpUrl(candidate.url);
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Extracts a byte size from a candidate value's `size` field, if present and valid.
|
|
80
|
+
*
|
|
81
|
+
* @param candidate - The candidate value.
|
|
82
|
+
* @returns The size in bytes, or null if not available.
|
|
83
|
+
*/
|
|
84
|
+
function extractSizeFromCandidate(candidate) {
|
|
85
|
+
if (!isPlainObject(candidate))
|
|
86
|
+
return null;
|
|
87
|
+
const raw = candidate.size;
|
|
88
|
+
return typeof raw === 'number' && Number.isFinite(raw) && raw >= 0 ? raw : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extracts a URL and, when available, a byte size from a candidate value
|
|
92
|
+
* (a string URL, or an object with `url`/`size` fields, e.g. uploader output).
|
|
93
|
+
*
|
|
94
|
+
* @param candidate - The candidate value.
|
|
95
|
+
* @returns The extracted file candidate, or null if no valid URL was found.
|
|
96
|
+
*/
|
|
97
|
+
export function extractFileCandidate(candidate) {
|
|
98
|
+
const url = extractUrlFromCandidate(candidate);
|
|
99
|
+
if (!url)
|
|
100
|
+
return null;
|
|
101
|
+
return { url, size: extractSizeFromCandidate(candidate) };
|
|
102
|
+
}
|
|
103
|
+
/** Maximum number of items accepted in an asset list to prevent unbounded processing. */
|
|
104
|
+
const MAX_ASSET_LIST_ITEMS = 100;
|
|
105
|
+
/**
|
|
106
|
+
* Collects and deduplicates valid file candidates (url + optional size) from a raw
|
|
107
|
+
* input representing an asset list.
|
|
108
|
+
*
|
|
109
|
+
* @param raw - The raw input data (can be array, JSON string, or single item).
|
|
110
|
+
* @returns The list of unique file candidates, or null if any item is invalid or the list exceeds the cap.
|
|
111
|
+
*/
|
|
112
|
+
export function collectAssetListItems(raw) {
|
|
113
|
+
if (raw === null)
|
|
114
|
+
return null;
|
|
115
|
+
const source = typeof raw === 'string' ? tryParseJson(raw) : raw;
|
|
116
|
+
const items = Array.isArray(source) ? source : [source];
|
|
117
|
+
if (items.length > MAX_ASSET_LIST_ITEMS)
|
|
118
|
+
return null;
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const item of items) {
|
|
122
|
+
if (item === null || item === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const candidate = extractFileCandidate(item);
|
|
125
|
+
if (!candidate)
|
|
126
|
+
return null;
|
|
127
|
+
if (!seen.has(candidate.url)) {
|
|
128
|
+
seen.add(candidate.url);
|
|
129
|
+
out.push(candidate);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|