@beechcms/core 0.9.0 → 0.9.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/dashboard-layout/content-view.d.ts +4 -4
- package/package.json +2 -2
- package/dist/automations-grammar-words.d.ts +0 -2
- package/dist/automations-grammar-words.d.ts.map +0 -1
- package/dist/automations-grammar-words.js +0 -8
- package/dist/automations.repository.interface.d.ts +0 -19
- package/dist/automations.repository.interface.d.ts.map +0 -1
- package/dist/automations.repository.interface.js +0 -3
- package/dist/automations.runner.interface.d.ts +0 -10
- package/dist/automations.runner.interface.d.ts.map +0 -1
- package/dist/automations.runner.interface.js +0 -3
- 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 +0 -75
- package/dist/automations.types.d.ts.map +0 -1
- package/dist/automations.types.js +0 -3
- package/dist/clock.d.ts +0 -25
- package/dist/clock.d.ts.map +0 -1
- package/dist/clock.js +0 -11
- package/dist/content-scan.repository.d.ts +0 -9
- package/dist/content-scan.repository.d.ts.map +0 -1
- package/dist/content-scan.repository.js +0 -3
- package/dist/content.repository.d.ts +0 -247
- package/dist/content.repository.d.ts.map +0 -1
- package/dist/content.repository.js +0 -59
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts +0 -19
- package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts.map +0 -1
- package/dist/dashboard-layout/kanban/kanban-position.repository.js +0 -3
- package/dist/dashboard-layout.d.ts +0 -154
- package/dist/dashboard-layout.d.ts.map +0 -1
- package/dist/dashboard-layout.js +0 -186
- package/dist/dashboard-layout.repository.d.ts +0 -18
- package/dist/dashboard-layout.repository.d.ts.map +0 -1
- package/dist/dashboard-layout.repository.js +0 -3
- package/dist/dashboard-permissions.d.ts +0 -3
- package/dist/dashboard-permissions.d.ts.map +0 -1
- package/dist/dashboard-permissions.js +0 -11
- package/dist/dashboard-scopes.d.ts +0 -12
- package/dist/dashboard-scopes.d.ts.map +0 -1
- package/dist/dashboard-scopes.js +0 -30
- package/dist/define-seed.d.ts +0 -3
- package/dist/define-seed.d.ts.map +0 -1
- package/dist/define-seed.js +0 -5
- package/dist/demo-data.repository.d.ts +0 -5
- package/dist/demo-data.repository.d.ts.map +0 -1
- package/dist/demo-data.repository.js +0 -3
- package/dist/email.provider.d.ts +0 -32
- package/dist/email.provider.d.ts.map +0 -1
- package/dist/email.provider.js +0 -4
- package/dist/engine/ddl/seed-ddl-destructive.d.ts +0 -40
- package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +0 -1
- package/dist/engine/ddl/seed-ddl-destructive.js +0 -149
- package/dist/engine/ddl.d.ts +0 -193
- package/dist/engine/ddl.d.ts.map +0 -1
- package/dist/engine/ddl.js +0 -518
- package/dist/engine/define-seed.d.ts +0 -3
- package/dist/engine/define-seed.d.ts.map +0 -1
- package/dist/engine/define-seed.js +0 -5
- package/dist/engine/introspection.d.ts +0 -101
- package/dist/engine/introspection.d.ts.map +0 -1
- package/dist/engine/introspection.js +0 -127
- package/dist/engine/localization.d.ts +0 -127
- package/dist/engine/localization.d.ts.map +0 -1
- package/dist/engine/localization.js +0 -212
- package/dist/engine/policies.d.ts +0 -58
- package/dist/engine/policies.d.ts.map +0 -1
- package/dist/engine/policies.js +0 -168
- package/dist/engine/privacy.service.d.ts +0 -65
- package/dist/engine/privacy.service.d.ts.map +0 -1
- package/dist/engine/privacy.service.js +0 -114
- package/dist/engine/query.d.ts +0 -23
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js +0 -236
- package/dist/engine/relations.d.ts +0 -28
- package/dist/engine/relations.d.ts.map +0 -1
- package/dist/engine/relations.js +0 -25
- package/dist/engine/schema-fingerprint.d.ts +0 -73
- package/dist/engine/schema-fingerprint.d.ts.map +0 -1
- package/dist/engine/schema-fingerprint.js +0 -97
- package/dist/engine/schema-mutator.d.ts +0 -26
- package/dist/engine/schema-mutator.d.ts.map +0 -1
- package/dist/engine/schema-mutator.js +0 -3
- package/dist/engine/seed-ddl-destructive.d.ts +0 -40
- package/dist/engine/seed-ddl-destructive.d.ts.map +0 -1
- package/dist/engine/seed-ddl-destructive.js +0 -149
- package/dist/engine/seed-ddl.d.ts +0 -40
- package/dist/engine/seed-ddl.d.ts.map +0 -1
- package/dist/engine/seed-ddl.js +0 -100
- package/dist/engine/seed-registry.d.ts +0 -71
- package/dist/engine/seed-registry.d.ts.map +0 -1
- package/dist/engine/seed-registry.js +0 -167
- package/dist/engine/seed-types-generator.d.ts +0 -10
- package/dist/engine/seed-types-generator.d.ts.map +0 -1
- package/dist/engine/seed-types-generator.js +0 -89
- package/dist/engine/seed-validation.d.ts +0 -30
- package/dist/engine/seed-validation.d.ts.map +0 -1
- package/dist/engine/seed-validation.js +0 -319
- package/dist/engine/seeds.d.ts +0 -20
- package/dist/engine/seeds.d.ts.map +0 -1
- package/dist/engine/seeds.js +0 -21
- package/dist/engine/serialize.d.ts +0 -20
- package/dist/engine/serialize.d.ts.map +0 -1
- package/dist/engine/serialize.js +0 -160
- package/dist/engine.d.ts +0 -144
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -741
- package/dist/file-types.d.ts +0 -13
- package/dist/file-types.d.ts.map +0 -1
- package/dist/file-types.js +0 -85
- package/dist/hooks.d.ts +0 -29
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -3
- package/dist/id-generator.d.ts +0 -26
- package/dist/id-generator.d.ts.map +0 -1
- package/dist/id-generator.js +0 -12
- package/dist/idempotency.repository.d.ts +0 -13
- package/dist/idempotency.repository.d.ts.map +0 -1
- package/dist/idempotency.repository.js +0 -3
- package/dist/kanban-position.repository.d.ts +0 -19
- package/dist/kanban-position.repository.d.ts.map +0 -1
- package/dist/kanban-position.repository.js +0 -3
- package/dist/kanban.d.ts +0 -75
- package/dist/kanban.d.ts.map +0 -1
- package/dist/kanban.js +0 -82
- package/dist/layout-permissions.d.ts +0 -3
- package/dist/layout-permissions.d.ts.map +0 -1
- package/dist/layout-permissions.js +0 -11
- package/dist/media.repository.d.ts +0 -61
- package/dist/media.repository.d.ts.map +0 -1
- package/dist/media.repository.js +0 -3
- package/dist/policies.d.ts +0 -10
- package/dist/policies.d.ts.map +0 -1
- package/dist/policies.js +0 -33
- package/dist/queue.interface.d.ts +0 -39
- package/dist/queue.interface.d.ts.map +0 -1
- package/dist/queue.interface.js +0 -3
- package/dist/queue.stub.d.ts +0 -6
- package/dist/queue.stub.d.ts.map +0 -1
- package/dist/queue.stub.js +0 -8
- package/dist/relations.d.ts +0 -28
- package/dist/relations.d.ts.map +0 -1
- package/dist/relations.js +0 -25
- package/dist/richtext-render.d.ts +0 -11
- package/dist/richtext-render.d.ts.map +0 -1
- package/dist/richtext-render.js +0 -87
- package/dist/richtext.d.ts +0 -11
- package/dist/richtext.d.ts.map +0 -1
- package/dist/richtext.js +0 -13
- package/dist/scheduler.interface.d.ts +0 -4
- package/dist/scheduler.interface.d.ts.map +0 -1
- package/dist/scheduler.interface.js +0 -3
- package/dist/scheduler.stub.d.ts +0 -5
- package/dist/scheduler.stub.d.ts.map +0 -1
- package/dist/scheduler.stub.js +0 -7
- package/dist/schema-mutator.d.ts +0 -26
- package/dist/schema-mutator.d.ts.map +0 -1
- package/dist/schema-mutator.js +0 -3
- package/dist/seed-ddl-destructive.d.ts +0 -40
- package/dist/seed-ddl-destructive.d.ts.map +0 -1
- package/dist/seed-ddl-destructive.js +0 -149
- package/dist/seed-ddl.d.ts +0 -40
- package/dist/seed-ddl.d.ts.map +0 -1
- package/dist/seed-ddl.js +0 -100
- package/dist/seed-layout.d.ts +0 -145
- package/dist/seed-layout.d.ts.map +0 -1
- package/dist/seed-layout.js +0 -192
- package/dist/seed-layout.repository.d.ts +0 -22
- package/dist/seed-layout.repository.d.ts.map +0 -1
- package/dist/seed-layout.repository.js +0 -3
- package/dist/seed-registry.d.ts +0 -71
- package/dist/seed-registry.d.ts.map +0 -1
- package/dist/seed-registry.js +0 -143
- package/dist/seed-types-generator.d.ts +0 -10
- package/dist/seed-types-generator.d.ts.map +0 -1
- package/dist/seed-types-generator.js +0 -86
- package/dist/seed-validation.d.ts +0 -23
- package/dist/seed-validation.d.ts.map +0 -1
- package/dist/seed-validation.js +0 -224
- package/dist/seed.repository.d.ts +0 -37
- package/dist/seed.repository.d.ts.map +0 -1
- package/dist/seed.repository.js +0 -3
- package/dist/seeds.d.ts +0 -20
- package/dist/seeds.d.ts.map +0 -1
- package/dist/seeds.js +0 -21
- package/dist/site-settings.repository.d.ts +0 -16
- package/dist/site-settings.repository.d.ts.map +0 -1
- package/dist/site-settings.repository.js +0 -3
- package/dist/slug-utils.d.ts +0 -18
- package/dist/slug-utils.d.ts.map +0 -1
- package/dist/slug-utils.js +0 -32
- package/dist/storage.d.ts +0 -46
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -3
- package/dist/types.d.ts +0 -243
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/validation.d.ts +0 -50
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -660
- package/dist/webhook-validation.d.ts +0 -3
- package/dist/webhook-validation.d.ts.map +0 -1
- package/dist/webhook-validation.js +0 -6
package/dist/engine/policies.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
/**
|
|
4
|
-
* Computes an un-salted SHA-256 hex digest of a string value.
|
|
5
|
-
* Used for basic digest operations.
|
|
6
|
-
* @param value - The raw string value to hash.
|
|
7
|
-
* @returns A Promise resolving to a 64-character hex string.
|
|
8
|
-
*/
|
|
9
|
-
export async function sha256hex(value) {
|
|
10
|
-
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
11
|
-
return Array.from(new Uint8Array(buf))
|
|
12
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
13
|
-
.join('');
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Verifies if a candidate string matches a stored SHA-256 hash digest.
|
|
17
|
-
* @param stored - The expected 64-character SHA-256 hex digest.
|
|
18
|
-
* @param candidate - The raw candidate string to verify.
|
|
19
|
-
* @returns A Promise resolving to true if candidate matches stored digest, false otherwise.
|
|
20
|
-
*/
|
|
21
|
-
export async function verifyHashField(stored, candidate) {
|
|
22
|
-
return stored === (await sha256hex(candidate));
|
|
23
|
-
}
|
|
24
|
-
/** Normalizes raw policy strings to canonical DataClassification enum. */
|
|
25
|
-
export function normalizeClassification(raw) {
|
|
26
|
-
if (!raw)
|
|
27
|
-
return 'public';
|
|
28
|
-
const lower = raw.toLowerCase();
|
|
29
|
-
if (lower === 'confidential')
|
|
30
|
-
return 'confidential';
|
|
31
|
-
if (lower === 'restricted' || lower === 'hash')
|
|
32
|
-
return 'restricted';
|
|
33
|
-
if (lower === 'encrypt')
|
|
34
|
-
return 'confidential';
|
|
35
|
-
if (lower === 'internal')
|
|
36
|
-
return 'internal';
|
|
37
|
-
return 'public';
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Resolves the 4-tier DataClassification for a branch, bundling its storage
|
|
41
|
-
* strategy at rest and its API visibility rules (public vs auth context).
|
|
42
|
-
* @param branch - The seed branch definition to inspect.
|
|
43
|
-
* @returns The resolved classification details and visibility rules.
|
|
44
|
-
*/
|
|
45
|
-
export function resolveClassification(branch) {
|
|
46
|
-
const rawClassification = branch.policies?.classification ?? branch.policies?.privacy;
|
|
47
|
-
const classification = normalizeClassification(rawClassification);
|
|
48
|
-
switch (classification) {
|
|
49
|
-
case 'confidential':
|
|
50
|
-
return {
|
|
51
|
-
classification: 'confidential',
|
|
52
|
-
storage: 'encrypt',
|
|
53
|
-
publicVisibility: 'hidden',
|
|
54
|
-
authVisibility: 'full',
|
|
55
|
-
};
|
|
56
|
-
case 'restricted':
|
|
57
|
-
return {
|
|
58
|
-
classification: 'restricted',
|
|
59
|
-
storage: 'hash',
|
|
60
|
-
publicVisibility: 'hidden',
|
|
61
|
-
authVisibility: 'hidden',
|
|
62
|
-
};
|
|
63
|
-
case 'internal':
|
|
64
|
-
return {
|
|
65
|
-
classification: 'internal',
|
|
66
|
-
storage: 'plain',
|
|
67
|
-
publicVisibility: 'hidden',
|
|
68
|
-
authVisibility: 'full',
|
|
69
|
-
};
|
|
70
|
-
case 'public':
|
|
71
|
-
default:
|
|
72
|
-
return {
|
|
73
|
-
classification: 'public',
|
|
74
|
-
storage: 'plain',
|
|
75
|
-
publicVisibility: 'full',
|
|
76
|
-
authVisibility: 'full',
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Resolves branch policies applying all defaults and classification rules.
|
|
82
|
-
* @param branch - The seed branch definition to inspect.
|
|
83
|
-
* @returns Complete required policies object for the branch.
|
|
84
|
-
*/
|
|
85
|
-
export function resolvePolicies(branch) {
|
|
86
|
-
const resolved = resolveClassification(branch);
|
|
87
|
-
const isRepeater = branch.type === 'repeater';
|
|
88
|
-
const isEncryptedOrHashed = resolved.storage !== 'plain';
|
|
89
|
-
const defaultVisibility = resolved.authVisibility;
|
|
90
|
-
const isPublicAllowed = branch.policies?.public ?? (resolved.publicVisibility === 'full');
|
|
91
|
-
const defaultFilter = isRepeater || resolved.storage === 'hash' ? false : branch.policies?.filter ?? true;
|
|
92
|
-
const defaultSort = isRepeater || isEncryptedOrHashed ? false : branch.policies?.sort ?? true;
|
|
93
|
-
const defaultSearch = isRepeater || isEncryptedOrHashed ? false : branch.policies?.search ?? true;
|
|
94
|
-
// publicEdit default: true for public fields (unless public: false), false for confidential/internal/restricted
|
|
95
|
-
const defaultPublicEdit = resolved.classification === 'public'
|
|
96
|
-
? (branch.policies?.publicEdit ?? isPublicAllowed)
|
|
97
|
-
: (branch.policies?.publicEdit ?? false);
|
|
98
|
-
return {
|
|
99
|
-
classification: resolved.classification,
|
|
100
|
-
privacy: resolved.storage,
|
|
101
|
-
visibility: branch.policies?.visibility ?? defaultVisibility,
|
|
102
|
-
search: defaultSearch,
|
|
103
|
-
filter: defaultFilter,
|
|
104
|
-
sort: defaultSort,
|
|
105
|
-
public: isPublicAllowed,
|
|
106
|
-
publicEdit: defaultPublicEdit,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
const SYSTEM_FIELDS = new Set(['id', 'slug', 'status', 'created_at', 'updated_at', 'version', 'has_pending_draft']);
|
|
110
|
-
/**
|
|
111
|
-
* Filters entry payload fields based on the caller's {@link ActorContext} and branch classification tier.
|
|
112
|
-
*
|
|
113
|
-
* Rules:
|
|
114
|
-
* - Public actor: receives `public` fields; `internal`, `confidential`, and `restricted` fields are omitted.
|
|
115
|
-
* - Authenticated actor: receives `public`, `internal`, and `confidential` fields; `restricted` fields are ALWAYS omitted.
|
|
116
|
-
* - System actor: receives all fields including `restricted` (used for internal worker/automation orchestration).
|
|
117
|
-
*
|
|
118
|
-
* @param data - Raw record fields object.
|
|
119
|
-
* @param seed - Content type seed definition containing branch definitions.
|
|
120
|
-
* @param actor - Context of the caller requesting the entry payload (defaults to authenticated).
|
|
121
|
-
* @returns Filtered data record containing only authorized fields for the given actor context.
|
|
122
|
-
*/
|
|
123
|
-
export function filterEntryForActor(data, seed, actor = { type: 'authenticated' }) {
|
|
124
|
-
const result = {};
|
|
125
|
-
for (const [key, value] of Object.entries(data)) {
|
|
126
|
-
if (SYSTEM_FIELDS.has(key)) {
|
|
127
|
-
result[key] = value;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
const branch = seed.branches.find((b) => b.alias === key);
|
|
131
|
-
if (!branch) {
|
|
132
|
-
// Pass through unknown/extra fields unless actor is public
|
|
133
|
-
if (actor.type !== 'public') {
|
|
134
|
-
result[key] = value;
|
|
135
|
-
}
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
const resolved = resolveClassification(branch);
|
|
139
|
-
const isPublicActor = actor.type === 'public';
|
|
140
|
-
const isSystemActor = actor.type === 'system';
|
|
141
|
-
// System actor sees all fields including restricted
|
|
142
|
-
if (isSystemActor) {
|
|
143
|
-
result[key] = value;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
// Public actor check
|
|
147
|
-
if (isPublicActor) {
|
|
148
|
-
if (resolved.publicVisibility === 'hidden')
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
// Authenticated actor check
|
|
153
|
-
if (resolved.authVisibility === 'hidden')
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
// Explicit visibility override check
|
|
157
|
-
if (branch.policies?.visibility === 'hidden')
|
|
158
|
-
continue;
|
|
159
|
-
// Masking rule check
|
|
160
|
-
if (branch.policies?.visibility === 'masked') {
|
|
161
|
-
result[key] = typeof value === 'string' && value.length > 0 ? '••••••••' : null;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
result[key] = value;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract for the Application-Level Encryption (ALE) and hashing privacy service.
|
|
3
|
-
* Used by the Botanical Engine to secure fields classified as `confidential` or `restricted`.
|
|
4
|
-
*/
|
|
5
|
-
export interface IPrivacyService {
|
|
6
|
-
/**
|
|
7
|
-
* Symmetrically encrypts a plaintext string using AES-256-GCM.
|
|
8
|
-
* @param plaintext - The raw string value to encrypt.
|
|
9
|
-
* @returns A Promise resolving to formatted ciphertext (`v1:<iv_base64>:<ciphertext_base64>`).
|
|
10
|
-
*/
|
|
11
|
-
encrypt(plaintext: string): Promise<string>;
|
|
12
|
-
/**
|
|
13
|
-
* Decrypts a formatted AES-256-GCM ciphertext payload.
|
|
14
|
-
* @param ciphertext - The formatted ciphertext string (`v1:<iv_base64>:<ciphertext_base64>`).
|
|
15
|
-
* @returns A Promise resolving to the original plaintext string.
|
|
16
|
-
* @throws Error if the ciphertext format is invalid or decryption fails (authentication tag mismatch).
|
|
17
|
-
*/
|
|
18
|
-
decrypt(ciphertext: string): Promise<string>;
|
|
19
|
-
/**
|
|
20
|
-
* Computes a deterministic HMAC SHA-256 digest of a plaintext string.
|
|
21
|
-
* Used for blind-indexing and one-way field digests (`restricted` classification).
|
|
22
|
-
* @param plaintext - The raw string value to hash.
|
|
23
|
-
* @returns A Promise resolving to a 64-character hexadecimal digest string.
|
|
24
|
-
*/
|
|
25
|
-
hash(plaintext: string): Promise<string>;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Concrete Edge-native implementation of {@link IPrivacyService}.
|
|
29
|
-
* Employs Web Crypto API (`crypto.subtle`) for zero-dependency execution in Cloudflare Workers.
|
|
30
|
-
*/
|
|
31
|
-
export declare class PrivacyService implements IPrivacyService {
|
|
32
|
-
private readonly masterKey;
|
|
33
|
-
private aesKeyPromise?;
|
|
34
|
-
private hmacKeyPromise?;
|
|
35
|
-
/**
|
|
36
|
-
* Initializes the PrivacyService with a Master Encryption Key.
|
|
37
|
-
* @param masterKey - Secret key used to derive cryptographic keys for AES-GCM and HMAC.
|
|
38
|
-
* @throws Error if `masterKey` is empty or undefined.
|
|
39
|
-
*/
|
|
40
|
-
constructor(masterKey: string);
|
|
41
|
-
/**
|
|
42
|
-
* Lazy-loads and caches the derived AES-256-GCM {@link CryptoKey}.
|
|
43
|
-
* Derives a 256-bit key from `masterKey` via SHA-256 digest.
|
|
44
|
-
*/
|
|
45
|
-
private getAesKey;
|
|
46
|
-
/**
|
|
47
|
-
* Lazy-loads and caches the derived HMAC SHA-256 {@link CryptoKey}.
|
|
48
|
-
*/
|
|
49
|
-
private getHmacKey;
|
|
50
|
-
/**
|
|
51
|
-
* Encrypts a plaintext string with AES-256-GCM and a fresh 96-bit (12-byte) IV.
|
|
52
|
-
* Output is formatted as `v1:<iv_base64>:<ciphertext_base64>`.
|
|
53
|
-
*/
|
|
54
|
-
encrypt(plaintext: string): Promise<string>;
|
|
55
|
-
/**
|
|
56
|
-
* Decrypts a `v1:<iv_base64>:<ciphertext_base64>` string back to its plaintext value.
|
|
57
|
-
* If the input string is not prefixed with `v1:`, returns the raw string as unencrypted legacy data.
|
|
58
|
-
*/
|
|
59
|
-
decrypt(ciphertext: string): Promise<string>;
|
|
60
|
-
/**
|
|
61
|
-
* Computes a deterministic HMAC SHA-256 hex digest for blind index generation.
|
|
62
|
-
*/
|
|
63
|
-
hash(plaintext: string): Promise<string>;
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=privacy.service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"privacy.service.d.ts","sourceRoot":"","sources":["../../src/engine/privacy.service.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3C;;;;;OAKG;IACH,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE5C;;;;;OAKG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACzC;AA4BD;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,aAAa,CAAC,CAAoB;IAC1C,OAAO,CAAC,cAAc,CAAC,CAAoB;IAE3C;;;;OAIG;IACH,YAAY,SAAS,EAAE,MAAM,EAK5B;IAED;;;OAGG;YACW,SAAS;IAmBvB;;OAEG;YACW,UAAU;IAexB;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYhD;IAED;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBjD;IAED;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAY7C;CACF"}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
/**
|
|
4
|
-
* Converts a Uint8Array byte sequence to a Base64-encoded string.
|
|
5
|
-
* Native Edge-compatible implementation without Node.js Buffer dependencies.
|
|
6
|
-
*/
|
|
7
|
-
function bytesToBase64(bytes) {
|
|
8
|
-
let binary = '';
|
|
9
|
-
const len = bytes.byteLength;
|
|
10
|
-
for (let i = 0; i < len; i++) {
|
|
11
|
-
binary += String.fromCharCode(bytes[i]);
|
|
12
|
-
}
|
|
13
|
-
return btoa(binary);
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Converts a Base64-encoded string back to a Uint8Array byte sequence.
|
|
17
|
-
* Native Edge-compatible implementation without Node.js Buffer dependencies.
|
|
18
|
-
*/
|
|
19
|
-
function base64ToBytes(base64) {
|
|
20
|
-
const binary = atob(base64);
|
|
21
|
-
const bytes = new Uint8Array(binary.length);
|
|
22
|
-
for (let i = 0; i < binary.length; i++) {
|
|
23
|
-
bytes[i] = binary.charCodeAt(i);
|
|
24
|
-
}
|
|
25
|
-
return bytes;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Concrete Edge-native implementation of {@link IPrivacyService}.
|
|
29
|
-
* Employs Web Crypto API (`crypto.subtle`) for zero-dependency execution in Cloudflare Workers.
|
|
30
|
-
*/
|
|
31
|
-
export class PrivacyService {
|
|
32
|
-
masterKey;
|
|
33
|
-
aesKeyPromise;
|
|
34
|
-
hmacKeyPromise;
|
|
35
|
-
/**
|
|
36
|
-
* Initializes the PrivacyService with a Master Encryption Key.
|
|
37
|
-
* @param masterKey - Secret key used to derive cryptographic keys for AES-GCM and HMAC.
|
|
38
|
-
* @throws Error if `masterKey` is empty or undefined.
|
|
39
|
-
*/
|
|
40
|
-
constructor(masterKey) {
|
|
41
|
-
if (!masterKey) {
|
|
42
|
-
throw new Error('PrivacyService requires a non-empty masterKey');
|
|
43
|
-
}
|
|
44
|
-
this.masterKey = masterKey;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Lazy-loads and caches the derived AES-256-GCM {@link CryptoKey}.
|
|
48
|
-
* Derives a 256-bit key from `masterKey` via SHA-256 digest.
|
|
49
|
-
*/
|
|
50
|
-
async getAesKey() {
|
|
51
|
-
if (!this.aesKeyPromise) {
|
|
52
|
-
this.aesKeyPromise = (async () => {
|
|
53
|
-
const keyData = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(this.masterKey));
|
|
54
|
-
return crypto.subtle.importKey('raw', keyData, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
55
|
-
})();
|
|
56
|
-
}
|
|
57
|
-
return this.aesKeyPromise;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Lazy-loads and caches the derived HMAC SHA-256 {@link CryptoKey}.
|
|
61
|
-
*/
|
|
62
|
-
async getHmacKey() {
|
|
63
|
-
if (!this.hmacKeyPromise) {
|
|
64
|
-
this.hmacKeyPromise = (async () => {
|
|
65
|
-
return crypto.subtle.importKey('raw', new TextEncoder().encode(this.masterKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
66
|
-
})();
|
|
67
|
-
}
|
|
68
|
-
return this.hmacKeyPromise;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Encrypts a plaintext string with AES-256-GCM and a fresh 96-bit (12-byte) IV.
|
|
72
|
-
* Output is formatted as `v1:<iv_base64>:<ciphertext_base64>`.
|
|
73
|
-
*/
|
|
74
|
-
async encrypt(plaintext) {
|
|
75
|
-
const key = await this.getAesKey();
|
|
76
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
77
|
-
const encodedText = new TextEncoder().encode(plaintext);
|
|
78
|
-
const encryptedBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, encodedText);
|
|
79
|
-
const ivBase64 = bytesToBase64(iv);
|
|
80
|
-
const ciphertextBase64 = bytesToBase64(new Uint8Array(encryptedBuffer));
|
|
81
|
-
return `v1:${ivBase64}:${ciphertextBase64}`;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Decrypts a `v1:<iv_base64>:<ciphertext_base64>` string back to its plaintext value.
|
|
85
|
-
* If the input string is not prefixed with `v1:`, returns the raw string as unencrypted legacy data.
|
|
86
|
-
*/
|
|
87
|
-
async decrypt(ciphertext) {
|
|
88
|
-
if (!ciphertext || !ciphertext.startsWith('v1:')) {
|
|
89
|
-
return ciphertext;
|
|
90
|
-
}
|
|
91
|
-
const parts = ciphertext.split(':');
|
|
92
|
-
if (parts.length !== 3) {
|
|
93
|
-
throw new Error('Invalid ciphertext format');
|
|
94
|
-
}
|
|
95
|
-
const [, ivBase64, cipherBase64] = parts;
|
|
96
|
-
const iv = base64ToBytes(ivBase64);
|
|
97
|
-
const cipherBytes = base64ToBytes(cipherBase64);
|
|
98
|
-
const key = await this.getAesKey();
|
|
99
|
-
const decryptedBuffer = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, cipherBytes);
|
|
100
|
-
return new TextDecoder().decode(decryptedBuffer);
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Computes a deterministic HMAC SHA-256 hex digest for blind index generation.
|
|
104
|
-
*/
|
|
105
|
-
async hash(plaintext) {
|
|
106
|
-
const key = await this.getHmacKey();
|
|
107
|
-
const encodedText = new TextEncoder().encode(plaintext);
|
|
108
|
-
const signatureBuffer = await crypto.subtle.sign('HMAC', key, encodedText);
|
|
109
|
-
const hashBytes = new Uint8Array(signatureBuffer);
|
|
110
|
-
return Array.from(hashBytes)
|
|
111
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
112
|
-
.join('');
|
|
113
|
-
}
|
|
114
|
-
}
|
package/dist/engine/query.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Seed, SelectOptions, ParameterizedQuery } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Builds a parameterized SQL SELECT query and bindings for a given Seed based on search, filtering, status, and ordering options.
|
|
4
|
-
*
|
|
5
|
-
* If `options.isCount` is set to `true`, it generates a counting query (`COUNT(*) as total`) rather than returning database rows.
|
|
6
|
-
* In count mode, column projections (`fields`), ordering (`orderBy` / `kanbanOrder`), and pagination (`LIMIT` / `OFFSET`) clauses and
|
|
7
|
-
* bindings are omitted, while join and filtering clauses are preserved.
|
|
8
|
-
*
|
|
9
|
-
* @param seed The seed definition.
|
|
10
|
-
* @param options Query configuration options.
|
|
11
|
-
* @returns The SQL query string and bindings.
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* const { sql, bindings } = buildSelectQuery(postSeed, {
|
|
15
|
-
* status: 'published',
|
|
16
|
-
* filters: [{ column: 'title', type: 'text', conditions: [{ op: 'contains', value: 'cms' }] }],
|
|
17
|
-
* orderBy: { column: 'created_at', dir: 'DESC' },
|
|
18
|
-
* pagination: { limit: 20, offset: 0 },
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export declare function buildSelectQuery(seed: Seed, options?: SelectOptions): ParameterizedQuery;
|
|
23
|
-
//# sourceMappingURL=query.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/engine/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,IAAI,EAMJ,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAKpB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAoG5F"}
|
package/dist/engine/query.js
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
import { tableName, ftsTableName, isValidColumn, indexableSearchBranches, SYSTEM_COLUMNS } from './ddl.js';
|
|
2
|
-
import { resolveClassification } from './policies.js';
|
|
3
|
-
/**
|
|
4
|
-
* Builds a parameterized SQL SELECT query and bindings for a given Seed based on search, filtering, status, and ordering options.
|
|
5
|
-
*
|
|
6
|
-
* If `options.isCount` is set to `true`, it generates a counting query (`COUNT(*) as total`) rather than returning database rows.
|
|
7
|
-
* In count mode, column projections (`fields`), ordering (`orderBy` / `kanbanOrder`), and pagination (`LIMIT` / `OFFSET`) clauses and
|
|
8
|
-
* bindings are omitted, while join and filtering clauses are preserved.
|
|
9
|
-
*
|
|
10
|
-
* @param seed The seed definition.
|
|
11
|
-
* @param options Query configuration options.
|
|
12
|
-
* @returns The SQL query string and bindings.
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* const { sql, bindings } = buildSelectQuery(postSeed, {
|
|
16
|
-
* status: 'published',
|
|
17
|
-
* filters: [{ column: 'title', type: 'text', conditions: [{ op: 'contains', value: 'cms' }] }],
|
|
18
|
-
* orderBy: { column: 'created_at', dir: 'DESC' },
|
|
19
|
-
* pagination: { limit: 20, offset: 0 },
|
|
20
|
-
* })
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function buildSelectQuery(seed, options = {}) {
|
|
24
|
-
const table = tableName(seed);
|
|
25
|
-
const { filters = [], orderBy, pagination, status, search, fields } = options;
|
|
26
|
-
const bindings = [];
|
|
27
|
-
const whereClauses = [];
|
|
28
|
-
let joinClause = '';
|
|
29
|
-
let kanbanOrderClause = '';
|
|
30
|
-
if (options.kanbanOrder) {
|
|
31
|
-
joinClause = `LEFT JOIN kanban_positions kp` +
|
|
32
|
-
` ON kp.seed_slug = ? AND kp.entry_id = ${table}.id AND kp.axis_branch_id = ?`;
|
|
33
|
-
bindings.push(options.kanbanOrder.seedSlug, options.kanbanOrder.axisBranchId);
|
|
34
|
-
kanbanOrderClause = ` ORDER BY (kp.position IS NULL) ASC, kp.position ASC`;
|
|
35
|
-
}
|
|
36
|
-
const rtBranches = indexableSearchBranches(seed);
|
|
37
|
-
if (search && rtBranches.length > 0) {
|
|
38
|
-
const ftsTable = ftsTableName(seed);
|
|
39
|
-
joinClause += (joinClause ? ' ' : '') + `INNER JOIN ${ftsTable} ON ${ftsTable}.entry_id = ${table}.id`;
|
|
40
|
-
whereClauses.push(`${ftsTable} MATCH ?`);
|
|
41
|
-
bindings.push(`"${search.replace(/"/g, '""')}"*`);
|
|
42
|
-
}
|
|
43
|
-
if (status !== undefined && status !== null) {
|
|
44
|
-
whereClauses.push(`${table}.status = ?`);
|
|
45
|
-
bindings.push(status);
|
|
46
|
-
}
|
|
47
|
-
const groupClauses = [];
|
|
48
|
-
for (const group of filters) {
|
|
49
|
-
if (!isValidColumn(seed, group.column))
|
|
50
|
-
continue;
|
|
51
|
-
const branch = seed.branches.find(b => b.alias === group.column);
|
|
52
|
-
const isEncrypted = branch ? resolveClassification(branch).storage === 'encrypt' : false;
|
|
53
|
-
const col = SYSTEM_COLUMNS.has(group.column)
|
|
54
|
-
? `${table}.${group.column}`
|
|
55
|
-
: group.column;
|
|
56
|
-
const condClauses = [];
|
|
57
|
-
for (const cond of group.conditions) {
|
|
58
|
-
if (isEncrypted) {
|
|
59
|
-
const { op } = cond;
|
|
60
|
-
if (op === 'eq' || op === 'neq' || op === 'in' || op === 'not_in') {
|
|
61
|
-
const targetCol = `${table}.${group.column}_bidx`;
|
|
62
|
-
const clause = buildFilterCondition(targetCol, group.type, cond, bindings);
|
|
63
|
-
if (clause)
|
|
64
|
-
condClauses.push(clause);
|
|
65
|
-
}
|
|
66
|
-
else if (op === 'is_empty' || op === 'is_not_empty') {
|
|
67
|
-
const clause = buildFilterCondition(col, group.type, cond, bindings);
|
|
68
|
-
if (clause)
|
|
69
|
-
condClauses.push(clause);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
const clause = buildFilterCondition(col, group.type, cond, bindings);
|
|
74
|
-
if (clause)
|
|
75
|
-
condClauses.push(clause);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (condClauses.length > 0) {
|
|
79
|
-
groupClauses.push(condClauses.length > 1 ? `(${condClauses.join(' AND ')})` : condClauses[0]);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (groupClauses.length > 0) {
|
|
83
|
-
const joiner = options.filterLogic === 'OR' ? ' OR ' : ' AND ';
|
|
84
|
-
whereClauses.push(groupClauses.length > 1 ? `(${groupClauses.join(joiner)})` : groupClauses[0]);
|
|
85
|
-
}
|
|
86
|
-
let selectCols = options.isCount ? 'COUNT(*) as total' : `${table}.*`;
|
|
87
|
-
if (!options.isCount && fields && fields.length > 0) {
|
|
88
|
-
const valid = fields.filter(f => isValidColumn(seed, f));
|
|
89
|
-
if (valid.length > 0) {
|
|
90
|
-
selectCols = valid
|
|
91
|
-
.map(f => (SYSTEM_COLUMNS.has(f) ? `${table}.${f}` : f))
|
|
92
|
-
.join(', ');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (!options.isCount && options.kanbanOrder)
|
|
96
|
-
selectCols += ', kp.position';
|
|
97
|
-
let sql = `SELECT ${selectCols} FROM ${table}`;
|
|
98
|
-
if (joinClause)
|
|
99
|
-
sql += ` ${joinClause}`;
|
|
100
|
-
if (whereClauses.length > 0)
|
|
101
|
-
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
102
|
-
if (!options.isCount) {
|
|
103
|
-
if (kanbanOrderClause) {
|
|
104
|
-
sql += kanbanOrderClause;
|
|
105
|
-
}
|
|
106
|
-
else if (orderBy && isValidColumn(seed, orderBy.column)) {
|
|
107
|
-
const dir = orderBy.dir === 'DESC' ? 'DESC' : 'ASC';
|
|
108
|
-
const col = SYSTEM_COLUMNS.has(orderBy.column)
|
|
109
|
-
? `${table}.${orderBy.column}`
|
|
110
|
-
: orderBy.column;
|
|
111
|
-
sql += ` ORDER BY ${col} ${dir}`;
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
sql += ` ORDER BY ${table}.created_at DESC`;
|
|
115
|
-
}
|
|
116
|
-
if (pagination) {
|
|
117
|
-
sql += ` LIMIT ? OFFSET ?`;
|
|
118
|
-
bindings.push(pagination.limit, pagination.offset);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return { sql, bindings };
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Normalizes user-provided filter values to a format appropriate for SQL execution.
|
|
125
|
-
*
|
|
126
|
-
* @param type The type of the filter.
|
|
127
|
-
* @param value The value to normalize.
|
|
128
|
-
* @returns The normalized value, or null.
|
|
129
|
-
*/
|
|
130
|
-
function normalizeFilterValue(type, value) {
|
|
131
|
-
if (value === null || value === undefined)
|
|
132
|
-
return null;
|
|
133
|
-
if (type === 'date') {
|
|
134
|
-
if (typeof value === 'number')
|
|
135
|
-
return value;
|
|
136
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
137
|
-
const d = new Date(value);
|
|
138
|
-
return Number.isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
if (type === 'boolean') {
|
|
143
|
-
return value ? 1 : 0;
|
|
144
|
-
}
|
|
145
|
-
if (type === 'number') {
|
|
146
|
-
if (typeof value === 'number')
|
|
147
|
-
return value;
|
|
148
|
-
if (typeof value === 'string' && value.trim() !== '') {
|
|
149
|
-
const n = Number(value);
|
|
150
|
-
return Number.isNaN(n) ? null : n;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
return value;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Translates a filter condition into its SQL clause and adds parameters to the bindings list.
|
|
158
|
-
*
|
|
159
|
-
* @param col The column name in the table.
|
|
160
|
-
* @param type The type of the filter.
|
|
161
|
-
* @param cond The condition containing the operator and value.
|
|
162
|
-
* @param bindings The array of bindings where parameterized values are appended.
|
|
163
|
-
* @returns The SQL condition fragment, or null if invalid.
|
|
164
|
-
*/
|
|
165
|
-
function buildFilterCondition(col, type, cond, bindings) {
|
|
166
|
-
const { op, value } = cond;
|
|
167
|
-
if (op === 'is_empty' || op === 'is_not_empty') {
|
|
168
|
-
const isEmpty = op === 'is_empty';
|
|
169
|
-
if (type === 'text') {
|
|
170
|
-
return isEmpty ? `(${col} IS NULL OR ${col} = '')` : `(${col} IS NOT NULL AND ${col} != '')`;
|
|
171
|
-
}
|
|
172
|
-
if (type === 'tags' || type === 'json') {
|
|
173
|
-
return isEmpty
|
|
174
|
-
? `(${col} IS NULL OR ${col} = '[]' OR ${col} = '{}')`
|
|
175
|
-
: `(${col} IS NOT NULL AND ${col} != '[]' AND ${col} != '{}')`;
|
|
176
|
-
}
|
|
177
|
-
return isEmpty ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
178
|
-
}
|
|
179
|
-
if (op === 'in' || op === 'not_in') {
|
|
180
|
-
if (!Array.isArray(value) || value.length === 0)
|
|
181
|
-
return null;
|
|
182
|
-
const normalizedValues = value
|
|
183
|
-
.map((v) => normalizeFilterValue(type, v))
|
|
184
|
-
.filter((v) => v !== null);
|
|
185
|
-
if (normalizedValues.length === 0)
|
|
186
|
-
return null;
|
|
187
|
-
const placeholders = normalizedValues.map(() => '?').join(', ');
|
|
188
|
-
bindings.push(...normalizedValues);
|
|
189
|
-
return `${col} ${op === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
|
|
190
|
-
}
|
|
191
|
-
if (op === 'has_tag' || op === 'has_any_tag' || op === 'has_all_tags') {
|
|
192
|
-
if (type !== 'tags' && type !== 'json')
|
|
193
|
-
return null;
|
|
194
|
-
const tags = (op === 'has_tag' ? [value] : Array.isArray(value) ? value : [value])
|
|
195
|
-
.map(t => String(t));
|
|
196
|
-
if (tags.length === 0)
|
|
197
|
-
return null;
|
|
198
|
-
// json_each on array → value is the element; on object → key is the property name.
|
|
199
|
-
// Tags stored as ["photo"] (array) or {"photo":"#3b82f6"} (object) must both match.
|
|
200
|
-
const tagRef = `CASE json_type(${col}) WHEN 'array' THEN value ELSE key END`;
|
|
201
|
-
if (op === 'has_tag' || op === 'has_any_tag') {
|
|
202
|
-
const placeholders = tags.map(() => '?').join(', ');
|
|
203
|
-
bindings.push(...tags);
|
|
204
|
-
return `EXISTS (SELECT 1 FROM json_each(${col}) WHERE ${tagRef} IN (${placeholders}))`;
|
|
205
|
-
}
|
|
206
|
-
const clauses = tags.map(() => `EXISTS (SELECT 1 FROM json_each(${col}) WHERE ${tagRef} = ?)`);
|
|
207
|
-
bindings.push(...tags);
|
|
208
|
-
return `(${clauses.join(' AND ')})`;
|
|
209
|
-
}
|
|
210
|
-
const normalized = normalizeFilterValue(type, value);
|
|
211
|
-
if (normalized === null)
|
|
212
|
-
return null;
|
|
213
|
-
if (op === 'eq' || op === 'neq') {
|
|
214
|
-
const sqlOp = op === 'eq' ? '=' : '!=';
|
|
215
|
-
bindings.push(normalized);
|
|
216
|
-
return `${col} ${sqlOp} ?`;
|
|
217
|
-
}
|
|
218
|
-
if (op === 'contains' || op === 'not_contains' || op === 'starts_with' || op === 'ends_with') {
|
|
219
|
-
const sqlOp = op === 'not_contains' ? 'NOT LIKE' : 'LIKE';
|
|
220
|
-
let pattern = String(value);
|
|
221
|
-
if (op === 'contains' || op === 'not_contains')
|
|
222
|
-
pattern = `%${pattern}%`;
|
|
223
|
-
else if (op === 'starts_with')
|
|
224
|
-
pattern = `${pattern}%`;
|
|
225
|
-
else if (op === 'ends_with')
|
|
226
|
-
pattern = `%${pattern}`;
|
|
227
|
-
bindings.push(pattern);
|
|
228
|
-
return `${col} ${sqlOp} ?`;
|
|
229
|
-
}
|
|
230
|
-
const mathOps = { gt: '>', gte: '>=', lt: '<', lte: '<=' };
|
|
231
|
-
if (mathOps[op]) {
|
|
232
|
-
bindings.push(normalized);
|
|
233
|
-
return `${col} ${mathOps[op]} ?`;
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Back-reference helpers for the Beech CMS relation system.
|
|
3
|
-
*
|
|
4
|
-
* `buildBackrefMap` is a pure function that iterates the seed registry once
|
|
5
|
-
* and builds a lookup map: targetSlug → sources that point to it.
|
|
6
|
-
* Build this once at app factory time and cache it for the process lifetime.
|
|
7
|
-
*/
|
|
8
|
-
import type { Seed } from './types.js';
|
|
9
|
-
export interface BackrefSource {
|
|
10
|
-
/** Slug of the seed that owns the relation branch */
|
|
11
|
-
sourceSlug: string;
|
|
12
|
-
/** `branch.alias` — the FK column name (single) or join-table name suffix (multi) */
|
|
13
|
-
branchAlias: string;
|
|
14
|
-
/** `branch.label` — human label for UI grouping */
|
|
15
|
-
branchLabel: string;
|
|
16
|
-
/** 'single' for direct FK, 'multi' for many-to-many join table */
|
|
17
|
-
relationship: 'single' | 'multi';
|
|
18
|
-
/** True when branch.onDelete === 'RESTRICT' — used to disable Delete button */
|
|
19
|
-
restricts: boolean;
|
|
20
|
-
}
|
|
21
|
-
/** Map: targetSlug → every source that points to it */
|
|
22
|
-
export type BackrefMap = Map<string, BackrefSource[]>;
|
|
23
|
-
/**
|
|
24
|
-
* Builds the back-reference map from an array of seeds.
|
|
25
|
-
* Pure function — no side-effects, safe to call at startup and cache forever.
|
|
26
|
-
*/
|
|
27
|
-
export declare function buildBackrefMap(seeds: Seed[]): BackrefMap;
|
|
28
|
-
//# sourceMappingURL=relations.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../../src/engine/relations.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAA;IACnB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,YAAY,EAAE,QAAQ,GAAG,OAAO,CAAA;IAChC,+EAA+E;IAC/E,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAA;AAErD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,UAAU,CAoBzD"}
|
package/dist/engine/relations.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
-
/**
|
|
4
|
-
* Builds the back-reference map from an array of seeds.
|
|
5
|
-
* Pure function — no side-effects, safe to call at startup and cache forever.
|
|
6
|
-
*/
|
|
7
|
-
export function buildBackrefMap(seeds) {
|
|
8
|
-
const map = new Map();
|
|
9
|
-
for (const seed of seeds) {
|
|
10
|
-
for (const branch of seed.branches) {
|
|
11
|
-
if (branch.type !== 'relation' || !branch.targetSeed)
|
|
12
|
-
continue;
|
|
13
|
-
const existing = map.get(branch.targetSeed) ?? [];
|
|
14
|
-
existing.push({
|
|
15
|
-
sourceSlug: seed.slug,
|
|
16
|
-
branchAlias: branch.alias,
|
|
17
|
-
branchLabel: branch.label,
|
|
18
|
-
relationship: branch.multiple === true ? 'multi' : 'single',
|
|
19
|
-
restricts: branch.onDelete === 'RESTRICT',
|
|
20
|
-
});
|
|
21
|
-
map.set(branch.targetSeed, existing);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return map;
|
|
25
|
-
}
|