@beechcms/core 0.8.1 → 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/antivirus/noop-antivirus.provider.d.ts.map +1 -1
- package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -1
- package/dist/auth/account-email.d.ts +3 -0
- package/dist/auth/account-email.d.ts.map +1 -0
- package/dist/auth/account-email.js +9 -0
- package/dist/auth/password-reset-token.repository.d.ts +13 -2
- package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
- package/dist/auth/user.repository.d.ts +69 -4
- package/dist/auth/user.repository.d.ts.map +1 -1
- package/dist/common/canonical-json.d.ts +2 -1
- package/dist/common/canonical-json.d.ts.map +1 -1
- package/dist/common/canonical-json.js +5 -3
- package/dist/content/content-scan.repository.d.ts +1 -1
- package/dist/content/content.repository.d.ts +156 -5
- package/dist/content/content.repository.d.ts.map +1 -1
- package/dist/content/content.repository.js +62 -0
- package/dist/content/seed-media-purge.repository.d.ts +34 -0
- package/dist/content/seed-media-purge.repository.d.ts.map +1 -0
- package/dist/content/seed.repository.d.ts +14 -0
- package/dist/content/seed.repository.d.ts.map +1 -1
- package/dist/dashboard-layout/content-view.d.ts +757 -0
- package/dist/dashboard-layout/content-view.d.ts.map +1 -0
- package/dist/dashboard-layout/content-view.js +285 -0
- package/dist/dashboard-layout/content-view.repository.d.ts +39 -0
- package/dist/dashboard-layout/content-view.repository.d.ts.map +1 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts +5 -0
- package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/dashboard-layout.js +17 -9
- package/dist/dashboard-layout/kanban/kanban.d.ts +32 -18
- package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -1
- package/dist/dashboard-layout/kanban/kanban.js +45 -21
- package/dist/dashboard-layout/seed-layout.d.ts +22 -31
- package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
- package/dist/dashboard-layout/seed-layout.js +93 -18
- package/dist/dashboard-layout/seed-layout.repository.d.ts +1 -5
- package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -1
- package/dist/engine/ddl/ddl.d.ts +31 -0
- package/dist/engine/ddl/ddl.d.ts.map +1 -1
- package/dist/engine/ddl/ddl.js +92 -13
- package/dist/engine/ddl/schema-mutator.d.ts +3 -0
- package/dist/engine/ddl/schema-mutator.d.ts.map +1 -1
- package/dist/engine/ddl/seed-purge-guards.d.ts +7 -0
- package/dist/engine/ddl/seed-purge-guards.d.ts.map +1 -0
- package/dist/engine/ddl/seed-purge-guards.js +19 -0
- package/dist/engine/engine.d.ts +4 -3
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +4 -3
- package/dist/engine/introspection/relation-audit.d.ts +46 -0
- package/dist/engine/introspection/relation-audit.d.ts.map +1 -0
- package/dist/engine/introspection/relation-audit.js +103 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts +2 -1
- package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -1
- package/dist/engine/introspection/schema-fingerprint.js +8 -3
- package/dist/engine/privacy/policies.d.ts +1 -1
- package/dist/engine/privacy/policies.d.ts.map +1 -1
- package/dist/engine/privacy/policies.js +12 -6
- package/dist/engine/privacy/privacy.service.d.ts +13 -0
- package/dist/engine/privacy/privacy.service.d.ts.map +1 -1
- package/dist/engine/privacy/privacy.service.js +24 -0
- package/dist/engine/query/active-clause.d.ts +19 -0
- package/dist/engine/query/active-clause.d.ts.map +1 -0
- package/dist/engine/query/active-clause.js +27 -0
- package/dist/engine/query/query.d.ts +14 -1
- package/dist/engine/query/query.d.ts.map +1 -1
- package/dist/engine/query/query.js +105 -13
- package/dist/engine/seeds/candidate-ids.d.ts +14 -0
- package/dist/engine/seeds/candidate-ids.d.ts.map +1 -0
- package/dist/engine/seeds/candidate-ids.js +40 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts +38 -0
- package/dist/engine/seeds/seed-definition-diff.d.ts.map +1 -0
- package/dist/engine/seeds/seed-definition-diff.js +51 -0
- package/dist/engine/seeds/seed-registry.d.ts +17 -0
- package/dist/engine/seeds/seed-registry.d.ts.map +1 -1
- package/dist/engine/seeds/seed-registry.js +27 -0
- package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -1
- package/dist/engine/seeds/seed-types-generator.js +4 -5
- package/dist/engine/seeds/seed-validation.d.ts +5 -0
- package/dist/engine/seeds/seed-validation.d.ts.map +1 -1
- package/dist/engine/seeds/seed-validation.js +39 -19
- package/dist/engine/seeds/tag-migration.d.ts +73 -0
- package/dist/engine/seeds/tag-migration.d.ts.map +1 -0
- package/dist/engine/seeds/tag-migration.js +212 -0
- package/dist/engine/seeds/tag-options.d.ts +15 -0
- package/dist/engine/seeds/tag-options.d.ts.map +1 -0
- package/dist/engine/seeds/tag-options.js +111 -0
- package/dist/engine/seeds/tag-value.d.ts +31 -0
- package/dist/engine/seeds/tag-value.d.ts.map +1 -0
- package/dist/engine/seeds/tag-value.js +51 -0
- package/dist/engine/types.d.ts +59 -7
- package/dist/engine/types.d.ts.map +1 -1
- package/dist/engine/validation/cache.d.ts.map +1 -1
- package/dist/engine/validation/cache.js +20 -4
- package/dist/engine/validation/index.d.ts +12 -1
- package/dist/engine/validation/index.d.ts.map +1 -1
- package/dist/engine/validation/index.js +29 -33
- package/dist/engine/validation/primitives.d.ts +12 -0
- package/dist/engine/validation/primitives.d.ts.map +1 -1
- package/dist/engine/validation/primitives.js +22 -0
- package/dist/engine/validation/richtext-sanitizer.d.ts +9 -1
- package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
- package/dist/engine/validation/richtext-sanitizer.js +36 -7
- package/dist/engine/validation/schema-builders.d.ts +16 -0
- package/dist/engine/validation/schema-builders.d.ts.map +1 -1
- package/dist/engine/validation/schema-builders.js +125 -18
- package/dist/index.d.ts +34 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -11
- package/dist/media/file-types.d.ts +12 -0
- package/dist/media/file-types.d.ts.map +1 -1
- package/dist/media/file-types.js +24 -4
- package/dist/media/magic-bytes.d.ts.map +1 -1
- package/dist/media/magic-bytes.js +19 -9
- package/dist/media/media-transform.d.ts +8 -4
- package/dist/media/media-transform.d.ts.map +1 -1
- package/dist/media/media-transform.js +10 -6
- package/dist/media/media.repository.d.ts +5 -0
- package/dist/media/media.repository.d.ts.map +1 -1
- package/dist/notifications/notification-service.d.ts +1 -1
- package/dist/notifications/notification.repository.d.ts +11 -2
- package/dist/notifications/notification.repository.d.ts.map +1 -1
- package/dist/oauth/authorization-code.repository.d.ts +10 -5
- package/dist/oauth/authorization-code.repository.d.ts.map +1 -1
- package/dist/oauth/pkce.d.ts +1 -1
- package/dist/oauth/role-guard.d.ts +30 -11
- package/dist/oauth/role-guard.d.ts.map +1 -1
- package/dist/oauth/role-guard.js +27 -5
- package/dist/oauth/scopes.d.ts +1 -1
- package/dist/oauth/scopes.d.ts.map +1 -1
- package/dist/oauth/token.repository.d.ts +2 -0
- package/dist/oauth/token.repository.d.ts.map +1 -1
- package/dist/queue/queue.interface.d.ts +9 -1
- package/dist/queue/queue.interface.d.ts.map +1 -1
- package/dist/queue/queue.stub.d.ts +1 -1
- package/dist/queue/queue.stub.d.ts.map +1 -1
- package/dist/queue/queue.stub.js +2 -3
- package/dist/queue/scheduler.stub.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts +7 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
- package/dist/rate-limit/token-bucket-rate-limiter.js +66 -0
- package/dist/rbac/invitation.repository.d.ts +28 -10
- package/dist/rbac/invitation.repository.d.ts.map +1 -1
- package/dist/rbac/types.d.ts +40 -28
- package/dist/rbac/types.d.ts.map +1 -1
- package/dist/rbac/types.js +2 -1
- package/dist/schema/manifest-seeds.js +3 -3
- package/dist/search/search.repository.d.ts +5 -4
- package/dist/search/search.repository.d.ts.map +1 -1
- package/dist/search/vector-extractor.d.ts.map +1 -1
- package/dist/search/vector-extractor.js +23 -2
- package/dist/search/vector.repository.d.ts +2 -1
- package/dist/search/vector.repository.d.ts.map +1 -1
- package/dist/security/time-trap-token.repository.d.ts +5 -3
- package/dist/security/time-trap-token.repository.d.ts.map +1 -1
- package/dist/settings/site-settings.repository.d.ts +8 -0
- package/dist/settings/site-settings.repository.d.ts.map +1 -1
- package/dist/transfer/flat-seed.d.ts.map +1 -1
- package/dist/transfer/flat-seed.js +6 -1
- package/dist/transfer/row-mapping.d.ts.map +1 -1
- package/dist/transfer/row-mapping.js +9 -2
- package/package.json +9 -4
- package/dist/.tsbuildinfo +0 -1
- 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.runner.interface.d.ts +0 -10
- package/dist/automations.runner.interface.d.ts.map +0 -1
- package/dist/automations.runner.stub.d.ts +0 -5
- package/dist/automations.runner.stub.d.ts.map +0 -1
- package/dist/automations.runner.stub.js +0 -7
- package/dist/automations.types.d.ts +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/{automations.repository.interface.js → content/seed-media-purge.repository.js} +0 -0
- /package/dist/{automations.runner.interface.js → dashboard-layout/content-view.repository.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relation-audit.d.ts","sourceRoot":"","sources":["../../../src/engine/introspection/relation-audit.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAU,IAAI,EAAE,MAAM,aAAa,CAAA;AAE/C,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAA;AAE3B,6DAA6D;AAC7D,MAAM,MAAM,oBAAoB;AAC9B,kFAAkF;AAChF,eAAe;AACjB,2DAA2D;GACzD,iBAAiB;AACnB,4DAA4D;GAC1D,oBAAoB;AACtB,qEAAqE;GACnE,uBAAuB,CAAA;AAE3B,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,oBAAoB,CAAA;IAC3B,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAA;IAChB,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAA;CACrB;AAkGD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,IAAI,EAAE,GACZ,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAiBnC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { junctionTableName } from '../ddl/ddl.js';
|
|
4
|
+
import { assertSafeIdentifier, introspectTable, } from './introspection.js';
|
|
5
|
+
/** Default `ON DELETE` rule, per storage shape (mirrors `engine/ddl`). */
|
|
6
|
+
function effectiveOnDelete(branch) {
|
|
7
|
+
return (branch.onDelete ?? (branch.multiple === true ? 'CASCADE' : 'SET NULL')).toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
async function countRows(executor, sql) {
|
|
10
|
+
const rows = await executor.all(sql);
|
|
11
|
+
return Number(rows[0]?.n ?? 0);
|
|
12
|
+
}
|
|
13
|
+
/** Checks the FK that carries the reference: the `alias` column, or the junction's `target_id`. */
|
|
14
|
+
function fkFindings(seed, branch, fk, location) {
|
|
15
|
+
const expectedTarget = `content_${branch.targetSeed}`;
|
|
16
|
+
const expectedRule = effectiveOnDelete(branch);
|
|
17
|
+
const base = { seed: seed.slug, alias: branch.alias, strandedRows: 0 };
|
|
18
|
+
const expected = `${location} -> ${expectedTarget}(id) ON DELETE ${expectedRule}`;
|
|
19
|
+
if (!fk) {
|
|
20
|
+
return [{ ...base, issue: 'fk_target_mismatch', expected, actual: `${location} has no foreign key` }];
|
|
21
|
+
}
|
|
22
|
+
const actual = `${location} -> ${fk.targetTable}(${fk.targetColumn}) ON DELETE ${fk.onDelete}`;
|
|
23
|
+
const findings = [];
|
|
24
|
+
if (fk.targetTable !== expectedTarget)
|
|
25
|
+
findings.push({ ...base, issue: 'fk_target_mismatch', expected, actual });
|
|
26
|
+
if (fk.onDelete !== expectedRule)
|
|
27
|
+
findings.push({ ...base, issue: 'fk_on_delete_mismatch', expected, actual });
|
|
28
|
+
return findings;
|
|
29
|
+
}
|
|
30
|
+
async function auditBranch(executor, seed, branch, contentColumns, contentForeignKeys) {
|
|
31
|
+
const isMultiple = branch.multiple === true;
|
|
32
|
+
const junction = junctionTableName(seed.slug, branch.alias);
|
|
33
|
+
const draftJunction = `${junction}_drafts`;
|
|
34
|
+
const alias = assertSafeIdentifier(branch.alias);
|
|
35
|
+
const base = { seed: seed.slug, alias: branch.alias };
|
|
36
|
+
const liveJunction = await introspectTable(executor, junction);
|
|
37
|
+
const liveDraftJunction = await introspectTable(executor, draftJunction);
|
|
38
|
+
const hasColumn = contentColumns.has(alias);
|
|
39
|
+
if (isMultiple) {
|
|
40
|
+
if (hasColumn) {
|
|
41
|
+
const strandedRows = await countRows(executor, `SELECT COUNT(*) AS n FROM content_${seed.slug} WHERE "${alias}" IS NOT NULL`);
|
|
42
|
+
return [{
|
|
43
|
+
...base,
|
|
44
|
+
issue: 'wrong_storage',
|
|
45
|
+
expected: `junction ${junction}`,
|
|
46
|
+
actual: `${liveJunction.exists ? `junction ${junction} and ` : ''}column content_${seed.slug}.${alias}`,
|
|
47
|
+
strandedRows,
|
|
48
|
+
}];
|
|
49
|
+
}
|
|
50
|
+
if (!liveJunction.exists) {
|
|
51
|
+
return [{ ...base, issue: 'missing_storage', expected: `junction ${junction}`, actual: 'no junction table', strandedRows: 0 }];
|
|
52
|
+
}
|
|
53
|
+
const findings = fkFindings(seed, branch, liveJunction.foreignKeys.find(fk => fk.column === 'target_id'), `${junction}.target_id`);
|
|
54
|
+
if (seed.allowDrafts && !liveDraftJunction.exists) {
|
|
55
|
+
findings.push({ ...base, issue: 'missing_storage', expected: `drafts junction ${draftJunction}`, actual: 'no drafts junction table', strandedRows: 0 });
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
// Single relation: the value lives in a column; any junction left behind holds stranded links.
|
|
60
|
+
if (liveJunction.exists || liveDraftJunction.exists) {
|
|
61
|
+
let strandedRows = 0;
|
|
62
|
+
if (liveJunction.exists)
|
|
63
|
+
strandedRows += await countRows(executor, `SELECT COUNT(*) AS n FROM ${junction}`);
|
|
64
|
+
if (liveDraftJunction.exists)
|
|
65
|
+
strandedRows += await countRows(executor, `SELECT COUNT(*) AS n FROM ${draftJunction}`);
|
|
66
|
+
return [{
|
|
67
|
+
...base,
|
|
68
|
+
issue: 'wrong_storage',
|
|
69
|
+
expected: `column content_${seed.slug}.${alias}`,
|
|
70
|
+
actual: `${hasColumn ? `column content_${seed.slug}.${alias} and ` : ''}junction ${junction}`,
|
|
71
|
+
strandedRows,
|
|
72
|
+
}];
|
|
73
|
+
}
|
|
74
|
+
if (!hasColumn) {
|
|
75
|
+
return [{ ...base, issue: 'missing_storage', expected: `column content_${seed.slug}.${alias}`, actual: 'no column', strandedRows: 0 }];
|
|
76
|
+
}
|
|
77
|
+
return fkFindings(seed, branch, contentForeignKeys.get(alias), `content_${seed.slug}.${alias}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Compares every relation branch of the given (active) seeds against the physical tables.
|
|
81
|
+
*
|
|
82
|
+
* Seeds whose `content_<slug>` table is missing are skipped: that is plain schema drift and is
|
|
83
|
+
* reported by the schema diff, not a relation-storage mismatch. Findings are ordered by seed, then
|
|
84
|
+
* branch order, so the report is deterministic.
|
|
85
|
+
*/
|
|
86
|
+
export async function auditRelationStorage(executor, seeds) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
const ordered = [...seeds].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
89
|
+
// Sequential on purpose: see `introspectSchema`.
|
|
90
|
+
for (const seed of ordered) {
|
|
91
|
+
const content = await introspectTable(executor, `content_${assertSafeIdentifier(seed.slug)}`);
|
|
92
|
+
if (!content.exists)
|
|
93
|
+
continue;
|
|
94
|
+
const columns = new Set(content.columns.map(column => column.name));
|
|
95
|
+
const foreignKeys = new Map(content.foreignKeys.map(fk => [fk.column, fk]));
|
|
96
|
+
for (const branch of seed.branches) {
|
|
97
|
+
if (branch.type !== 'relation' || !branch.targetSeed)
|
|
98
|
+
continue;
|
|
99
|
+
findings.push(...await auditBranch(executor, seed, branch, columns, foreignKeys));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { Branch, Seed } from '../types.js';
|
|
|
4
4
|
* fingerprints computed by two builds are no longer comparable. It is carried in the fingerprint
|
|
5
5
|
* string itself so a version skew reads as a mismatch instead of as an accidental match.
|
|
6
6
|
*/
|
|
7
|
-
export declare const SCHEMA_FINGERPRINT_VERSION =
|
|
7
|
+
export declare const SCHEMA_FINGERPRINT_VERSION = 2;
|
|
8
8
|
/** The response-shape-bearing subset of a `Branch`. */
|
|
9
9
|
export interface ContractBranch {
|
|
10
10
|
alias: string;
|
|
@@ -52,6 +52,7 @@ export interface SchemaContract {
|
|
|
52
52
|
* `policies.classification|privacy` — behaviour and storage concerns that leave the response
|
|
53
53
|
* shape identical. (`privacy` DOES change a stored value's encoding, but the API type stays
|
|
54
54
|
* `string`; drift there is a data concern, not a type concern.)
|
|
55
|
+
* - option `color` — presentation only; a color edit cannot change a response byte.
|
|
55
56
|
* - anything physical (column order, index names) — see the VETO Audit.
|
|
56
57
|
*
|
|
57
58
|
* Seeds are sorted by slug; BRANCH ORDER IS PRESERVED, because it is the declared field order the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-fingerprint.d.ts","sourceRoot":"","sources":["../../../src/engine/introspection/schema-fingerprint.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"schema-fingerprint.d.ts","sourceRoot":"","sources":["../../../src/engine/introspection/schema-fingerprint.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAG/C;;;;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;AA4BD;;;;;;;;;;;;;;;;GAgBG;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"}
|
|
@@ -16,21 +16,25 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { canonicalStringify } from '../../common/canonical-json.js';
|
|
18
18
|
import { resolvePolicies } from '../privacy/policies.js';
|
|
19
|
+
import { isSingleChoiceTags, tagOptionValues } from '../seeds/tag-options.js';
|
|
19
20
|
/**
|
|
20
21
|
* Bumped ONLY when the projection below or the canonical byte format changes — i.e. when
|
|
21
22
|
* fingerprints computed by two builds are no longer comparable. It is carried in the fingerprint
|
|
22
23
|
* string itself so a version skew reads as a mismatch instead of as an accidental match.
|
|
23
24
|
*/
|
|
24
|
-
export const SCHEMA_FINGERPRINT_VERSION =
|
|
25
|
+
export const SCHEMA_FINGERPRINT_VERSION = 2;
|
|
25
26
|
function projectBranch(branch) {
|
|
26
27
|
const policies = resolvePolicies(branch);
|
|
27
28
|
return {
|
|
28
29
|
alias: branch.alias,
|
|
29
30
|
type: branch.type,
|
|
30
31
|
...(branch.format !== undefined ? { format: branch.format } : {}),
|
|
31
|
-
|
|
32
|
+
// `tags` always carry the resolved mode, so an omitted `multiple` hashes like `true`.
|
|
33
|
+
...(branch.type === 'tags'
|
|
34
|
+
? { multiple: !isSingleChoiceTags(branch) }
|
|
35
|
+
: branch.multiple !== undefined ? { multiple: branch.multiple } : {}),
|
|
32
36
|
// Option order is the contract: it is the order a select renders and a union type lists.
|
|
33
|
-
...(branch.options !== undefined ? { options:
|
|
37
|
+
...(branch.options !== undefined ? { options: tagOptionValues(branch) } : {}),
|
|
34
38
|
requiredOnCreate: branch.requiredOnCreate ?? false,
|
|
35
39
|
requiredOnUpdate: branch.requiredOnUpdate ?? false,
|
|
36
40
|
...(branch.targetSeed !== undefined ? { targetSeed: branch.targetSeed } : {}),
|
|
@@ -55,6 +59,7 @@ function projectBranch(branch) {
|
|
|
55
59
|
* `policies.classification|privacy` — behaviour and storage concerns that leave the response
|
|
56
60
|
* shape identical. (`privacy` DOES change a stored value's encoding, but the API type stays
|
|
57
61
|
* `string`; drift there is a data concern, not a type concern.)
|
|
62
|
+
* - option `color` — presentation only; a color edit cannot change a response byte.
|
|
58
63
|
* - anything physical (column order, index names) — see the VETO Audit.
|
|
59
64
|
*
|
|
60
65
|
* Seeds are sorted by slug; BRANCH ORDER IS PRESERVED, because it is the declared field order the
|
|
@@ -7,7 +7,7 @@ import type { ActorContext, Branch, DataClassification, Seed } from '../types.js
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function sha256hex(value: string): Promise<string>;
|
|
9
9
|
/**
|
|
10
|
-
* Verifies if a candidate string matches a stored SHA-256 hash digest.
|
|
10
|
+
* Verifies if a candidate string matches a stored SHA-256 hash digest, comparing in constant time.
|
|
11
11
|
* @param stored - The expected 64-character SHA-256 hex digest.
|
|
12
12
|
* @param candidate - The raw candidate string to verify.
|
|
13
13
|
* @returns A Promise resolving to true if candidate matches stored digest, false otherwise.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAIjF;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,cAAc,EAAE,kBAAkB,CAAA;IAClC,6FAA6F;IAC7F,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAA;IACrC,6FAA6F;IAC7F,gBAAgB,EAAE,MAAM,GAAG,QAAQ,CAAA;IACnC,oFAAoF;IACpF,cAAc,EAAE,MAAM,GAAG,QAAQ,CAAA;CAClC;AAED,0EAA0E;AAC1E,wBAAgB,uBAAuB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAmC5E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CA+BzF;AAuBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,EAAE,IAAI,EACV,KAAK,GAAE,YAAwC,GAC9C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiDzB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
3
|
import { isLocaleDictionary, isLocalizedBranch } from '../localization/localization.js';
|
|
4
|
+
import { timingSafeEqual } from '../../webhooks/webhook-crypto.js';
|
|
4
5
|
/**
|
|
5
6
|
* Computes an un-salted SHA-256 hex digest of a string value.
|
|
6
7
|
* Used for basic digest operations.
|
|
@@ -14,13 +15,13 @@ export async function sha256hex(value) {
|
|
|
14
15
|
.join('');
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
* Verifies if a candidate string matches a stored SHA-256 hash digest.
|
|
18
|
+
* Verifies if a candidate string matches a stored SHA-256 hash digest, comparing in constant time.
|
|
18
19
|
* @param stored - The expected 64-character SHA-256 hex digest.
|
|
19
20
|
* @param candidate - The raw candidate string to verify.
|
|
20
21
|
* @returns A Promise resolving to true if candidate matches stored digest, false otherwise.
|
|
21
22
|
*/
|
|
22
23
|
export async function verifyHashField(stored, candidate) {
|
|
23
|
-
return stored
|
|
24
|
+
return timingSafeEqual(stored, await sha256hex(candidate));
|
|
24
25
|
}
|
|
25
26
|
/** Normalizes raw policy strings to canonical DataClassification enum. */
|
|
26
27
|
export function normalizeClassification(raw) {
|
|
@@ -89,9 +90,12 @@ export function resolvePolicies(branch) {
|
|
|
89
90
|
const isEncryptedOrHashed = resolved.storage !== 'plain';
|
|
90
91
|
const defaultVisibility = resolved.authVisibility;
|
|
91
92
|
const isPublicAllowed = branch.policies?.public ?? (resolved.publicVisibility === 'full');
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const
|
|
93
|
+
const visibility = branch.policies?.visibility ?? defaultVisibility;
|
|
94
|
+
// Concealed values must not be queryable: filter/sort/search leak them
|
|
95
|
+
const isConcealed = visibility === 'hidden' || visibility === 'masked';
|
|
96
|
+
const defaultFilter = branch.policies?.filter ?? (isRepeater || resolved.storage === 'hash' ? false : !isConcealed);
|
|
97
|
+
const defaultSort = branch.policies?.sort ?? (isRepeater || isEncryptedOrHashed ? false : !isConcealed);
|
|
98
|
+
const defaultSearch = branch.policies?.search ?? (isRepeater || isEncryptedOrHashed ? false : !isConcealed);
|
|
95
99
|
// publicEdit default: true for public fields (unless public: false), false for confidential/internal/restricted
|
|
96
100
|
const defaultPublicEdit = resolved.classification === 'public'
|
|
97
101
|
? (branch.policies?.publicEdit ?? isPublicAllowed)
|
|
@@ -99,7 +103,7 @@ export function resolvePolicies(branch) {
|
|
|
99
103
|
return {
|
|
100
104
|
classification: resolved.classification,
|
|
101
105
|
privacy: resolved.storage,
|
|
102
|
-
visibility
|
|
106
|
+
visibility,
|
|
103
107
|
search: defaultSearch,
|
|
104
108
|
filter: defaultFilter,
|
|
105
109
|
sort: defaultSort,
|
|
@@ -164,6 +168,8 @@ export function filterEntryForActor(data, seed, actor = { type: 'authenticated'
|
|
|
164
168
|
if (isPublicActor) {
|
|
165
169
|
if (resolved.publicVisibility === 'hidden')
|
|
166
170
|
continue;
|
|
171
|
+
if (branch.policies?.public === false)
|
|
172
|
+
continue;
|
|
167
173
|
}
|
|
168
174
|
else {
|
|
169
175
|
// Authenticated actor check
|
|
@@ -62,4 +62,17 @@ export declare class PrivacyService implements IPrivacyService {
|
|
|
62
62
|
*/
|
|
63
63
|
hash(plaintext: string): Promise<string>;
|
|
64
64
|
}
|
|
65
|
+
/** Thrown when a protected field is processed but no master key is configured. */
|
|
66
|
+
export declare class PrivacyNotConfiguredError extends Error {
|
|
67
|
+
constructor();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fail-closed {@link IPrivacyService} for installs without a master key.
|
|
71
|
+
* Never returns plaintext as protected output; legacy non-`v1:` values still read through.
|
|
72
|
+
*/
|
|
73
|
+
export declare class UnconfiguredPrivacyService implements IPrivacyService {
|
|
74
|
+
encrypt(_plaintext: string): Promise<string>;
|
|
75
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
76
|
+
hash(_plaintext: string): Promise<string>;
|
|
77
|
+
}
|
|
65
78
|
//# sourceMappingURL=privacy.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"privacy.service.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/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;gBACS,SAAS,EAAE,MAAM;IAO7B;;;OAGG;YACW,SAAS;IAmBvB;;OAEG;YACW,UAAU;IAexB;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAcjD;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBlD;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAa/C"}
|
|
1
|
+
{"version":3,"file":"privacy.service.d.ts","sourceRoot":"","sources":["../../../src/engine/privacy/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;gBACS,SAAS,EAAE,MAAM;IAO7B;;;OAGG;YACW,SAAS;IAmBvB;;OAEG;YACW,UAAU;IAexB;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAcjD;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoBlD;;OAEG;IACG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAa/C;AAED,kFAAkF;AAClF,qBAAa,yBAA0B,SAAQ,KAAK;;CAKnD;AAED;;;GAGG;AACH,qBAAa,0BAA2B,YAAW,eAAe;IAC1D,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAI5C,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK5C,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAGhD"}
|
|
@@ -112,3 +112,27 @@ export class PrivacyService {
|
|
|
112
112
|
.join('');
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
/** Thrown when a protected field is processed but no master key is configured. */
|
|
116
|
+
export class PrivacyNotConfiguredError extends Error {
|
|
117
|
+
constructor() {
|
|
118
|
+
super('PRIVACY_MASTER_KEY is not configured: cannot encrypt or hash protected fields');
|
|
119
|
+
this.name = 'PrivacyNotConfiguredError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Fail-closed {@link IPrivacyService} for installs without a master key.
|
|
124
|
+
* Never returns plaintext as protected output; legacy non-`v1:` values still read through.
|
|
125
|
+
*/
|
|
126
|
+
export class UnconfiguredPrivacyService {
|
|
127
|
+
async encrypt(_plaintext) {
|
|
128
|
+
throw new PrivacyNotConfiguredError();
|
|
129
|
+
}
|
|
130
|
+
async decrypt(ciphertext) {
|
|
131
|
+
if (ciphertext?.startsWith('v1:'))
|
|
132
|
+
throw new PrivacyNotConfiguredError();
|
|
133
|
+
return ciphertext;
|
|
134
|
+
}
|
|
135
|
+
async hash(_plaintext) {
|
|
136
|
+
throw new PrivacyNotConfiguredError();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Seed, TrashedMode } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the SQL condition (e.g. `ce.deleted_at IS NULL`), or `null` if the
|
|
4
|
+
* seed does not use soft delete or mode is 'any'.
|
|
5
|
+
*
|
|
6
|
+
* @param seed - The seed schema definition.
|
|
7
|
+
* @param mode - 'active' (default), 'trashed', or 'any'.
|
|
8
|
+
* @param tableAlias - Optional table or alias qualifier prefix.
|
|
9
|
+
*/
|
|
10
|
+
export declare function activeCondition(seed: Seed, mode?: TrashedMode, tableAlias?: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Returns ` AND <activeCondition>` for a soft-delete seed, or `''` otherwise.
|
|
13
|
+
*
|
|
14
|
+
* @param seed - The seed schema definition.
|
|
15
|
+
* @param mode - 'active' (default), 'trashed', or 'any'.
|
|
16
|
+
* @param tableAlias - Optional table or alias qualifier prefix.
|
|
17
|
+
*/
|
|
18
|
+
export declare function activeClause(seed: Seed, mode?: TrashedMode, tableAlias?: string): string;
|
|
19
|
+
//# sourceMappingURL=active-clause.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-clause.d.ts","sourceRoot":"","sources":["../../../src/engine/query/active-clause.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEpD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,IAAI,EACV,IAAI,GAAE,WAAsB,EAC5B,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,IAAI,EACV,IAAI,GAAE,WAAsB,EAC5B,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAGR"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
/**
|
|
4
|
+
* Returns the SQL condition (e.g. `ce.deleted_at IS NULL`), or `null` if the
|
|
5
|
+
* seed does not use soft delete or mode is 'any'.
|
|
6
|
+
*
|
|
7
|
+
* @param seed - The seed schema definition.
|
|
8
|
+
* @param mode - 'active' (default), 'trashed', or 'any'.
|
|
9
|
+
* @param tableAlias - Optional table or alias qualifier prefix.
|
|
10
|
+
*/
|
|
11
|
+
export function activeCondition(seed, mode = 'active', tableAlias) {
|
|
12
|
+
if (!seed.softDelete || mode === 'any')
|
|
13
|
+
return null;
|
|
14
|
+
const col = tableAlias ? `${tableAlias}.deleted_at` : 'deleted_at';
|
|
15
|
+
return mode === 'trashed' ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Returns ` AND <activeCondition>` for a soft-delete seed, or `''` otherwise.
|
|
19
|
+
*
|
|
20
|
+
* @param seed - The seed schema definition.
|
|
21
|
+
* @param mode - 'active' (default), 'trashed', or 'any'.
|
|
22
|
+
* @param tableAlias - Optional table or alias qualifier prefix.
|
|
23
|
+
*/
|
|
24
|
+
export function activeClause(seed, mode = 'active', tableAlias) {
|
|
25
|
+
const cond = activeCondition(seed, mode, tableAlias);
|
|
26
|
+
return cond ? ` AND ${cond}` : '';
|
|
27
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Seed, SelectOptions, ParameterizedQuery } from '../types.js';
|
|
1
|
+
import type { Seed, FilterGroup, SelectOptions, ParameterizedQuery } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Builds a parameterized SQL SELECT query and bindings for a given Seed based on search, filtering, status, and ordering options.
|
|
4
4
|
*
|
|
@@ -20,4 +20,17 @@ import type { Seed, SelectOptions, ParameterizedQuery } from '../types.js';
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
export declare function buildSelectQuery(seed: Seed, options?: SelectOptions): ParameterizedQuery;
|
|
23
|
+
/**
|
|
24
|
+
* Lists the filter groups `buildSelectQuery` would silently leave out of the WHERE clause: an unknown
|
|
25
|
+
* column, or a condition whose value cannot be bound for the field type (empty or unparsable
|
|
26
|
+
* number/date, `in`/`not_in` value that is not an array). An empty `in` is not listed:
|
|
27
|
+
* it compiles to constant false and still constrains the read. A caller that needs the filter to constrain the read
|
|
28
|
+
* (e.g. automations resolving placeholders at run time) uses this to refuse the query instead of
|
|
29
|
+
* running it unconstrained.
|
|
30
|
+
*
|
|
31
|
+
* @param seed The seed the filters apply to.
|
|
32
|
+
* @param filters The filter groups to check.
|
|
33
|
+
* @returns The groups that would be dropped, in input order.
|
|
34
|
+
*/
|
|
35
|
+
export declare function findUnappliedFilters(seed: Seed, filters: FilterGroup[]): FilterGroup[];
|
|
23
36
|
//# sourceMappingURL=query.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../src/engine/query/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,IAAI,
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../../src/engine/query/query.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,IAAI,EAGJ,WAAW,EAGX,aAAa,EAEb,kBAAkB,EACnB,MAAM,aAAa,CAAC;AA8FrB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAgH5F;AAGD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAKtF"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { tableName, ftsTableName, isValidColumn, indexableSearchBranches, SYSTEM_COLUMNS } from '../ddl/ddl.js';
|
|
2
2
|
import { resolveClassification } from '../privacy/policies.js';
|
|
3
3
|
import { isLocaleCode, isLocalizedBranch } from '../localization/localization.js';
|
|
4
|
+
import { activeCondition } from './active-clause.js';
|
|
5
|
+
import { isSingleChoiceTags, tagOptionValues } from '../seeds/tag-options.js';
|
|
6
|
+
import { normalizeTagValue, TAG_OPTIONS_MAX_COUNT } from '../seeds/tag-value.js';
|
|
4
7
|
/** SQLite GLOB twins of `LOCALE_CODE_RE` (`it`, `ast`, `pt-BR`, `es-419`, …). Keep both in sync. */
|
|
5
8
|
const LOCALE_KEY_GLOBS = [
|
|
6
9
|
'[a-z][a-z]',
|
|
@@ -57,6 +60,27 @@ function columnSql(seed, table, alias, locale) {
|
|
|
57
60
|
}
|
|
58
61
|
return alias;
|
|
59
62
|
}
|
|
63
|
+
/** A multiple-choice `tags` column has no meaningful order, so `orderBy` on it is ignored. */
|
|
64
|
+
function isMultipleChoiceTagsColumn(seed, alias) {
|
|
65
|
+
const branch = seed.branches.find((b) => b.alias === alias);
|
|
66
|
+
return branch?.type === 'tags' && !isSingleChoiceTags(branch);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* ORDER BY clause for a single-choice tags column: empty values (`NULL`, `[]`) always last, then the option
|
|
70
|
+
* position (a value outside the options ranks after every option) or, without options, the value alphabetically.
|
|
71
|
+
* Option values are pushed to `bindings`.
|
|
72
|
+
*/
|
|
73
|
+
function tagsOrderClause(col, branch, dir, bindings) {
|
|
74
|
+
const head = `(CASE WHEN json_valid(${col}) THEN json_extract(${col}, '$[0]') END)`;
|
|
75
|
+
const options = tagOptionValues(branch);
|
|
76
|
+
let rank = head;
|
|
77
|
+
if (options.length > 0) {
|
|
78
|
+
const branches = options.map((_, position) => `WHEN ? THEN ${position}`).join(' ');
|
|
79
|
+
rank = `CASE ${head} ${branches} ELSE ${options.length} END`;
|
|
80
|
+
bindings.push(...options);
|
|
81
|
+
}
|
|
82
|
+
return ` ORDER BY (${head} IS NULL) ASC, ${rank} ${dir}`;
|
|
83
|
+
}
|
|
60
84
|
/**
|
|
61
85
|
* Builds a parameterized SQL SELECT query and bindings for a given Seed based on search, filtering, status, and ordering options.
|
|
62
86
|
*
|
|
@@ -106,11 +130,9 @@ export function buildSelectQuery(seed, options = {}) {
|
|
|
106
130
|
// which all reach SQL only through repository.findMany.
|
|
107
131
|
if (seed.softDelete) {
|
|
108
132
|
const trashed = options.trashed ?? 'active';
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
else if (trashed === 'trashed') {
|
|
113
|
-
whereClauses.push(`${table}.deleted_at IS NOT NULL`);
|
|
133
|
+
const cond = activeCondition(seed, trashed, table);
|
|
134
|
+
if (cond) {
|
|
135
|
+
whereClauses.push(cond);
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
const groupClauses = [];
|
|
@@ -135,6 +157,9 @@ export function buildSelectQuery(seed, options = {}) {
|
|
|
135
157
|
if (clause)
|
|
136
158
|
condClauses.push(clause);
|
|
137
159
|
}
|
|
160
|
+
else {
|
|
161
|
+
throw new TypeError(`Invalid filter: operator '${op}' is not supported for encrypted field '${group.column}'`);
|
|
162
|
+
}
|
|
138
163
|
}
|
|
139
164
|
else {
|
|
140
165
|
const clause = buildFilterCondition(col, group.type, cond, bindings);
|
|
@@ -170,10 +195,13 @@ export function buildSelectQuery(seed, options = {}) {
|
|
|
170
195
|
if (kanbanOrderClause) {
|
|
171
196
|
sql += kanbanOrderClause;
|
|
172
197
|
}
|
|
173
|
-
else if (orderBy && isValidColumn(seed, orderBy.column)) {
|
|
198
|
+
else if (orderBy && isValidColumn(seed, orderBy.column) && !isMultipleChoiceTagsColumn(seed, orderBy.column)) {
|
|
174
199
|
const dir = orderBy.dir === 'DESC' ? 'DESC' : 'ASC';
|
|
175
200
|
const col = columnSql(seed, table, orderBy.column, options.locale);
|
|
176
|
-
|
|
201
|
+
const orderedBranch = seed.branches.find((b) => b.alias === orderBy.column);
|
|
202
|
+
sql += orderedBranch && isSingleChoiceTags(orderedBranch)
|
|
203
|
+
? tagsOrderClause(col, orderedBranch, dir, bindings)
|
|
204
|
+
: ` ORDER BY ${col} ${dir}`;
|
|
177
205
|
}
|
|
178
206
|
else {
|
|
179
207
|
sql += ` ORDER BY ${table}.created_at DESC`;
|
|
@@ -185,6 +213,22 @@ export function buildSelectQuery(seed, options = {}) {
|
|
|
185
213
|
}
|
|
186
214
|
return { sql, bindings };
|
|
187
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Lists the filter groups `buildSelectQuery` would silently leave out of the WHERE clause: an unknown
|
|
218
|
+
* column, or a condition whose value cannot be bound for the field type (empty or unparsable
|
|
219
|
+
* number/date, `in`/`not_in` value that is not an array). An empty `in` is not listed:
|
|
220
|
+
* it compiles to constant false and still constrains the read. A caller that needs the filter to constrain the read
|
|
221
|
+
* (e.g. automations resolving placeholders at run time) uses this to refuse the query instead of
|
|
222
|
+
* running it unconstrained.
|
|
223
|
+
*
|
|
224
|
+
* @param seed The seed the filters apply to.
|
|
225
|
+
* @param filters The filter groups to check.
|
|
226
|
+
* @returns The groups that would be dropped, in input order.
|
|
227
|
+
*/
|
|
228
|
+
export function findUnappliedFilters(seed, filters) {
|
|
229
|
+
return filters.filter((group) => !isValidColumn(seed, group.column)
|
|
230
|
+
|| group.conditions.some((cond) => buildFilterCondition('col', group.type, cond, []) === null));
|
|
231
|
+
}
|
|
188
232
|
/**
|
|
189
233
|
* Normalizes user-provided filter values to a format appropriate for SQL execution.
|
|
190
234
|
*
|
|
@@ -218,6 +262,35 @@ function normalizeFilterValue(type, value) {
|
|
|
218
262
|
}
|
|
219
263
|
return value;
|
|
220
264
|
}
|
|
265
|
+
/** On a `tags` column, equality-like operators test membership; `negated` ones are the complement. */
|
|
266
|
+
const TAGS_MEMBERSHIP_OPS = {
|
|
267
|
+
eq: { op: 'has_tag', negated: false },
|
|
268
|
+
contains: { op: 'has_tag', negated: false },
|
|
269
|
+
in: { op: 'has_any_tag', negated: false },
|
|
270
|
+
neq: { op: 'has_tag', negated: true },
|
|
271
|
+
not_contains: { op: 'has_tag', negated: true },
|
|
272
|
+
not_in: { op: 'has_any_tag', negated: true },
|
|
273
|
+
};
|
|
274
|
+
function buildTagsMembershipCondition(col, op, value, bindings) {
|
|
275
|
+
const { op: membershipOp, negated } = TAGS_MEMBERSHIP_OPS[op];
|
|
276
|
+
const isSetOp = membershipOp === 'has_any_tag';
|
|
277
|
+
let tags;
|
|
278
|
+
if (isSetOp) {
|
|
279
|
+
if (!Array.isArray(value))
|
|
280
|
+
return null;
|
|
281
|
+
tags = value.filter((v) => typeof v === 'string' || typeof v === 'number').map(String);
|
|
282
|
+
// Same rule as in/not_in: an empty set matches nothing, its negation adds no clause.
|
|
283
|
+
if (tags.length === 0)
|
|
284
|
+
return negated ? null : '0';
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
288
|
+
return null;
|
|
289
|
+
tags = [String(value)];
|
|
290
|
+
}
|
|
291
|
+
const condition = buildFilterCondition(col, 'tags', { op: membershipOp, value: isSetOp ? tags : tags[0] }, bindings);
|
|
292
|
+
return negated && condition ? `NOT (${condition})` : condition;
|
|
293
|
+
}
|
|
221
294
|
/**
|
|
222
295
|
* Translates a filter condition into its SQL clause and adds parameters to the bindings list.
|
|
223
296
|
*
|
|
@@ -234,21 +307,26 @@ function buildFilterCondition(col, type, cond, bindings) {
|
|
|
234
307
|
if (type === 'text') {
|
|
235
308
|
return isEmpty ? `(${col} IS NULL OR ${col} = '')` : `(${col} IS NOT NULL AND ${col} != '')`;
|
|
236
309
|
}
|
|
237
|
-
if (type === 'tags' || type === 'json') {
|
|
310
|
+
if (type === 'tags' || type === 'json' || type === 'repeater') {
|
|
238
311
|
return isEmpty
|
|
239
312
|
? `(${col} IS NULL OR ${col} = '[]' OR ${col} = '{}')`
|
|
240
313
|
: `(${col} IS NOT NULL AND ${col} != '[]' AND ${col} != '{}')`;
|
|
241
314
|
}
|
|
242
315
|
return isEmpty ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
243
316
|
}
|
|
317
|
+
if (type === 'tags' && Object.hasOwn(TAGS_MEMBERSHIP_OPS, op)) {
|
|
318
|
+
return buildTagsMembershipCondition(col, op, value, bindings);
|
|
319
|
+
}
|
|
244
320
|
if (op === 'in' || op === 'not_in') {
|
|
245
|
-
if (!Array.isArray(value)
|
|
321
|
+
if (!Array.isArray(value))
|
|
246
322
|
return null;
|
|
247
323
|
const normalizedValues = value
|
|
248
324
|
.map((v) => normalizeFilterValue(type, v))
|
|
249
325
|
.filter((v) => v !== null);
|
|
326
|
+
// Membership in an empty set is false, not "no constraint"; dropping it would widen the result set.
|
|
327
|
+
// `not_in` over an empty set is vacuously true, so it adds no clause.
|
|
250
328
|
if (normalizedValues.length === 0)
|
|
251
|
-
return null;
|
|
329
|
+
return op === 'in' ? '0' : null;
|
|
252
330
|
const placeholders = normalizedValues.map(() => '?').join(', ');
|
|
253
331
|
bindings.push(...normalizedValues);
|
|
254
332
|
return `${col} ${op === 'in' ? 'IN' : 'NOT IN'} (${placeholders})`;
|
|
@@ -256,10 +334,24 @@ function buildFilterCondition(col, type, cond, bindings) {
|
|
|
256
334
|
if (op === 'has_tag' || op === 'has_any_tag' || op === 'has_all_tags') {
|
|
257
335
|
if (type !== 'tags' && type !== 'json')
|
|
258
336
|
return null;
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
if (tags.length === 0)
|
|
337
|
+
const requested = [...new Set((op === 'has_tag' ? [value] : Array.isArray(value) ? value : [value]).map(t => String(t)))];
|
|
338
|
+
if (requested.length === 0)
|
|
262
339
|
return null;
|
|
340
|
+
// One bound parameter per tag and D1 allows 100 per statement: an oversized set is a malformed filter.
|
|
341
|
+
if (requested.length > TAG_OPTIONS_MAX_COUNT)
|
|
342
|
+
return null;
|
|
343
|
+
// A stored `tags` value is always normalized, so the needle is too. One that cannot be normalized
|
|
344
|
+
// (blank, too long, ill-formed) matches nothing, and `has_all_tags` over it can never be satisfied.
|
|
345
|
+
let tags = requested;
|
|
346
|
+
if (type === 'tags') {
|
|
347
|
+
const normalized = requested.flatMap((t) => {
|
|
348
|
+
const result = normalizeTagValue(t);
|
|
349
|
+
return result.ok && result.value.length > 0 ? [result.value] : [];
|
|
350
|
+
});
|
|
351
|
+
if (normalized.length === 0 || (op === 'has_all_tags' && normalized.length < requested.length))
|
|
352
|
+
return '0';
|
|
353
|
+
tags = [...new Set(normalized)];
|
|
354
|
+
}
|
|
263
355
|
// json_each on array → value is the element; on object → key is the property name.
|
|
264
356
|
// Tags stored as ["photo"] (array) or {"photo":"#3b82f6"} (object) must both match.
|
|
265
357
|
const tagRef = `CASE json_type(${col}) WHEN 'array' THEN value ELSE key END`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Seed } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fills the id of every branch (and repeater sub-field) that lacks one.
|
|
4
|
+
*
|
|
5
|
+
* The stored id wins by alias match; only an alias the stored definition does not know gets a fresh
|
|
6
|
+
* `nextBranchId`, minted past every stored id and every explicit id anywhere in the candidate, so it
|
|
7
|
+
* never collides with a later explicit id nor recycles the id of a branch the candidate omits. An id
|
|
8
|
+
* already present on the candidate is kept as-is: callers that author without ids (hand-written
|
|
9
|
+
* manifests) must send none rather than invented ones.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveCandidateIds<T extends {
|
|
12
|
+
branches?: unknown;
|
|
13
|
+
}>(candidate: T, stored: Seed | null): T;
|
|
14
|
+
//# sourceMappingURL=candidate-ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidate-ids.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/candidate-ids.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAU,IAAI,EAAE,MAAM,aAAa,CAAA;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,CAAC,CAM1G"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
/**
|
|
4
|
+
* @module engine/seeds/candidate-ids
|
|
5
|
+
* Resolves branch ids on a candidate seed definition against the stored one.
|
|
6
|
+
*/
|
|
7
|
+
import { nextBranchId } from './seed-registry.js';
|
|
8
|
+
/**
|
|
9
|
+
* Fills the id of every branch (and repeater sub-field) that lacks one.
|
|
10
|
+
*
|
|
11
|
+
* The stored id wins by alias match; only an alias the stored definition does not know gets a fresh
|
|
12
|
+
* `nextBranchId`, minted past every stored id and every explicit id anywhere in the candidate, so it
|
|
13
|
+
* never collides with a later explicit id nor recycles the id of a branch the candidate omits. An id
|
|
14
|
+
* already present on the candidate is kept as-is: callers that author without ids (hand-written
|
|
15
|
+
* manifests) must send none rather than invented ones.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveCandidateIds(candidate, stored) {
|
|
18
|
+
const storedBranches = stored?.branches ?? [];
|
|
19
|
+
return {
|
|
20
|
+
...candidate,
|
|
21
|
+
branches: assignIds(Array.isArray(candidate.branches) ? candidate.branches : [], storedBranches),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function assignIds(branches, stored) {
|
|
25
|
+
const storedByAlias = new Map(stored.map(b => [b.alias, b]));
|
|
26
|
+
const taken = [...stored, ...branches.filter(b => b.id)];
|
|
27
|
+
const resolved = [];
|
|
28
|
+
for (const source of branches) {
|
|
29
|
+
const match = source.alias === undefined ? undefined : storedByAlias.get(source.alias);
|
|
30
|
+
const branch = { ...source };
|
|
31
|
+
if (!branch.id) {
|
|
32
|
+
branch.id = match?.id || nextBranchId({ branches: taken });
|
|
33
|
+
taken.push(branch);
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(source.fields))
|
|
36
|
+
branch.fields = assignIds(source.fields, match?.fields ?? []);
|
|
37
|
+
resolved.push(branch);
|
|
38
|
+
}
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|