@plumix/core 0.5.0 → 0.7.0
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/cache/embedded-tags.d.ts +5 -0
- package/dist/cache/embedded-tags.d.ts.map +1 -0
- package/dist/cache/embedded-tags.js +30 -0
- package/dist/cache/embedded-tags.js.map +1 -0
- package/dist/context/app.d.ts +12 -0
- package/dist/context/app.d.ts.map +1 -1
- package/dist/context/app.js +10 -4
- package/dist/context/app.js.map +1 -1
- package/dist/context/memo.d.ts +22 -0
- package/dist/context/memo.d.ts.map +1 -0
- package/dist/context/memo.js +27 -0
- package/dist/context/memo.js.map +1 -0
- package/dist/context/traced-slots.d.ts +17 -0
- package/dist/context/traced-slots.d.ts.map +1 -0
- package/dist/context/traced-slots.js +57 -0
- package/dist/context/traced-slots.js.map +1 -0
- package/dist/entries/read-service.d.ts +6 -0
- package/dist/entries/read-service.d.ts.map +1 -1
- package/dist/entries/read-service.js +19 -3
- package/dist/entries/read-service.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/errors.d.ts +7 -1
- package/dist/plugin/errors.d.ts.map +1 -1
- package/dist/plugin/errors.js +5 -0
- package/dist/plugin/errors.js.map +1 -1
- package/dist/plugin/fields/builder.d.ts +106 -0
- package/dist/plugin/fields/builder.d.ts.map +1 -0
- package/dist/plugin/fields/builder.js +162 -0
- package/dist/plugin/fields/builder.js.map +1 -0
- package/dist/plugin/fields/color.d.ts +78 -18
- package/dist/plugin/fields/color.d.ts.map +1 -1
- package/dist/plugin/fields/color.js +116 -25
- package/dist/plugin/fields/color.js.map +1 -1
- package/dist/plugin/fields/condition.d.ts +40 -0
- package/dist/plugin/fields/condition.d.ts.map +1 -0
- package/dist/plugin/fields/condition.js +115 -0
- package/dist/plugin/fields/condition.js.map +1 -0
- package/dist/plugin/fields/contributions.d.ts +233 -0
- package/dist/plugin/fields/contributions.d.ts.map +1 -0
- package/dist/plugin/fields/contributions.js +1 -0
- package/dist/plugin/fields/contributions.js.map +1 -0
- package/dist/plugin/fields/entry.d.ts +24 -20
- package/dist/plugin/fields/entry.d.ts.map +1 -1
- package/dist/plugin/fields/entry.js +13 -21
- package/dist/plugin/fields/entry.js.map +1 -1
- package/dist/plugin/fields/errors.d.ts +23 -11
- package/dist/plugin/fields/errors.d.ts.map +1 -1
- package/dist/plugin/fields/errors.js +20 -13
- package/dist/plugin/fields/errors.js.map +1 -1
- package/dist/plugin/fields/group.d.ts +90 -0
- package/dist/plugin/fields/group.d.ts.map +1 -0
- package/dist/plugin/fields/group.js +124 -0
- package/dist/plugin/fields/group.js.map +1 -0
- package/dist/plugin/fields/index.d.ts +22 -45
- package/dist/plugin/fields/index.d.ts.map +1 -1
- package/dist/plugin/fields/index.js +25 -30
- package/dist/plugin/fields/index.js.map +1 -1
- package/dist/plugin/fields/json.d.ts +71 -9
- package/dist/plugin/fields/json.d.ts.map +1 -1
- package/dist/plugin/fields/json.js +110 -16
- package/dist/plugin/fields/json.js.map +1 -1
- package/dist/plugin/fields/link.d.ts +96 -0
- package/dist/plugin/fields/link.d.ts.map +1 -0
- package/dist/plugin/fields/link.js +160 -0
- package/dist/plugin/fields/link.js.map +1 -0
- package/dist/plugin/fields/number.d.ts +86 -13
- package/dist/plugin/fields/number.d.ts.map +1 -1
- package/dist/plugin/fields/number.js +133 -20
- package/dist/plugin/fields/number.js.map +1 -1
- package/dist/plugin/fields/range.d.ts +101 -17
- package/dist/plugin/fields/range.d.ts.map +1 -1
- package/dist/plugin/fields/range.js +153 -38
- package/dist/plugin/fields/range.js.map +1 -1
- package/dist/plugin/fields/reference.d.ts +171 -0
- package/dist/plugin/fields/reference.d.ts.map +1 -0
- package/dist/plugin/fields/reference.js +193 -0
- package/dist/plugin/fields/reference.js.map +1 -0
- package/dist/plugin/fields/repeater.d.ts +111 -8
- package/dist/plugin/fields/repeater.d.ts.map +1 -1
- package/dist/plugin/fields/repeater.js +157 -66
- package/dist/plugin/fields/repeater.js.map +1 -1
- package/dist/plugin/fields/richtext-validate.d.ts +7 -0
- package/dist/plugin/fields/richtext-validate.d.ts.map +1 -1
- package/dist/plugin/fields/richtext-validate.js +11 -5
- package/dist/plugin/fields/richtext-validate.js.map +1 -1
- package/dist/plugin/fields/richtext.d.ts +78 -12
- package/dist/plugin/fields/richtext.d.ts.map +1 -1
- package/dist/plugin/fields/richtext.js +121 -26
- package/dist/plugin/fields/richtext.js.map +1 -1
- package/dist/plugin/fields/select.d.ts +150 -10
- package/dist/plugin/fields/select.d.ts.map +1 -1
- package/dist/plugin/fields/select.js +205 -17
- package/dist/plugin/fields/select.js.map +1 -1
- package/dist/plugin/fields/sub-fields.d.ts +12 -0
- package/dist/plugin/fields/sub-fields.d.ts.map +1 -0
- package/dist/plugin/fields/sub-fields.js +62 -0
- package/dist/plugin/fields/sub-fields.js.map +1 -0
- package/dist/plugin/fields/temporal.d.ts +124 -0
- package/dist/plugin/fields/temporal.d.ts.map +1 -0
- package/dist/plugin/fields/temporal.js +180 -0
- package/dist/plugin/fields/temporal.js.map +1 -0
- package/dist/plugin/fields/term.d.ts +11 -16
- package/dist/plugin/fields/term.d.ts.map +1 -1
- package/dist/plugin/fields/term.js +11 -19
- package/dist/plugin/fields/term.js.map +1 -1
- package/dist/plugin/fields/toggle.d.ts +97 -0
- package/dist/plugin/fields/toggle.d.ts.map +1 -0
- package/dist/plugin/fields/toggle.js +135 -0
- package/dist/plugin/fields/toggle.js.map +1 -0
- package/dist/plugin/fields/user.d.ts +14 -26
- package/dist/plugin/fields/user.d.ts.map +1 -1
- package/dist/plugin/fields/user.js +11 -25
- package/dist/plugin/fields/user.js.map +1 -1
- package/dist/plugin/lookup.d.ts +74 -10
- package/dist/plugin/lookup.d.ts.map +1 -1
- package/dist/plugin/manifest.d.ts +319 -160
- package/dist/plugin/manifest.d.ts.map +1 -1
- package/dist/plugin/manifest.js +94 -4
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/plugin/setup-context.d.ts +10 -4
- package/dist/plugin/setup-context.d.ts.map +1 -1
- package/dist/plugin/setup-context.js +10 -3
- package/dist/plugin/setup-context.js.map +1 -1
- package/dist/plugin/validation/meta-box-fields.d.ts.map +1 -1
- package/dist/plugin/validation/meta-box-fields.js +16 -0
- package/dist/plugin/validation/meta-box-fields.js.map +1 -1
- package/dist/route/render/asset-manifest.d.ts +1 -1
- package/dist/route/render/asset-manifest.d.ts.map +1 -1
- package/dist/route/render/asset-manifest.js +7 -1
- package/dist/route/render/asset-manifest.js.map +1 -1
- package/dist/route/render/build-resolved-entries.d.ts.map +1 -1
- package/dist/route/render/build-resolved-entries.js +25 -12
- package/dist/route/render/build-resolved-entries.js.map +1 -1
- package/dist/route/render/render-template.d.ts +1 -1
- package/dist/route/render/render-template.d.ts.map +1 -1
- package/dist/route/render/render-template.js +50 -23
- package/dist/route/render/render-template.js.map +1 -1
- package/dist/route/render/template-builders.d.ts +15 -10
- package/dist/route/render/template-builders.d.ts.map +1 -1
- package/dist/route/render/template-builders.js.map +1 -1
- package/dist/rpc/base.d.ts +7 -0
- package/dist/rpc/base.d.ts.map +1 -1
- package/dist/rpc/errors.d.ts +7 -0
- package/dist/rpc/errors.d.ts.map +1 -1
- package/dist/rpc/errors.js +16 -0
- package/dist/rpc/errors.js.map +1 -1
- package/dist/rpc/meta/core.d.ts +92 -14
- package/dist/rpc/meta/core.d.ts.map +1 -1
- package/dist/rpc/meta/core.js +524 -406
- package/dist/rpc/meta/core.js.map +1 -1
- package/dist/rpc/meta/field-messages.d.ts +67 -0
- package/dist/rpc/meta/field-messages.d.ts.map +1 -0
- package/dist/rpc/meta/field-messages.js +67 -0
- package/dist/rpc/meta/field-messages.js.map +1 -0
- package/dist/rpc/meta/field-pipeline.d.ts +52 -0
- package/dist/rpc/meta/field-pipeline.d.ts.map +1 -0
- package/dist/rpc/meta/field-pipeline.js +501 -0
- package/dist/rpc/meta/field-pipeline.js.map +1 -0
- package/dist/rpc/procedures/auth/allowed-domains/create.d.ts +7 -0
- package/dist/rpc/procedures/auth/allowed-domains/create.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/allowed-domains/delete.d.ts +7 -0
- package/dist/rpc/procedures/auth/allowed-domains/delete.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/allowed-domains/index.d.ts +28 -0
- package/dist/rpc/procedures/auth/allowed-domains/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/allowed-domains/list.d.ts +7 -0
- package/dist/rpc/procedures/auth/allowed-domains/list.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/allowed-domains/update.d.ts +7 -0
- package/dist/rpc/procedures/auth/allowed-domains/update.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/admin-list.d.ts +7 -0
- package/dist/rpc/procedures/auth/api-tokens/admin-list.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/admin-revoke.d.ts +7 -0
- package/dist/rpc/procedures/auth/api-tokens/admin-revoke.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/create.d.ts +7 -0
- package/dist/rpc/procedures/auth/api-tokens/create.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/index.d.ts +35 -0
- package/dist/rpc/procedures/auth/api-tokens/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/list.d.ts +7 -0
- package/dist/rpc/procedures/auth/api-tokens/list.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/api-tokens/revoke.d.ts +7 -0
- package/dist/rpc/procedures/auth/api-tokens/revoke.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/credentials/delete.d.ts +7 -0
- package/dist/rpc/procedures/auth/credentials/delete.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/credentials/index.d.ts +21 -0
- package/dist/rpc/procedures/auth/credentials/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/credentials/list.d.ts +7 -0
- package/dist/rpc/procedures/auth/credentials/list.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/credentials/rename.d.ts +7 -0
- package/dist/rpc/procedures/auth/credentials/rename.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/device-flow/approve.d.ts +7 -0
- package/dist/rpc/procedures/auth/device-flow/approve.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/device-flow/deny.d.ts +7 -0
- package/dist/rpc/procedures/auth/device-flow/deny.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/device-flow/index.d.ts +21 -0
- package/dist/rpc/procedures/auth/device-flow/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/device-flow/lookup.d.ts +7 -0
- package/dist/rpc/procedures/auth/device-flow/lookup.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/index.d.ts +154 -0
- package/dist/rpc/procedures/auth/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/login-links.d.ts +7 -0
- package/dist/rpc/procedures/auth/login-links.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/mailer/index.d.ts +7 -0
- package/dist/rpc/procedures/auth/mailer/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/mailer/test-send.d.ts +7 -0
- package/dist/rpc/procedures/auth/mailer/test-send.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/oauth-providers.d.ts +7 -0
- package/dist/rpc/procedures/auth/oauth-providers.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/session.d.ts +7 -0
- package/dist/rpc/procedures/auth/session.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/sessions/index.d.ts +21 -0
- package/dist/rpc/procedures/auth/sessions/index.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/sessions/list.d.ts +7 -0
- package/dist/rpc/procedures/auth/sessions/list.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/sessions/revoke-others.d.ts +7 -0
- package/dist/rpc/procedures/auth/sessions/revoke-others.d.ts.map +1 -1
- package/dist/rpc/procedures/auth/sessions/revoke.d.ts +7 -0
- package/dist/rpc/procedures/auth/sessions/revoke.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/activity.d.ts +7 -0
- package/dist/rpc/procedures/entry/activity.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/bulk.d.ts +21 -0
- package/dist/rpc/procedures/entry/bulk.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/create-preview-link.d.ts +7 -0
- package/dist/rpc/procedures/entry/create-preview-link.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/create.d.ts +8 -1
- package/dist/rpc/procedures/entry/create.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/create.js +8 -8
- package/dist/rpc/procedures/entry/create.js.map +1 -1
- package/dist/rpc/procedures/entry/dashboard.d.ts +14 -0
- package/dist/rpc/procedures/entry/dashboard.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/delete-permanent.d.ts +7 -0
- package/dist/rpc/procedures/entry/delete-permanent.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/discard-draft.d.ts +7 -0
- package/dist/rpc/procedures/entry/discard-draft.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/duplicate.d.ts +7 -0
- package/dist/rpc/procedures/entry/duplicate.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/duplicate.js +2 -2
- package/dist/rpc/procedures/entry/duplicate.js.map +1 -1
- package/dist/rpc/procedures/entry/get.d.ts +7 -0
- package/dist/rpc/procedures/entry/get.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/get.js +2 -2
- package/dist/rpc/procedures/entry/get.js.map +1 -1
- package/dist/rpc/procedures/entry/index.d.ts +156 -2
- package/dist/rpc/procedures/entry/index.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/list.d.ts +7 -0
- package/dist/rpc/procedures/entry/list.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/lookup.d.ts +8 -2
- package/dist/rpc/procedures/entry/lookup.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/lookup.js +74 -12
- package/dist/rpc/procedures/entry/lookup.js.map +1 -1
- package/dist/rpc/procedures/entry/meta.d.ts +37 -3
- package/dist/rpc/procedures/entry/meta.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/meta.js +63 -7
- package/dist/rpc/procedures/entry/meta.js.map +1 -1
- package/dist/rpc/procedures/entry/publish.d.ts +7 -0
- package/dist/rpc/procedures/entry/publish.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/publish.js +6 -4
- package/dist/rpc/procedures/entry/publish.js.map +1 -1
- package/dist/rpc/procedures/entry/refresh-block-loader.d.ts +7 -0
- package/dist/rpc/procedures/entry/refresh-block-loader.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/restore.d.ts +7 -0
- package/dist/rpc/procedures/entry/restore.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/revisions.d.ts +56 -0
- package/dist/rpc/procedures/entry/revisions.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/schemas.d.ts +2 -2
- package/dist/rpc/procedures/entry/schemas.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/schemas.js +9 -2
- package/dist/rpc/procedures/entry/schemas.js.map +1 -1
- package/dist/rpc/procedures/entry/trash.d.ts +7 -0
- package/dist/rpc/procedures/entry/trash.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/update.d.ts +8 -1
- package/dist/rpc/procedures/entry/update.d.ts.map +1 -1
- package/dist/rpc/procedures/entry/update.js +77 -18
- package/dist/rpc/procedures/entry/update.js.map +1 -1
- package/dist/rpc/procedures/lookup/index.d.ts +14 -0
- package/dist/rpc/procedures/lookup/index.d.ts.map +1 -1
- package/dist/rpc/procedures/lookup/list.d.ts +7 -0
- package/dist/rpc/procedures/lookup/list.d.ts.map +1 -1
- package/dist/rpc/procedures/lookup/resolve.d.ts +7 -0
- package/dist/rpc/procedures/lookup/resolve.d.ts.map +1 -1
- package/dist/rpc/procedures/lookup/resolve.js +1 -1
- package/dist/rpc/procedures/lookup/resolve.js.map +1 -1
- package/dist/rpc/procedures/lookup.errors.d.ts +2 -1
- package/dist/rpc/procedures/lookup.errors.d.ts.map +1 -1
- package/dist/rpc/procedures/lookup.errors.js +3 -0
- package/dist/rpc/procedures/lookup.errors.js.map +1 -1
- package/dist/rpc/procedures/search/index.d.ts +7 -0
- package/dist/rpc/procedures/search/index.d.ts.map +1 -1
- package/dist/rpc/procedures/settings/get.d.ts +7 -0
- package/dist/rpc/procedures/settings/get.d.ts.map +1 -1
- package/dist/rpc/procedures/settings/index.d.ts +14 -0
- package/dist/rpc/procedures/settings/index.d.ts.map +1 -1
- package/dist/rpc/procedures/settings/upsert.d.ts +7 -0
- package/dist/rpc/procedures/settings/upsert.d.ts.map +1 -1
- package/dist/rpc/procedures/settings/upsert.js +9 -0
- package/dist/rpc/procedures/settings/upsert.js.map +1 -1
- package/dist/rpc/procedures/term/create.d.ts +7 -0
- package/dist/rpc/procedures/term/create.d.ts.map +1 -1
- package/dist/rpc/procedures/term/create.js +3 -3
- package/dist/rpc/procedures/term/create.js.map +1 -1
- package/dist/rpc/procedures/term/delete.d.ts +7 -0
- package/dist/rpc/procedures/term/delete.d.ts.map +1 -1
- package/dist/rpc/procedures/term/get.d.ts +7 -0
- package/dist/rpc/procedures/term/get.d.ts.map +1 -1
- package/dist/rpc/procedures/term/index.d.ts +35 -0
- package/dist/rpc/procedures/term/index.d.ts.map +1 -1
- package/dist/rpc/procedures/term/list.d.ts +7 -0
- package/dist/rpc/procedures/term/list.d.ts.map +1 -1
- package/dist/rpc/procedures/term/lookup.d.ts +7 -2
- package/dist/rpc/procedures/term/lookup.d.ts.map +1 -1
- package/dist/rpc/procedures/term/lookup.js +29 -8
- package/dist/rpc/procedures/term/lookup.js.map +1 -1
- package/dist/rpc/procedures/term/meta.d.ts +3 -2
- package/dist/rpc/procedures/term/meta.d.ts.map +1 -1
- package/dist/rpc/procedures/term/meta.js +7 -5
- package/dist/rpc/procedures/term/meta.js.map +1 -1
- package/dist/rpc/procedures/term/update.d.ts +7 -0
- package/dist/rpc/procedures/term/update.d.ts.map +1 -1
- package/dist/rpc/procedures/term/update.js +4 -4
- package/dist/rpc/procedures/term/update.js.map +1 -1
- package/dist/rpc/procedures/user/cancel-email-change.d.ts +7 -0
- package/dist/rpc/procedures/user/cancel-email-change.d.ts.map +1 -1
- package/dist/rpc/procedures/user/delete.d.ts +7 -0
- package/dist/rpc/procedures/user/delete.d.ts.map +1 -1
- package/dist/rpc/procedures/user/disable.d.ts +7 -0
- package/dist/rpc/procedures/user/disable.d.ts.map +1 -1
- package/dist/rpc/procedures/user/enable.d.ts +7 -0
- package/dist/rpc/procedures/user/enable.d.ts.map +1 -1
- package/dist/rpc/procedures/user/get.d.ts +7 -0
- package/dist/rpc/procedures/user/get.d.ts.map +1 -1
- package/dist/rpc/procedures/user/get.js +2 -2
- package/dist/rpc/procedures/user/get.js.map +1 -1
- package/dist/rpc/procedures/user/index.d.ts +77 -0
- package/dist/rpc/procedures/user/index.d.ts.map +1 -1
- package/dist/rpc/procedures/user/invite.d.ts +7 -0
- package/dist/rpc/procedures/user/invite.d.ts.map +1 -1
- package/dist/rpc/procedures/user/list.d.ts +7 -0
- package/dist/rpc/procedures/user/list.d.ts.map +1 -1
- package/dist/rpc/procedures/user/lookup.d.ts +6 -2
- package/dist/rpc/procedures/user/lookup.d.ts.map +1 -1
- package/dist/rpc/procedures/user/lookup.js +29 -0
- package/dist/rpc/procedures/user/lookup.js.map +1 -1
- package/dist/rpc/procedures/user/meta.d.ts +3 -2
- package/dist/rpc/procedures/user/meta.d.ts.map +1 -1
- package/dist/rpc/procedures/user/meta.js +7 -5
- package/dist/rpc/procedures/user/meta.js.map +1 -1
- package/dist/rpc/procedures/user/pending-email-change.d.ts +7 -0
- package/dist/rpc/procedures/user/pending-email-change.d.ts.map +1 -1
- package/dist/rpc/procedures/user/request-email-change.d.ts +7 -0
- package/dist/rpc/procedures/user/request-email-change.d.ts.map +1 -1
- package/dist/rpc/procedures/user/set-locale.d.ts +7 -0
- package/dist/rpc/procedures/user/set-locale.d.ts.map +1 -1
- package/dist/rpc/procedures/user/update.d.ts +7 -0
- package/dist/rpc/procedures/user/update.d.ts.map +1 -1
- package/dist/rpc/procedures/user/update.js +4 -4
- package/dist/rpc/procedures/user/update.js.map +1 -1
- package/dist/rpc/router.d.ts +457 -2
- package/dist/rpc/router.d.ts.map +1 -1
- package/dist/rpc/validation.d.ts +1 -0
- package/dist/rpc/validation.d.ts.map +1 -1
- package/dist/rpc/validation.js +1 -0
- package/dist/rpc/validation.js.map +1 -1
- package/dist/runtime/dispatcher.d.ts.map +1 -1
- package/dist/runtime/dispatcher.js +90 -39
- package/dist/runtime/dispatcher.js.map +1 -1
- package/dist/seo/feed.d.ts.map +1 -1
- package/dist/seo/feed.js +7 -1
- package/dist/seo/feed.js.map +1 -1
- package/dist/settings-core.js +1 -1
- package/dist/settings-core.js.map +1 -1
- package/dist/telemetry-otel.d.ts +29 -0
- package/dist/telemetry-otel.d.ts.map +1 -0
- package/dist/telemetry-otel.js +218 -0
- package/dist/telemetry-otel.js.map +1 -0
- package/dist/template-deps-core.d.ts.map +1 -1
- package/dist/template-deps-core.js +31 -14
- package/dist/template-deps-core.js.map +1 -1
- package/dist/template-deps.d.ts.map +1 -1
- package/dist/template-deps.js +12 -8
- package/dist/template-deps.js.map +1 -1
- package/dist/template-registry.d.ts +0 -16
- package/dist/template-registry.d.ts.map +1 -1
- package/dist/terms/read-service.js +2 -2
- package/dist/terms/read-service.js.map +1 -1
- package/dist/test/index.d.ts +3 -0
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +4 -0
- package/dist/test/index.js.map +1 -1
- package/dist/test/traced-context.d.ts +17 -0
- package/dist/test/traced-context.d.ts.map +1 -0
- package/dist/test/traced-context.js +28 -0
- package/dist/test/traced-context.js.map +1 -0
- package/package.json +6 -2
- package/dist/plugin/fields/checkbox.d.ts +0 -19
- package/dist/plugin/fields/checkbox.d.ts.map +0 -1
- package/dist/plugin/fields/checkbox.js +0 -20
- package/dist/plugin/fields/checkbox.js.map +0 -1
- package/dist/plugin/fields/date.d.ts +0 -24
- package/dist/plugin/fields/date.d.ts.map +0 -1
- package/dist/plugin/fields/date.js +0 -23
- package/dist/plugin/fields/date.js.map +0 -1
- package/dist/plugin/fields/datetime.d.ts +0 -26
- package/dist/plugin/fields/datetime.d.ts.map +0 -1
- package/dist/plugin/fields/datetime.js +0 -25
- package/dist/plugin/fields/datetime.js.map +0 -1
- package/dist/plugin/fields/email.d.ts +0 -17
- package/dist/plugin/fields/email.d.ts.map +0 -1
- package/dist/plugin/fields/email.js +0 -18
- package/dist/plugin/fields/email.js.map +0 -1
- package/dist/plugin/fields/entry-list.d.ts +0 -28
- package/dist/plugin/fields/entry-list.d.ts.map +0 -1
- package/dist/plugin/fields/entry-list.js +0 -31
- package/dist/plugin/fields/entry-list.js.map +0 -1
- package/dist/plugin/fields/multiselect.d.ts +0 -21
- package/dist/plugin/fields/multiselect.d.ts.map +0 -1
- package/dist/plugin/fields/multiselect.js +0 -43
- package/dist/plugin/fields/multiselect.js.map +0 -1
- package/dist/plugin/fields/password.d.ts +0 -24
- package/dist/plugin/fields/password.d.ts.map +0 -1
- package/dist/plugin/fields/password.js +0 -25
- package/dist/plugin/fields/password.js.map +0 -1
- package/dist/plugin/fields/radio.d.ts +0 -16
- package/dist/plugin/fields/radio.d.ts.map +0 -1
- package/dist/plugin/fields/radio.js +0 -17
- package/dist/plugin/fields/radio.js.map +0 -1
- package/dist/plugin/fields/repeater-validate.d.ts +0 -34
- package/dist/plugin/fields/repeater-validate.d.ts.map +0 -1
- package/dist/plugin/fields/repeater-validate.js +0 -104
- package/dist/plugin/fields/repeater-validate.js.map +0 -1
- package/dist/plugin/fields/term-list.d.ts +0 -27
- package/dist/plugin/fields/term-list.d.ts.map +0 -1
- package/dist/plugin/fields/term-list.js +0 -30
- package/dist/plugin/fields/term-list.js.map +0 -1
- package/dist/plugin/fields/text.d.ts +0 -37
- package/dist/plugin/fields/text.d.ts.map +0 -1
- package/dist/plugin/fields/text.js +0 -31
- package/dist/plugin/fields/text.js.map +0 -1
- package/dist/plugin/fields/textarea.d.ts +0 -20
- package/dist/plugin/fields/textarea.d.ts.map +0 -1
- package/dist/plugin/fields/textarea.js +0 -21
- package/dist/plugin/fields/textarea.js.map +0 -1
- package/dist/plugin/fields/time.d.ts +0 -24
- package/dist/plugin/fields/time.d.ts.map +0 -1
- package/dist/plugin/fields/time.js +0 -23
- package/dist/plugin/fields/time.js.map +0 -1
- package/dist/plugin/fields/url.d.ts +0 -17
- package/dist/plugin/fields/url.d.ts.map +0 -1
- package/dist/plugin/fields/url.js +0 -18
- package/dist/plugin/fields/url.js.map +0 -1
- package/dist/plugin/fields/user-list.d.ts +0 -29
- package/dist/plugin/fields/user-list.d.ts.map +0 -1
- package/dist/plugin/fields/user-list.js +0 -31
- package/dist/plugin/fields/user-list.js.map +0 -1
package/dist/rpc/meta/core.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { sql } from "drizzle-orm";
|
|
2
|
+
import { accumulateEmbeddedTags } from "../../cache/embedded-tags.js";
|
|
2
3
|
import { eq } from "../../db/index.js";
|
|
4
|
+
import { isConditionHidden } from "../../plugin/fields/condition.js";
|
|
5
|
+
import { anchorTemporalUtc } from "../../plugin/manifest.js";
|
|
3
6
|
import { MetaReferenceError } from "./errors.js";
|
|
7
|
+
import { META_FIELD_MESSAGES } from "./field-messages.js";
|
|
8
|
+
import { extractStringId, healReferenceValue, isGroupField, isRepeaterField, referenceTargetOf, runFieldPipeline, } from "./field-pipeline.js";
|
|
4
9
|
// Shared meta plumbing for every entity that stores a `meta` JSON
|
|
5
10
|
// column (entries, terms, and — eventually — users). The storage
|
|
6
11
|
// contract: writes merge into the bag via SQLite `json_set` /
|
|
@@ -32,70 +37,100 @@ export class MetaSanitizationError extends Error {
|
|
|
32
37
|
return new MetaSanitizationError(ctx.key, "value_too_large");
|
|
33
38
|
}
|
|
34
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The whole-patch rejection produced by `sanitizeMetaInput` when any
|
|
42
|
+
* field's pipeline reports errors: every `{ path, message }` across
|
|
43
|
+
* every key of the request, so the admin form can address each
|
|
44
|
+
* offending input in one round-trip. Nothing is written when this
|
|
45
|
+
* throws.
|
|
46
|
+
*/
|
|
47
|
+
export class MetaValidationError extends Error {
|
|
48
|
+
static {
|
|
49
|
+
MetaValidationError.prototype.name = "MetaValidationError";
|
|
50
|
+
}
|
|
51
|
+
errors;
|
|
52
|
+
constructor(errors) {
|
|
53
|
+
super(`meta validation failed: ${errors.map((error) => error.path).join(", ")}`);
|
|
54
|
+
this.errors = errors;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
35
57
|
/**
|
|
36
58
|
* Validate an incoming meta map against a field-lookup fn produced by
|
|
37
59
|
* the caller (entry vs term differ only in which registry they walk).
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
60
|
+
* `null` / `undefined` values are deletion requests; everything else
|
|
61
|
+
* runs the per-field pipeline (coercion → `.sanitize()` → declarative
|
|
62
|
+
* constraints → `.validate()`). Pipeline rejections aggregate across
|
|
63
|
+
* the whole patch into one `MetaValidationError`; unregistered keys
|
|
64
|
+
* and oversized values keep the legacy fail-fast
|
|
65
|
+
* `MetaSanitizationError` surface.
|
|
41
66
|
*/
|
|
42
|
-
export function sanitizeMetaInput(findField, input) {
|
|
67
|
+
export async function sanitizeMetaInput(findField, input, mode = "strict") {
|
|
43
68
|
if (input === undefined)
|
|
44
69
|
return null;
|
|
45
70
|
const upserts = new Map();
|
|
46
71
|
const deletes = [];
|
|
72
|
+
const fieldErrors = [];
|
|
47
73
|
for (const [key, rawValue] of Object.entries(input)) {
|
|
48
74
|
const field = findField(key);
|
|
49
75
|
if (!field) {
|
|
76
|
+
// A delete request (null/undefined) for a key the field system doesn't
|
|
77
|
+
// own is a harmless no-op — never fail the whole write over an untracked
|
|
78
|
+
// foreign key (e.g. one written by another plugin). Unknown *upserts*
|
|
79
|
+
// still reject so junk can't enter through this gate.
|
|
80
|
+
if (rawValue === null || rawValue === undefined)
|
|
81
|
+
continue;
|
|
50
82
|
throw MetaSanitizationError.notRegistered({ key });
|
|
51
83
|
}
|
|
52
|
-
if (
|
|
84
|
+
if (isConditionHidden(field, input))
|
|
85
|
+
continue;
|
|
86
|
+
const result = await runFieldPipeline(field, rawValue, key, mode);
|
|
87
|
+
if (result.errors.length > 0) {
|
|
88
|
+
fieldErrors.push(...result.errors);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (result.isDeletion === true) {
|
|
53
92
|
deletes.push(key);
|
|
54
93
|
continue;
|
|
55
94
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
upserts.set(key, sanitized);
|
|
95
|
+
assertEncodedSize(key, result.value);
|
|
96
|
+
upserts.set(key, result.value);
|
|
97
|
+
}
|
|
98
|
+
if (fieldErrors.length > 0) {
|
|
99
|
+
throw new MetaValidationError(fieldErrors);
|
|
62
100
|
}
|
|
63
101
|
return { upserts, deletes };
|
|
64
102
|
}
|
|
65
103
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* throw vanilla errors without importing the package-internal error
|
|
73
|
-
* class.
|
|
104
|
+
* Thin wrapper that translates thrown meta errors into the RPC
|
|
105
|
+
* handler's CONFLICT envelope. Pipeline rejections ship their
|
|
106
|
+
* `{ path, message }` list under `data.errors` (with `key` pointing
|
|
107
|
+
* at the first error's top-level field for legacy consumers); the
|
|
108
|
+
* fail-fast `MetaSanitizationError` reasons keep their existing
|
|
109
|
+
* `data.reason`/`data.key` shape.
|
|
74
110
|
*/
|
|
75
|
-
function runSanitize(sanitize, key, value) {
|
|
76
|
-
try {
|
|
77
|
-
return sanitize(value);
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
if (error instanceof MetaSanitizationError)
|
|
81
|
-
throw error;
|
|
82
|
-
// Buggy sanitize callbacks otherwise round to a generic
|
|
83
|
-
// `invalid_value` envelope, which is fine for the editor's UX
|
|
84
|
-
// but loses the underlying stack. Log it before translating so
|
|
85
|
-
// server logs preserve the diagnostic trail.
|
|
86
|
-
console.error(`[plumix] sanitize callback for meta key ${JSON.stringify(key)} threw:`, error);
|
|
87
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
111
|
/**
|
|
91
|
-
*
|
|
92
|
-
* the
|
|
112
|
+
* Map a whole-patch meta validation failure onto the RPC `CONFLICT`
|
|
113
|
+
* envelope — one place so the wire shape (`key` at the first error's
|
|
114
|
+
* top-level field, the full `errors` list) can't drift between the write
|
|
115
|
+
* path and the publish gate.
|
|
93
116
|
*/
|
|
94
|
-
export function
|
|
117
|
+
export function metaValidationConflict(error, errors) {
|
|
118
|
+
return errors.CONFLICT({
|
|
119
|
+
data: {
|
|
120
|
+
reason: "meta_invalid_value",
|
|
121
|
+
key: error.errors[0]?.path.split(".")[0],
|
|
122
|
+
errors: [...error.errors],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export async function sanitizeMetaForRpc(findField, input, errors, mode = "strict") {
|
|
95
127
|
try {
|
|
96
|
-
return sanitizeMetaInput(findField, input);
|
|
128
|
+
return await sanitizeMetaInput(findField, input, mode);
|
|
97
129
|
}
|
|
98
130
|
catch (error) {
|
|
131
|
+
if (error instanceof MetaValidationError) {
|
|
132
|
+
throw metaValidationConflict(error, errors);
|
|
133
|
+
}
|
|
99
134
|
if (error instanceof MetaSanitizationError) {
|
|
100
135
|
throw errors.CONFLICT({
|
|
101
136
|
data: { reason: `meta_${error.reason}`, key: error.key },
|
|
@@ -104,6 +139,58 @@ export function sanitizeMetaForRpc(findField, input, errors) {
|
|
|
104
139
|
throw error;
|
|
105
140
|
}
|
|
106
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Strict publish gate: validate a whole *stored* meta bag against the full
|
|
144
|
+
* field list before `entry.publish` promotes it onto the live row. Draft
|
|
145
|
+
* autosaves are lenient (business rules skipped so work-in-progress always
|
|
146
|
+
* saves), so publish is where required fields, bounds, formats, option
|
|
147
|
+
* membership, and row counts are finally enforced. It walks the field
|
|
148
|
+
* *definitions*, not just the bag, so a required field ABSENT from the bag
|
|
149
|
+
* is caught as well as one stored empty — `runFieldPipeline` sees
|
|
150
|
+
* `undefined` and rejects it in strict mode.
|
|
151
|
+
*
|
|
152
|
+
* Rejections aggregate across the bag into one `MetaValidationError` the
|
|
153
|
+
* RPC layer maps onto the admin inputs, blocking the publish. A passing bag
|
|
154
|
+
* comes back canonicalized (each registered value re-run through
|
|
155
|
+
* `.sanitize()`); a conditionally-hidden field can't be required, so it's
|
|
156
|
+
* skipped; keys the field system doesn't own (e.g. from an uninstalled
|
|
157
|
+
* plugin) pass through untouched, matching the read path. Field
|
|
158
|
+
* capabilities and reference existence are not re-checked here — a
|
|
159
|
+
* whole-bag gate would block a publisher over a co-author's field, and both
|
|
160
|
+
* were already enforced at write time.
|
|
161
|
+
*/
|
|
162
|
+
export async function validateAndPromoteMetaBag(fields, bag) {
|
|
163
|
+
const out = {};
|
|
164
|
+
const owned = new Set();
|
|
165
|
+
const fieldErrors = [];
|
|
166
|
+
for (const field of fields) {
|
|
167
|
+
owned.add(field.key);
|
|
168
|
+
// A hidden field is inactive, so it can't be required — but its stored
|
|
169
|
+
// value is kept untouched (not validated, not dropped), or the value a
|
|
170
|
+
// driver hides would be lost on publish and gone when the driver flips
|
|
171
|
+
// back.
|
|
172
|
+
if (isConditionHidden(field, bag)) {
|
|
173
|
+
if (field.key in bag)
|
|
174
|
+
out[field.key] = bag[field.key];
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const result = await runFieldPipeline(field, bag[field.key], field.key);
|
|
178
|
+
if (result.errors.length > 0) {
|
|
179
|
+
fieldErrors.push(...result.errors);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (result.isDeletion === true)
|
|
183
|
+
continue;
|
|
184
|
+
out[field.key] = result.value;
|
|
185
|
+
}
|
|
186
|
+
for (const [key, value] of Object.entries(bag)) {
|
|
187
|
+
if (!owned.has(key))
|
|
188
|
+
out[key] = value;
|
|
189
|
+
}
|
|
190
|
+
if (fieldErrors.length > 0)
|
|
191
|
+
throw new MetaValidationError(fieldErrors);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
107
194
|
/**
|
|
108
195
|
* Walk a sanitized patch, group every reference upsert by
|
|
109
196
|
* `(kind, scope)`, and issue one `LookupAdapter.list({ ids })`
|
|
@@ -118,7 +205,7 @@ export function sanitizeMetaForRpc(findField, input, errors) {
|
|
|
118
205
|
* TOCTOU note: validate runs in a separate query from the eventual
|
|
119
206
|
* `applyMetaPatch`, and callers don't share a transaction. A
|
|
120
207
|
* concurrent delete between validate and apply leaves an orphan id
|
|
121
|
-
* in the meta bag; `
|
|
208
|
+
* in the meta bag; `hydrateMetaBags` masks it on read. Wrap the
|
|
122
209
|
* validate/apply pair in `ctx.db.transaction()` if a caller needs
|
|
123
210
|
* serializable consistency.
|
|
124
211
|
*/
|
|
@@ -133,58 +220,41 @@ export async function validateMetaReferences(ctx, findField, patch) {
|
|
|
133
220
|
throw MetaSanitizationError.invalidValue({ key });
|
|
134
221
|
}
|
|
135
222
|
const ids = referenceIdsForValidation(key, value, target, fieldMax(field));
|
|
223
|
+
// Normalize eagerly — a validation failure below aborts the whole
|
|
224
|
+
// save, so a rewritten patch never persists on the failure path.
|
|
225
|
+
patch.upserts.set(key, target.multiple ? ids : ids[0]);
|
|
136
226
|
const group = upsertGroup(groups, target, registered);
|
|
137
227
|
for (const id of ids)
|
|
138
228
|
group.ids.add(id);
|
|
139
|
-
group.contributions.push({
|
|
140
|
-
errorKey: key,
|
|
141
|
-
ids,
|
|
142
|
-
applyNormalize: (rowsById) => {
|
|
143
|
-
if (target.valueShape !== "object")
|
|
144
|
-
return;
|
|
145
|
-
if (target.multiple) {
|
|
146
|
-
patch.upserts.set(key, ids.map((id) => buildCachedReference(id, rowsById)));
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const [singleId] = ids;
|
|
150
|
-
if (singleId !== undefined) {
|
|
151
|
-
patch.upserts.set(key, buildCachedReference(singleId, rowsById));
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
});
|
|
229
|
+
group.contributions.push({ errorKey: key, ids });
|
|
155
230
|
continue;
|
|
156
231
|
}
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// `
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// log, not the wire response
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
continue;
|
|
167
|
-
collectRepeaterReferences(ctx, key, field, value, groups);
|
|
232
|
+
// Composite fields (repeater / group) don't carry a
|
|
233
|
+
// `referenceTarget` themselves but their rows / members can, at any
|
|
234
|
+
// nesting depth. Walk them so nested `entry` / `term` / `user` /
|
|
235
|
+
// `media` refs flow through the same `(kind, scope)` batch as
|
|
236
|
+
// top-level fields. Errors attribute to the top-level key — the
|
|
237
|
+
// dotted sub-path lives in the developer log, not the wire response.
|
|
238
|
+
if (isRepeaterField(field) || isGroupField(field)) {
|
|
239
|
+
collectCompositeReferences(ctx, key, key, field, value, groups);
|
|
240
|
+
}
|
|
168
241
|
}
|
|
169
242
|
for (const group of groups.values()) {
|
|
170
243
|
if (group.ids.size === 0)
|
|
171
244
|
continue;
|
|
172
|
-
const
|
|
173
|
-
const rowsById = new Map(liveRows.map((r) => [r.id, r]));
|
|
245
|
+
const liveIds = await fetchLiveIds(ctx, group.registered, group.scope, group.ids, "validateMetaReferences");
|
|
174
246
|
for (const contribution of group.contributions) {
|
|
175
247
|
for (const id of contribution.ids) {
|
|
176
|
-
if (!
|
|
248
|
+
if (!liveIds.has(id)) {
|
|
177
249
|
if (contribution.diagnostic !== undefined) {
|
|
178
250
|
// Wire error keys on the top-level field; this log line
|
|
179
|
-
// is the only place the
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`row ${String(contribution.diagnostic.rowIdx)} ` +
|
|
187
|
-
`subField ${JSON.stringify(contribution.diagnostic.subKey)} ` +
|
|
251
|
+
// is the only place the nested sub-path surfaces, so an
|
|
252
|
+
// engineer debugging a `meta_invalid_value` on a composite
|
|
253
|
+
// field can locate the offending cell without bisecting the
|
|
254
|
+
// saved bag. `JSON.stringify` escapes control characters so
|
|
255
|
+
// a user-supplied id with newlines can't poison the log.
|
|
256
|
+
console.error(`[plumix] meta composite ${JSON.stringify(contribution.errorKey)} ` +
|
|
257
|
+
`at ${JSON.stringify(contribution.diagnostic.path)} ` +
|
|
188
258
|
`references missing id ${JSON.stringify(id)}`);
|
|
189
259
|
}
|
|
190
260
|
throw MetaSanitizationError.invalidValue({
|
|
@@ -192,61 +262,60 @@ export async function validateMetaReferences(ctx, findField, patch) {
|
|
|
192
262
|
});
|
|
193
263
|
}
|
|
194
264
|
}
|
|
195
|
-
contribution.applyNormalize(rowsById);
|
|
196
265
|
}
|
|
197
266
|
}
|
|
198
267
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
211
|
-
continue;
|
|
212
|
-
const rowObj = row;
|
|
213
|
-
for (const subField of field.subFields) {
|
|
214
|
-
const target = referenceTargetOf(subField);
|
|
215
|
-
if (!target)
|
|
268
|
+
// Walk a composite field's live value, collecting + normalizing every
|
|
269
|
+
// nested reference in place. `path` is the dotted address of `field`
|
|
270
|
+
// from the top-level key (developer-log only). Recurses through nested
|
|
271
|
+
// repeaters and groups, so a reference at any depth flows through the
|
|
272
|
+
// same `(kind, scope)` batch and is orphan-checked at write time.
|
|
273
|
+
function collectCompositeReferences(ctx, topKey, path, field, value, groups) {
|
|
274
|
+
if (isRepeaterField(field)) {
|
|
275
|
+
if (!Array.isArray(value))
|
|
276
|
+
return;
|
|
277
|
+
for (const [rowIdx, row] of value.entries()) {
|
|
278
|
+
if (!isPlainObject(row))
|
|
216
279
|
continue;
|
|
217
|
-
|
|
280
|
+
collectMemberReferences(ctx, topKey, `${path}.${String(rowIdx)}`, field.subFields, row, groups);
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (isGroupField(field)) {
|
|
285
|
+
if (!isPlainObject(value))
|
|
286
|
+
return;
|
|
287
|
+
collectMemberReferences(ctx, topKey, path, field.fields, value, groups);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function collectMemberReferences(ctx, topKey, path, members, container, groups) {
|
|
291
|
+
for (const member of members) {
|
|
292
|
+
const memberPath = `${path}.${member.key}`;
|
|
293
|
+
const subValue = container[member.key];
|
|
294
|
+
const target = referenceTargetOf(member);
|
|
295
|
+
if (target) {
|
|
218
296
|
if (subValue === undefined || subValue === null)
|
|
219
297
|
continue;
|
|
220
298
|
const registered = ctx.plugins.lookupAdapters.get(target.kind);
|
|
221
299
|
if (!registered) {
|
|
222
300
|
throw MetaSanitizationError.invalidValue({ key: topKey });
|
|
223
301
|
}
|
|
224
|
-
const ids = referenceIdsForValidation(topKey, subValue, target, fieldMax(
|
|
302
|
+
const ids = referenceIdsForValidation(topKey, subValue, target, fieldMax(member));
|
|
303
|
+
// Normalize the cell in place — the top-level upsert value is the
|
|
304
|
+
// live storage shape, so mutating an object inside it is what the
|
|
305
|
+
// caller serializes.
|
|
306
|
+
container[member.key] = target.multiple ? ids : ids[0];
|
|
225
307
|
const group = upsertGroup(groups, target, registered);
|
|
226
308
|
for (const id of ids)
|
|
227
309
|
group.ids.add(id);
|
|
228
|
-
const subKey = subField.key;
|
|
229
310
|
group.contributions.push({
|
|
230
311
|
errorKey: topKey,
|
|
231
312
|
ids,
|
|
232
|
-
diagnostic: {
|
|
233
|
-
applyNormalize: (rowsById) => {
|
|
234
|
-
if (target.valueShape !== "object")
|
|
235
|
-
return;
|
|
236
|
-
// Rewrite the row's cached-object value in place — the
|
|
237
|
-
// top-level upsert array is the live storage shape, so
|
|
238
|
-
// mutating an object inside it is what the caller will
|
|
239
|
-
// serialize.
|
|
240
|
-
if (target.multiple) {
|
|
241
|
-
rowObj[subKey] = ids.map((id) => buildCachedReference(id, rowsById));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
const [singleId] = ids;
|
|
245
|
-
if (singleId !== undefined) {
|
|
246
|
-
rowObj[subKey] = buildCachedReference(singleId, rowsById);
|
|
247
|
-
}
|
|
248
|
-
},
|
|
313
|
+
diagnostic: { path: memberPath },
|
|
249
314
|
});
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (isRepeaterField(member) || isGroupField(member)) {
|
|
318
|
+
collectCompositeReferences(ctx, topKey, memberPath, member, subValue, groups);
|
|
250
319
|
}
|
|
251
320
|
}
|
|
252
321
|
}
|
|
@@ -264,13 +333,6 @@ function upsertGroup(groups, target, registered) {
|
|
|
264
333
|
}
|
|
265
334
|
return group;
|
|
266
335
|
}
|
|
267
|
-
// Adapter cached fields override anything; user-supplied non-id keys
|
|
268
|
-
// don't survive. Per-usage user fields (e.g. alt overrides) are a
|
|
269
|
-
// post-v0.1 feature — when added, this is where they'd merge in.
|
|
270
|
-
function buildCachedReference(id, rowsById) {
|
|
271
|
-
const row = rowsById.get(id);
|
|
272
|
-
return { id, ...(row?.cached ?? {}) };
|
|
273
|
-
}
|
|
274
336
|
// Defense-in-depth ceiling on the internal-aggregated id-batch size.
|
|
275
337
|
// Above this, the live-id fetch throws rather than silently truncating.
|
|
276
338
|
// The wire cap (100) and per-field cap (`HARD_MULTI_REFERENCE_LIMIT`,
|
|
@@ -305,20 +367,17 @@ function referenceGroupKey(target) {
|
|
|
305
367
|
// 100, so we only hit the ceiling when many fields share `(kind, scope)`
|
|
306
368
|
// and aggregate — at which point throwing beats silent truncation
|
|
307
369
|
// (truncation would either reject valid writes or hide live targets).
|
|
308
|
-
|
|
309
|
-
// Returns `LookupResult[]` rather than just live ids so the validator
|
|
310
|
-
// can read `cached` for the cached-object normalize step. Callers that
|
|
311
|
-
// only care about presence build a `Set` from the row ids.
|
|
312
|
-
async function fetchLiveRows(ctx, registered, scope, ids, callsite) {
|
|
370
|
+
async function fetchLiveIds(ctx, registered, scope, ids, callsite) {
|
|
313
371
|
if (ids.size > MAX_REFERENCE_GROUP_BATCH) {
|
|
314
372
|
throw MetaReferenceError.batchSizeExceeded(callsite, ids.size, MAX_REFERENCE_GROUP_BATCH);
|
|
315
373
|
}
|
|
316
374
|
const idList = [...ids];
|
|
317
|
-
|
|
375
|
+
const rows = await registered.adapter.list(ctx, {
|
|
318
376
|
ids: idList,
|
|
319
377
|
scope,
|
|
320
378
|
limit: idList.length,
|
|
321
379
|
});
|
|
380
|
+
return new Set(rows.map((row) => row.id));
|
|
322
381
|
}
|
|
323
382
|
// Defensive upper bound on multi-reference array length, applied
|
|
324
383
|
// per-field before grouping. Even with the batched `list({ ids })`
|
|
@@ -328,18 +387,11 @@ async function fetchLiveRows(ctx, registered, scope, ids, callsite) {
|
|
|
328
387
|
// can declare a lower `max` and the validator picks the smaller.
|
|
329
388
|
const HARD_MULTI_REFERENCE_LIMIT = 100;
|
|
330
389
|
// Validates the wire shape of a reference value and returns the ids
|
|
331
|
-
// to feed into the group's batched `list({ ids })` call.
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
// cached-object reference
|
|
337
|
-
// (or bare strings for
|
|
338
|
-
// leniency on first write)
|
|
339
|
-
// - multi=false, valueShape="id" (default): bare string id
|
|
340
|
-
// - multi=false, valueShape="object": `{ id: string, ... }`
|
|
341
|
-
// (or a bare string for
|
|
342
|
-
// leniency on first write)
|
|
390
|
+
// to feed into the group's batched `list({ ids })` call. Storage is
|
|
391
|
+
// plain ids — a bare string (single) or string[] (multi) — but each
|
|
392
|
+
// slot leniently accepts the retired cached-object shape
|
|
393
|
+
// (`{ id, ... }`) so legacy values self-heal to the plain form on
|
|
394
|
+
// the entity's next save.
|
|
343
395
|
function referenceIdsForValidation(key, value, target, max) {
|
|
344
396
|
if (target.multiple) {
|
|
345
397
|
if (!Array.isArray(value)) {
|
|
@@ -351,56 +403,17 @@ function referenceIdsForValidation(key, value, target, max) {
|
|
|
351
403
|
if (max !== undefined && value.length > max) {
|
|
352
404
|
throw MetaSanitizationError.invalidValue({ key });
|
|
353
405
|
}
|
|
354
|
-
|
|
355
|
-
// Lenient on input: each item can be `"42"` (first-write
|
|
356
|
-
// convenience) or `{ id: "42", ... }`. Always rewritten to the
|
|
357
|
-
// canonical `{ id, ...cached }` shape after the live-id check.
|
|
358
|
-
const ids = [];
|
|
359
|
-
for (const item of value) {
|
|
360
|
-
if (typeof item === "string" && item !== "") {
|
|
361
|
-
ids.push(item);
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
const id = extractStringId(item);
|
|
365
|
-
if (id !== null && id !== "") {
|
|
366
|
-
ids.push(id);
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
370
|
-
}
|
|
371
|
-
return ids;
|
|
372
|
-
}
|
|
373
|
-
for (const item of value) {
|
|
374
|
-
if (typeof item !== "string" || item === "") {
|
|
375
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return value;
|
|
406
|
+
return value.map((item) => referenceItemId(key, item));
|
|
379
407
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
390
|
-
}
|
|
391
|
-
if (typeof value !== "string") {
|
|
392
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
393
|
-
}
|
|
394
|
-
return [value];
|
|
395
|
-
}
|
|
396
|
-
// Returns the `id` string of a `{ id: string, ... }` object, or null
|
|
397
|
-
// for any other shape (string, array, null, primitive, missing key).
|
|
398
|
-
function extractStringId(value) {
|
|
399
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
const id = value.id;
|
|
403
|
-
return typeof id === "string" ? id : null;
|
|
408
|
+
return [referenceItemId(key, value)];
|
|
409
|
+
}
|
|
410
|
+
function referenceItemId(key, item) {
|
|
411
|
+
if (typeof item === "string" && item !== "")
|
|
412
|
+
return item;
|
|
413
|
+
const id = extractStringId(item);
|
|
414
|
+
if (id !== null && id !== "")
|
|
415
|
+
return id;
|
|
416
|
+
throw MetaSanitizationError.invalidValue({ key });
|
|
404
417
|
}
|
|
405
418
|
function fieldMax(field) {
|
|
406
419
|
return field?.max;
|
|
@@ -418,201 +431,318 @@ export async function validateMetaReferencesForRpc(ctx, findField, patch, errors
|
|
|
418
431
|
catch (error) {
|
|
419
432
|
if (error instanceof MetaSanitizationError) {
|
|
420
433
|
throw errors.CONFLICT({
|
|
421
|
-
data: {
|
|
434
|
+
data: {
|
|
435
|
+
reason: `meta_${error.reason}`,
|
|
436
|
+
key: error.key,
|
|
437
|
+
// Reference failures address the top-level field (row/subKey
|
|
438
|
+
// detail stays in the server log) — shipping them under
|
|
439
|
+
// `errors` too lets the admin form surface them inline.
|
|
440
|
+
errors: [
|
|
441
|
+
{ path: error.key, message: META_FIELD_MESSAGES.invalidOption },
|
|
442
|
+
],
|
|
443
|
+
},
|
|
422
444
|
});
|
|
423
445
|
}
|
|
424
446
|
throw error;
|
|
425
447
|
}
|
|
426
448
|
}
|
|
449
|
+
// A reference field authored with `.returns("id")` reads the bare
|
|
450
|
+
// stored id — the hydration walk skips it so no lookup query runs and
|
|
451
|
+
// the id survives untouched. `"returns" in field` narrows to the field
|
|
452
|
+
// variants that carry the flag (temporal's `"date"` never matches).
|
|
453
|
+
function readsRawReferenceId(field) {
|
|
454
|
+
return field !== undefined && "returns" in field && field.returns === "id";
|
|
455
|
+
}
|
|
427
456
|
/**
|
|
428
457
|
* Yield every reference-field occurrence in a decoded meta bag: top-level
|
|
429
|
-
* reference fields, plus
|
|
430
|
-
* structural walk so the
|
|
431
|
-
*
|
|
458
|
+
* reference fields, plus references nested inside repeater rows and
|
|
459
|
+
* groups at any depth. One structural walk so the hydration pass doesn't
|
|
460
|
+
* reinline the traversal twice.
|
|
432
461
|
*/
|
|
433
462
|
function* referenceOccurrences(entries, findField) {
|
|
434
463
|
for (const [key, value] of entries) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
464
|
+
yield* fieldOccurrences([key], findField(key), value);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Recurse a single field's decoded value, yielding reference occurrences
|
|
468
|
+
// with their full path. Nested field definitions come straight off the
|
|
469
|
+
// composite field (`subFields` / `fields`) — `findField` only resolves
|
|
470
|
+
// top-level keys.
|
|
471
|
+
function* fieldOccurrences(path, field, value) {
|
|
472
|
+
const target = referenceTargetOf(field);
|
|
473
|
+
if (target) {
|
|
474
|
+
// `.returns("id")` opts out of the hydration join at any depth —
|
|
475
|
+
// leave the stored id(s) untouched (no resolve, no orphan-strip).
|
|
476
|
+
if (!readsRawReferenceId(field))
|
|
477
|
+
yield { path, target, value };
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (isRepeaterField(field)) {
|
|
443
481
|
if (!Array.isArray(value))
|
|
444
|
-
|
|
482
|
+
return;
|
|
445
483
|
for (const [rowIdx, row] of value.entries()) {
|
|
446
|
-
if (!
|
|
484
|
+
if (!isPlainObject(row))
|
|
447
485
|
continue;
|
|
448
|
-
const rowObj = row;
|
|
449
486
|
for (const subField of field.subFields) {
|
|
450
|
-
|
|
451
|
-
if (!subTarget)
|
|
452
|
-
continue;
|
|
453
|
-
yield {
|
|
454
|
-
key,
|
|
455
|
-
target: subTarget,
|
|
456
|
-
value: rowObj[subField.key],
|
|
457
|
-
nested: { rowIdx, subKey: subField.key },
|
|
458
|
-
};
|
|
487
|
+
yield* fieldOccurrences([...path, rowIdx, subField.key], subField, row[subField.key]);
|
|
459
488
|
}
|
|
460
489
|
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (isGroupField(field)) {
|
|
493
|
+
if (!isPlainObject(value))
|
|
494
|
+
return;
|
|
495
|
+
for (const member of field.fields) {
|
|
496
|
+
yield* fieldOccurrences([...path, member.key], member, value[member.key]);
|
|
497
|
+
}
|
|
461
498
|
}
|
|
462
499
|
}
|
|
463
|
-
|
|
500
|
+
/** Single-bag convenience over {@link hydrateMetaBags}. */
|
|
501
|
+
export async function hydrateMetaReferences(ctx, findField, decoded) {
|
|
502
|
+
const [bag] = await hydrateMetaBags(ctx, [{ findField, decoded }]);
|
|
503
|
+
// hydrateMetaBags returns one bag per input by construction.
|
|
504
|
+
return bag ?? decoded;
|
|
505
|
+
}
|
|
506
|
+
export async function hydrateMetaBags(ctx, bags) {
|
|
464
507
|
const candidates = [];
|
|
465
508
|
const groups = new Map();
|
|
466
|
-
// Top-level refs and repeater
|
|
467
|
-
// treatment, so the single `referenceOccurrences` walk feeds both.
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const groupKey = referenceGroupKey(occ.target);
|
|
478
|
-
candidates.push({
|
|
479
|
-
key: occ.key,
|
|
480
|
-
multiple: occ.target.multiple === true,
|
|
481
|
-
valueShape: occ.target.valueShape ?? "id",
|
|
482
|
-
groupKey,
|
|
483
|
-
ids,
|
|
484
|
-
nested: occ.nested,
|
|
485
|
-
});
|
|
486
|
-
if (ids.length === 0)
|
|
487
|
-
continue;
|
|
488
|
-
let group = groups.get(groupKey);
|
|
489
|
-
if (!group) {
|
|
490
|
-
group = { registered, scope: occ.target.scope, ids: new Set() };
|
|
491
|
-
groups.set(groupKey, group);
|
|
492
|
-
}
|
|
493
|
-
for (const id of ids)
|
|
494
|
-
group.ids.add(id);
|
|
495
|
-
}
|
|
496
|
-
// Pass 2: one `list({ ids })` per group, keyed for O(1) apply lookup.
|
|
497
|
-
const liveIdsByGroup = new Map();
|
|
498
|
-
for (const [groupKey, group] of groups) {
|
|
499
|
-
const rows = await fetchLiveRows(ctx, group.registered, group.scope, group.ids, "filterMetaOrphans");
|
|
500
|
-
liveIdsByGroup.set(groupKey, new Set(rows.map((row) => row.id)));
|
|
501
|
-
}
|
|
502
|
-
// Pass 3: apply the filter. Orphan handling differs by shape:
|
|
503
|
-
// - multi + id: filter the string[] in place, dropping missing
|
|
504
|
-
// - multi + object: filter the object[] preserving order
|
|
505
|
-
// - single + id: null on missing
|
|
506
|
-
// - single + object: null on missing; otherwise keep the stored
|
|
507
|
-
// cached value as-is (no read-time refresh — write rewrites)
|
|
508
|
-
// Nested-in-repeater candidates rewrite the row's subField slot in
|
|
509
|
-
// a per-key cloned array so the input bag stays untouched.
|
|
510
|
-
const out = { ...decoded };
|
|
511
|
-
const empty = new Set();
|
|
512
|
-
for (const candidate of candidates) {
|
|
513
|
-
const { key, multiple, valueShape, groupKey, ids, nested } = candidate;
|
|
514
|
-
const liveIds = liveIdsByGroup.get(groupKey) ?? empty;
|
|
515
|
-
if (nested !== undefined) {
|
|
516
|
-
const rowObj = takeWritableRow(out, decoded, key, nested.rowIdx);
|
|
517
|
-
if (!rowObj)
|
|
509
|
+
// Top-level refs and nested (repeater / group) refs get identical
|
|
510
|
+
// treatment, so the single `referenceOccurrences` walk feeds both.
|
|
511
|
+
// Every candidate carries its full path; Pass 3 rewrites the slot in a
|
|
512
|
+
// per-path copy-on-write so callers' bags stay untouched.
|
|
513
|
+
const out = [];
|
|
514
|
+
for (const bag of bags) {
|
|
515
|
+
const outBag = { ...bag.decoded };
|
|
516
|
+
out.push(outBag);
|
|
517
|
+
for (const occ of referenceOccurrences(Object.entries(bag.decoded), bag.findField)) {
|
|
518
|
+
const registered = ctx.plugins.lookupAdapters.get(occ.target.kind);
|
|
519
|
+
if (!registered)
|
|
518
520
|
continue;
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
521
|
+
const ids = referenceCandidateIds(occ.target, occ.value);
|
|
522
|
+
if (ids === null)
|
|
523
|
+
continue; // non-array multi / non-string single — leave untouched
|
|
524
|
+
const groupKey = referenceGroupKey(occ.target);
|
|
525
|
+
candidates.push({
|
|
526
|
+
outBag,
|
|
527
|
+
decoded: bag.decoded,
|
|
528
|
+
path: occ.path,
|
|
529
|
+
multiple: occ.target.multiple === true,
|
|
530
|
+
groupKey,
|
|
531
|
+
ids,
|
|
532
|
+
});
|
|
533
|
+
if (ids.length === 0)
|
|
529
534
|
continue;
|
|
535
|
+
let group = groups.get(groupKey);
|
|
536
|
+
if (!group) {
|
|
537
|
+
group = { registered, scope: occ.target.scope, ids: new Set() };
|
|
538
|
+
groups.set(groupKey, group);
|
|
530
539
|
}
|
|
531
|
-
|
|
532
|
-
|
|
540
|
+
for (const id of ids)
|
|
541
|
+
group.ids.add(id);
|
|
533
542
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
543
|
+
}
|
|
544
|
+
// Pass 2: one `hydrate({ ids })` (or `list({ ids })` fallback) per
|
|
545
|
+
// group, keyed for O(1) apply lookup. Groups are independent, so a
|
|
546
|
+
// bag mixing entry + user + media refs resolves them concurrently.
|
|
547
|
+
const resolutions = new Map(await Promise.all([...groups].map(async ([groupKey, group]) => [
|
|
548
|
+
groupKey,
|
|
549
|
+
await resolveGroup(ctx, group.registered.adapter, group.scope, group.ids),
|
|
550
|
+
])));
|
|
551
|
+
// Pass 3: apply. Hydrated groups replace ids with their payloads;
|
|
552
|
+
// id-only groups keep ids. Multi refs stay dense and in stored
|
|
553
|
+
// order; single refs null on missing. Nested candidates copy-on-write
|
|
554
|
+
// each container down their path so callers' bags stay untouched.
|
|
555
|
+
const emptyResolution = { kind: "ids", liveIds: new Set() };
|
|
556
|
+
for (const candidate of candidates) {
|
|
557
|
+
const { outBag, decoded, path, multiple, groupKey, ids } = candidate;
|
|
558
|
+
const resolution = resolutions.get(groupKey) ?? emptyResolution;
|
|
559
|
+
const slot = takeWritableSlot(outBag, decoded, path);
|
|
560
|
+
if (!slot)
|
|
561
|
+
continue;
|
|
562
|
+
applyResolutionToSlot(slot.parent, slot.leafKey, multiple, ids, resolution);
|
|
539
563
|
}
|
|
540
564
|
return out;
|
|
541
565
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
566
|
+
/**
|
|
567
|
+
* Batched, tag-accounted hydration of a raw id set for one reference kind
|
|
568
|
+
* — the theme-facing counterpart to the meta pipeline's hydration (#1508).
|
|
569
|
+
* A theme holding an id-only reference field (a field declared
|
|
570
|
+
* `.returns("id")`, or one contributed by a third-party plugin) resolves
|
|
571
|
+
* it here instead of hand-rolled per-item fetches: ids resolve through the
|
|
572
|
+
* adapter's batched `hydrate` (chunked, one in-query per chunk) and every
|
|
573
|
+
* resolved entity folds its cache tag into the page through the same
|
|
574
|
+
* accumulator the meta pipeline uses, so the page is purged when an
|
|
575
|
+
* embedded entity changes.
|
|
576
|
+
*
|
|
577
|
+
* Returns the hydrated payloads dense and in the requested id order — ids
|
|
578
|
+
* that are gone or out of scope are dropped, mirroring multi-reference
|
|
579
|
+
* field hydration. An unregistered kind, an adapter without the `hydrate`
|
|
580
|
+
* contract, or an empty id set yields `[]`.
|
|
581
|
+
*/
|
|
582
|
+
export async function hydrateReferences(ctx, kind, ids, options = {}) {
|
|
583
|
+
const registered = ctx.plugins.lookupAdapters.get(kind);
|
|
584
|
+
if (!registered?.adapter.hydrate)
|
|
585
|
+
return [];
|
|
586
|
+
const unique = new Set(ids.filter((id) => id !== ""));
|
|
587
|
+
if (unique.size === 0)
|
|
588
|
+
return [];
|
|
589
|
+
const resolution = await resolveGroup(ctx, registered.adapter, options.scope, unique);
|
|
590
|
+
if (resolution.kind !== "hydrated")
|
|
591
|
+
return [];
|
|
592
|
+
return ids
|
|
593
|
+
.map((id) => resolution.byId.get(id))
|
|
594
|
+
.filter((p) => p !== undefined);
|
|
595
|
+
}
|
|
596
|
+
// Resolve one `(kind, scope)` group's aggregated ids. Chunked at
|
|
597
|
+
// `HYDRATION_QUERY_ID_LIMIT` per in-query: a response-level group can
|
|
598
|
+
// legitimately aggregate more ids than one query may carry (a
|
|
599
|
+
// 100-entry archive × multi-reference fields), and a read-path throw
|
|
600
|
+
// would kill the render — unlike the write-side `fetchLiveIds`, which
|
|
601
|
+
// keeps throwing because a single patch exceeding the ceiling is a
|
|
602
|
+
// caller bug. Ids are de-duped before chunking, so per-query batches
|
|
603
|
+
// stay bounded and nothing is truncated.
|
|
604
|
+
async function resolveGroup(ctx, adapter, scope, ids) {
|
|
605
|
+
const idList = [...ids];
|
|
606
|
+
if (adapter.hydrate) {
|
|
607
|
+
const byId = new Map();
|
|
608
|
+
for (const chunk of chunkIds(idList)) {
|
|
609
|
+
const payloads = await adapter.hydrate(ctx, { ids: chunk, scope });
|
|
610
|
+
for (const payload of payloads) {
|
|
611
|
+
byId.set(payload.id, payload);
|
|
612
|
+
// Fold this embedded entity's cache tag into the page's tags so
|
|
613
|
+
// a change to it purges the page that hydrated it (#1508). Runs
|
|
614
|
+
// on every read surface; only the public read-through reads the
|
|
615
|
+
// accumulator back, so admin/REST reads populate it harmlessly.
|
|
616
|
+
if (adapter.embeddedCacheTags) {
|
|
617
|
+
accumulateEmbeddedTags(ctx, adapter.embeddedCacheTags(payload));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return { kind: "hydrated", byId };
|
|
622
|
+
}
|
|
623
|
+
const liveIds = new Set();
|
|
624
|
+
for (const chunk of chunkIds(idList)) {
|
|
625
|
+
const rows = await adapter.list(ctx, {
|
|
626
|
+
ids: chunk,
|
|
627
|
+
scope,
|
|
628
|
+
limit: chunk.length,
|
|
629
|
+
});
|
|
630
|
+
for (const row of rows)
|
|
631
|
+
liveIds.add(row.id);
|
|
632
|
+
}
|
|
633
|
+
return { kind: "ids", liveIds };
|
|
634
|
+
}
|
|
635
|
+
// Per-query id cap for the read path. 100 (not the 1000 aggregate
|
|
636
|
+
// ceiling) because Cloudflare D1 caps bound parameters at 100 per
|
|
637
|
+
// statement and `inArray` binds one per id — a bigger chunk works on
|
|
638
|
+
// local SQLite and dies in production.
|
|
639
|
+
const HYDRATION_QUERY_ID_LIMIT = 100;
|
|
640
|
+
function* chunkIds(ids) {
|
|
641
|
+
for (let i = 0; i < ids.length; i += HYDRATION_QUERY_ID_LIMIT) {
|
|
642
|
+
yield ids.slice(i, i + HYDRATION_QUERY_ID_LIMIT);
|
|
555
643
|
}
|
|
556
|
-
if (!Array.isArray(arr))
|
|
557
|
-
return null;
|
|
558
|
-
const row = arr[rowIdx];
|
|
559
|
-
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
560
|
-
return null;
|
|
561
|
-
return row;
|
|
562
644
|
}
|
|
563
|
-
|
|
645
|
+
// Write one candidate's resolved value into its slot — the leaf object
|
|
646
|
+
// key of a container reached by walking the candidate's path.
|
|
647
|
+
function applyResolutionToSlot(slot, key, multiple, ids, resolution) {
|
|
564
648
|
if (multiple) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
rowObj[subKey] = ids.filter((id) => liveIds.has(id));
|
|
649
|
+
slot[key] =
|
|
650
|
+
resolution.kind === "hydrated"
|
|
651
|
+
? ids
|
|
652
|
+
.map((id) => resolution.byId.get(id))
|
|
653
|
+
.filter((payload) => payload !== undefined)
|
|
654
|
+
: ids.filter((id) => resolution.liveIds.has(id));
|
|
574
655
|
return;
|
|
575
656
|
}
|
|
576
657
|
const [singleId] = ids;
|
|
577
658
|
if (singleId === undefined)
|
|
659
|
+
return; // single non-string already filtered upstream
|
|
660
|
+
if (resolution.kind === "hydrated") {
|
|
661
|
+
slot[key] = resolution.byId.get(singleId) ?? null;
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (!resolution.liveIds.has(singleId))
|
|
665
|
+
slot[key] = null;
|
|
666
|
+
}
|
|
667
|
+
// Walk the candidate's path from the output bag to the parent container
|
|
668
|
+
// of its leaf slot, copy-on-writing each container the first time it's
|
|
669
|
+
// descended so the caller's `decoded` bag stays untouched. Containers
|
|
670
|
+
// cloned by an earlier candidate (identity no longer matches `decoded`)
|
|
671
|
+
// are reused, so sibling references in the same row/group land in one
|
|
672
|
+
// clone. Returns the writable parent object and the leaf key, or null if
|
|
673
|
+
// any segment's runtime shape doesn't match the declared structure
|
|
674
|
+
// (hand-edited / migrated bags).
|
|
675
|
+
function takeWritableSlot(outBag, decoded, path) {
|
|
676
|
+
let outContainer = outBag;
|
|
677
|
+
let decContainer = decoded;
|
|
678
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
679
|
+
const seg = path[i];
|
|
680
|
+
if (seg === undefined)
|
|
681
|
+
return null;
|
|
682
|
+
const outChild = readSegment(outContainer, seg);
|
|
683
|
+
const decChild = readSegment(decContainer, seg);
|
|
684
|
+
let writable = outChild;
|
|
685
|
+
// Still the shared decoded child → clone it into the output tree.
|
|
686
|
+
if (outChild === decChild) {
|
|
687
|
+
const clone = cloneContainer(outChild);
|
|
688
|
+
if (clone === null)
|
|
689
|
+
return null;
|
|
690
|
+
writeSegment(outContainer, seg, clone);
|
|
691
|
+
writable = clone;
|
|
692
|
+
}
|
|
693
|
+
outContainer = writable;
|
|
694
|
+
decContainer = decChild;
|
|
695
|
+
}
|
|
696
|
+
const leaf = path[path.length - 1];
|
|
697
|
+
if (typeof leaf !== "string")
|
|
698
|
+
return null;
|
|
699
|
+
if (!isPlainObject(outContainer))
|
|
700
|
+
return null;
|
|
701
|
+
return { parent: outContainer, leafKey: leaf };
|
|
702
|
+
}
|
|
703
|
+
function readSegment(container, seg) {
|
|
704
|
+
if (typeof seg === "number") {
|
|
705
|
+
return Array.isArray(container) ? container[seg] : undefined;
|
|
706
|
+
}
|
|
707
|
+
return isPlainObject(container) ? container[seg] : undefined;
|
|
708
|
+
}
|
|
709
|
+
function writeSegment(container, seg, value) {
|
|
710
|
+
if (typeof seg === "number") {
|
|
711
|
+
if (Array.isArray(container))
|
|
712
|
+
container[seg] = value;
|
|
578
713
|
return;
|
|
579
|
-
|
|
580
|
-
|
|
714
|
+
}
|
|
715
|
+
if (isPlainObject(container))
|
|
716
|
+
container[seg] = value;
|
|
717
|
+
}
|
|
718
|
+
// Shallow clone an array or plain object; null for any other shape (the
|
|
719
|
+
// path expected a container but the stored value isn't one).
|
|
720
|
+
function cloneContainer(value) {
|
|
721
|
+
// `Array.isArray` widens to `any[]`; cast before spread so the clone
|
|
722
|
+
// stays `unknown[]` rather than leaking `any` into the walk.
|
|
723
|
+
if (Array.isArray(value))
|
|
724
|
+
return [...value];
|
|
725
|
+
if (isPlainObject(value))
|
|
726
|
+
return { ...value };
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
function isPlainObject(value) {
|
|
730
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
581
731
|
}
|
|
582
732
|
// Returns `null` to mean "no candidates from this key" (skip), or
|
|
583
733
|
// the ids to feed into the group's batch query. Mirrors the storage-
|
|
584
734
|
// shape guards in the apply step so we don't enqueue work that's
|
|
585
|
-
// going to be skipped.
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
function orphanCandidateIds(target, value) {
|
|
735
|
+
// going to be skipped. Callers pass values already decoded through
|
|
736
|
+
// `decodeMetaBag`, so legacy object shapes have been healed to plain
|
|
737
|
+
// ids by the time this runs.
|
|
738
|
+
function referenceCandidateIds(target, value) {
|
|
590
739
|
if (target.multiple) {
|
|
591
740
|
if (!Array.isArray(value))
|
|
592
741
|
return null;
|
|
593
|
-
if (target.valueShape === "object") {
|
|
594
|
-
const ids = [];
|
|
595
|
-
for (const item of value) {
|
|
596
|
-
const id = extractStringId(item);
|
|
597
|
-
if (id !== null)
|
|
598
|
-
ids.push(id);
|
|
599
|
-
}
|
|
600
|
-
return ids;
|
|
601
|
-
}
|
|
602
742
|
return value.filter((id) => typeof id === "string");
|
|
603
743
|
}
|
|
604
|
-
if (target.valueShape === "object") {
|
|
605
|
-
const id = extractStringId(value);
|
|
606
|
-
return id !== null ? [id] : null;
|
|
607
|
-
}
|
|
608
744
|
return typeof value === "string" ? [value] : null;
|
|
609
745
|
}
|
|
610
|
-
function referenceTargetOf(field) {
|
|
611
|
-
if (!field)
|
|
612
|
-
return undefined;
|
|
613
|
-
return field
|
|
614
|
-
.referenceTarget;
|
|
615
|
-
}
|
|
616
746
|
/**
|
|
617
747
|
* Decode a raw meta bag (as returned by drizzle's JSON-mode column)
|
|
618
748
|
* into the plugin-typed shape RPC consumers expect. Unregistered keys
|
|
@@ -626,10 +756,64 @@ export function decodeMetaBag(findField, raw) {
|
|
|
626
756
|
const out = {};
|
|
627
757
|
for (const [key, value] of Object.entries(raw)) {
|
|
628
758
|
const field = findField(key);
|
|
629
|
-
out[key] = field ?
|
|
759
|
+
out[key] = field ? decodeFieldValue(field, value) : value;
|
|
630
760
|
}
|
|
631
761
|
return out;
|
|
632
762
|
}
|
|
763
|
+
// Reference storage is plain ids, but bags written before the
|
|
764
|
+
// write-time snapshot machinery was removed may hold `{ id, ... }`
|
|
765
|
+
// objects. Reads yield the id; the next save persists the plain form
|
|
766
|
+
// (`referenceItemId` accepts the legacy shape on write).
|
|
767
|
+
function decodeFieldValue(field, value) {
|
|
768
|
+
const target = referenceTargetOf(field);
|
|
769
|
+
if (target)
|
|
770
|
+
return healReferenceValue(target, value);
|
|
771
|
+
if (isRepeaterField(field) && Array.isArray(value)) {
|
|
772
|
+
return value.map((row) => healRepeaterRow(field, row));
|
|
773
|
+
}
|
|
774
|
+
if (isTemporalField(field) && field.returns === "date") {
|
|
775
|
+
return projectTemporalDate(field.inputType, value);
|
|
776
|
+
}
|
|
777
|
+
return coerceOnRead(field.type, value);
|
|
778
|
+
}
|
|
779
|
+
function isTemporalField(field) {
|
|
780
|
+
return (field.inputType === "date" ||
|
|
781
|
+
field.inputType === "datetime" ||
|
|
782
|
+
field.inputType === "time");
|
|
783
|
+
}
|
|
784
|
+
// `.returns("date")` decode projection. All three variants anchor to
|
|
785
|
+
// UTC — `date` at UTC midnight, `time` on 1970-01-01 UTC — so the
|
|
786
|
+
// wall-clock components survive every server/browser timezone
|
|
787
|
+
// combination (decode runs server-side, often UTC on Workers, while
|
|
788
|
+
// the admin formats in the viewer's browser). Consumers read the
|
|
789
|
+
// parts back with `getUTC*` or `timeZone: "UTC"` formatting; the
|
|
790
|
+
// projection is the exact inverse of the write-side `Date` encoding
|
|
791
|
+
// (`formatTemporalValue`). Unparseable stored values round to "no
|
|
792
|
+
// value", matching the forgiving-read posture of `coerceOnRead`.
|
|
793
|
+
function projectTemporalDate(inputType, value) {
|
|
794
|
+
if (typeof value !== "string" || value === "")
|
|
795
|
+
return undefined;
|
|
796
|
+
const parsed = new Date(anchorTemporalUtc(inputType, value));
|
|
797
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
798
|
+
}
|
|
799
|
+
function healRepeaterRow(field, row) {
|
|
800
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
801
|
+
return row;
|
|
802
|
+
const rowObj = row;
|
|
803
|
+
let healed = null;
|
|
804
|
+
for (const subField of field.subFields) {
|
|
805
|
+
const target = referenceTargetOf(subField);
|
|
806
|
+
if (!target)
|
|
807
|
+
continue;
|
|
808
|
+
const value = rowObj[subField.key];
|
|
809
|
+
const next = healReferenceValue(target, value);
|
|
810
|
+
if (next !== value) {
|
|
811
|
+
healed ??= { ...rowObj };
|
|
812
|
+
healed[subField.key] = next;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return healed ?? row;
|
|
816
|
+
}
|
|
633
817
|
export function isEmptyMetaPatch(patch) {
|
|
634
818
|
return (patch === null || (patch.upserts.size === 0 && patch.deletes.length === 0));
|
|
635
819
|
}
|
|
@@ -674,72 +858,6 @@ export async function loadMeta(ctx, table, idColumn, id, findField) {
|
|
|
674
858
|
return decodeMetaBag(findField, row?.meta);
|
|
675
859
|
}
|
|
676
860
|
// --- internals below ---------------------------------------------------
|
|
677
|
-
function coerceToType(type, key, value) {
|
|
678
|
-
switch (type) {
|
|
679
|
-
case "string":
|
|
680
|
-
return coerceString(key, value);
|
|
681
|
-
case "number":
|
|
682
|
-
return coerceNumber(key, value);
|
|
683
|
-
case "boolean":
|
|
684
|
-
return coerceBoolean(key, value);
|
|
685
|
-
case "json":
|
|
686
|
-
return coerceJson(key, value);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
function coerceString(key, value) {
|
|
690
|
-
if (typeof value === "string")
|
|
691
|
-
return value;
|
|
692
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
693
|
-
return String(value);
|
|
694
|
-
if (typeof value === "boolean")
|
|
695
|
-
return String(value);
|
|
696
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
697
|
-
}
|
|
698
|
-
function coerceNumber(key, value) {
|
|
699
|
-
if (typeof value === "number" && Number.isFinite(value))
|
|
700
|
-
return value;
|
|
701
|
-
if (typeof value === "string") {
|
|
702
|
-
// Empty string comes from cleared form inputs; the admin dispatcher
|
|
703
|
-
// already sends `null` for those, but a direct RPC caller might send
|
|
704
|
-
// "" — reject here so we don't silently coerce to 0 (`Number("") === 0`).
|
|
705
|
-
if (value.trim() === "") {
|
|
706
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
707
|
-
}
|
|
708
|
-
const parsed = Number(value);
|
|
709
|
-
if (Number.isFinite(parsed))
|
|
710
|
-
return parsed;
|
|
711
|
-
}
|
|
712
|
-
if (typeof value === "boolean")
|
|
713
|
-
return value ? 1 : 0;
|
|
714
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
715
|
-
}
|
|
716
|
-
function coerceBoolean(key, value) {
|
|
717
|
-
if (typeof value === "boolean")
|
|
718
|
-
return value;
|
|
719
|
-
if (value === 1 || value === "1" || value === "true")
|
|
720
|
-
return true;
|
|
721
|
-
if (value === 0 || value === "0" || value === "false")
|
|
722
|
-
return false;
|
|
723
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
724
|
-
}
|
|
725
|
-
function coerceJson(key, value) {
|
|
726
|
-
// json keys take anything round-trippable through JSON.stringify —
|
|
727
|
-
// we reject values that throw (BigInt) or silently drop (functions,
|
|
728
|
-
// Symbols) so reads don't hand back `undefined` for something a
|
|
729
|
-
// plugin thought it stored. TS types JSON.stringify as always-
|
|
730
|
-
// string, but at runtime it returns `undefined` for unserializable
|
|
731
|
-
// inputs — hence the cast.
|
|
732
|
-
try {
|
|
733
|
-
const encoded = JSON.stringify(value);
|
|
734
|
-
if (encoded === undefined) {
|
|
735
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
736
|
-
}
|
|
737
|
-
return JSON.parse(encoded);
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
throw MetaSanitizationError.invalidValue({ key });
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
861
|
function assertEncodedSize(key, value) {
|
|
744
862
|
const encoded = JSON.stringify(value);
|
|
745
863
|
if (encoded === undefined)
|