dineway 0.2.33 → 0.2.35
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/{activity-CfryM8jO.mjs → activity-BbtYp_4V.mjs} +2 -2
- package/dist/{activity-events-BVo9rMUz.mjs → activity-events-CiFN61xq.mjs} +1 -1
- package/dist/{actor-DrJJsvy6.mjs → actor-DxUAJgQX.mjs} +20 -6
- package/dist/{adapters-DHt3f2lu.d.mts → adapters-Rol-inz6.d.mts} +5 -7
- package/dist/after-BVaUn7bK.mjs +24 -0
- package/dist/{allowed-origins-DGSyhRCz.mjs → allowed-origins-Cp7BI3L9.mjs} +1 -1
- package/dist/api/route-utils.d.mts +10 -10
- package/dist/api/route-utils.mjs +23 -19
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +5 -5
- package/dist/{api-CTf1SLmg.mjs → api-CwhrR7H5.mjs} +55 -1215
- package/dist/api-tokens-DA18zxza.mjs +3 -0
- package/dist/{api-tokens-CSomx-l3.mjs → api-tokens-OAgvq4xg.mjs} +1 -1
- package/dist/{apply-Bs2jXsms.mjs → apply-BTfAReIv.mjs} +109 -93
- package/dist/astro/index.d.mts +16 -12
- package/dist/astro/index.mjs +50 -9
- package/dist/astro/middleware/auth.d.mts +9 -9
- package/dist/astro/middleware/auth.mjs +29 -8
- package/dist/astro/middleware/redirect.mjs +8 -7
- package/dist/astro/middleware/request-context.mjs +17 -10
- package/dist/astro/middleware/seed.d.mts +1 -1
- package/dist/astro/middleware/seed.mjs +93 -71
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware/workers.d.mts +6 -0
- package/dist/astro/middleware/workers.mjs +8 -0
- package/dist/astro/middleware.mjs +287 -159
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +7 -7
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +7 -7
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +7 -7
- package/dist/astro/routes/api/admin/briefing.mjs +25 -23
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +11 -10
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +11 -10
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +11 -10
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +22 -20
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +22 -20
- package/dist/astro/routes/api/admin/bylines/index.mjs +22 -20
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -16
- package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -7
- package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/counts.mjs +8 -7
- package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
- package/dist/astro/routes/api/admin/context/_id_/history.mjs +42 -31
- package/dist/astro/routes/api/admin/context/_id_/index.mjs +42 -31
- package/dist/astro/routes/api/admin/context/_id_/review.mjs +47 -36
- package/dist/astro/routes/api/admin/context/_id_/supersede.mjs +47 -36
- package/dist/astro/routes/api/admin/context/diff.mjs +46 -35
- package/dist/astro/routes/api/admin/context/index.mjs +47 -36
- package/dist/astro/routes/api/admin/context/stale.mjs +46 -35
- package/dist/astro/routes/api/admin/hitl-requests/_id_/index.mjs +42 -31
- package/dist/astro/routes/api/admin/hitl-requests/_id_/resolve.mjs +46 -35
- package/dist/astro/routes/api/admin/hitl-requests/index.mjs +46 -35
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +43 -32
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +7 -7
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +7 -7
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +71 -66
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +71 -66
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +63 -58
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +70 -65
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +70 -65
- package/dist/astro/routes/api/admin/plugins/index.mjs +63 -58
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +63 -58
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +70 -65
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +63 -58
- package/dist/astro/routes/api/admin/plugins/updates.mjs +63 -58
- package/dist/astro/routes/api/admin/review-requests/_id_/image.mjs +26 -23
- package/dist/astro/routes/api/admin/review-requests/_id_/index.mjs +39 -25
- package/dist/astro/routes/api/admin/review-requests/_id_/resolve.mjs +55 -43
- package/dist/astro/routes/api/admin/review-requests/_id_/revoke.mjs +10 -9
- package/dist/astro/routes/api/admin/review-requests/index.mjs +53 -41
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +63 -58
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +63 -58
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +20 -12
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +40 -32
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
- package/dist/astro/routes/api/admin/users/index.mjs +7 -7
- package/dist/astro/routes/api/auth/dev-bypass.mjs +6 -5
- package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +12 -12
- package/dist/astro/routes/api/auth/invite/index.mjs +9 -9
- package/dist/astro/routes/api/auth/invite/register-options.mjs +11 -11
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +12 -11
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +11 -11
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +5 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +5 -4
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +7 -7
- package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +11 -11
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +12 -12
- package/dist/astro/routes/api/auth/passkey/verify.mjs +12 -12
- package/dist/astro/routes/api/auth/setup-token/index.mjs +32 -23
- package/dist/astro/routes/api/auth/setup-token/verify.mjs +32 -26
- package/dist/astro/routes/api/auth/signup/complete.mjs +12 -12
- package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
- package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.d.mts +73 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +91 -49
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +19 -16
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +8 -7
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +8 -7
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +8 -7
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +24 -21
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +34 -22
- package/dist/astro/routes/api/content/_collection_/_id_/release-readiness.mjs +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +8 -7
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +34 -22
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +23 -19
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +8 -7
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +14 -13
- package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/index.mjs +14 -13
- package/dist/astro/routes/api/content/_collection_/trash.mjs +8 -8
- package/dist/astro/routes/api/dashboard.mjs +16 -11
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/health.mjs +1 -1
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +25 -24
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +12 -11
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/execute.mjs +50 -42
- package/dist/astro/routes/api/import/wordpress/media.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/media.mjs +14 -14
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +51 -40
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +49 -38
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +29 -27
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +55 -47
- package/dist/astro/routes/api/integration/artifact.mjs +11 -8
- package/dist/astro/routes/api/integration/dispatch.mjs +17 -14
- package/dist/astro/routes/api/integrations/_provider_.mjs +11 -8
- package/dist/astro/routes/api/integrations/index.mjs +10 -7
- package/dist/astro/routes/api/manifest.mjs +5 -5
- package/dist/astro/routes/api/mcp.mjs +97 -88
- package/dist/astro/routes/api/media/_id_/confirm.mjs +12 -12
- package/dist/astro/routes/api/media/_id_.mjs +9 -9
- package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
- package/dist/astro/routes/api/media/providers/index.mjs +2 -2
- package/dist/astro/routes/api/media/upload-url.mjs +13 -13
- package/dist/astro/routes/api/media.mjs +16 -16
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +47 -36
- package/dist/astro/routes/api/menus/_name_/items.mjs +47 -36
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +47 -36
- package/dist/astro/routes/api/menus/_name_/translations.mjs +47 -36
- package/dist/astro/routes/api/menus/_name_.mjs +47 -36
- package/dist/astro/routes/api/menus/index.mjs +47 -36
- package/dist/astro/routes/api/oauth/authorize.mjs +9 -7
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -6
- package/dist/astro/routes/api/oauth/device/code.mjs +11 -9
- package/dist/astro/routes/api/oauth/device/token.mjs +10 -8
- package/dist/astro/routes/api/oauth/register.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -6
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -6
- package/dist/astro/routes/api/oauth/token.mjs +8 -6
- package/dist/astro/routes/api/openapi.json.mjs +5 -5
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +15 -14
- package/dist/astro/routes/api/redirects/404s/summary.mjs +15 -14
- package/dist/astro/routes/api/redirects/_id_.mjs +47 -37
- package/dist/astro/routes/api/redirects/index.mjs +47 -37
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +2 -2
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +65 -60
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +65 -60
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +65 -60
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +65 -60
- package/dist/astro/routes/api/schema/collections/index.mjs +65 -60
- package/dist/astro/routes/api/schema/index.mjs +12 -11
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +63 -58
- package/dist/astro/routes/api/schema/orphans/index.mjs +63 -58
- package/dist/astro/routes/api/search/enable.mjs +15 -14
- package/dist/astro/routes/api/search/index.mjs +14 -13
- package/dist/astro/routes/api/search/rebuild.mjs +15 -14
- package/dist/astro/routes/api/search/stats.mjs +9 -8
- package/dist/astro/routes/api/search/suggest.mjs +14 -13
- package/dist/astro/routes/api/sections/_slug_.mjs +47 -36
- package/dist/astro/routes/api/sections/index.mjs +47 -36
- package/dist/astro/routes/api/settings/email.mjs +5 -5
- package/dist/astro/routes/api/settings/reviews.mjs +11 -10
- package/dist/astro/routes/api/settings.mjs +47 -36
- package/dist/astro/routes/api/setup/admin-verify.mjs +14 -14
- package/dist/astro/routes/api/setup/admin.mjs +12 -12
- package/dist/astro/routes/api/setup/dev-bypass.mjs +35 -31
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +37 -33
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +11 -9
- package/dist/astro/routes/api/system/heartbeat.mjs +2 -2
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +50 -39
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +50 -39
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +50 -39
- package/dist/astro/routes/api/taxonomies/index.mjs +50 -39
- package/dist/astro/routes/api/themes/preview.mjs +8 -6
- package/dist/astro/routes/api/typegen.mjs +4 -4
- package/dist/astro/routes/api/well-known/auth.mjs +3 -3
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +3 -3
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +46 -35
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +47 -36
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +47 -36
- package/dist/astro/routes/api/widget-areas/_name_.mjs +44 -33
- package/dist/astro/routes/api/widget-areas/index.mjs +47 -36
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +11 -10
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +20 -18
- package/dist/astro/routes/sitemap.xml.mjs +19 -17
- package/dist/astro/session-driver.d.mts +1 -1
- package/dist/astro/session-driver.mjs +4 -4
- package/dist/astro/types.d.mts +13 -13
- package/dist/atomic-authority-CnBQJ14T.mjs +33 -0
- package/dist/{audit-CYjfxG9i.mjs → audit-F5iWkbcJ.mjs} +11 -4
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{auth-control-guard-CITT8ZA_.mjs → auth-control-guard-DPOSBNFI.mjs} +1 -1
- package/dist/{authorize-KcI0pAfN.mjs → authorize-CXIjxIfC.mjs} +1 -1
- package/dist/{briefing-BgQTljQt.mjs → briefing-5zf0YZbR.mjs} +445 -447
- package/dist/{briefing-r3hXCDQC.mjs → briefing-DiupPvyN.mjs} +1 -1
- package/dist/{bundle-S6zx-V4D.mjs → bundle-DImnxo71.mjs} +3 -3
- package/dist/{byline-Ctil4rij.mjs → byline-DEZKlzD9.mjs} +107 -113
- package/dist/{byline-fields-51ja6JDa.d.mts → byline-fields-BO96fXLv.d.mts} +8 -8
- package/dist/{byline-fields-DiCyW6eQ.mjs → byline-fields-CY6V0ew1.mjs} +1 -1
- package/dist/{byline-fields-QpmruT5r.mjs → byline-fields-DGNKitg3.mjs} +3 -3
- package/dist/{byline-registry-rkr73qSE.mjs → byline-registry-CkuVVeJJ.mjs} +46 -76
- package/dist/{bylines-v_ZUQnR-.mjs → bylines-CXyKpVLQ.mjs} +6 -6
- package/dist/{bylines-BcuBpzVi.mjs → bylines-gSAfpHl4.mjs} +4 -4
- package/dist/{cache-V2BCplaY.mjs → cache-DZDOBJv3.mjs} +2 -2
- package/dist/{chunks-CnuL55JE.mjs → chunks-BnG3LPMN.mjs} +1 -1
- package/dist/cli/index.mjs +452 -263
- package/dist/cli/plugin-kit.d.mts +9 -9
- package/dist/cli/plugin-kit.mjs +2 -2
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -11
- package/dist/client/index.mjs +2 -2
- package/dist/{client-factory-C-y9VZDX.mjs → client-factory-6vLLT06O.mjs} +1 -1
- package/dist/{comment-_nvJSS-A.mjs → comment-kgLTdtQD.mjs} +15 -8
- package/dist/{comment-reaction-DhVgJ17S.mjs → comment-reaction-Cun5eIXL.mjs} +1 -1
- package/dist/{comments-D4mIDjvA.mjs → comments-ZYK_cZvx.mjs} +1 -1
- package/dist/{content-CVCrcuvD.mjs → content-D9xKUSno.mjs} +287 -165
- package/dist/{content-refresh-QvBIyb_g.mjs → content-refresh-9M9cc55-.mjs} +170 -193
- package/dist/{context-JjKucxBm.mjs → context-BU8AbLmY.mjs} +2 -2
- package/dist/{context-MftkHbWx.mjs → context-C5lPtfeh.mjs} +95 -77
- package/dist/{context-route-helpers-e1APrqSm.mjs → context-route-helpers-CWAHJFl3.mjs} +2 -2
- package/dist/{cron-mGVPQ5e6.mjs → cron-BVYo08mi.mjs} +36 -32
- package/dist/d1-binding-D6HZa3ks.mjs +8 -0
- package/dist/d1-dialect-BBxPg6Om.mjs +48 -0
- package/dist/{dashboard-ZJ7sMcBI.mjs → dashboard-BnD5krLg.mjs} +5 -5
- package/dist/database/batch.d.mts +22 -0
- package/dist/database/batch.mjs +92 -0
- package/dist/database/d1/index.d.mts +18 -0
- package/dist/database/d1/index.mjs +8 -0
- package/dist/db/d1.d.mts +7 -0
- package/dist/db/d1.mjs +13 -0
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +10 -7
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +46 -3
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{device-flow-eOcL5m0Q.mjs → device-flow-Prqg6ycD.mjs} +130 -94
- package/dist/{dialect-helpers-fk43GbDb.mjs → dialect-helpers-C6xtO-Ui.mjs} +1 -1
- package/dist/{discovery-CJkuHGhZ.mjs → discovery-CYcZJ1o3.mjs} +5 -5
- package/dist/{enrich-KErvJOuI.mjs → enrich-BjF-J4sF.mjs} +1 -1
- package/dist/{evidence-6IUv9ML3.mjs → evidence-krrA3uiG.mjs} +7 -7
- package/dist/{field-defs-cache-CwrIyZFT.mjs → field-defs-cache-BKKFFf_V.mjs} +5 -5
- package/dist/{forgeway-account-lYJ8iX3G.mjs → forgeway-account-CnWOIusE.mjs} +1 -1
- package/dist/{fts-manager-DX7ZkrCa.mjs → fts-manager-DA74x_uP.mjs} +72 -57
- package/dist/{hitl-requests-CmYVunm-.mjs → hitl-requests-CGjA1jND.mjs} +2 -2
- package/dist/{hitl-route-helpers-CRm537zS.mjs → hitl-route-helpers-BxSL0iZB.mjs} +5 -5
- package/dist/{import-DqRAgsKr.mjs → import-CayDCPaH.mjs} +9 -9
- package/dist/{index-Co7xtjkd.d.mts → index-mlJSqFkG.d.mts} +7 -4
- package/dist/index.d.mts +16 -16
- package/dist/index.mjs +103 -96
- package/dist/{integration-credentials-qudN6yRr.mjs → integration-credentials-Br16M1VE.mjs} +2 -2
- package/dist/{jsonld-BpvnSp2m.d.mts → jsonld-BUiFEiCZ.d.mts} +2 -2
- package/dist/{load-fCOciq0Q.mjs → load-B2oitobd.mjs} +1 -1
- package/dist/{loader-BDje8ME2.mjs → loader-Cgsx23wU.mjs} +4 -4
- package/dist/{manifest-schema-CfKD0thC.mjs → manifest-schema-DBXM_g8l.mjs} +1 -1
- package/dist/{mcp-reviewed-executor-DmhM3Cy6.mjs → mcp-reviewed-executor-RJ71uyQb.mjs} +7 -7
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +2 -2
- package/dist/media/local-runtime.d.mts +11 -11
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-dvRjj4po.mjs → media-BZqwtRTZ.mjs} +12 -4
- package/dist/{media-allowlist-DaG6lNoy.mjs → media-allowlist-CyByjUXh.mjs} +1 -1
- package/dist/{menus-CZbz5L_V.mjs → menus-2o-DVcwA.mjs} +8 -8
- package/dist/{menus-CpuR4Qkm.mjs → menus-5liJusbp.mjs} +138 -90
- package/dist/migrations-C7YXse7V.mjs +76 -0
- package/dist/oauth-authority-DOjME4Hh.mjs +43 -0
- package/dist/{oauth-authorization-Jf5e3azH.mjs → oauth-authorization-B_mmz7O2.mjs} +89 -98
- package/dist/{oauth-clients-tovQ4D4h.mjs → oauth-clients-Cr4pdIAE.mjs} +2 -2
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{object-cache-D51qOseS.mjs → object-cache-BGjIlY9s.mjs} +1 -1
- package/dist/page/index.d.mts +10 -10
- package/dist/page/index.mjs +1 -1
- package/dist/{parse-IsGn-5lt.mjs → parse-BscLr3p2.mjs} +1 -1
- package/dist/plugin-utils.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{plugins-DXyLI3Hb.mjs → plugins-f48yrwfI.mjs} +35 -84
- package/dist/{policy-DKdNMv8y.mjs → policy-CrK1vl8l.mjs} +2 -2
- package/dist/{preview-BXmS1SXL.mjs → preview-E1xKZGsX.mjs} +2 -2
- package/dist/{provider-loader-DDq7R_RN.d.mts → provider-loader-H2wdGD0o.d.mts} +1 -1
- package/dist/{public-url-Ci7UbC0W.d.mts → public-url-CI700CW4.d.mts} +1 -1
- package/dist/{public-url-BQ9vf0AS.d.mts → public-url-zoO-QbN6.d.mts} +2 -2
- package/dist/{publication-authorization-C-vYPGD8.mjs → publication-authorization-Dq_20SZR.mjs} +458 -33
- package/dist/{query-DcB9uGTW.mjs → query-BkWWdxyb.mjs} +4 -4
- package/dist/{query-CrW2yI6G.mjs → query-D6oNo9iz.mjs} +15 -15
- package/dist/{rate-limit-DEkoxTsP.mjs → rate-limit-RaBJEgyt.mjs} +2 -2
- package/dist/{redirect-DG-o0CKV.mjs → redirect-HLI3wT5Y.mjs} +26 -14
- package/dist/{redirects-Bjp4iKJU.mjs → redirects-D16GFaOa.mjs} +5 -5
- package/dist/{redirects-N8gmWhVM.mjs → redirects-DpRiuRWn.mjs} +3 -3
- package/dist/{registry-D-IV_h50.mjs → registry-DcV93OL_.mjs} +183 -186
- package/dist/release-CZfAeyXm.mjs +5148 -0
- package/dist/request-context.d.mts +8 -0
- package/dist/{request-meta-BW0the1R.mjs → request-meta-C6yGqnGD.mjs} +1 -1
- package/dist/{resolve-pZ4NQM-X.mjs → resolve-CpOV2jt_.mjs} +1 -1
- package/dist/review-payload-LLIi1sxO.mjs +1386 -0
- package/dist/{review-requests-DICrYH2-.mjs → review-requests-BF2eEKr8.mjs} +93 -37
- package/dist/{review-requests-BTKfeWlS.mjs → review-requests-C7prIbCo.mjs} +2 -2
- package/dist/{review-route-helpers-DaZBBps8.mjs → review-route-helpers-BYx3Px90.mjs} +2 -2
- package/dist/{revision-De7OmYVD.mjs → revision-090sZyGc.mjs} +28 -22
- package/dist/revocation-DZ8e4JCS.mjs +53 -0
- package/dist/{runner-DJiRFyLI.mjs → runner-CVeq0266.mjs} +594 -491
- package/dist/{runner-Dye73myO.d.mts → runner-Cxl-vyk2.d.mts} +1 -1
- package/dist/{runs-DEujUDOj.mjs → runs-B8kCirJ6.mjs} +5 -5
- package/dist/runtime-env-v-1UXPAH.mjs +19 -0
- package/dist/runtime.d.mts +10 -10
- package/dist/runtime.mjs +5 -5
- package/dist/schema-DFeC5axi.mjs +8 -0
- package/dist/{search-DxUKN9Zz.mjs → search-C9qOBgvo.mjs} +4 -4
- package/dist/{secrets-AdCrGg1c.mjs → secrets-BP2YTPWn.mjs} +10 -11
- package/dist/{sections-BPFOEMK6.mjs → sections-CkoQY7Z9.mjs} +2 -2
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +29 -25
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +11 -10
- package/dist/{seo-GfovzqpU.mjs → seo-Bypxm1iD.mjs} +1 -1
- package/dist/{seo-DWHdo5aW.mjs → seo-DYpc_rQ1.mjs} +25 -15
- package/dist/{service-BrNKVCjC.mjs → service-DOFqfUVv.mjs} +46 -21
- package/dist/{service-DbgjHWyW.mjs → service-mJBl4ZP4.mjs} +2 -2
- package/dist/{session-user-D8WwGEPH.mjs → session-user-DacoiKrJ.mjs} +1 -1
- package/dist/{settings-BKEHw6Bu.mjs → settings-2p7TuNWG.mjs} +2 -2
- package/dist/{settings-CAsQLi0N.mjs → settings-DRFy8JQx.mjs} +8 -8
- package/dist/{setup-complete-CHCYS5KV.mjs → setup-complete-CWMnrNFN.mjs} +1 -1
- package/dist/{site-activity-7oezuHXK.mjs → site-activity-BrN2NviL.mjs} +11 -4
- package/dist/{site-context-CA7rbxux.mjs → site-context-D9oETfxF.mjs} +50 -37
- package/dist/{site-url-DoZf_jVj.mjs → site-url-DoTPKRND.mjs} +1 -1
- package/dist/state-D8AogXCV.mjs +30 -0
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +3 -3
- package/dist/{store-c4qx3gVh.mjs → store-CjmmGlVw.mjs} +37 -12
- package/dist/submission-BLvkvewK.mjs +411 -0
- package/dist/{taxonomies-ALDiRQ84.mjs → taxonomies-C5ayPp3T.mjs} +3 -3
- package/dist/{taxonomies-93DV-1QT.mjs → taxonomies-DW6Fps-w.mjs} +7 -7
- package/dist/{taxonomy-BOBTJ_aE.mjs → taxonomy-DWxxLpp5.mjs} +3 -3
- package/dist/{tokens-c-qxNqzC.mjs → tokens-I3tSjoFI.mjs} +1 -1
- package/dist/{trusted-proxy-CdEl6g6B.mjs → trusted-proxy-CvDP0g0F.mjs} +4 -12
- package/dist/{types-6KmeXInJ.d.mts → types-BFoQ5a1v.d.mts} +1 -1
- package/dist/{types-BpSTdH1H.mjs → types-BqxIv8Il.mjs} +2 -2
- package/dist/{types-BRCUO8NR.d.mts → types-CVRNRAA5.d.mts} +1 -0
- package/dist/{types-CfpX1og5.d.mts → types-Tt_IGMno.d.mts} +360 -172
- package/dist/ui/server-runtime.d.mts +13 -13
- package/dist/ui/server-runtime.mjs +28 -26
- package/dist/{user-D0NJ7gYf.mjs → user-1-YV30lf.mjs} +2 -2
- package/dist/{utils-BK2efsHv.mjs → utils-BoWkoty8.mjs} +2 -2
- package/dist/{validate-CDWVODvc.d.mts → validate-CCOsvPg2.d.mts} +3 -3
- package/dist/{validate-C9g9iX3q.mjs → validate-CupwWNWL.mjs} +3 -3
- package/dist/version-DpT-k_Ny.mjs +6 -0
- package/dist/{widgets-Cm5Qx2Np.mjs → widgets-DbOctkXv.mjs} +3 -3
- package/dist/{wordpress-slugs-WdPZaVXf.mjs → wordpress-slugs-pQlLXbKn.mjs} +1 -1
- package/dist/{worker-Bhuu7eqS.mjs → worker-wjRq4FI9.mjs} +76 -47
- package/dist/workers-build-QVrquLLe.mjs +36 -0
- package/dist/{zod-generator-DS5oZNaL.mjs → zod-generator-BzACAoR5.mjs} +2 -2
- package/package.json +34 -8
- package/dist/after-BfniwgIf.mjs +0 -9
- package/dist/api-tokens-CmT83Bgj.mjs +0 -3
- package/dist/db/libsql-http.d.mts +0 -27
- package/dist/db/libsql-http.mjs +0 -29
- package/dist/libsql-dialect-CANq6UNI.mjs +0 -47
- package/dist/oauth-user-lookup-B451pZmd.mjs +0 -25
- package/dist/review-payload-f1L4lzQg.mjs +0 -111
- package/dist/revocation-CkTOhdlw.mjs +0 -42
- package/dist/schema-IjuRxkqq.mjs +0 -8
- package/dist/submission-C47cHBIg.mjs +0 -67
- package/dist/transaction-BGQgnwUi.mjs +0 -28
- package/dist/version-CJqc1mwj.mjs +0 -6
- /package/dist/{base64-F8-DUraK.mjs → base64-Cz-aU0X1.mjs} +0 -0
- /package/dist/{challenge-store-CDKfye2r.mjs → challenge-store-DFGsDtCj.mjs} +0 -0
- /package/dist/{components-DAq_4iPX.mjs → components-gMTtJqHg.mjs} +0 -0
- /package/dist/{config-CVssduLe.mjs → config-XW5tMrH8.mjs} +0 -0
- /package/dist/{content-index-names-Bbu1FY1r.mjs → content-index-names-CDc2IQIx.mjs} +0 -0
- /package/dist/{context-types-CPWAJRs5.mjs → context-types-DLD_HnKT.mjs} +0 -0
- /package/dist/{default-BeIzUa1e.mjs → default-w3MwLng7.mjs} +0 -0
- /package/dist/{email-console-Bfi_exVg.mjs → email-console-CpvgEN1w.mjs} +0 -0
- /package/dist/{error-DLg1H2LT.mjs → error-B-44r5TD.mjs} +0 -0
- /package/dist/{escape-C6lkys0j.mjs → escape-B1GM0yJU.mjs} +0 -0
- /package/dist/{experimental-workflows-B9x8delL.mjs → experimental-workflows-fzJHB3ZI.mjs} +0 -0
- /package/dist/{hash-jhbF98tT.mjs → hash-DvMZXj2L.mjs} +0 -0
- /package/dist/{media-url-CL3NzgLh.mjs → media-url-D-elxQjL.mjs} +0 -0
- /package/dist/{mime-CDTtL7jE.mjs → mime-D_ae-u_U.mjs} +0 -0
- /package/dist/{mode-DJeacFCZ.mjs → mode-BJwb_MgK.mjs} +0 -0
- /package/dist/{normalize-Ev3FiBMx.mjs → normalize-5rct_U0I.mjs} +0 -0
- /package/dist/{oauth-state-store-Do1a_o3H.mjs → oauth-state-store-D2H9hKcN.mjs} +0 -0
- /package/dist/{options-1dBRkoCl.mjs → options-BcqHWpc-.mjs} +0 -0
- /package/dist/{output-8j9oltTf.mjs → output-CdmbQioY.mjs} +0 -0
- /package/dist/{parser-C9rzPuSZ.mjs → parser-ehIlj32k.mjs} +0 -0
- /package/dist/{passkey-config-CRN44T04.mjs → passkey-config-sUQoLEj_.mjs} +0 -0
- /package/dist/{patterns-3XE9bU4r.mjs → patterns-CZ7D4y_R.mjs} +0 -0
- /package/dist/{placeholder-D03pGvqM.d.mts → placeholder-CQKI6vLd.d.mts} +0 -0
- /package/dist/{portable-text-BdRm4uZq.d.mts → portable-text-BE7nszX6.d.mts} +0 -0
- /package/dist/{portable-text-tIk9_uz_.mjs → portable-text-DH3Om_2D.mjs} +0 -0
- /package/dist/{process-cache-B-L-A9zg.mjs → process-cache-MHM4Sqzs.mjs} +0 -0
- /package/dist/{provider-loader-C4i82ll1.mjs → provider-loader-Dnv2neOu.mjs} +0 -0
- /package/dist/{public-discovery-CyDzK1-S.mjs → public-discovery-CTql6jdZ.mjs} +0 -0
- /package/dist/{public-url-BVIjzMo3.mjs → public-url-4sMO36Gw.mjs} +0 -0
- /package/dist/{public-url-oudwI7K0.mjs → public-url-DNt9A1yS.mjs} +0 -0
- /package/dist/{redirect-Dp5u-8dH.mjs → redirect-Cjjb5VYr.mjs} +0 -0
- /package/dist/{request-cache-BruBnFAR.mjs → request-cache-DPmV5nk4.mjs} +0 -0
- /package/dist/{seo-contributions-BAtRDXEQ.mjs → seo-contributions-atY3jZEV.mjs} +0 -0
- /package/dist/{service-DeX1PRNt.mjs → service-CeEi6nax.mjs} +0 -0
- /package/dist/{setup-nonce-D-CEYlw7.mjs → setup-nonce-BUmfCiY1.mjs} +0 -0
- /package/dist/{sidecar-client-Bw2C0XTf.mjs → sidecar-client-CbrX8nuw.mjs} +0 -0
- /package/dist/{slugify-KSsVD_D3.mjs → slugify-Di8Q_MFq.mjs} +0 -0
- /package/dist/{ssrf-C7tTSD1r.mjs → ssrf-COxLChKv.mjs} +0 -0
- /package/dist/{stable-hash-DF7R5eN8.mjs → stable-hash-DEtJgcp7.mjs} +0 -0
- /package/dist/{status-Bba6RbkS.mjs → status-DBSB6tdS.mjs} +0 -0
- /package/dist/{transport-ktLL9r2O.d.mts → transport-CZbtxgKZ.d.mts} +0 -0
- /package/dist/{transport-BddGhTAI.mjs → transport-aPKvM53n.mjs} +0 -0
- /package/dist/{types-CKIt_jmM.d.mts → types-1qU0-N1P.d.mts} +0 -0
- /package/dist/{types-BQZFeZjd.mjs → types-BKsk4nnM.mjs} +0 -0
- /package/dist/{types-CLMOusxP.d.mts → types-BWsEjujW.d.mts} +0 -0
- /package/dist/{types--3oJQ0hs.d.mts → types-CAD4OvmF.d.mts} +0 -0
- /package/dist/{types-BbkkkAb6.mjs → types-ChSk0dmw.mjs} +0 -0
- /package/dist/{types-C0DPvhR5.mjs → types-CyBH89BS.mjs} +0 -0
- /package/dist/{types-CU3Wn2HC.d.mts → types-DNPP-eu4.d.mts} +0 -0
- /package/dist/{types-CPaVj_GC.d.mts → types-SddyDti7.d.mts} +0 -0
- /package/dist/{url-C_pLSrGZ.mjs → url-Ce0qtSdB.mjs} +0 -0
- /package/dist/{url-Dumsft7G.mjs → url-DfqolpO2.mjs} +0 -0
- /package/dist/{validate-VPnKoIzW.mjs → validate-Co3j6T_K.mjs} +0 -0
|
@@ -0,0 +1,1386 @@
|
|
|
1
|
+
import { AtomicWriteConflict, atomicBatch, rowUnchanged } from "./database/batch.mjs";
|
|
2
|
+
import { t as validateIdentifier } from "./validate-Co3j6T_K.mjs";
|
|
3
|
+
import { r as isI18nEnabled } from "./config-XW5tMrH8.mjs";
|
|
4
|
+
import { r as encodeBase64, t as decodeBase64 } from "./base64-Cz-aU0X1.mjs";
|
|
5
|
+
import { n as ScheduledNotDueError, t as DinewayValidationError } from "./types-BqxIv8Il.mjs";
|
|
6
|
+
import { t as contentWriteChecks } from "./atomic-authority-CnBQJ14T.mjs";
|
|
7
|
+
import { o as invalidateCollectionCache, s as invalidateCommentObjectCache } from "./object-cache-BGjIlY9s.mjs";
|
|
8
|
+
import { t as RevisionRepository } from "./revision-090sZyGc.mjs";
|
|
9
|
+
import { t as ContentRepository } from "./content-D9xKUSno.mjs";
|
|
10
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BnG3LPMN.mjs";
|
|
11
|
+
import { t as UserRepository } from "./user-1-YV30lf.mjs";
|
|
12
|
+
import { n as stableStringify, t as buildStableHash } from "./stable-hash-DEtJgcp7.mjs";
|
|
13
|
+
import { t as RedirectRepository } from "./redirect-HLI3wT5Y.mjs";
|
|
14
|
+
import { r as requestCached } from "./request-cache-DPmV5nk4.mjs";
|
|
15
|
+
import { t as BylineRepository } from "./byline-DEZKlzD9.mjs";
|
|
16
|
+
import { r as parseAllowedMimeTypes, t as matchesMimeAllowlist } from "./mime-D_ae-u_U.mjs";
|
|
17
|
+
import { i as invalidateRedirectCache } from "./cache-DZDOBJv3.mjs";
|
|
18
|
+
import { t as SeoRepository } from "./seo-DYpc_rQ1.mjs";
|
|
19
|
+
import { sql } from "kysely";
|
|
20
|
+
|
|
21
|
+
//#region src/api/rev.ts
|
|
22
|
+
/**
|
|
23
|
+
* Generate a _rev token from a content item's version and updatedAt.
|
|
24
|
+
*/
|
|
25
|
+
function encodeRev(item) {
|
|
26
|
+
return encodeBase64(`${item.version}:${item.updatedAt}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decode a _rev token into its components.
|
|
30
|
+
* Returns null if the token is malformed.
|
|
31
|
+
*/
|
|
32
|
+
function decodeRev(rev) {
|
|
33
|
+
try {
|
|
34
|
+
const decoded = decodeBase64(rev);
|
|
35
|
+
const colonIdx = decoded.indexOf(":");
|
|
36
|
+
if (colonIdx === -1) return null;
|
|
37
|
+
const version = parseInt(decoded.slice(0, colonIdx), 10);
|
|
38
|
+
const updatedAt = decoded.slice(colonIdx + 1);
|
|
39
|
+
if (isNaN(version) || !updatedAt) return null;
|
|
40
|
+
return {
|
|
41
|
+
version,
|
|
42
|
+
updatedAt
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate a _rev token against a content item.
|
|
50
|
+
* Returns null if valid (or if no _rev provided), or an error message if invalid.
|
|
51
|
+
*/
|
|
52
|
+
function validateRev(rev, item) {
|
|
53
|
+
if (!rev) return { valid: true };
|
|
54
|
+
const decoded = decodeRev(rev);
|
|
55
|
+
if (!decoded) return {
|
|
56
|
+
valid: false,
|
|
57
|
+
message: "Malformed _rev token"
|
|
58
|
+
};
|
|
59
|
+
if (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) return {
|
|
60
|
+
valid: false,
|
|
61
|
+
message: "Content has been modified since last read (version conflict)"
|
|
62
|
+
};
|
|
63
|
+
return { valid: true };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/api/handlers/validate-media-fields.ts
|
|
68
|
+
function asMediaRef(value) {
|
|
69
|
+
if (value === null || value === void 0) return null;
|
|
70
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function fail(message) {
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: {
|
|
77
|
+
code: "INVALID_MIME_FOR_FIELD",
|
|
78
|
+
message
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function loadMediaFieldsForCollection(db, collectionSlug) {
|
|
83
|
+
const rows = await db.selectFrom("_dineway_fields").innerJoin("_dineway_collections", "_dineway_collections.id", "_dineway_fields.collection_id").select(["_dineway_fields.slug", "_dineway_fields.validation"]).where("_dineway_collections.slug", "=", collectionSlug).where("_dineway_fields.type", "in", ["file", "image"]).execute();
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
const list = parseAllowedMimeTypes(row.validation);
|
|
87
|
+
if (!list) continue;
|
|
88
|
+
out.push({
|
|
89
|
+
slug: row.slug,
|
|
90
|
+
allowedMimeTypes: list
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
async function validateMediaFields(db, collectionSlug, data) {
|
|
96
|
+
const fields = await requestCached(`mediaFields:${collectionSlug}`, () => loadMediaFieldsForCollection(db, collectionSlug));
|
|
97
|
+
if (fields.length === 0) return {
|
|
98
|
+
success: true,
|
|
99
|
+
data: true
|
|
100
|
+
};
|
|
101
|
+
const localIds = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const field of fields) {
|
|
103
|
+
const ref = asMediaRef(data[field.slug]);
|
|
104
|
+
if (!ref) continue;
|
|
105
|
+
if ((typeof ref.provider === "string" ? ref.provider : "local") === "local" && typeof ref.id === "string") localIds.add(ref.id);
|
|
106
|
+
}
|
|
107
|
+
const mimeById = /* @__PURE__ */ new Map();
|
|
108
|
+
const idList = [...localIds];
|
|
109
|
+
if (idList.length > 0) for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
|
|
110
|
+
const rows = await db.selectFrom("media").select(["id", "mime_type"]).where("id", "in", batch).execute();
|
|
111
|
+
for (const row of rows) mimeById.set(row.id, row.mime_type);
|
|
112
|
+
}
|
|
113
|
+
for (const field of fields) {
|
|
114
|
+
const value = data[field.slug];
|
|
115
|
+
if (value === null || value === void 0) continue;
|
|
116
|
+
const ref = asMediaRef(value);
|
|
117
|
+
if (!ref) continue;
|
|
118
|
+
const provider = typeof ref.provider === "string" ? ref.provider : "local";
|
|
119
|
+
let mime;
|
|
120
|
+
if (provider === "local") {
|
|
121
|
+
if (typeof ref.id !== "string") return fail(`Field '${field.slug}' references media with an invalid id`);
|
|
122
|
+
mime = mimeById.get(ref.id);
|
|
123
|
+
if (!mime) return fail(`Field '${field.slug}' references media with unknown MIME type`);
|
|
124
|
+
} else {
|
|
125
|
+
if (typeof ref.mimeType !== "string") return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
|
|
126
|
+
mime = ref.mimeType;
|
|
127
|
+
}
|
|
128
|
+
if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) return fail(`Field '${field.slug}' does not accept ${mime}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
data: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/api/handlers/content.ts
|
|
138
|
+
/**
|
|
139
|
+
* Extract a slug source (title or name) from content data.
|
|
140
|
+
* Returns null if no suitable string field is found.
|
|
141
|
+
*/
|
|
142
|
+
function getSlugSource(data) {
|
|
143
|
+
if (typeof data.title === "string" && data.title.length > 0) return data.title;
|
|
144
|
+
if (typeof data.name === "string" && data.name.length > 0) return data.name;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/** Default SEO values for content without an explicit SEO row */
|
|
148
|
+
const SEO_DEFAULTS = {
|
|
149
|
+
title: null,
|
|
150
|
+
description: null,
|
|
151
|
+
image: null,
|
|
152
|
+
canonical: null,
|
|
153
|
+
noIndex: false
|
|
154
|
+
};
|
|
155
|
+
var ContentUpdateApiError = class extends Error {
|
|
156
|
+
constructor(code, message) {
|
|
157
|
+
super(message);
|
|
158
|
+
this.code = code;
|
|
159
|
+
this.name = "ContentUpdateApiError";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
async function contentWriteConfiguration(db, collection) {
|
|
163
|
+
const row = await db.selectFrom("_dineway_collections").selectAll().where("slug", "=", collection).executeTakeFirst();
|
|
164
|
+
const fields = row ? await db.selectFrom("_dineway_fields").selectAll().where("collection_id", "=", row.id).execute() : [];
|
|
165
|
+
return {
|
|
166
|
+
row,
|
|
167
|
+
fields,
|
|
168
|
+
checks: row ? [
|
|
169
|
+
rowUnchanged("_dineway_collections", row),
|
|
170
|
+
sql`(SELECT COUNT(*) FROM _dineway_fields WHERE collection_id = ${row.id}) = ${fields.length}`,
|
|
171
|
+
...fields.map((field) => rowUnchanged("_dineway_fields", field))
|
|
172
|
+
] : [sql`NOT EXISTS (SELECT 1 FROM _dineway_collections WHERE slug = ${collection})`]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Check if a collection has SEO enabled.
|
|
177
|
+
*/
|
|
178
|
+
async function collectionHasSeo(db, collection) {
|
|
179
|
+
return (await db.selectFrom("_dineway_collections").select("has_seo").where("slug", "=", collection).executeTakeFirst())?.has_seo === 1;
|
|
180
|
+
}
|
|
181
|
+
async function collectionSupportsRevisions(db, collection) {
|
|
182
|
+
const row = await db.selectFrom("_dineway_collections").select("supports").where("slug", "=", collection).executeTakeFirst();
|
|
183
|
+
if (!row?.supports) return false;
|
|
184
|
+
try {
|
|
185
|
+
const supports = JSON.parse(row.supports);
|
|
186
|
+
return Array.isArray(supports) && supports.includes("revisions");
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function getCollectionFieldSlugs(db, collection) {
|
|
192
|
+
const collectionRow = await db.selectFrom("_dineway_collections").select("id").where("slug", "=", collection).executeTakeFirst();
|
|
193
|
+
if (!collectionRow) return null;
|
|
194
|
+
return (await db.selectFrom("_dineway_fields").select("slug").where("collection_id", "=", collectionRow.id).orderBy("sort_order", "asc").orderBy("created_at", "asc").execute()).map((row) => row.slug);
|
|
195
|
+
}
|
|
196
|
+
async function validateContentDataFields(db, collection, data) {
|
|
197
|
+
const allowedSlugs = await getCollectionFieldSlugs(db, collection);
|
|
198
|
+
if (!allowedSlugs) return null;
|
|
199
|
+
const allowed = new Set(allowedSlugs);
|
|
200
|
+
const unknownFields = Object.keys(data).filter((field) => !allowed.has(field));
|
|
201
|
+
if (unknownFields.length === 0) return null;
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: {
|
|
205
|
+
code: "VALIDATION_ERROR",
|
|
206
|
+
message: `Unknown ${unknownFields.length === 1 ? "field" : "fields"} ${unknownFields.map((field) => `"${field}"`).join(", ")} for collection "${collection}". Allowed fields: ${allowedSlugs.join(", ")}.`
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Hydrate SEO data on a single content item if the collection has SEO enabled.
|
|
212
|
+
*/
|
|
213
|
+
async function hydrateSeo(db, collection, item, hasSeo) {
|
|
214
|
+
if (!hasSeo) return;
|
|
215
|
+
item.seo = await new SeoRepository(db).get(collection, item.id);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Hydrate SEO data on multiple content items using a single batch query.
|
|
219
|
+
*/
|
|
220
|
+
async function hydrateSeoMany(db, collection, items, hasSeo) {
|
|
221
|
+
if (!hasSeo || items.length === 0) return;
|
|
222
|
+
const seoMap = await new SeoRepository(db).getMany(collection, items.map((i) => i.id));
|
|
223
|
+
for (const item of items) item.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };
|
|
224
|
+
}
|
|
225
|
+
async function hydrateBylines(db, collection, item) {
|
|
226
|
+
const bylineRepo = new BylineRepository(db);
|
|
227
|
+
const localeOpt = item.locale ? { locale: item.locale } : void 0;
|
|
228
|
+
const bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);
|
|
229
|
+
if (bylines.length > 0) {
|
|
230
|
+
item.bylines = bylines.map((c) => ({
|
|
231
|
+
...c,
|
|
232
|
+
source: "explicit"
|
|
233
|
+
}));
|
|
234
|
+
item.byline = bylines[0]?.byline ?? null;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (item.primaryBylineId) {
|
|
238
|
+
item.bylines = [];
|
|
239
|
+
item.byline = null;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (item.authorId) {
|
|
243
|
+
const fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);
|
|
244
|
+
if (fallback) {
|
|
245
|
+
item.bylines = [{
|
|
246
|
+
byline: fallback,
|
|
247
|
+
sortOrder: 0,
|
|
248
|
+
roleLabel: null,
|
|
249
|
+
source: "inferred"
|
|
250
|
+
}];
|
|
251
|
+
item.byline = fallback;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
item.bylines = [];
|
|
256
|
+
item.byline = null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.
|
|
260
|
+
*/
|
|
261
|
+
async function hydrateBylinesMany(db, collection, items) {
|
|
262
|
+
if (items.length === 0) return;
|
|
263
|
+
const bylineRepo = new BylineRepository(db);
|
|
264
|
+
const localeBuckets = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const item of items) {
|
|
266
|
+
const key = item.locale ?? null;
|
|
267
|
+
const bucket = localeBuckets.get(key);
|
|
268
|
+
if (bucket) bucket.push(item);
|
|
269
|
+
else localeBuckets.set(key, [item]);
|
|
270
|
+
}
|
|
271
|
+
const bylinesMap = /* @__PURE__ */ new Map();
|
|
272
|
+
const itemsNeedingAuthorCheck = [];
|
|
273
|
+
for (const [locale, bucket] of localeBuckets) {
|
|
274
|
+
const localeOpt = locale ? { locale } : void 0;
|
|
275
|
+
const credits = await bylineRepo.getContentBylinesMany(collection, bucket.map((item) => item.id), localeOpt);
|
|
276
|
+
for (const [id, list] of credits) bylinesMap.set(id, list);
|
|
277
|
+
for (const item of bucket) {
|
|
278
|
+
const explicit = credits.get(item.id);
|
|
279
|
+
if (explicit && explicit.length > 0) continue;
|
|
280
|
+
if (item.authorId && !item.primaryBylineId) itemsNeedingAuthorCheck.push(item);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const fallbackByItem = /* @__PURE__ */ new Map();
|
|
284
|
+
const authorBuckets = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const item of itemsNeedingAuthorCheck) {
|
|
286
|
+
const key = item.locale ?? null;
|
|
287
|
+
const bucket = authorBuckets.get(key);
|
|
288
|
+
if (bucket) bucket.push(item);
|
|
289
|
+
else authorBuckets.set(key, [item]);
|
|
290
|
+
}
|
|
291
|
+
for (const [locale, bucket] of authorBuckets) {
|
|
292
|
+
const authorIds = bucket.map((item) => item.authorId).filter((id) => id !== null);
|
|
293
|
+
const uniqueAuthorIds = [...new Set(authorIds)];
|
|
294
|
+
if (uniqueAuthorIds.length === 0) continue;
|
|
295
|
+
const localeOpt = locale ? { locale } : void 0;
|
|
296
|
+
const authorBylineMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);
|
|
297
|
+
for (const item of bucket) {
|
|
298
|
+
if (!item.authorId) continue;
|
|
299
|
+
const fallback = authorBylineMap.get(item.authorId);
|
|
300
|
+
if (fallback) fallbackByItem.set(item.id, fallback);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
for (const item of items) {
|
|
304
|
+
const explicit = bylinesMap.get(item.id);
|
|
305
|
+
if (explicit && explicit.length > 0) {
|
|
306
|
+
item.bylines = explicit.map((c) => ({
|
|
307
|
+
...c,
|
|
308
|
+
source: "explicit"
|
|
309
|
+
}));
|
|
310
|
+
item.byline = explicit[0]?.byline ?? null;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (item.primaryBylineId) {
|
|
314
|
+
item.bylines = [];
|
|
315
|
+
item.byline = null;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const fallback = fallbackByItem.get(item.id);
|
|
319
|
+
if (fallback) {
|
|
320
|
+
item.bylines = [{
|
|
321
|
+
byline: fallback,
|
|
322
|
+
sortOrder: 0,
|
|
323
|
+
roleLabel: null,
|
|
324
|
+
source: "inferred"
|
|
325
|
+
}];
|
|
326
|
+
item.byline = fallback;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
item.bylines = [];
|
|
330
|
+
item.byline = null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Resolve an identifier (ID or slug) to a real content ID.
|
|
335
|
+
* Returns the ID if found, null if not found.
|
|
336
|
+
* When locale is provided, slug lookups are scoped to that locale.
|
|
337
|
+
*/
|
|
338
|
+
async function resolveId(repo, collection, identifier, locale) {
|
|
339
|
+
return (await repo.findByIdOrSlug(collection, identifier, locale))?.id ?? null;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Resolve an identifier (ID or slug) to a real content ID,
|
|
343
|
+
* including trashed (soft-deleted) items.
|
|
344
|
+
*/
|
|
345
|
+
async function resolveIdIncludingTrashed(repo, collection, identifier, locale) {
|
|
346
|
+
return (await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale))?.id ?? null;
|
|
347
|
+
}
|
|
348
|
+
async function resolveSearchColumns(db, collection) {
|
|
349
|
+
const columns = ["slug"];
|
|
350
|
+
const fieldSlugs = await getCollectionFieldSlugs(db, collection);
|
|
351
|
+
if (!fieldSlugs) return columns;
|
|
352
|
+
const fields = new Set(fieldSlugs);
|
|
353
|
+
for (const candidate of ["title", "name"]) if (fields.has(candidate)) columns.push(candidate);
|
|
354
|
+
return columns;
|
|
355
|
+
}
|
|
356
|
+
const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
357
|
+
function normalizeDateBound(value, edge) {
|
|
358
|
+
if (!value) return void 0;
|
|
359
|
+
if (!DATE_ONLY_RE.test(value)) return value;
|
|
360
|
+
return edge === "start" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Create content list handler
|
|
364
|
+
*/
|
|
365
|
+
async function handleContentList(db, collection, params) {
|
|
366
|
+
try {
|
|
367
|
+
const repo = new ContentRepository(db);
|
|
368
|
+
const where = {};
|
|
369
|
+
if (params.status) where.status = params.status;
|
|
370
|
+
if (params.locale) where.locale = params.locale;
|
|
371
|
+
if (params.authorId) where.authorId = params.authorId;
|
|
372
|
+
if (params.dateField && (params.dateFrom || params.dateTo)) where.dateFilter = {
|
|
373
|
+
field: params.dateField,
|
|
374
|
+
from: normalizeDateBound(params.dateFrom, "start"),
|
|
375
|
+
to: normalizeDateBound(params.dateTo, "end")
|
|
376
|
+
};
|
|
377
|
+
const q = params.q?.trim();
|
|
378
|
+
if (q) {
|
|
379
|
+
where.q = q;
|
|
380
|
+
where.searchColumns = await resolveSearchColumns(db, collection);
|
|
381
|
+
}
|
|
382
|
+
const result = await repo.findMany(collection, {
|
|
383
|
+
cursor: params.cursor,
|
|
384
|
+
limit: params.limit || 50,
|
|
385
|
+
where: Object.keys(where).length > 0 ? where : void 0,
|
|
386
|
+
orderBy: params.orderBy ? {
|
|
387
|
+
field: params.orderBy,
|
|
388
|
+
direction: params.order || "desc"
|
|
389
|
+
} : void 0
|
|
390
|
+
});
|
|
391
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
392
|
+
await hydrateSeoMany(db, collection, result.items, hasSeo);
|
|
393
|
+
await hydrateBylinesMany(db, collection, result.items);
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
data: {
|
|
397
|
+
items: result.items,
|
|
398
|
+
nextCursor: result.nextCursor,
|
|
399
|
+
total: result.total
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error("Content list error:", error);
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: {
|
|
407
|
+
code: "CONTENT_LIST_ERROR",
|
|
408
|
+
message: "Failed to list content"
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function handleContentAuthors(db, collection) {
|
|
414
|
+
try {
|
|
415
|
+
const authorIds = await new ContentRepository(db).findDistinctAuthorIds(collection);
|
|
416
|
+
if (authorIds.length === 0) return {
|
|
417
|
+
success: true,
|
|
418
|
+
data: { items: [] }
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
data: { items: (await new UserRepository(db).findByIds(authorIds)).map((user) => ({
|
|
423
|
+
id: user.id,
|
|
424
|
+
name: user.name,
|
|
425
|
+
email: user.email,
|
|
426
|
+
avatarUrl: user.avatarUrl
|
|
427
|
+
})).toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email)) }
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error("Content authors error:", error);
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
error: {
|
|
434
|
+
code: "CONTENT_AUTHORS_ERROR",
|
|
435
|
+
message: "Failed to list content authors"
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Get single content item
|
|
442
|
+
*/
|
|
443
|
+
async function handleContentGet(db, collection, id, locale) {
|
|
444
|
+
try {
|
|
445
|
+
const item = await new ContentRepository(db).findByIdOrSlug(collection, id, locale);
|
|
446
|
+
if (!item) return {
|
|
447
|
+
success: false,
|
|
448
|
+
error: {
|
|
449
|
+
code: "NOT_FOUND",
|
|
450
|
+
message: `Content item not found: ${id}`
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
454
|
+
await hydrateBylines(db, collection, item);
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
data: {
|
|
458
|
+
item,
|
|
459
|
+
_rev: encodeRev(item)
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.error("Content get error:", error);
|
|
464
|
+
return {
|
|
465
|
+
success: false,
|
|
466
|
+
error: {
|
|
467
|
+
code: "CONTENT_GET_ERROR",
|
|
468
|
+
message: "Failed to get content"
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Get a content item by id, including trashed items.
|
|
475
|
+
* Used by restore endpoint for ownership checks on soft-deleted items.
|
|
476
|
+
*/
|
|
477
|
+
async function handleContentGetIncludingTrashed(db, collection, id, locale) {
|
|
478
|
+
try {
|
|
479
|
+
const item = await new ContentRepository(db).findByIdOrSlugIncludingTrashed(collection, id, locale);
|
|
480
|
+
if (!item) return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: {
|
|
483
|
+
code: "NOT_FOUND",
|
|
484
|
+
message: `Content item not found: ${id}`
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
488
|
+
await hydrateBylines(db, collection, item);
|
|
489
|
+
return {
|
|
490
|
+
success: true,
|
|
491
|
+
data: {
|
|
492
|
+
item,
|
|
493
|
+
_rev: encodeRev(item)
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.error("Content get error:", error);
|
|
498
|
+
return {
|
|
499
|
+
success: false,
|
|
500
|
+
error: {
|
|
501
|
+
code: "CONTENT_GET_ERROR",
|
|
502
|
+
message: "Failed to get content"
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Create content item.
|
|
509
|
+
*
|
|
510
|
+
* Content + SEO writes are wrapped in a transaction so either both succeed
|
|
511
|
+
* or neither does. If `body.seo` is provided for a non-SEO collection, the
|
|
512
|
+
* API returns a validation error rather than silently dropping it.
|
|
513
|
+
*/
|
|
514
|
+
async function handleContentCreate(db, collection, body) {
|
|
515
|
+
try {
|
|
516
|
+
const configuration = await contentWriteConfiguration(db, collection);
|
|
517
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
518
|
+
const usesDraftRevisions = await collectionSupportsRevisions(db, collection);
|
|
519
|
+
if (body.seo && !hasSeo) return {
|
|
520
|
+
success: false,
|
|
521
|
+
error: {
|
|
522
|
+
code: "VALIDATION_ERROR",
|
|
523
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const fieldCheck = await validateContentDataFields(db, collection, body.data);
|
|
527
|
+
if (fieldCheck) return fieldCheck;
|
|
528
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
529
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
530
|
+
const repo = new ContentRepository(db);
|
|
531
|
+
let slug = body.slug;
|
|
532
|
+
if (!slug) {
|
|
533
|
+
const source = getSlugSource(body.data);
|
|
534
|
+
if (source) slug = await repo.generateUniqueSlug(collection, source, body.locale);
|
|
535
|
+
}
|
|
536
|
+
const prepared = await repo.prepareCreate({
|
|
537
|
+
type: collection,
|
|
538
|
+
slug,
|
|
539
|
+
data: body.data,
|
|
540
|
+
status: body.status || "draft",
|
|
541
|
+
authorId: body.authorId,
|
|
542
|
+
locale: body.locale,
|
|
543
|
+
translationOf: body.translationOf,
|
|
544
|
+
createdAt: body.createdAt,
|
|
545
|
+
publishedAt: body.publishedAt
|
|
546
|
+
});
|
|
547
|
+
const checks = [...prepared.batch.checks ?? [], ...configuration.checks];
|
|
548
|
+
const statements = [...prepared.batch.statements];
|
|
549
|
+
if (usesDraftRevisions && (!body.status || body.status === "draft")) {
|
|
550
|
+
const draft = await repo.prepareDraftSnapshot(collection, prepared.id, body.authorId);
|
|
551
|
+
checks.push(...draft.checks ?? []);
|
|
552
|
+
statements.push(...draft.statements);
|
|
553
|
+
}
|
|
554
|
+
if (body.bylines !== void 0) {
|
|
555
|
+
const bylines = await new BylineRepository(db).prepareContentBylines(collection, prepared.id, body.bylines);
|
|
556
|
+
checks.push(...bylines.checks ?? []);
|
|
557
|
+
statements.push(...bylines.statements);
|
|
558
|
+
}
|
|
559
|
+
if (body.seo && hasSeo) {
|
|
560
|
+
const seo = new SeoRepository(db).prepareUpsert(collection, prepared.id, body.seo);
|
|
561
|
+
if (seo) statements.push(seo);
|
|
562
|
+
}
|
|
563
|
+
await atomicBatch(db, {
|
|
564
|
+
checks,
|
|
565
|
+
statements
|
|
566
|
+
});
|
|
567
|
+
invalidateCollectionCache(collection);
|
|
568
|
+
const item = await repo.findById(collection, prepared.id);
|
|
569
|
+
if (!item) throw new Error("Failed to reload content");
|
|
570
|
+
await hydrateBylines(db, collection, item);
|
|
571
|
+
await hydrateSeo(db, collection, item, hasSeo);
|
|
572
|
+
return {
|
|
573
|
+
success: true,
|
|
574
|
+
data: {
|
|
575
|
+
item,
|
|
576
|
+
_rev: encodeRev(item)
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error("Content create error:", error);
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: {
|
|
584
|
+
code: "CONTENT_CREATE_ERROR",
|
|
585
|
+
message: "Failed to create content"
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Update content item.
|
|
592
|
+
* If `_rev` is provided, validates it against the current version before writing.
|
|
593
|
+
* No `_rev` = blind write (backwards-compatible for admin UI).
|
|
594
|
+
*
|
|
595
|
+
* Content + SEO writes are wrapped in a transaction for atomicity.
|
|
596
|
+
*/
|
|
597
|
+
async function handleContentUpdate(db, collection, id, body, options = {}) {
|
|
598
|
+
try {
|
|
599
|
+
const configuration = await contentWriteConfiguration(db, collection);
|
|
600
|
+
const hasSeo = await collectionHasSeo(db, collection);
|
|
601
|
+
if (body.seo && !hasSeo) return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: {
|
|
604
|
+
code: "VALIDATION_ERROR",
|
|
605
|
+
message: `Collection "${collection}" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
if (body.data) {
|
|
609
|
+
const fieldCheck = await validateContentDataFields(db, collection, body.data);
|
|
610
|
+
if (fieldCheck) return fieldCheck;
|
|
611
|
+
const mimeCheck = await validateMediaFields(db, collection, body.data);
|
|
612
|
+
if (!mimeCheck.success) return mimeCheck;
|
|
613
|
+
}
|
|
614
|
+
const repo = new ContentRepository(db);
|
|
615
|
+
const resolvedId = await resolveId(repo, collection, id, body.locale) ?? id;
|
|
616
|
+
const snapshot = await repo.snapshot(collection, resolvedId);
|
|
617
|
+
if (!snapshot) throw new ContentUpdateApiError("NOT_FOUND", `Content item not found: ${id}`);
|
|
618
|
+
const existing = snapshot.item;
|
|
619
|
+
const revCheck = validateRev(body._rev, existing);
|
|
620
|
+
if (!revCheck.valid) throw new ContentUpdateApiError("CONFLICT", revCheck.message);
|
|
621
|
+
const checks = [
|
|
622
|
+
...configuration.checks,
|
|
623
|
+
snapshot.check,
|
|
624
|
+
...contentWriteChecks(collection, resolvedId, "edit")
|
|
625
|
+
];
|
|
626
|
+
const statements = [];
|
|
627
|
+
const stageDraft = options.stageDraft && body.data && await collectionSupportsRevisions(db, collection);
|
|
628
|
+
if (stageDraft) {
|
|
629
|
+
const revisions = new RevisionRepository(db);
|
|
630
|
+
const draft = existing.draftRevisionId ? await db.selectFrom("revisions").selectAll().where("id", "=", existing.draftRevisionId).executeTakeFirst() : null;
|
|
631
|
+
if (existing.draftRevisionId && (!draft || draft.collection !== collection || draft.entry_id !== resolvedId)) throw new ContentUpdateApiError("CONFLICT", "The draft revision is missing or belongs to another entry");
|
|
632
|
+
if (draft) checks.push(rowUnchanged("revisions", draft));
|
|
633
|
+
const data = {
|
|
634
|
+
...draft ? JSON.parse(draft.data) : existing.data,
|
|
635
|
+
...body.data,
|
|
636
|
+
...body.slug !== void 0 ? { _slug: body.slug } : {}
|
|
637
|
+
};
|
|
638
|
+
if (options.skipRevision && draft && draft.id !== existing.liveRevisionId) statements.push(db.updateTable("revisions").set({ data: JSON.stringify(data) }).where("id", "=", draft.id).compile());
|
|
639
|
+
else {
|
|
640
|
+
const revision = revisions.prepareCreateSnapshot({
|
|
641
|
+
collection,
|
|
642
|
+
entryId: resolvedId,
|
|
643
|
+
data: sql`${JSON.stringify(data)}`,
|
|
644
|
+
authorId: body.authorId ?? void 0
|
|
645
|
+
});
|
|
646
|
+
statements.push(revision.statement, sql`UPDATE ${sql.table(`ec_${collection}`)} SET draft_revision_id = ${revision.id} WHERE id = ${resolvedId}`.compile(db));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
statements.push(repo.prepareUpdate(collection, resolvedId, {
|
|
650
|
+
data: stageDraft ? void 0 : body.data,
|
|
651
|
+
slug: stageDraft ? void 0 : body.slug,
|
|
652
|
+
status: body.status,
|
|
653
|
+
authorId: body.authorId,
|
|
654
|
+
publishedAt: body.publishedAt
|
|
655
|
+
}));
|
|
656
|
+
if (body.bylines !== void 0) {
|
|
657
|
+
const bylines = await new BylineRepository(db).prepareContentBylines(collection, resolvedId, body.bylines);
|
|
658
|
+
checks.push(...bylines.checks ?? []);
|
|
659
|
+
statements.push(...bylines.statements);
|
|
660
|
+
}
|
|
661
|
+
const slugChanged = !stageDraft && body.slug && existing.slug && existing.slug !== body.slug;
|
|
662
|
+
if (slugChanged) statements.push(...new RedirectRepository(db).prepareAutoRedirect(collection, existing.slug, body.slug, resolvedId, configuration.row?.url_pattern ?? null).statements);
|
|
663
|
+
if (!stageDraft && isI18nEnabled() && body.data && existing.translationGroup) {
|
|
664
|
+
const sync = prepareNonTranslatableFields(db, collection, resolvedId, existing.translationGroup, body.data, configuration.fields.filter((field) => field.translatable === 0).map((field) => field.slug));
|
|
665
|
+
if (sync) statements.push(sync);
|
|
666
|
+
}
|
|
667
|
+
if (body.seo && hasSeo) {
|
|
668
|
+
const seo = new SeoRepository(db).prepareUpsert(collection, resolvedId, body.seo);
|
|
669
|
+
if (seo) statements.push(seo);
|
|
670
|
+
}
|
|
671
|
+
await atomicBatch(db, {
|
|
672
|
+
checks,
|
|
673
|
+
statements
|
|
674
|
+
});
|
|
675
|
+
invalidateCollectionCache(collection);
|
|
676
|
+
if (slugChanged) invalidateRedirectCache();
|
|
677
|
+
const item = await repo.findById(collection, resolvedId);
|
|
678
|
+
if (!item) throw new Error("Failed to reload content");
|
|
679
|
+
await hydrateSeo(db, collection, item, hasSeo);
|
|
680
|
+
await hydrateBylines(db, collection, item);
|
|
681
|
+
return {
|
|
682
|
+
success: true,
|
|
683
|
+
data: {
|
|
684
|
+
item,
|
|
685
|
+
_rev: encodeRev(item)
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (error instanceof AtomicWriteConflict) return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: {
|
|
692
|
+
code: "CONFLICT",
|
|
693
|
+
message: "Content or its configuration changed during the save. Reload and try again."
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
if (error instanceof ContentUpdateApiError) return {
|
|
697
|
+
success: false,
|
|
698
|
+
error: {
|
|
699
|
+
code: error.code,
|
|
700
|
+
message: error.message
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
console.error("Content update error:", error);
|
|
704
|
+
return {
|
|
705
|
+
success: false,
|
|
706
|
+
error: {
|
|
707
|
+
code: "CONTENT_UPDATE_ERROR",
|
|
708
|
+
message: "Failed to update content"
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Duplicate content item.
|
|
715
|
+
*
|
|
716
|
+
* Only copies SEO data if the collection has SEO enabled.
|
|
717
|
+
* Always returns consistent `seo` shape for SEO-enabled collections.
|
|
718
|
+
*/
|
|
719
|
+
async function handleContentDuplicate(db, collection, id, authorId) {
|
|
720
|
+
try {
|
|
721
|
+
const configuration = await contentWriteConfiguration(db, collection);
|
|
722
|
+
const hasSeo = configuration.row?.has_seo === 1;
|
|
723
|
+
const repo = new ContentRepository(db);
|
|
724
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
725
|
+
const prepared = await repo.prepareDuplicate(collection, resolvedId, authorId);
|
|
726
|
+
const bylines = await new BylineRepository(db).prepareCopyContentBylines(collection, resolvedId, prepared.id);
|
|
727
|
+
const checks = [
|
|
728
|
+
...configuration.checks,
|
|
729
|
+
...prepared.batch.checks ?? [],
|
|
730
|
+
...bylines.checks ?? []
|
|
731
|
+
];
|
|
732
|
+
const statements = [...prepared.batch.statements, ...bylines.statements];
|
|
733
|
+
if (await collectionSupportsRevisions(db, collection)) {
|
|
734
|
+
const draft = await repo.prepareDraftSnapshot(collection, prepared.id, authorId);
|
|
735
|
+
checks.push(...draft.checks ?? []);
|
|
736
|
+
statements.push(...draft.statements);
|
|
737
|
+
}
|
|
738
|
+
if (hasSeo) statements.push(new SeoRepository(db).prepareCopyForDuplicate(collection, resolvedId, prepared.id));
|
|
739
|
+
await atomicBatch(db, {
|
|
740
|
+
checks,
|
|
741
|
+
statements
|
|
742
|
+
});
|
|
743
|
+
invalidateCollectionCache(collection);
|
|
744
|
+
const duplicate = await repo.findById(collection, prepared.id);
|
|
745
|
+
if (!duplicate) throw new Error("Failed to reload duplicate");
|
|
746
|
+
await hydrateBylines(db, collection, duplicate);
|
|
747
|
+
await hydrateSeo(db, collection, duplicate, hasSeo);
|
|
748
|
+
return {
|
|
749
|
+
success: true,
|
|
750
|
+
data: { item: duplicate }
|
|
751
|
+
};
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err instanceof DinewayValidationError) return {
|
|
754
|
+
success: false,
|
|
755
|
+
error: {
|
|
756
|
+
code: "NOT_FOUND",
|
|
757
|
+
message: err.message
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
console.error("Content duplicate error:", err);
|
|
761
|
+
return {
|
|
762
|
+
success: false,
|
|
763
|
+
error: {
|
|
764
|
+
code: "CONTENT_DUPLICATE_ERROR",
|
|
765
|
+
message: "Failed to duplicate content"
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Delete content item (soft delete - moves to trash)
|
|
772
|
+
*/
|
|
773
|
+
async function handleContentDelete(db, collection, id) {
|
|
774
|
+
try {
|
|
775
|
+
const repo = new ContentRepository(db);
|
|
776
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
777
|
+
const result = {
|
|
778
|
+
id: resolvedId,
|
|
779
|
+
deleted: await repo.delete(collection, resolvedId)
|
|
780
|
+
};
|
|
781
|
+
if (!result.deleted) return {
|
|
782
|
+
success: false,
|
|
783
|
+
error: {
|
|
784
|
+
code: "NOT_FOUND",
|
|
785
|
+
message: `Content item not found: ${id}`
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
return {
|
|
789
|
+
success: true,
|
|
790
|
+
data: {
|
|
791
|
+
deleted: true,
|
|
792
|
+
id: result.id
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
} catch (error) {
|
|
796
|
+
console.error("Content delete error:", error);
|
|
797
|
+
return {
|
|
798
|
+
success: false,
|
|
799
|
+
error: {
|
|
800
|
+
code: "CONTENT_DELETE_ERROR",
|
|
801
|
+
message: "Failed to delete content"
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Restore content item from trash
|
|
808
|
+
*/
|
|
809
|
+
async function handleContentRestore(db, collection, id) {
|
|
810
|
+
try {
|
|
811
|
+
const repo = new ContentRepository(db);
|
|
812
|
+
const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
|
|
813
|
+
const item = await repo.restore(collection, resolvedId);
|
|
814
|
+
if (!item) return {
|
|
815
|
+
success: false,
|
|
816
|
+
error: {
|
|
817
|
+
code: "NOT_FOUND",
|
|
818
|
+
message: `Trashed content item not found: ${id}`
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
return {
|
|
822
|
+
success: true,
|
|
823
|
+
data: {
|
|
824
|
+
restored: true,
|
|
825
|
+
item
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
} catch (error) {
|
|
829
|
+
console.error("Content restore error:", error);
|
|
830
|
+
return {
|
|
831
|
+
success: false,
|
|
832
|
+
error: {
|
|
833
|
+
code: "CONTENT_RESTORE_ERROR",
|
|
834
|
+
message: "Failed to restore content"
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Permanently delete content item (cannot be undone).
|
|
841
|
+
* Also cleans up associated SEO data.
|
|
842
|
+
*/
|
|
843
|
+
async function handleContentPermanentDelete(db, collection, id) {
|
|
844
|
+
try {
|
|
845
|
+
const repo = new ContentRepository(db);
|
|
846
|
+
const resolvedId = await resolveIdIncludingTrashed(repo, collection, id) ?? id;
|
|
847
|
+
const snapshot = await repo.snapshot(collection, resolvedId, true);
|
|
848
|
+
const deleted = !!snapshot?.deleted;
|
|
849
|
+
if (snapshot && deleted) {
|
|
850
|
+
await atomicBatch(db, {
|
|
851
|
+
checks: [snapshot.check, ...contentWriteChecks(collection, resolvedId, "permanent")],
|
|
852
|
+
statements: [
|
|
853
|
+
sql`DELETE FROM ${sql.table(`ec_${collection}`)} WHERE id = ${resolvedId} AND deleted_at IS NOT NULL`.compile(db),
|
|
854
|
+
db.deleteFrom("_dineway_seo").where("collection", "=", collection).where("content_id", "=", resolvedId).compile(),
|
|
855
|
+
db.deleteFrom("_dineway_comments").where("collection", "=", collection).where("content_id", "=", resolvedId).compile(),
|
|
856
|
+
db.deleteFrom("revisions").where("collection", "=", collection).where("entry_id", "=", resolvedId).compile(),
|
|
857
|
+
db.deleteFrom("_dineway_content_bylines").where("collection_slug", "=", collection).where("content_id", "=", resolvedId).compile()
|
|
858
|
+
]
|
|
859
|
+
});
|
|
860
|
+
invalidateCollectionCache(collection);
|
|
861
|
+
invalidateCommentObjectCache();
|
|
862
|
+
}
|
|
863
|
+
if (!deleted) return {
|
|
864
|
+
success: false,
|
|
865
|
+
error: {
|
|
866
|
+
code: "NOT_FOUND",
|
|
867
|
+
message: `Content item not found: ${id}`
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
success: true,
|
|
872
|
+
data: {
|
|
873
|
+
deleted: true,
|
|
874
|
+
id: resolvedId
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
} catch (error) {
|
|
878
|
+
console.error("Content permanent delete error:", error);
|
|
879
|
+
return {
|
|
880
|
+
success: false,
|
|
881
|
+
error: {
|
|
882
|
+
code: "CONTENT_DELETE_ERROR",
|
|
883
|
+
message: "Failed to permanently delete content"
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* List trashed content items
|
|
890
|
+
*/
|
|
891
|
+
async function handleContentListTrashed(db, collection, options = {}) {
|
|
892
|
+
try {
|
|
893
|
+
const result = await new ContentRepository(db).findTrashed(collection, {
|
|
894
|
+
limit: options.limit,
|
|
895
|
+
cursor: options.cursor
|
|
896
|
+
});
|
|
897
|
+
return {
|
|
898
|
+
success: true,
|
|
899
|
+
data: {
|
|
900
|
+
items: result.items.map((item) => ({
|
|
901
|
+
id: item.id,
|
|
902
|
+
type: item.type,
|
|
903
|
+
slug: item.slug,
|
|
904
|
+
status: item.status,
|
|
905
|
+
data: item.data,
|
|
906
|
+
authorId: item.authorId,
|
|
907
|
+
createdAt: item.createdAt,
|
|
908
|
+
updatedAt: item.updatedAt,
|
|
909
|
+
publishedAt: item.publishedAt,
|
|
910
|
+
deletedAt: item.deletedAt
|
|
911
|
+
})),
|
|
912
|
+
nextCursor: result.nextCursor
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error("Content list trashed error:", error);
|
|
917
|
+
return {
|
|
918
|
+
success: false,
|
|
919
|
+
error: {
|
|
920
|
+
code: "CONTENT_LIST_ERROR",
|
|
921
|
+
message: "Failed to list trashed content"
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Count trashed content items
|
|
928
|
+
*/
|
|
929
|
+
async function handleContentCountTrashed(db, collection) {
|
|
930
|
+
try {
|
|
931
|
+
return {
|
|
932
|
+
success: true,
|
|
933
|
+
data: { count: await new ContentRepository(db).countTrashed(collection) }
|
|
934
|
+
};
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.error("Content count trashed error:", error);
|
|
937
|
+
return {
|
|
938
|
+
success: false,
|
|
939
|
+
error: {
|
|
940
|
+
code: "CONTENT_COUNT_ERROR",
|
|
941
|
+
message: "Failed to count trashed content"
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Schedule content for future publishing
|
|
948
|
+
*/
|
|
949
|
+
async function handleContentSchedule(db, collection, id, scheduledAt) {
|
|
950
|
+
try {
|
|
951
|
+
const repo = new ContentRepository(db);
|
|
952
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
953
|
+
const item = await repo.schedule(collection, resolvedId, scheduledAt);
|
|
954
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
955
|
+
return {
|
|
956
|
+
success: true,
|
|
957
|
+
data: { item }
|
|
958
|
+
};
|
|
959
|
+
} catch (error) {
|
|
960
|
+
if (error instanceof DinewayValidationError) return {
|
|
961
|
+
success: false,
|
|
962
|
+
error: {
|
|
963
|
+
code: "VALIDATION_ERROR",
|
|
964
|
+
message: error.message
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
console.error("Content schedule error:", error);
|
|
968
|
+
return {
|
|
969
|
+
success: false,
|
|
970
|
+
error: {
|
|
971
|
+
code: "CONTENT_SCHEDULE_ERROR",
|
|
972
|
+
message: "Failed to schedule content"
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Unschedule content (revert to draft)
|
|
979
|
+
*/
|
|
980
|
+
async function handleContentUnschedule(db, collection, id) {
|
|
981
|
+
try {
|
|
982
|
+
const repo = new ContentRepository(db);
|
|
983
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
984
|
+
const item = await repo.unschedule(collection, resolvedId);
|
|
985
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
986
|
+
return {
|
|
987
|
+
success: true,
|
|
988
|
+
data: { item }
|
|
989
|
+
};
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (error instanceof DinewayValidationError) return {
|
|
992
|
+
success: false,
|
|
993
|
+
error: {
|
|
994
|
+
code: "VALIDATION_ERROR",
|
|
995
|
+
message: error.message
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
console.error("Content unschedule error:", error);
|
|
999
|
+
return {
|
|
1000
|
+
success: false,
|
|
1001
|
+
error: {
|
|
1002
|
+
code: "CONTENT_UNSCHEDULE_ERROR",
|
|
1003
|
+
message: "Failed to unschedule content"
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Publish content immediately.
|
|
1010
|
+
*
|
|
1011
|
+
* Wrapped in a transaction because publish performs multiple writes
|
|
1012
|
+
* (syncDataColumns, slug sync, status/revision update) that must
|
|
1013
|
+
* be atomic to prevent FTS shadow table corruption on crash.
|
|
1014
|
+
*/
|
|
1015
|
+
async function prepareContentPublication(db, collection, id, options = {}) {
|
|
1016
|
+
const configuration = await contentWriteConfiguration(db, collection);
|
|
1017
|
+
const repo = new ContentRepository(db);
|
|
1018
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1019
|
+
const prepared = await repo.preparePublish(collection, resolvedId, options.publishedAt, options.requireScheduledDue);
|
|
1020
|
+
const statements = [...prepared.batch.statements];
|
|
1021
|
+
const existing = prepared.previous;
|
|
1022
|
+
const slugChanged = existing.status === "published" && existing.slug && prepared.slug && existing.slug !== prepared.slug;
|
|
1023
|
+
if (slugChanged) statements.push(...new RedirectRepository(db).prepareAutoRedirect(collection, existing.slug, prepared.slug, resolvedId, configuration.row?.url_pattern ?? null).statements);
|
|
1024
|
+
return {
|
|
1025
|
+
resolvedId,
|
|
1026
|
+
previous: existing,
|
|
1027
|
+
slugChanged,
|
|
1028
|
+
batch: {
|
|
1029
|
+
checks: [...configuration.checks, ...prepared.batch.checks],
|
|
1030
|
+
statements
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
async function handleContentPublish(db, collection, id, options = {}) {
|
|
1035
|
+
try {
|
|
1036
|
+
const prepared = await prepareContentPublication(db, collection, id, options);
|
|
1037
|
+
await atomicBatch(db, prepared.batch);
|
|
1038
|
+
const { resolvedId, slugChanged } = prepared;
|
|
1039
|
+
const repo = new ContentRepository(db);
|
|
1040
|
+
invalidateCollectionCache(collection);
|
|
1041
|
+
if (slugChanged) invalidateRedirectCache();
|
|
1042
|
+
const item = await repo.findById(collection, resolvedId);
|
|
1043
|
+
if (!item) throw new Error("Content not found");
|
|
1044
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1045
|
+
return {
|
|
1046
|
+
success: true,
|
|
1047
|
+
data: { item }
|
|
1048
|
+
};
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (error instanceof DinewayValidationError) return {
|
|
1051
|
+
success: false,
|
|
1052
|
+
error: {
|
|
1053
|
+
code: "VALIDATION_ERROR",
|
|
1054
|
+
message: error.message
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
if (error instanceof ScheduledNotDueError) return {
|
|
1058
|
+
success: false,
|
|
1059
|
+
error: {
|
|
1060
|
+
code: "NOT_DUE",
|
|
1061
|
+
message: error.message
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
console.error("Content publish error:", error);
|
|
1065
|
+
return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: {
|
|
1068
|
+
code: "CONTENT_PUBLISH_ERROR",
|
|
1069
|
+
message: "Failed to publish content"
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Unpublish content (revert to draft).
|
|
1076
|
+
*
|
|
1077
|
+
* Wrapped in a transaction — unpublish may create a draft revision
|
|
1078
|
+
* from the live version then update the status, which is multi-step.
|
|
1079
|
+
*/
|
|
1080
|
+
async function handleContentUnpublish(db, collection, id) {
|
|
1081
|
+
try {
|
|
1082
|
+
const repo = new ContentRepository(db);
|
|
1083
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1084
|
+
const item = await repo.unpublish(collection, resolvedId);
|
|
1085
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1086
|
+
return {
|
|
1087
|
+
success: true,
|
|
1088
|
+
data: { item }
|
|
1089
|
+
};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error instanceof DinewayValidationError) return {
|
|
1092
|
+
success: false,
|
|
1093
|
+
error: {
|
|
1094
|
+
code: "VALIDATION_ERROR",
|
|
1095
|
+
message: error.message
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
console.error("Content unpublish error:", error);
|
|
1099
|
+
return {
|
|
1100
|
+
success: false,
|
|
1101
|
+
error: {
|
|
1102
|
+
code: "CONTENT_UNPUBLISH_ERROR",
|
|
1103
|
+
message: "Failed to unpublish content"
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Count scheduled content items
|
|
1110
|
+
*/
|
|
1111
|
+
async function handleContentCountScheduled(db, collection) {
|
|
1112
|
+
try {
|
|
1113
|
+
return {
|
|
1114
|
+
success: true,
|
|
1115
|
+
data: { count: await new ContentRepository(db).countScheduled(collection) }
|
|
1116
|
+
};
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error("Content count scheduled error:", error);
|
|
1119
|
+
return {
|
|
1120
|
+
success: false,
|
|
1121
|
+
error: {
|
|
1122
|
+
code: "CONTENT_COUNT_ERROR",
|
|
1123
|
+
message: "Failed to count scheduled content"
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Discard draft changes (revert to live version)
|
|
1130
|
+
*/
|
|
1131
|
+
async function handleContentDiscardDraft(db, collection, id) {
|
|
1132
|
+
try {
|
|
1133
|
+
const repo = new ContentRepository(db);
|
|
1134
|
+
const resolvedId = await resolveId(repo, collection, id) ?? id;
|
|
1135
|
+
const item = await repo.discardDraft(collection, resolvedId);
|
|
1136
|
+
await hydrateSeo(db, collection, item, await collectionHasSeo(db, collection));
|
|
1137
|
+
return {
|
|
1138
|
+
success: true,
|
|
1139
|
+
data: { item }
|
|
1140
|
+
};
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
if (error instanceof DinewayValidationError) return {
|
|
1143
|
+
success: false,
|
|
1144
|
+
error: {
|
|
1145
|
+
code: "NOT_FOUND",
|
|
1146
|
+
message: error.message
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
console.error("Content discard draft error:", error);
|
|
1150
|
+
return {
|
|
1151
|
+
success: false,
|
|
1152
|
+
error: {
|
|
1153
|
+
code: "CONTENT_DISCARD_DRAFT_ERROR",
|
|
1154
|
+
message: "Failed to discard draft"
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Compare live and draft revisions
|
|
1161
|
+
*/
|
|
1162
|
+
async function handleContentCompare(db, collection, id) {
|
|
1163
|
+
try {
|
|
1164
|
+
const entry = await new ContentRepository(db).findByIdOrSlug(collection, id);
|
|
1165
|
+
if (!entry) return {
|
|
1166
|
+
success: false,
|
|
1167
|
+
error: {
|
|
1168
|
+
code: "NOT_FOUND",
|
|
1169
|
+
message: `Content item not found: ${id}`
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
const revisionRepo = new RevisionRepository(db);
|
|
1173
|
+
const live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;
|
|
1174
|
+
const draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;
|
|
1175
|
+
return {
|
|
1176
|
+
success: true,
|
|
1177
|
+
data: {
|
|
1178
|
+
hasChanges: entry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,
|
|
1179
|
+
live: live?.data ?? null,
|
|
1180
|
+
draft: draft?.data ?? null
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
console.error("Content compare error:", error);
|
|
1185
|
+
return {
|
|
1186
|
+
success: false,
|
|
1187
|
+
error: {
|
|
1188
|
+
code: "CONTENT_COMPARE_ERROR",
|
|
1189
|
+
message: "Failed to compare revisions"
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Get all translations for a content item.
|
|
1196
|
+
* Returns the item's translation group members with locale and status info.
|
|
1197
|
+
*/
|
|
1198
|
+
async function handleContentTranslations(db, collection, id) {
|
|
1199
|
+
try {
|
|
1200
|
+
const repo = new ContentRepository(db);
|
|
1201
|
+
const item = await repo.findByIdOrSlug(collection, id);
|
|
1202
|
+
if (!item) return {
|
|
1203
|
+
success: false,
|
|
1204
|
+
error: {
|
|
1205
|
+
code: "NOT_FOUND",
|
|
1206
|
+
message: `Content item not found: ${id}`
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
if (!item.translationGroup) return {
|
|
1210
|
+
success: true,
|
|
1211
|
+
data: {
|
|
1212
|
+
translationGroup: item.id,
|
|
1213
|
+
translations: [{
|
|
1214
|
+
id: item.id,
|
|
1215
|
+
locale: item.locale,
|
|
1216
|
+
slug: item.slug,
|
|
1217
|
+
status: item.status,
|
|
1218
|
+
updatedAt: item.updatedAt
|
|
1219
|
+
}]
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
const translations = await repo.findTranslations(collection, item.translationGroup);
|
|
1223
|
+
return {
|
|
1224
|
+
success: true,
|
|
1225
|
+
data: {
|
|
1226
|
+
translationGroup: item.translationGroup,
|
|
1227
|
+
translations: translations.map((t) => ({
|
|
1228
|
+
id: t.id,
|
|
1229
|
+
locale: t.locale,
|
|
1230
|
+
slug: t.slug,
|
|
1231
|
+
status: t.status,
|
|
1232
|
+
updatedAt: t.updatedAt
|
|
1233
|
+
}))
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (error instanceof Error) console.error("Content translations error:", error);
|
|
1238
|
+
return {
|
|
1239
|
+
success: false,
|
|
1240
|
+
error: {
|
|
1241
|
+
code: "CONTENT_TRANSLATIONS_ERROR",
|
|
1242
|
+
message: "Failed to get translations"
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Sync non-translatable fields to sibling locales.
|
|
1249
|
+
*
|
|
1250
|
+
* When a content item is updated and it belongs to a translation group,
|
|
1251
|
+
* any non-translatable fields in the update data are written to all other
|
|
1252
|
+
* rows in the same translation group within the same transaction.
|
|
1253
|
+
*
|
|
1254
|
+
* Non-translatable fields are **copied, not linked** — each row owns its
|
|
1255
|
+
* own data. This keeps queries simple and avoids cross-row joins.
|
|
1256
|
+
*/
|
|
1257
|
+
function prepareNonTranslatableFields(db, collectionSlug, updatedItemId, translationGroup, data, nonTranslatableSlugs) {
|
|
1258
|
+
const syncData = {};
|
|
1259
|
+
for (const slug of nonTranslatableSlugs) if (slug in data) syncData[slug] = data[slug];
|
|
1260
|
+
if (Object.keys(syncData).length === 0) return null;
|
|
1261
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
1262
|
+
const tableName = `ec_${collectionSlug}`;
|
|
1263
|
+
const setClauses = Object.entries(syncData).map(([key, value]) => {
|
|
1264
|
+
validateIdentifier(key, "field slug");
|
|
1265
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : typeof value === "boolean" ? Number(value) : value;
|
|
1266
|
+
return sql`${sql.ref(key)} = ${serialized}`;
|
|
1267
|
+
});
|
|
1268
|
+
return sql`
|
|
1269
|
+
UPDATE ${sql.ref(tableName)}
|
|
1270
|
+
SET ${sql.join(setClauses, sql`, `)}
|
|
1271
|
+
WHERE translation_group = ${translationGroup}
|
|
1272
|
+
AND id != ${updatedItemId}
|
|
1273
|
+
`.compile(db);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/site-context/action-hash.ts
|
|
1278
|
+
async function buildStableActionHash(value) {
|
|
1279
|
+
return buildStableHash(value);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
//#endregion
|
|
1283
|
+
//#region src/site-context/review-payload.ts
|
|
1284
|
+
var ReviewPayloadBuilder = class {
|
|
1285
|
+
constructor(handlers) {
|
|
1286
|
+
this.handlers = handlers;
|
|
1287
|
+
}
|
|
1288
|
+
async buildContentReviewPayload(input) {
|
|
1289
|
+
const actionType = input.actionType ?? "content.publish";
|
|
1290
|
+
const content = await this.loadContent(input.collection, input.id);
|
|
1291
|
+
const compare = await this.loadCompare(input.collection, content.entryId);
|
|
1292
|
+
const live = asRecord(compare.live);
|
|
1293
|
+
const draft = asRecord(compare.draft) ?? content.data;
|
|
1294
|
+
const displayTitle = stringOrNull(draft.title) ?? content.displayTitle;
|
|
1295
|
+
const hasChanges = typeof compare.hasChanges === "boolean" ? compare.hasChanges : stableStringify(live) !== stableStringify(draft);
|
|
1296
|
+
const actionHash = await buildStableActionHash({
|
|
1297
|
+
actionType,
|
|
1298
|
+
collection: input.collection,
|
|
1299
|
+
entryId: content.entryId,
|
|
1300
|
+
liveRevisionId: content.liveRevisionId,
|
|
1301
|
+
draftRevisionId: content.draftRevisionId,
|
|
1302
|
+
reviewedRev: content.reviewedRev,
|
|
1303
|
+
live,
|
|
1304
|
+
draft
|
|
1305
|
+
});
|
|
1306
|
+
const scope = `content:${input.collection}:${content.entryId}`;
|
|
1307
|
+
const summary = input.summary ?? `Review ${actionType} for ${input.collection}/${displayTitle ?? content.entryId}`;
|
|
1308
|
+
return {
|
|
1309
|
+
collection: input.collection,
|
|
1310
|
+
entryId: content.entryId,
|
|
1311
|
+
scope,
|
|
1312
|
+
liveRevisionId: content.liveRevisionId,
|
|
1313
|
+
draftRevisionId: content.draftRevisionId,
|
|
1314
|
+
reviewedRev: content.reviewedRev,
|
|
1315
|
+
actionType,
|
|
1316
|
+
actionHash,
|
|
1317
|
+
reviewPayload: {
|
|
1318
|
+
kind: "content_review",
|
|
1319
|
+
actionType,
|
|
1320
|
+
summary,
|
|
1321
|
+
target: {
|
|
1322
|
+
collection: input.collection,
|
|
1323
|
+
entryId: content.entryId,
|
|
1324
|
+
scope,
|
|
1325
|
+
liveRevisionId: content.liveRevisionId,
|
|
1326
|
+
draftRevisionId: content.draftRevisionId,
|
|
1327
|
+
reviewedRev: content.reviewedRev,
|
|
1328
|
+
actionHash
|
|
1329
|
+
},
|
|
1330
|
+
content: {
|
|
1331
|
+
id: content.entryId,
|
|
1332
|
+
slug: content.slug,
|
|
1333
|
+
status: content.status,
|
|
1334
|
+
title: displayTitle,
|
|
1335
|
+
updatedAt: content.updatedAt
|
|
1336
|
+
},
|
|
1337
|
+
diff: {
|
|
1338
|
+
hasChanges,
|
|
1339
|
+
live,
|
|
1340
|
+
draft
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
async loadContent(collection, id) {
|
|
1346
|
+
const result = await this.handlers.handleContentGet(collection, id);
|
|
1347
|
+
if (!result.success) throw new Error(result.error?.message ?? `Content item not found: ${collection}/${id}`);
|
|
1348
|
+
const payload = asRecord(result.data);
|
|
1349
|
+
const item = asRecord(payload?.item);
|
|
1350
|
+
if (!payload || !item) throw new Error("Content get response did not include an item");
|
|
1351
|
+
const entryId = stringOrNull(item.id);
|
|
1352
|
+
const reviewedRev = stringOrNull(payload._rev);
|
|
1353
|
+
if (!entryId) throw new Error("Content item is missing id");
|
|
1354
|
+
if (!reviewedRev) throw new Error("Content item is missing _rev for review binding");
|
|
1355
|
+
const data = asRecord(item.data) ?? {};
|
|
1356
|
+
const title = stringOrNull(data.title) ?? stringOrNull(item.title) ?? stringOrNull(item.slug);
|
|
1357
|
+
return {
|
|
1358
|
+
entryId,
|
|
1359
|
+
reviewedRev,
|
|
1360
|
+
liveRevisionId: stringOrNull(item.liveRevisionId),
|
|
1361
|
+
draftRevisionId: stringOrNull(item.draftRevisionId),
|
|
1362
|
+
slug: stringOrNull(item.slug),
|
|
1363
|
+
status: stringOrNull(item.status),
|
|
1364
|
+
updatedAt: stringOrNull(item.updatedAt),
|
|
1365
|
+
displayTitle: title,
|
|
1366
|
+
data
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
async loadCompare(collection, id) {
|
|
1370
|
+
const result = await this.handlers.handleContentCompare(collection, id);
|
|
1371
|
+
if (!result.success) throw new Error(result.error?.message ?? `Failed to compare content: ${collection}/${id}`);
|
|
1372
|
+
return asRecord(result.data) ?? {};
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
function asRecord(value) {
|
|
1376
|
+
return isRecord(value) ? value : null;
|
|
1377
|
+
}
|
|
1378
|
+
function isRecord(value) {
|
|
1379
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1380
|
+
}
|
|
1381
|
+
function stringOrNull(value) {
|
|
1382
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
//#endregion
|
|
1386
|
+
export { validateRev as A, handleContentUpdate as C, validateMediaFields as D, validateContentDataFields as E, decodeRev as O, handleContentUnschedule as S, prepareNonTranslatableFields as T, handleContentPublish as _, handleContentCompare as a, handleContentTranslations as b, handleContentCreate as c, handleContentDuplicate as d, handleContentGet as f, handleContentPermanentDelete as g, handleContentListTrashed as h, handleContentAuthors as i, encodeRev as k, handleContentDelete as l, handleContentList as m, buildStableActionHash as n, handleContentCountScheduled as o, handleContentGetIncludingTrashed as p, contentWriteConfiguration as r, handleContentCountTrashed as s, ReviewPayloadBuilder as t, handleContentDiscardDraft as u, handleContentRestore as v, prepareContentPublication as w, handleContentUnpublish as x, handleContentSchedule as y };
|