@beechcms/core 0.7.0 → 0.8.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/.tsbuildinfo +1 -1
- package/dist/antivirus/antivirus.interface.d.ts +12 -0
- package/dist/antivirus/antivirus.interface.d.ts.map +1 -0
- package/dist/antivirus/antivirus.interface.js +3 -0
- package/dist/antivirus/noop-antivirus.provider.d.ts +6 -0
- package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -0
- package/dist/antivirus/noop-antivirus.provider.js +8 -0
- package/dist/antivirus/virustotal-antivirus.provider.d.ts +8 -0
- package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -0
- package/dist/antivirus/virustotal-antivirus.provider.js +48 -0
- package/dist/automations-grammar-words.d.ts +2 -0
- package/dist/automations-grammar-words.d.ts.map +1 -0
- package/dist/automations-grammar-words.js +8 -0
- package/dist/automations.repository.interface.d.ts +19 -0
- package/dist/automations.repository.interface.d.ts.map +1 -0
- package/dist/automations.repository.interface.js +3 -0
- package/dist/automations.runner.interface.d.ts +10 -0
- package/dist/automations.runner.interface.d.ts.map +1 -0
- package/dist/automations.runner.interface.js +3 -0
- package/dist/automations.runner.stub.d.ts +5 -0
- package/dist/automations.runner.stub.d.ts.map +1 -0
- package/dist/automations.runner.stub.js +7 -0
- package/dist/automations.types.d.ts +75 -0
- package/dist/automations.types.d.ts.map +1 -0
- package/dist/automations.types.js +3 -0
- package/dist/clock.d.ts +25 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +11 -0
- package/dist/common/canonical-json.d.ts +30 -0
- package/dist/common/canonical-json.d.ts.map +1 -0
- package/dist/common/canonical-json.js +89 -0
- package/dist/common/hooks.d.ts +3 -0
- package/dist/common/hooks.d.ts.map +1 -1
- package/dist/content/content.repository.d.ts +2 -2
- package/dist/content/content.repository.d.ts.map +1 -1
- package/dist/content/deletion-ledger.d.ts +39 -0
- package/dist/content/deletion-ledger.d.ts.map +1 -0
- package/dist/content/deletion-ledger.js +3 -0
- package/dist/content/richtext/richtext-render.d.ts.map +1 -1
- package/dist/content/richtext/richtext-render.js +14 -1
- package/dist/content/seed.repository.d.ts +22 -0
- package/dist/content/seed.repository.d.ts.map +1 -1
- package/dist/content-scan.repository.d.ts +9 -0
- package/dist/content-scan.repository.d.ts.map +1 -0
- package/dist/content-scan.repository.js +3 -0
- package/dist/content.repository.d.ts +247 -0
- package/dist/content.repository.d.ts.map +1 -0
- package/dist/content.repository.js +59 -0
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.js +13 -6
- 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 +3 -0
- package/dist/define-seed.d.ts.map +1 -0
- package/dist/define-seed.js +5 -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/email.provider.d.ts +32 -0
- package/dist/email.provider.d.ts.map +1 -0
- package/dist/email.provider.js +4 -0
- package/dist/engine/ddl/ddl.d.ts +216 -0
- package/dist/engine/ddl/ddl.d.ts.map +1 -0
- package/dist/engine/ddl/ddl.js +574 -0
- package/dist/engine/ddl/schema-mutator.d.ts +26 -0
- package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
- package/dist/engine/ddl/schema-mutator.js +3 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
- package/dist/engine/ddl/seed-ddl.d.ts +43 -0
- package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl.js +115 -0
- package/dist/engine/ddl.d.ts +15 -0
- package/dist/engine/ddl.d.ts.map +1 -1
- package/dist/engine/ddl.js +30 -1
- package/dist/engine/introspection/introspection.d.ts +101 -0
- package/dist/engine/introspection/introspection.d.ts.map +1 -0
- package/dist/engine/introspection/introspection.js +127 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts +73 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/introspection/schema-fingerprint.js +97 -0
- package/dist/engine/introspection.d.ts +101 -0
- package/dist/engine/introspection.d.ts.map +1 -0
- package/dist/engine/introspection.js +127 -0
- package/dist/engine/localization/localization.d.ts +127 -0
- package/dist/engine/localization/localization.d.ts.map +1 -0
- package/dist/engine/localization/localization.js +212 -0
- package/dist/engine/localization.d.ts +127 -0
- package/dist/engine/localization.d.ts.map +1 -0
- package/dist/engine/localization.js +212 -0
- package/dist/engine/policies.d.ts.map +1 -1
- package/dist/engine/policies.js +5 -0
- package/dist/engine/privacy/policies.d.ts +58 -0
- package/dist/engine/privacy/policies.d.ts.map +1 -0
- package/dist/engine/privacy/policies.js +185 -0
- package/dist/engine/privacy/privacy.service.d.ts +65 -0
- package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
- package/dist/engine/privacy/privacy.service.js +114 -0
- package/dist/engine/privacy.service.d.ts.map +1 -1
- package/dist/engine/query/query.d.ts +23 -0
- package/dist/engine/query/query.d.ts.map +1 -0
- package/dist/engine/query/query.js +301 -0
- package/dist/engine/query/relations.d.ts +28 -0
- package/dist/engine/query/relations.d.ts.map +1 -0
- package/dist/engine/query/relations.js +25 -0
- package/dist/engine/query/serialize.d.ts +22 -0
- package/dist/engine/query/serialize.d.ts.map +1 -0
- package/dist/engine/query/serialize.js +175 -0
- package/dist/engine/schema-fingerprint.d.ts +73 -0
- package/dist/engine/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/schema-fingerprint.js +97 -0
- package/dist/engine/seed-registry.d.ts.map +1 -1
- package/dist/engine/seed-registry.js +23 -4
- package/dist/engine/seed-types-generator.d.ts.map +1 -1
- package/dist/engine/seed-types-generator.js +8 -5
- package/dist/engine/seed-validation.d.ts +7 -0
- package/dist/engine/seed-validation.d.ts.map +1 -1
- package/dist/engine/seed-validation.js +59 -1
- package/dist/engine/seeds/define-seed.d.ts +3 -0
- package/dist/engine/seeds/define-seed.d.ts.map +1 -0
- package/dist/engine/seeds/define-seed.js +5 -0
- package/dist/engine/seeds/seed-registry.d.ts +71 -0
- package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
- package/dist/engine/seeds/seed-registry.js +167 -0
- package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
- package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
- package/dist/engine/seeds/seed-types-generator.js +117 -0
- package/dist/engine/seeds/seed-validation.d.ts +30 -0
- package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
- package/dist/engine/seeds/seed-validation.js +364 -0
- package/dist/engine/seeds/seeds.d.ts +20 -0
- package/dist/engine/seeds/seeds.d.ts.map +1 -0
- package/dist/engine/seeds/seeds.js +21 -0
- package/dist/engine/types.d.ts +7 -0
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/validation/emptiness.d.ts +12 -0
- package/dist/engine/validation/emptiness.d.ts.map +1 -0
- package/dist/engine/validation/emptiness.js +33 -0
- package/dist/engine/validation/index.d.ts +1 -1
- package/dist/engine/validation/index.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.js +2 -1
- package/dist/engine.d.ts +144 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +741 -0
- 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/hooks.d.ts +29 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +3 -0
- package/dist/id-generator.d.ts +26 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +12 -0
- package/dist/idempotency.repository.d.ts +13 -0
- package/dist/idempotency.repository.d.ts.map +1 -0
- package/dist/idempotency.repository.js +3 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/kanban-position.repository.d.ts +19 -0
- package/dist/kanban-position.repository.d.ts.map +1 -0
- package/dist/kanban-position.repository.js +3 -0
- package/dist/kanban.d.ts +75 -0
- package/dist/kanban.d.ts.map +1 -0
- package/dist/kanban.js +82 -0
- 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/file-types.d.ts +103 -9
- package/dist/media/file-types.d.ts.map +1 -1
- package/dist/media/file-types.js +336 -52
- package/dist/media/image-transformer.d.ts +34 -0
- package/dist/media/image-transformer.d.ts.map +1 -0
- package/dist/media/image-transformer.js +3 -0
- package/dist/media/magic-bytes.d.ts +21 -0
- package/dist/media/magic-bytes.d.ts.map +1 -0
- package/dist/media/magic-bytes.js +66 -0
- package/dist/media/media-transform.d.ts +88 -0
- package/dist/media/media-transform.d.ts.map +1 -0
- package/dist/media/media-transform.js +193 -0
- package/dist/media.repository.d.ts +61 -0
- package/dist/media.repository.d.ts.map +1 -0
- package/dist/media.repository.js +3 -0
- package/dist/notifications/notification-service.d.ts +1 -1
- package/dist/oauth/authorization-code.repository.d.ts +33 -0
- package/dist/oauth/authorization-code.repository.d.ts.map +1 -0
- package/dist/oauth/authorization-code.repository.js +3 -0
- package/dist/oauth/client.repository.d.ts +17 -0
- package/dist/oauth/client.repository.d.ts.map +1 -0
- package/dist/oauth/client.repository.js +3 -0
- package/dist/oauth/consent.repository.d.ts +25 -0
- package/dist/oauth/consent.repository.d.ts.map +1 -0
- package/dist/oauth/consent.repository.js +3 -0
- package/dist/oauth/pkce.d.ts +21 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +47 -0
- package/dist/oauth/role-guard.d.ts +32 -0
- package/dist/oauth/role-guard.d.ts.map +1 -0
- package/dist/oauth/role-guard.js +14 -0
- package/dist/oauth/scopes.d.ts +27 -0
- package/dist/oauth/scopes.d.ts.map +1 -0
- package/dist/oauth/scopes.js +41 -0
- package/dist/oauth/token.repository.d.ts +46 -0
- package/dist/oauth/token.repository.d.ts.map +1 -0
- package/dist/oauth/token.repository.js +3 -0
- package/dist/observability/activity-logger.d.ts +1 -1
- package/dist/observability/activity-logger.d.ts.map +1 -1
- package/dist/policies.d.ts +10 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +33 -0
- package/dist/queue/queue.stub.d.ts.map +1 -1
- package/dist/queue/scheduler.stub.d.ts.map +1 -1
- package/dist/queue.interface.d.ts +39 -0
- package/dist/queue.interface.d.ts.map +1 -0
- package/dist/queue.interface.js +3 -0
- package/dist/queue.stub.d.ts +6 -0
- package/dist/queue.stub.d.ts.map +1 -0
- package/dist/queue.stub.js +8 -0
- package/dist/rate-limit/rate-limiter.d.ts +13 -5
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts +20 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/token-bucket-rate-limiter.js +63 -0
- package/dist/rbac/evaluate.d.ts +57 -0
- package/dist/rbac/evaluate.d.ts.map +1 -0
- package/dist/rbac/evaluate.js +113 -0
- package/dist/rbac/invitation.repository.d.ts +66 -0
- package/dist/rbac/invitation.repository.d.ts.map +1 -0
- package/dist/rbac/invitation.repository.js +3 -0
- package/dist/rbac/permissions.d.ts +39 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +43 -0
- package/dist/rbac/types.d.ts +118 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +3 -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 +11 -0
- package/dist/richtext-render.d.ts.map +1 -0
- package/dist/richtext-render.js +87 -0
- package/dist/richtext.d.ts +11 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +13 -0
- package/dist/scheduler.interface.d.ts +4 -0
- package/dist/scheduler.interface.d.ts.map +1 -0
- package/dist/scheduler.interface.js +3 -0
- package/dist/scheduler.stub.d.ts +5 -0
- package/dist/scheduler.stub.d.ts.map +1 -0
- package/dist/scheduler.stub.js +7 -0
- package/dist/schema/canonical.d.ts +15 -0
- package/dist/schema/canonical.d.ts.map +1 -0
- package/dist/schema/canonical.js +37 -0
- package/dist/schema/define.d.ts +42 -0
- package/dist/schema/define.d.ts.map +1 -0
- package/dist/schema/define.js +56 -0
- package/dist/schema/emit.d.ts +14 -0
- package/dist/schema/emit.d.ts.map +1 -0
- package/dist/schema/emit.js +43 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/manifest-seeds.d.ts +12 -0
- package/dist/schema/manifest-seeds.d.ts.map +1 -0
- package/dist/schema/manifest-seeds.js +48 -0
- package/dist/schema/manifest-validation.d.ts +13 -0
- package/dist/schema/manifest-validation.d.ts.map +1 -0
- package/dist/schema/manifest-validation.js +41 -0
- package/dist/schema/manifest.types.d.ts +54 -0
- package/dist/schema/manifest.types.d.ts.map +1 -0
- package/dist/schema/manifest.types.js +7 -0
- package/dist/schema-mutator.d.ts +26 -0
- package/dist/schema-mutator.d.ts.map +1 -0
- package/dist/schema-mutator.js +3 -0
- package/dist/search/vector-extractor.d.ts +11 -0
- package/dist/search/vector-extractor.d.ts.map +1 -0
- package/dist/search/vector-extractor.js +25 -0
- package/dist/search/vector.repository.d.ts +13 -0
- package/dist/search/vector.repository.d.ts.map +1 -0
- package/dist/search/vector.repository.js +3 -0
- package/dist/security/time-trap-token.repository.d.ts +9 -0
- package/dist/security/time-trap-token.repository.d.ts.map +1 -0
- package/dist/security/time-trap-token.repository.js +3 -0
- package/dist/security/time-trap.d.ts +7 -0
- package/dist/security/time-trap.d.ts.map +1 -0
- package/dist/security/time-trap.js +49 -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 +145 -0
- package/dist/seed-layout.d.ts.map +1 -0
- package/dist/seed-layout.js +192 -0
- package/dist/seed-layout.repository.d.ts +22 -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 +71 -0
- package/dist/seed-registry.d.ts.map +1 -0
- package/dist/seed-registry.js +143 -0
- package/dist/seed-types-generator.d.ts +1 -1
- package/dist/seed-types-generator.d.ts.map +1 -1
- 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 +20 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +21 -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 +18 -0
- package/dist/slug-utils.d.ts.map +1 -0
- package/dist/slug-utils.js +32 -0
- package/dist/storage.d.ts +46 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +3 -0
- package/dist/transfer/csv.d.ts +34 -0
- package/dist/transfer/csv.d.ts.map +1 -0
- package/dist/transfer/csv.js +135 -0
- package/dist/transfer/flat-seed.d.ts +13 -0
- package/dist/transfer/flat-seed.d.ts.map +1 -0
- package/dist/transfer/flat-seed.js +45 -0
- package/dist/transfer/index.d.ts +7 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +8 -0
- package/dist/transfer/ndjson.d.ts +25 -0
- package/dist/transfer/ndjson.d.ts.map +1 -0
- package/dist/transfer/ndjson.js +62 -0
- package/dist/transfer/row-mapping.d.ts +36 -0
- package/dist/transfer/row-mapping.d.ts.map +1 -0
- package/dist/transfer/row-mapping.js +90 -0
- package/dist/transfer/transfer.constants.d.ts +20 -0
- package/dist/transfer/transfer.constants.d.ts.map +1 -0
- package/dist/transfer/transfer.constants.js +21 -0
- package/dist/transfer/transfer.types.d.ts +46 -0
- package/dist/transfer/transfer.types.d.ts.map +1 -0
- package/dist/transfer/transfer.types.js +6 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +50 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +660 -0
- package/dist/webhook-crypto.d.ts +2 -0
- package/dist/webhook-crypto.d.ts.map +1 -1
- package/dist/webhook-crypto.js +2 -2
- 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/webhooks/webhook-crypto.d.ts +2 -0
- package/dist/webhooks/webhook-crypto.d.ts.map +1 -1
- package/dist/webhooks/webhook-crypto.js +2 -2
- package/package.json +23 -15
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { tableName, ftsTableName, isValidColumn, indexableSearchBranches, SYSTEM_COLUMNS } from '../ddl/ddl.js';
|
|
2
|
+
import { resolveClassification } from '../privacy/policies.js';
|
|
3
|
+
import { isLocaleCode, isLocalizedBranch } from '../localization/localization.js';
|
|
4
|
+
/** SQLite GLOB twins of `LOCALE_CODE_RE` (`it`, `ast`, `pt-BR`, `es-419`, …). Keep both in sync. */
|
|
5
|
+
const LOCALE_KEY_GLOBS = [
|
|
6
|
+
'[a-z][a-z]',
|
|
7
|
+
'[a-z][a-z][a-z]',
|
|
8
|
+
'[a-z][a-z]-[A-Z][A-Z]',
|
|
9
|
+
'[a-z][a-z][a-z]-[A-Z][A-Z]',
|
|
10
|
+
'[a-z][a-z]-[0-9][0-9][0-9]',
|
|
11
|
+
'[a-z][a-z][a-z]-[0-9][0-9][0-9]',
|
|
12
|
+
];
|
|
13
|
+
/** A locale code checked for inlining into SQL. The grammar check IS the injection guard — never skip it. */
|
|
14
|
+
function inlineLocale(code) {
|
|
15
|
+
if (!isLocaleCode(code))
|
|
16
|
+
throw new TypeError(`Invalid locale code '${code}'`);
|
|
17
|
+
return code;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* SQL twin of `resolveLocalizedValue`: the value of a localized column in `locale.code`. Self-contained
|
|
21
|
+
* (no bindings), because `buildFilterCondition` may interpolate a column more than once per clause.
|
|
22
|
+
* The nested CASE is deliberate: SQLite does not guarantee AND short-circuits, and `json_type` /
|
|
23
|
+
* `json_each` raise on malformed JSON (a legacy plain-text value).
|
|
24
|
+
* Dictionary detection mirrors `isStoredLocaleDictionary`: a non-empty object whose keys all match the
|
|
25
|
+
* locale grammar, and — for `json` only — at least one registered locale key.
|
|
26
|
+
*/
|
|
27
|
+
function localizedColumnSql(column, branch, locale) {
|
|
28
|
+
const keyIsLocale = LOCALE_KEY_GLOBS.map((glob) => `key GLOB '${glob}'`).join(' OR ');
|
|
29
|
+
const conditions = [
|
|
30
|
+
`json_type(${column}) = 'object'`,
|
|
31
|
+
`EXISTS (SELECT 1 FROM json_each(${column}))`,
|
|
32
|
+
`NOT EXISTS (SELECT 1 FROM json_each(${column}) WHERE NOT (${keyIsLocale}))`,
|
|
33
|
+
];
|
|
34
|
+
if (branch.type === 'json') {
|
|
35
|
+
const registered = locale.config.locales.map((code) => `'${inlineLocale(code)}'`).join(', ');
|
|
36
|
+
conditions.push(`EXISTS (SELECT 1 FROM json_each(${column}) WHERE key IN (${registered}))`);
|
|
37
|
+
}
|
|
38
|
+
const path = (code) => `'$."${inlineLocale(code)}"'`;
|
|
39
|
+
const resolved = `COALESCE(` +
|
|
40
|
+
`NULLIF(json_extract(${column}, ${path(locale.code)}), ''), ` +
|
|
41
|
+
`NULLIF(json_extract(${column}, ${path(locale.config.defaultLocale)}), ''), ` +
|
|
42
|
+
`(SELECT value FROM json_each(${column}) WHERE value IS NOT NULL AND value != '' LIMIT 1))`;
|
|
43
|
+
return `(CASE WHEN json_valid(${column}) THEN ` +
|
|
44
|
+
`(CASE WHEN ${conditions.join(' AND ')} THEN ${resolved} ELSE ${column} END) ` +
|
|
45
|
+
`ELSE ${column} END)`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The SQL reference for `alias` in WHERE / ORDER BY: table-qualified system column, localized expression
|
|
49
|
+
* when `locale` is set, bare alias otherwise (unchanged pre-localization behaviour).
|
|
50
|
+
*/
|
|
51
|
+
function columnSql(seed, table, alias, locale) {
|
|
52
|
+
if (SYSTEM_COLUMNS.has(alias))
|
|
53
|
+
return `${table}.${alias}`;
|
|
54
|
+
const branch = seed.branches.find((b) => b.alias === alias);
|
|
55
|
+
if (locale && branch && isLocalizedBranch(branch)) {
|
|
56
|
+
return localizedColumnSql(`${table}.${alias}`, branch, locale);
|
|
57
|
+
}
|
|
58
|
+
return alias;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Builds a parameterized SQL SELECT query and bindings for a given Seed based on search, filtering, status, and ordering options.
|
|
62
|
+
*
|
|
63
|
+
* If `options.isCount` is set to `true`, it generates a counting query (`COUNT(*) as total`) rather than returning database rows.
|
|
64
|
+
* In count mode, column projections (`fields`), ordering (`orderBy` / `kanbanOrder`), and pagination (`LIMIT` / `OFFSET`) clauses and
|
|
65
|
+
* bindings are omitted, while join and filtering clauses are preserved.
|
|
66
|
+
*
|
|
67
|
+
* @param seed The seed definition.
|
|
68
|
+
* @param options Query configuration options.
|
|
69
|
+
* @returns The SQL query string and bindings.
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const { sql, bindings } = buildSelectQuery(postSeed, {
|
|
73
|
+
* status: 'published',
|
|
74
|
+
* filters: [{ column: 'title', type: 'text', conditions: [{ op: 'contains', value: 'cms' }] }],
|
|
75
|
+
* orderBy: { column: 'created_at', dir: 'DESC' },
|
|
76
|
+
* pagination: { limit: 20, offset: 0 },
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export function buildSelectQuery(seed, options = {}) {
|
|
81
|
+
const table = tableName(seed);
|
|
82
|
+
const { filters = [], orderBy, pagination, status, search, fields } = options;
|
|
83
|
+
const bindings = [];
|
|
84
|
+
const whereClauses = [];
|
|
85
|
+
let joinClause = '';
|
|
86
|
+
let kanbanOrderClause = '';
|
|
87
|
+
if (options.kanbanOrder) {
|
|
88
|
+
joinClause = `LEFT JOIN kanban_positions kp` +
|
|
89
|
+
` ON kp.seed_slug = ? AND kp.entry_id = ${table}.id AND kp.axis_branch_id = ?`;
|
|
90
|
+
bindings.push(options.kanbanOrder.seedSlug, options.kanbanOrder.axisBranchId);
|
|
91
|
+
kanbanOrderClause = ` ORDER BY (kp.position IS NULL) ASC, kp.position ASC`;
|
|
92
|
+
}
|
|
93
|
+
const rtBranches = indexableSearchBranches(seed);
|
|
94
|
+
if (search && rtBranches.length > 0) {
|
|
95
|
+
const ftsTable = ftsTableName(seed);
|
|
96
|
+
joinClause += (joinClause ? ' ' : '') + `INNER JOIN ${ftsTable} ON ${ftsTable}.entry_id = ${table}.id`;
|
|
97
|
+
whereClauses.push(`${ftsTable} MATCH ?`);
|
|
98
|
+
bindings.push(`"${search.replace(/"/g, '""')}"*`);
|
|
99
|
+
}
|
|
100
|
+
if (status !== undefined && status !== null) {
|
|
101
|
+
whereClauses.push(`${table}.status = ?`);
|
|
102
|
+
bindings.push(status);
|
|
103
|
+
}
|
|
104
|
+
// Soft delete: 'active' is the default, so a caller that forgets the option can never
|
|
105
|
+
// observe a trashed row — including the Public API, relation expansion and subqueries,
|
|
106
|
+
// which all reach SQL only through repository.findMany.
|
|
107
|
+
if (seed.softDelete) {
|
|
108
|
+
const trashed = options.trashed ?? 'active';
|
|
109
|
+
if (trashed === 'active') {
|
|
110
|
+
whereClauses.push(`${table}.deleted_at IS NULL`);
|
|
111
|
+
}
|
|
112
|
+
else if (trashed === 'trashed') {
|
|
113
|
+
whereClauses.push(`${table}.deleted_at IS NOT NULL`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const groupClauses = [];
|
|
117
|
+
for (const group of filters) {
|
|
118
|
+
if (!isValidColumn(seed, group.column))
|
|
119
|
+
continue;
|
|
120
|
+
const branch = seed.branches.find(b => b.alias === group.column);
|
|
121
|
+
const isEncrypted = branch ? resolveClassification(branch).storage === 'encrypt' : false;
|
|
122
|
+
const col = columnSql(seed, table, group.column, options.locale);
|
|
123
|
+
const condClauses = [];
|
|
124
|
+
for (const cond of group.conditions) {
|
|
125
|
+
if (isEncrypted) {
|
|
126
|
+
const { op } = cond;
|
|
127
|
+
if (op === 'eq' || op === 'neq' || op === 'in' || op === 'not_in') {
|
|
128
|
+
const targetCol = `${table}.${group.column}_bidx`;
|
|
129
|
+
const clause = buildFilterCondition(targetCol, group.type, cond, bindings);
|
|
130
|
+
if (clause)
|
|
131
|
+
condClauses.push(clause);
|
|
132
|
+
}
|
|
133
|
+
else if (op === 'is_empty' || op === 'is_not_empty') {
|
|
134
|
+
const clause = buildFilterCondition(col, group.type, cond, bindings);
|
|
135
|
+
if (clause)
|
|
136
|
+
condClauses.push(clause);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const clause = buildFilterCondition(col, group.type, cond, bindings);
|
|
141
|
+
if (clause)
|
|
142
|
+
condClauses.push(clause);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (condClauses.length > 0) {
|
|
146
|
+
groupClauses.push(condClauses.length > 1 ? `(${condClauses.join(' AND ')})` : condClauses[0]);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (groupClauses.length > 0) {
|
|
150
|
+
const joiner = options.filterLogic === 'OR' ? ' OR ' : ' AND ';
|
|
151
|
+
whereClauses.push(groupClauses.length > 1 ? `(${groupClauses.join(joiner)})` : groupClauses[0]);
|
|
152
|
+
}
|
|
153
|
+
let selectCols = options.isCount ? 'COUNT(*) as total' : `${table}.*`;
|
|
154
|
+
if (!options.isCount && fields && fields.length > 0) {
|
|
155
|
+
const valid = fields.filter(f => isValidColumn(seed, f));
|
|
156
|
+
if (valid.length > 0) {
|
|
157
|
+
selectCols = valid
|
|
158
|
+
.map(f => (SYSTEM_COLUMNS.has(f) ? `${table}.${f}` : f))
|
|
159
|
+
.join(', ');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!options.isCount && options.kanbanOrder)
|
|
163
|
+
selectCols += ', kp.position';
|
|
164
|
+
let sql = `SELECT ${selectCols} FROM ${table}`;
|
|
165
|
+
if (joinClause)
|
|
166
|
+
sql += ` ${joinClause}`;
|
|
167
|
+
if (whereClauses.length > 0)
|
|
168
|
+
sql += ` WHERE ${whereClauses.join(' AND ')}`;
|
|
169
|
+
if (!options.isCount) {
|
|
170
|
+
if (kanbanOrderClause) {
|
|
171
|
+
sql += kanbanOrderClause;
|
|
172
|
+
}
|
|
173
|
+
else if (orderBy && isValidColumn(seed, orderBy.column)) {
|
|
174
|
+
const dir = orderBy.dir === 'DESC' ? 'DESC' : 'ASC';
|
|
175
|
+
const col = columnSql(seed, table, orderBy.column, options.locale);
|
|
176
|
+
sql += ` ORDER BY ${col} ${dir}`;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
sql += ` ORDER BY ${table}.created_at DESC`;
|
|
180
|
+
}
|
|
181
|
+
if (pagination) {
|
|
182
|
+
sql += ` LIMIT ? OFFSET ?`;
|
|
183
|
+
bindings.push(pagination.limit, pagination.offset);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { sql, bindings };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Normalizes user-provided filter values to a format appropriate for SQL execution.
|
|
190
|
+
*
|
|
191
|
+
* @param type The type of the filter.
|
|
192
|
+
* @param value The value to normalize.
|
|
193
|
+
* @returns The normalized value, or null.
|
|
194
|
+
*/
|
|
195
|
+
function normalizeFilterValue(type, value) {
|
|
196
|
+
if (value === null || value === undefined)
|
|
197
|
+
return null;
|
|
198
|
+
if (type === 'date') {
|
|
199
|
+
if (typeof value === 'number')
|
|
200
|
+
return value;
|
|
201
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
202
|
+
const d = new Date(value);
|
|
203
|
+
return Number.isNaN(d.getTime()) ? null : Math.floor(d.getTime() / 1000);
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (type === 'boolean') {
|
|
208
|
+
return value ? 1 : 0;
|
|
209
|
+
}
|
|
210
|
+
if (type === 'number') {
|
|
211
|
+
if (typeof value === 'number')
|
|
212
|
+
return value;
|
|
213
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
214
|
+
const n = Number(value);
|
|
215
|
+
return Number.isNaN(n) ? null : n;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Translates a filter condition into its SQL clause and adds parameters to the bindings list.
|
|
223
|
+
*
|
|
224
|
+
* @param col The column name in the table.
|
|
225
|
+
* @param type The type of the filter.
|
|
226
|
+
* @param cond The condition containing the operator and value.
|
|
227
|
+
* @param bindings The array of bindings where parameterized values are appended.
|
|
228
|
+
* @returns The SQL condition fragment, or null if invalid.
|
|
229
|
+
*/
|
|
230
|
+
function buildFilterCondition(col, type, cond, bindings) {
|
|
231
|
+
const { op, value } = cond;
|
|
232
|
+
if (op === 'is_empty' || op === 'is_not_empty') {
|
|
233
|
+
const isEmpty = op === 'is_empty';
|
|
234
|
+
if (type === 'text') {
|
|
235
|
+
return isEmpty ? `(${col} IS NULL OR ${col} = '')` : `(${col} IS NOT NULL AND ${col} != '')`;
|
|
236
|
+
}
|
|
237
|
+
if (type === 'tags' || type === 'json') {
|
|
238
|
+
return isEmpty
|
|
239
|
+
? `(${col} IS NULL OR ${col} = '[]' OR ${col} = '{}')`
|
|
240
|
+
: `(${col} IS NOT NULL AND ${col} != '[]' AND ${col} != '{}')`;
|
|
241
|
+
}
|
|
242
|
+
return isEmpty ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
243
|
+
}
|
|
244
|
+
if (op === 'in' || op === 'not_in') {
|
|
245
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
246
|
+
return null;
|
|
247
|
+
const normalizedValues = value
|
|
248
|
+
.map((v) => normalizeFilterValue(type, v))
|
|
249
|
+
.filter((v) => v !== null);
|
|
250
|
+
if (normalizedValues.length === 0)
|
|
251
|
+
return null;
|
|
252
|
+
const placeholders = normalizedValues.map(() => '?').join(', ');
|
|
253
|
+
bindings.push(...normalizedValues);
|
|
254
|
+
return `${col} ${op === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
|
|
255
|
+
}
|
|
256
|
+
if (op === 'has_tag' || op === 'has_any_tag' || op === 'has_all_tags') {
|
|
257
|
+
if (type !== 'tags' && type !== 'json')
|
|
258
|
+
return null;
|
|
259
|
+
const tags = (op === 'has_tag' ? [value] : Array.isArray(value) ? value : [value])
|
|
260
|
+
.map(t => String(t));
|
|
261
|
+
if (tags.length === 0)
|
|
262
|
+
return null;
|
|
263
|
+
// json_each on array → value is the element; on object → key is the property name.
|
|
264
|
+
// Tags stored as ["photo"] (array) or {"photo":"#3b82f6"} (object) must both match.
|
|
265
|
+
const tagRef = `CASE json_type(${col}) WHEN 'array' THEN value ELSE key END`;
|
|
266
|
+
if (op === 'has_tag' || op === 'has_any_tag') {
|
|
267
|
+
const placeholders = tags.map(() => '?').join(', ');
|
|
268
|
+
bindings.push(...tags);
|
|
269
|
+
return `EXISTS (SELECT 1 FROM json_each(${col}) WHERE ${tagRef} IN (${placeholders}))`;
|
|
270
|
+
}
|
|
271
|
+
const clauses = tags.map(() => `EXISTS (SELECT 1 FROM json_each(${col}) WHERE ${tagRef} = ?)`);
|
|
272
|
+
bindings.push(...tags);
|
|
273
|
+
return `(${clauses.join(' AND ')})`;
|
|
274
|
+
}
|
|
275
|
+
const normalized = normalizeFilterValue(type, value);
|
|
276
|
+
if (normalized === null)
|
|
277
|
+
return null;
|
|
278
|
+
if (op === 'eq' || op === 'neq') {
|
|
279
|
+
const sqlOp = op === 'eq' ? '=' : '!=';
|
|
280
|
+
bindings.push(normalized);
|
|
281
|
+
return `${col} ${sqlOp} ?`;
|
|
282
|
+
}
|
|
283
|
+
if (op === 'contains' || op === 'not_contains' || op === 'starts_with' || op === 'ends_with') {
|
|
284
|
+
const sqlOp = op === 'not_contains' ? 'NOT LIKE' : 'LIKE';
|
|
285
|
+
let pattern = String(value);
|
|
286
|
+
if (op === 'contains' || op === 'not_contains')
|
|
287
|
+
pattern = `%${pattern}%`;
|
|
288
|
+
else if (op === 'starts_with')
|
|
289
|
+
pattern = `${pattern}%`;
|
|
290
|
+
else if (op === 'ends_with')
|
|
291
|
+
pattern = `%${pattern}`;
|
|
292
|
+
bindings.push(pattern);
|
|
293
|
+
return `${col} ${sqlOp} ?`;
|
|
294
|
+
}
|
|
295
|
+
const mathOps = { gt: '>', gte: '>=', lt: '<', lte: '<=' };
|
|
296
|
+
if (mathOps[op]) {
|
|
297
|
+
bindings.push(normalized);
|
|
298
|
+
return `${col} ${mathOps[op]} ?`;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../../../src/engine/query/relations.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Branch } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Serializes a value for writing to the DB.
|
|
4
|
+
* boolean → 0/1 | date → Unix timestamp | json/tags/richtext/repeater → JSON string
|
|
5
|
+
* Localized branches: locale dictionary ↔ compact JSON.
|
|
6
|
+
*
|
|
7
|
+
* @param branch The branch definition.
|
|
8
|
+
* @param value The value to serialize.
|
|
9
|
+
* @returns The serialized DB value (string, number, or null).
|
|
10
|
+
*/
|
|
11
|
+
export declare function serializeForDb(branch: Branch, value: unknown): string | number | null;
|
|
12
|
+
/**
|
|
13
|
+
* Deserializes a value read from the DB to its API/JS representation.
|
|
14
|
+
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object/array
|
|
15
|
+
* Localized branches: locale dictionary ↔ compact JSON.
|
|
16
|
+
*
|
|
17
|
+
* @param branch The branch definition.
|
|
18
|
+
* @param value The raw database value.
|
|
19
|
+
* @returns The deserialized value.
|
|
20
|
+
*/
|
|
21
|
+
export declare function deserializeFromDb(branch: Branch, value: unknown): unknown;
|
|
22
|
+
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../../src/engine/query/serialize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,MAAM,EAOP,MAAM,aAAa,CAAC;AAoErB;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CA6CrF;AAGD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CA0CzE"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { compactLocalizedDictionary, isLocaleDictionary, isLocalizedBranch } from '../localization/localization.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a branch represents a multiple asset list field.
|
|
4
|
+
*
|
|
5
|
+
* @param branch The branch definition.
|
|
6
|
+
* @returns True if it's an asset list, false otherwise.
|
|
7
|
+
*/
|
|
8
|
+
function isAssetListBranch(branch) {
|
|
9
|
+
return branch.type === 'file' && (branch.multiple === true || branch.format === 'asset-list');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes an unknown value to a valid HTTP URL string, or null.
|
|
13
|
+
*
|
|
14
|
+
* @param value The value to normalize.
|
|
15
|
+
* @returns The normalized URL string, or null if invalid.
|
|
16
|
+
*/
|
|
17
|
+
function normalizeHttpUrl(value) {
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return null;
|
|
20
|
+
const cleaned = value.trim();
|
|
21
|
+
if (!cleaned)
|
|
22
|
+
return null;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(cleaned);
|
|
25
|
+
return parsed.protocol.startsWith('http') ? cleaned : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Safely parses a JSON string, falling back to the raw string on error.
|
|
33
|
+
*
|
|
34
|
+
* @param value The string to parse.
|
|
35
|
+
* @returns The parsed object, or the original string.
|
|
36
|
+
*/
|
|
37
|
+
function parseJsonSafe(value) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Normalizes raw asset list values to a flat list of valid URL strings.
|
|
47
|
+
*
|
|
48
|
+
* @param rawValue The raw value (string, array, or object) to normalize.
|
|
49
|
+
* @returns An array of unique normalized URL strings.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeAssetListValue(rawValue) {
|
|
52
|
+
const input = typeof rawValue === 'string' ? parseJsonSafe(rawValue) : rawValue;
|
|
53
|
+
const values = Array.isArray(input) ? input : [input];
|
|
54
|
+
const normalized = [];
|
|
55
|
+
for (const item of values) {
|
|
56
|
+
if (item == null)
|
|
57
|
+
continue;
|
|
58
|
+
const direct = normalizeHttpUrl(item);
|
|
59
|
+
if (direct) {
|
|
60
|
+
normalized.push(direct);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (typeof item === 'object' && !Array.isArray(item)) {
|
|
64
|
+
const fromObj = normalizeHttpUrl(item.url);
|
|
65
|
+
if (fromObj)
|
|
66
|
+
normalized.push(fromObj);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...new Set(normalized)];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Serializes a value for writing to the DB.
|
|
73
|
+
* boolean → 0/1 | date → Unix timestamp | json/tags/richtext/repeater → JSON string
|
|
74
|
+
* Localized branches: locale dictionary ↔ compact JSON.
|
|
75
|
+
*
|
|
76
|
+
* @param branch The branch definition.
|
|
77
|
+
* @param value The value to serialize.
|
|
78
|
+
* @returns The serialized DB value (string, number, or null).
|
|
79
|
+
*/
|
|
80
|
+
export function serializeForDb(branch, value) {
|
|
81
|
+
if (value === null || value === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
// Must precede the type switch: its `text` case returns null for any object, which would silently
|
|
84
|
+
// erase a dictionary on a localized text branch.
|
|
85
|
+
if (isLocalizedBranch(branch) && isLocaleDictionary(value)) {
|
|
86
|
+
const compact = compactLocalizedDictionary(value);
|
|
87
|
+
return compact === null ? null : JSON.stringify(compact);
|
|
88
|
+
}
|
|
89
|
+
switch (branch.type) {
|
|
90
|
+
case 'boolean':
|
|
91
|
+
return value ? 1 : 0;
|
|
92
|
+
case 'json':
|
|
93
|
+
case 'tags':
|
|
94
|
+
case 'richtext':
|
|
95
|
+
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
96
|
+
case 'date': {
|
|
97
|
+
if (typeof value === 'number')
|
|
98
|
+
return value;
|
|
99
|
+
if (typeof value === 'string') {
|
|
100
|
+
const d = new Date(value);
|
|
101
|
+
if (Number.isNaN(d.getTime()))
|
|
102
|
+
return null;
|
|
103
|
+
if (branch.format === 'date') {
|
|
104
|
+
const midnight = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
105
|
+
return Math.floor(midnight / 1000);
|
|
106
|
+
}
|
|
107
|
+
return Math.floor(d.getTime() / 1000);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
case 'file':
|
|
112
|
+
if (isAssetListBranch(branch)) {
|
|
113
|
+
return Array.isArray(value) ? JSON.stringify(value) : typeof value === 'string' ? value : null;
|
|
114
|
+
}
|
|
115
|
+
return typeof value === 'string' ? value : null;
|
|
116
|
+
case 'repeater':
|
|
117
|
+
return JSON.stringify(Array.isArray(value) ? value : []);
|
|
118
|
+
default:
|
|
119
|
+
return typeof value === 'string' ? value : typeof value === 'number' ? value : null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Deserializes a value read from the DB to its API/JS representation.
|
|
124
|
+
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object/array
|
|
125
|
+
* Localized branches: locale dictionary ↔ compact JSON.
|
|
126
|
+
*
|
|
127
|
+
* @param branch The branch definition.
|
|
128
|
+
* @param value The raw database value.
|
|
129
|
+
* @returns The deserialized value.
|
|
130
|
+
*/
|
|
131
|
+
export function deserializeFromDb(branch, value) {
|
|
132
|
+
if (branch.type === 'repeater') {
|
|
133
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
134
|
+
return [];
|
|
135
|
+
const parsed = parseJsonSafe(value);
|
|
136
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
137
|
+
}
|
|
138
|
+
if (value === null || value === undefined)
|
|
139
|
+
return null;
|
|
140
|
+
// richtext/json already JSON.parse below; text must opt in. A legacy plain string written before the
|
|
141
|
+
// branch became localized is returned unchanged — the read-side fallback chain handles it.
|
|
142
|
+
if (branch.type === 'text' && isLocalizedBranch(branch) && typeof value === 'string' && value.startsWith('{')) {
|
|
143
|
+
const parsed = parseJsonSafe(value);
|
|
144
|
+
return isLocaleDictionary(parsed) ? parsed : value;
|
|
145
|
+
}
|
|
146
|
+
switch (branch.type) {
|
|
147
|
+
case 'boolean':
|
|
148
|
+
return value === 1 || value === true;
|
|
149
|
+
case 'json':
|
|
150
|
+
case 'tags':
|
|
151
|
+
case 'richtext': {
|
|
152
|
+
if (typeof value === 'string') {
|
|
153
|
+
try {
|
|
154
|
+
return JSON.parse(value);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
case 'date': {
|
|
163
|
+
if (typeof value !== 'number')
|
|
164
|
+
return null;
|
|
165
|
+
const d = new Date(value * 1000);
|
|
166
|
+
return branch.format === 'date' ? d.toISOString().slice(0, 10) : d.toISOString();
|
|
167
|
+
}
|
|
168
|
+
case 'file':
|
|
169
|
+
if (isAssetListBranch(branch))
|
|
170
|
+
return normalizeAssetListValue(value);
|
|
171
|
+
return typeof value === 'string' ? value : null;
|
|
172
|
+
default:
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Branch, Seed } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bumped ONLY when the projection below or the canonical byte format changes — i.e. when
|
|
4
|
+
* fingerprints computed by two builds are no longer comparable. It is carried in the fingerprint
|
|
5
|
+
* string itself so a version skew reads as a mismatch instead of as an accidental match.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SCHEMA_FINGERPRINT_VERSION = 1;
|
|
8
|
+
/** The response-shape-bearing subset of a `Branch`. */
|
|
9
|
+
export interface ContractBranch {
|
|
10
|
+
alias: string;
|
|
11
|
+
type: Branch['type'];
|
|
12
|
+
format?: Branch['format'];
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
options?: string[];
|
|
15
|
+
requiredOnCreate: boolean;
|
|
16
|
+
requiredOnUpdate: boolean;
|
|
17
|
+
targetSeed?: string;
|
|
18
|
+
/** From `resolvePolicies`, so an explicitly-written default hashes like an omitted one. */
|
|
19
|
+
visibility: 'full' | 'masked' | 'hidden';
|
|
20
|
+
publicRead: boolean;
|
|
21
|
+
publicEdit: boolean;
|
|
22
|
+
minItems?: number;
|
|
23
|
+
maxItems?: number;
|
|
24
|
+
/** Present only when true: the dictionary shape is part of the public contract (`?lang=all`, writes). */
|
|
25
|
+
localized?: boolean;
|
|
26
|
+
/** Repeater sub-fields, same projection, recursively. */
|
|
27
|
+
fields?: ContractBranch[];
|
|
28
|
+
}
|
|
29
|
+
/** The response-shape-bearing subset of a `Seed`. */
|
|
30
|
+
export interface ContractSeed {
|
|
31
|
+
slug: string;
|
|
32
|
+
displayNameAlias: string;
|
|
33
|
+
allowDrafts: boolean;
|
|
34
|
+
allowPublicRead: boolean;
|
|
35
|
+
allowPublicPost: boolean;
|
|
36
|
+
allowPublicEdit: boolean;
|
|
37
|
+
branches: ContractBranch[];
|
|
38
|
+
}
|
|
39
|
+
/** The exact value that gets canonicalized and hashed. */
|
|
40
|
+
export interface SchemaContract {
|
|
41
|
+
fingerprintVersion: number;
|
|
42
|
+
seeds: ContractSeed[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reduces seeds to what a response shape depends on.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately EXCLUDED, and each for a reason a future reader must not "fix":
|
|
48
|
+
* - `id` (`br_XX`) — a stable internal handle; it never appears in a payload, and minting order
|
|
49
|
+
* differs between an environment built by migration and one grown through the dashboard.
|
|
50
|
+
* - `label`, `labelPlural`, `hint`, `dashboard`, `layout` — presentation, never response shape.
|
|
51
|
+
* - `retentionDays`, `onDelete`, `policies.search|filter|sort`, `numberOptions`, `fileOptions`,
|
|
52
|
+
* `policies.classification|privacy` — behaviour and storage concerns that leave the response
|
|
53
|
+
* shape identical. (`privacy` DOES change a stored value's encoding, but the API type stays
|
|
54
|
+
* `string`; drift there is a data concern, not a type concern.)
|
|
55
|
+
* - anything physical (column order, index names) — see the VETO Audit.
|
|
56
|
+
*
|
|
57
|
+
* Seeds are sorted by slug; BRANCH ORDER IS PRESERVED, because it is the declared field order the
|
|
58
|
+
* generated types and the dashboard form both follow.
|
|
59
|
+
*/
|
|
60
|
+
export declare function projectSchemaContract(seeds: Seed[]): SchemaContract;
|
|
61
|
+
/**
|
|
62
|
+
* `v{VERSION}:{32 lowercase hex}` — SHA-256 over the canonical JSON of the contract projection,
|
|
63
|
+
* truncated to 128 bits.
|
|
64
|
+
*
|
|
65
|
+
* Truncation is deliberate: this value travels on EVERY public response as `X-Schema-Revision`,
|
|
66
|
+
* and 128 bits of a SHA-256 digest carries no realistic collision risk for a value space of a few
|
|
67
|
+
* hundred schema revisions. It is a revision marker, never a security token.
|
|
68
|
+
*
|
|
69
|
+
* Web Crypto only — the same API the Worker and Node ≥ 18 both expose, as `webhook-crypto.ts`
|
|
70
|
+
* already relies on. `@beechcms/core` gains no dependency.
|
|
71
|
+
*/
|
|
72
|
+
export declare function computeSchemaFingerprint(seeds: Seed[]): Promise<string>;
|
|
73
|
+
//# sourceMappingURL=schema-fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-fingerprint.d.ts","sourceRoot":"","sources":["../../src/engine/schema-fingerprint.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE9C;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,gBAAgB,EAAE,OAAO,CAAA;IACzB,gBAAgB,EAAE,OAAO,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2FAA2F;IAC3F,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACxC,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yGAAyG;IACzG,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,yDAAyD;IACzD,MAAM,CAAC,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,qDAAqD;AACrD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,gBAAgB,EAAE,MAAM,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,eAAe,EAAE,OAAO,CAAA;IACxB,eAAe,EAAE,OAAO,CAAA;IACxB,eAAe,EAAE,OAAO,CAAA;IACxB,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,kBAAkB,EAAE,MAAM,CAAA;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB;AAyBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,cAAc,CAenE;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7E"}
|