emdash 0.7.0 → 0.9.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/{adapters-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
- package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
- package/dist/apply-BzltprvY.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +194 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +34 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +17 -12
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +9 -6
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +301 -165
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +34 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
- package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
- package/dist/byline-BSaNL1w7.mjs.map +1 -0
- package/dist/bylines-CvJ3PYz2.mjs +113 -0
- package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
- package/dist/cache-C6N_hhN7.mjs +65 -0
- package/dist/cache-C6N_hhN7.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
- package/dist/cli/index.mjs +229 -31
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
- package/dist/{content-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
- package/dist/content-8lOYF0pr.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
- package/dist/error-D_-tqP-I.mjs.map +1 -0
- package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
- package/dist/index-BFRaVcD6.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -25
- package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
- package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
- package/dist/loader-CKLbBnhK.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -3
- package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
- package/dist/media-BW32b4gi.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
- package/dist/mode-ier8jbBk.mjs.map +1 -0
- package/dist/options-BVp3UsTS.mjs +117 -0
- package/dist/options-BVp3UsTS.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
- package/dist/placeholder-CIJejMlK.mjs.map +1 -0
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-DByxYjUw.mjs +51 -0
- package/dist/public-url-DByxYjUw.mjs.map +1 -0
- package/dist/{query-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
- package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
- package/dist/redirect-BhUBKRc1.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
- package/dist/registry-Dw70ChxB.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
- package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
- package/dist/runner-Bnoj7vjK.d.mts +44 -0
- package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
- package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +4 -4
- package/dist/{search-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
- package/dist/search-dOGEccMa.mjs.map +1 -0
- package/dist/secrets-CW3reAnU.mjs +314 -0
- package/dist/secrets-CW3reAnU.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- package/dist/seo/index.d.mts +1 -1
- 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.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
- package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
- package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
- package/dist/types-4fVtCIm0.mjs +68 -0
- package/dist/types-4fVtCIm0.mjs.map +1 -0
- package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
- package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
- package/dist/types-BuBIptGk.d.mts.map +1 -0
- package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
- package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
- package/dist/types-CRxNbK-Z.mjs +68 -0
- package/dist/types-CRxNbK-Z.mjs.map +1 -0
- package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
- package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
- package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
- package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
- package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
- package/dist/validation-BfEI7tNe.mjs +144 -0
- package/dist/validation-BfEI7tNe.mjs.map +1 -0
- package/dist/version-DoxrVdYf.mjs +7 -0
- package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
- package/dist/zod-generator-CC0xNe_K.mjs +132 -0
- package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
- package/locals.d.ts +1 -6
- package/package.json +21 -7
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +19 -4
- package/src/api/handlers/content.ts +151 -4
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +131 -22
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +54 -5
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +30 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +49 -1
- package/src/astro/integration/virtual-modules.ts +73 -2
- package/src/astro/integration/vite-config.ts +49 -13
- package/src/astro/middleware/auth.ts +34 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +41 -10
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/index.ts +0 -1
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +32 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/passkey-config.ts +35 -13
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/types.ts +114 -4
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +31 -9
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +6 -4
- package/src/components/Image.astro +9 -4
- package/src/components/InlinePortableTextEditor.tsx +106 -19
- package/src/components/LiveSearch.astro +5 -14
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/runner.ts +156 -23
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +76 -52
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +26 -19
- package/src/database/repositories/taxonomy.ts +40 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +388 -247
- package/src/index.ts +14 -1
- package/src/loader.ts +30 -6
- package/src/mcp/server.ts +781 -141
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +34 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +49 -2
- package/src/plugins/types.ts +174 -13
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +149 -6
- package/src/redirects/cache.ts +38 -18
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +97 -5
- package/src/schema/zod-generator.ts +27 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +2 -0
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/storage/s3.ts +12 -6
- package/src/taxonomies/index.ts +11 -1
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/bylines-hPTW79hw.mjs +0 -157
- package/dist/bylines-hPTW79hw.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-D7J5y73J.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
- package/dist/query-g4Ug-9j9.mjs.map +0 -1
- package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-BR2xKwhn.d.mts +0 -34
- package/dist/runner-BR2xKwhn.d.mts.map +0 -1
- package/dist/search-B0effn3j.mjs.map +0 -1
- package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-BnTKdfam.mjs +0 -7
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Kysely } from "kysely";
|
|
2
2
|
import { sql } from "kysely";
|
|
3
3
|
|
|
4
|
+
import { columnExists } from "../dialect-helpers.js";
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Migration: Bounded 404 logging
|
|
6
8
|
*
|
|
@@ -19,16 +21,22 @@ import { sql } from "kysely";
|
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
24
|
+
const hitsExists = await columnExists(db, "_emdash_404_log", "hits");
|
|
25
|
+
|
|
22
26
|
// 1. Add columns.
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (!hitsExists) {
|
|
28
|
+
await db.schema
|
|
29
|
+
.alterTable("_emdash_404_log")
|
|
30
|
+
.addColumn("hits", "integer", (col) => col.notNull().defaultTo(1))
|
|
31
|
+
.execute();
|
|
32
|
+
}
|
|
27
33
|
|
|
28
34
|
// SQLite won't accept a non-constant default when adding a NOT NULL column
|
|
29
35
|
// to a table with existing rows, so backfill in two steps: add nullable,
|
|
30
36
|
// populate, then rely on the application layer / future inserts to set it.
|
|
31
|
-
await db
|
|
37
|
+
if (!(await columnExists(db, "_emdash_404_log", "last_seen_at"))) {
|
|
38
|
+
await db.schema.alterTable("_emdash_404_log").addColumn("last_seen_at", "text").execute();
|
|
39
|
+
}
|
|
32
40
|
|
|
33
41
|
// Backfill last_seen_at from created_at for existing rows.
|
|
34
42
|
await sql`
|
|
@@ -44,68 +52,77 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
|
|
44
52
|
// (3.25+, 2018) and Postgres. The previous GROUP BY approach was
|
|
45
53
|
// accepted by SQLite but invalid on Postgres because `id` wasn't in
|
|
46
54
|
// the GROUP BY or wrapped in an aggregate.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
id,
|
|
51
|
-
path,
|
|
52
|
-
ROW_NUMBER() OVER (
|
|
53
|
-
PARTITION BY path
|
|
54
|
-
ORDER BY created_at DESC, id DESC
|
|
55
|
-
) AS rn,
|
|
56
|
-
COUNT(*) OVER (PARTITION BY path) AS path_count,
|
|
57
|
-
MAX(created_at) OVER (PARTITION BY path) AS latest_created_at
|
|
58
|
-
FROM _emdash_404_log
|
|
59
|
-
)
|
|
60
|
-
UPDATE _emdash_404_log
|
|
61
|
-
SET
|
|
62
|
-
hits = (SELECT path_count FROM ranked WHERE ranked.id = _emdash_404_log.id),
|
|
63
|
-
last_seen_at = (SELECT latest_created_at FROM ranked WHERE ranked.id = _emdash_404_log.id)
|
|
64
|
-
WHERE id IN (SELECT id FROM ranked WHERE rn = 1)
|
|
65
|
-
`.execute(db);
|
|
66
|
-
|
|
67
|
-
// Delete the non-keepers (every row except the freshest per path).
|
|
68
|
-
await sql`
|
|
69
|
-
DELETE FROM _emdash_404_log
|
|
70
|
-
WHERE id IN (
|
|
71
|
-
SELECT id FROM (
|
|
55
|
+
if (!hitsExists) {
|
|
56
|
+
await sql`
|
|
57
|
+
WITH ranked AS (
|
|
72
58
|
SELECT
|
|
73
59
|
id,
|
|
60
|
+
path,
|
|
74
61
|
ROW_NUMBER() OVER (
|
|
75
62
|
PARTITION BY path
|
|
76
63
|
ORDER BY created_at DESC, id DESC
|
|
77
|
-
) AS rn
|
|
64
|
+
) AS rn,
|
|
65
|
+
COUNT(*) OVER (PARTITION BY path) AS path_count,
|
|
66
|
+
MAX(created_at) OVER (PARTITION BY path) AS latest_created_at
|
|
78
67
|
FROM _emdash_404_log
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
)
|
|
69
|
+
UPDATE _emdash_404_log
|
|
70
|
+
SET
|
|
71
|
+
hits = (SELECT path_count FROM ranked WHERE ranked.id = _emdash_404_log.id),
|
|
72
|
+
last_seen_at = (SELECT latest_created_at FROM ranked WHERE ranked.id = _emdash_404_log.id)
|
|
73
|
+
WHERE id IN (SELECT id FROM ranked WHERE rn = 1)
|
|
74
|
+
`.execute(db);
|
|
75
|
+
|
|
76
|
+
// Delete the non-keepers (every row except the freshest per path).
|
|
77
|
+
await sql`
|
|
78
|
+
DELETE FROM _emdash_404_log
|
|
79
|
+
WHERE id IN (
|
|
80
|
+
SELECT id FROM (
|
|
81
|
+
SELECT
|
|
82
|
+
id,
|
|
83
|
+
ROW_NUMBER() OVER (
|
|
84
|
+
PARTITION BY path
|
|
85
|
+
ORDER BY created_at DESC, id DESC
|
|
86
|
+
) AS rn
|
|
87
|
+
FROM _emdash_404_log
|
|
88
|
+
) AS ranked
|
|
89
|
+
WHERE rn > 1
|
|
90
|
+
)
|
|
91
|
+
`.execute(db);
|
|
92
|
+
}
|
|
83
93
|
|
|
84
94
|
// 3. Add unique index on path for upsert semantics.
|
|
85
95
|
await db.schema
|
|
86
96
|
.createIndex("idx_404_log_path_unique")
|
|
97
|
+
.ifNotExists()
|
|
87
98
|
.on("_emdash_404_log")
|
|
88
99
|
.column("path")
|
|
89
100
|
.unique()
|
|
90
101
|
.execute();
|
|
91
102
|
|
|
92
103
|
// Drop the old non-unique index; the unique one covers the same lookups.
|
|
93
|
-
await db.schema.dropIndex("idx_404_log_path").execute();
|
|
104
|
+
await db.schema.dropIndex("idx_404_log_path").ifExists().execute();
|
|
94
105
|
|
|
95
106
|
// 4. Index on last_seen_at for eviction ordering.
|
|
96
107
|
await db.schema
|
|
97
108
|
.createIndex("idx_404_log_last_seen")
|
|
109
|
+
.ifNotExists()
|
|
98
110
|
.on("_emdash_404_log")
|
|
99
111
|
.column("last_seen_at")
|
|
100
112
|
.execute();
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
104
|
-
await db.schema.dropIndex("idx_404_log_last_seen").execute();
|
|
105
|
-
await db.schema.dropIndex("idx_404_log_path_unique").execute();
|
|
116
|
+
await db.schema.dropIndex("idx_404_log_last_seen").ifExists().execute();
|
|
117
|
+
await db.schema.dropIndex("idx_404_log_path_unique").ifExists().execute();
|
|
106
118
|
|
|
107
119
|
// Restore the original non-unique path index.
|
|
108
|
-
await db.schema
|
|
120
|
+
await db.schema
|
|
121
|
+
.createIndex("idx_404_log_path")
|
|
122
|
+
.ifNotExists()
|
|
123
|
+
.on("_emdash_404_log")
|
|
124
|
+
.column("path")
|
|
125
|
+
.execute();
|
|
109
126
|
|
|
110
127
|
await db.schema.alterTable("_emdash_404_log").dropColumn("last_seen_at").execute();
|
|
111
128
|
await db.schema.alterTable("_emdash_404_log").dropColumn("hits").execute();
|
|
@@ -123,29 +123,156 @@ export async function getMigrationStatus(db: Kysely<Database>): Promise<Migratio
|
|
|
123
123
|
return { applied, pending };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/** Pattern for escaping special regex characters. Matches the shared helper in `database/repositories/content.ts`. */
|
|
127
|
+
const REGEX_ESCAPE_PATTERN = /[.*+?^${}()|[\]\\]/g;
|
|
128
|
+
|
|
129
|
+
/** Escape special regex characters so a string can be embedded literally in `new RegExp()`. */
|
|
130
|
+
function escapeRegExp(value: string): string {
|
|
131
|
+
return value.replace(REGEX_ESCAPE_PATTERN, "\\$&");
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
/**
|
|
127
|
-
*
|
|
135
|
+
* Pattern used to detect the concurrent-migration race. The Kysely
|
|
136
|
+
* `SqliteAdapter.acquireMigrationLock` is a no-op (inherited by `kysely-d1`
|
|
137
|
+
* and our `EmDashD1Dialect`), so two isolates running migrations against the
|
|
138
|
+
* same database can both attempt `INSERT INTO _emdash_migrations` for the
|
|
139
|
+
* same migration name. The losing insert fails with a UNIQUE constraint
|
|
140
|
+
* error, which is benign: the other isolate is applying the same schema.
|
|
128
141
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* `
|
|
133
|
-
*
|
|
134
|
-
*
|
|
142
|
+
* We match on the table name (not the full error text) because different
|
|
143
|
+
* SQLite drivers phrase the message differently
|
|
144
|
+
* (`UNIQUE constraint failed: _emdash_migrations.name` for better-sqlite3,
|
|
145
|
+
* `D1_ERROR: UNIQUE constraint failed: _emdash_migrations.name: SQLITE_CONSTRAINT`
|
|
146
|
+
* for D1, etc.). The pattern is built from `MIGRATION_TABLE` so a rename
|
|
147
|
+
* cannot silently disable race detection.
|
|
135
148
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
const MIGRATION_RACE_PATTERN = new RegExp(
|
|
150
|
+
`UNIQUE constraint failed: ${escapeRegExp(MIGRATION_TABLE)}\\.name`,
|
|
151
|
+
"i",
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/** How long to wait for a concurrent migrator to finish before giving up. */
|
|
155
|
+
const MIGRATION_RACE_WAIT_MS = 10_000;
|
|
156
|
+
/** Polling interval while waiting for a concurrent migrator. */
|
|
157
|
+
const MIGRATION_RACE_POLL_MS = 100;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Pattern used to detect "table does not exist" errors across the dialects
|
|
161
|
+
* EmDash supports. The phrasing differs by driver:
|
|
162
|
+
*
|
|
163
|
+
* - better-sqlite3: `no such table: _emdash_migrations`
|
|
164
|
+
* - D1: `D1_ERROR: no such table: _emdash_migrations: SQLITE_ERROR`
|
|
165
|
+
* - PostgreSQL: `relation "_emdash_migrations" does not exist`
|
|
166
|
+
* (also occasionally `table "_emdash_migrations" does not exist`)
|
|
167
|
+
*
|
|
168
|
+
* We deliberately match on the migration table name (rather than using the
|
|
169
|
+
* generic `isMissingTableError` helper) so an unexpected missing-table error
|
|
170
|
+
* naming a different table — implausible today since
|
|
171
|
+
* `getAppliedMigrationCount` only references `MIGRATION_TABLE`, but cheap
|
|
172
|
+
* insurance against future edits — is not silently swallowed. The pattern is
|
|
173
|
+
* built from `MIGRATION_TABLE` so a rename cannot drift.
|
|
174
|
+
*/
|
|
175
|
+
const MIGRATION_TABLE_MISSING_PATTERN = new RegExp(
|
|
176
|
+
`(?:no such table:\\s*${escapeRegExp(MIGRATION_TABLE)}\\b` +
|
|
177
|
+
`|(?:relation|table)\\s+"?${escapeRegExp(MIGRATION_TABLE)}"?\\s+does(?:n't| not) exist\\b)`,
|
|
178
|
+
"i",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Read the count of applied migrations.
|
|
183
|
+
*
|
|
184
|
+
* Returns `null` only when the migration table does not exist yet (which is
|
|
185
|
+
* the normal state on a fresh database before the first migration runs).
|
|
186
|
+
* Any other error is rethrown so callers — particularly
|
|
187
|
+
* `waitForConcurrentMigrator` — don't silently mask connection failures,
|
|
188
|
+
* permission errors, or other unexpected driver problems behind a 10s wait
|
|
189
|
+
* and a bogus "we're done" verdict.
|
|
190
|
+
*/
|
|
191
|
+
async function getAppliedMigrationCount(db: Kysely<Database>): Promise<number | null> {
|
|
139
192
|
try {
|
|
140
193
|
const result = await sql<{ count: number }>`
|
|
141
194
|
SELECT COUNT(*) as count FROM ${sql.ref(MIGRATION_TABLE)}
|
|
142
195
|
`.execute(db);
|
|
143
|
-
|
|
144
|
-
|
|
196
|
+
return Number(result.rows[0]?.count ?? 0);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (MIGRATION_TABLE_MISSING_PATTERN.test(deepErrorMessage(error))) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Wait for a concurrent migrator to finish applying all migrations.
|
|
207
|
+
*
|
|
208
|
+
* Resolves to `true` once the migration table contains at least
|
|
209
|
+
* `MIGRATION_COUNT` rows (i.e. every migration this build knows about has
|
|
210
|
+
* been recorded), `false` if the deadline elapses first. We use `>=` rather
|
|
211
|
+
* than `===` so that an old isolate observing a database that has already
|
|
212
|
+
* been migrated by a newer build still treats the wait as settled instead
|
|
213
|
+
* of timing out.
|
|
214
|
+
*/
|
|
215
|
+
async function waitForConcurrentMigrator(db: Kysely<Database>): Promise<boolean> {
|
|
216
|
+
const deadline = Date.now() + MIGRATION_RACE_WAIT_MS;
|
|
217
|
+
while (Date.now() < deadline) {
|
|
218
|
+
const count = await getAppliedMigrationCount(db);
|
|
219
|
+
if (count !== null && count >= MIGRATION_COUNT) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, MIGRATION_RACE_POLL_MS));
|
|
223
|
+
}
|
|
224
|
+
const finalCount = await getAppliedMigrationCount(db);
|
|
225
|
+
return finalCount !== null && finalCount >= MIGRATION_COUNT;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Extract the deepest error message available from a thrown value. */
|
|
229
|
+
function deepErrorMessage(error: unknown): string {
|
|
230
|
+
if (error instanceof Error) {
|
|
231
|
+
const own = error.message ?? "";
|
|
232
|
+
if (error.cause) {
|
|
233
|
+
const causeMsg = deepErrorMessage(error.cause);
|
|
234
|
+
return own ? `${own}: ${causeMsg}` : causeMsg;
|
|
145
235
|
}
|
|
236
|
+
return own;
|
|
237
|
+
}
|
|
238
|
+
if (typeof error === "string") return error;
|
|
239
|
+
try {
|
|
240
|
+
return JSON.stringify(error);
|
|
146
241
|
} catch {
|
|
147
|
-
|
|
148
|
-
|
|
242
|
+
return String(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Run all pending migrations.
|
|
248
|
+
*
|
|
249
|
+
* Includes a fast-path: if the migration table already exists and contains
|
|
250
|
+
* at least MIGRATION_COUNT rows, all migrations this build knows about have
|
|
251
|
+
* been applied and we can skip the Kysely Migrator entirely. This avoids
|
|
252
|
+
* the expensive `pragma_table_info` introspection that Kysely runs for
|
|
253
|
+
* every table in the database (twice!) just to check if the migration
|
|
254
|
+
* tables exist. On D1 with ~57 tables, that's ~116 queries saved per init.
|
|
255
|
+
*
|
|
256
|
+
* Concurrent-migration safety: the Kysely Migrator's `acquireMigrationLock`
|
|
257
|
+
* is a no-op for SQLite (and therefore D1), so two callers running this
|
|
258
|
+
* concurrently against the same database will both try to apply pending
|
|
259
|
+
* migrations. SQLite serializes the writes, but the loser still surfaces a
|
|
260
|
+
* `UNIQUE constraint failed: _emdash_migrations.name` error. We treat that
|
|
261
|
+
* specific error as benign: another caller is already applying the same
|
|
262
|
+
* schema. We wait for the concurrent migrator to finish, then return
|
|
263
|
+
* success. This matches the user-observable expectation that running
|
|
264
|
+
* migrations twice in a row is a no-op.
|
|
265
|
+
*/
|
|
266
|
+
export async function runMigrations(db: Kysely<Database>): Promise<{ applied: string[] }> {
|
|
267
|
+
// Fast path: check if all migrations are already applied.
|
|
268
|
+
// A single cheap query vs the Migrator's full schema introspection.
|
|
269
|
+
// We use `>=` rather than `===` so a database with extra rows from a
|
|
270
|
+
// newer build (e.g. mid-deploy old isolate, or downgrade) still skips
|
|
271
|
+
// the migrator instead of falling through to the race-recovery path
|
|
272
|
+
// unnecessarily.
|
|
273
|
+
const initialCount = await getAppliedMigrationCount(db);
|
|
274
|
+
if (initialCount !== null && initialCount >= MIGRATION_COUNT) {
|
|
275
|
+
return { applied: [] };
|
|
149
276
|
}
|
|
150
277
|
|
|
151
278
|
const migrator = new Migrator({
|
|
@@ -160,17 +287,23 @@ export async function runMigrations(db: Kysely<Database>): Promise<{ applied: st
|
|
|
160
287
|
const applied = results?.filter((r) => r.status === "Success").map((r) => r.migrationName) ?? [];
|
|
161
288
|
|
|
162
289
|
if (error) {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
if (!msg && error instanceof Error && error.cause) {
|
|
167
|
-
msg = error.cause instanceof Error ? error.cause.message : JSON.stringify(error.cause);
|
|
168
|
-
}
|
|
290
|
+
// Walk error.cause to get the underlying driver message — Kysely
|
|
291
|
+
// often wraps with an empty top-level message.
|
|
292
|
+
const msg = deepErrorMessage(error);
|
|
169
293
|
const failedMigration = results?.find((r) => r.status === "Error");
|
|
170
|
-
|
|
171
|
-
|
|
294
|
+
|
|
295
|
+
// Concurrent-migration race: another caller is applying (or just
|
|
296
|
+
// applied) the same migration. Wait for it to finish, then verify
|
|
297
|
+
// the schema is fully migrated and treat as success.
|
|
298
|
+
if (MIGRATION_RACE_PATTERN.test(msg)) {
|
|
299
|
+
const settled = await waitForConcurrentMigrator(db);
|
|
300
|
+
if (settled) {
|
|
301
|
+
return { applied };
|
|
302
|
+
}
|
|
172
303
|
}
|
|
173
|
-
|
|
304
|
+
|
|
305
|
+
const failedSuffix = failedMigration ? ` (migration: ${failedMigration.migrationName})` : "";
|
|
306
|
+
throw new Error(`Migration failed: ${msg || "unknown error"}${failedSuffix}`);
|
|
174
307
|
}
|
|
175
308
|
|
|
176
309
|
return { applied };
|
|
@@ -143,14 +143,12 @@ export class AuditRepository {
|
|
|
143
143
|
|
|
144
144
|
if (query.cursor) {
|
|
145
145
|
const decoded = decodeCursor(query.cursor);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
eb.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
}
|
|
146
|
+
q = q.where((eb) =>
|
|
147
|
+
eb.or([
|
|
148
|
+
eb("timestamp", "<", decoded.orderValue),
|
|
149
|
+
eb.and([eb("timestamp", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
const rows = await q.execute();
|
|
@@ -123,14 +123,12 @@ export class BylineRepository {
|
|
|
123
123
|
|
|
124
124
|
if (options?.cursor) {
|
|
125
125
|
const decoded = decodeCursor(options.cursor);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
eb.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
133
|
-
}
|
|
126
|
+
query = query.where((eb) =>
|
|
127
|
+
eb.or([
|
|
128
|
+
eb("created_at", "<", decoded.orderValue),
|
|
129
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
130
|
+
]),
|
|
131
|
+
);
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
const rows = await query.execute();
|
|
@@ -143,14 +143,12 @@ export class CommentRepository {
|
|
|
143
143
|
// Cursor pagination (ascending by created_at)
|
|
144
144
|
if (options.cursor) {
|
|
145
145
|
const decoded = decodeCursor(options.cursor);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
eb.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
}
|
|
146
|
+
query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
|
|
147
|
+
eb.or([
|
|
148
|
+
eb("created_at", ">", decoded.orderValue),
|
|
149
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", ">", decoded.id)]),
|
|
150
|
+
]),
|
|
151
|
+
);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
query = query
|
|
@@ -202,14 +200,12 @@ export class CommentRepository {
|
|
|
202
200
|
// Cursor pagination (descending by created_at)
|
|
203
201
|
if (options.cursor) {
|
|
204
202
|
const decoded = decodeCursor(options.cursor);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
eb.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
}
|
|
203
|
+
query = query.where((eb: ExpressionBuilder<Database, "_emdash_comments">) =>
|
|
204
|
+
eb.or([
|
|
205
|
+
eb("created_at", "<", decoded.orderValue),
|
|
206
|
+
eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)]),
|
|
207
|
+
]),
|
|
208
|
+
);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
211
|
query = query
|
|
@@ -489,27 +489,26 @@ export class ContentRepository {
|
|
|
489
489
|
query = query.where("locale" as any, "=", options.where.locale);
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
// Handle cursor pagination
|
|
492
|
+
// Handle cursor pagination — decodeCursor throws InvalidCursorError
|
|
493
|
+
// on malformed input; let it propagate so handlers surface a
|
|
494
|
+
// structured INVALID_CURSOR rather than silently returning page 1.
|
|
493
495
|
if (options.cursor) {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
eb.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
eb.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
]),
|
|
511
|
-
);
|
|
512
|
-
}
|
|
496
|
+
const { orderValue, id: cursorId } = decodeCursor(options.cursor);
|
|
497
|
+
|
|
498
|
+
if (safeOrderDirection === "DESC") {
|
|
499
|
+
query = query.where((eb) =>
|
|
500
|
+
eb.or([
|
|
501
|
+
eb(dbField as any, "<", orderValue),
|
|
502
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
|
|
503
|
+
]),
|
|
504
|
+
);
|
|
505
|
+
} else {
|
|
506
|
+
query = query.where((eb) =>
|
|
507
|
+
eb.or([
|
|
508
|
+
eb(dbField as any, ">", orderValue),
|
|
509
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
|
|
510
|
+
]),
|
|
511
|
+
);
|
|
513
512
|
}
|
|
514
513
|
}
|
|
515
514
|
|
|
@@ -671,27 +670,24 @@ export class ContentRepository {
|
|
|
671
670
|
.selectAll()
|
|
672
671
|
.where("deleted_at" as never, "is not", null);
|
|
673
672
|
|
|
674
|
-
// Handle cursor pagination
|
|
673
|
+
// Handle cursor pagination — decodeCursor throws on invalid input.
|
|
675
674
|
if (options.cursor) {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
eb.
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
eb.
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
]),
|
|
693
|
-
);
|
|
694
|
-
}
|
|
675
|
+
const { orderValue, id: cursorId } = decodeCursor(options.cursor);
|
|
676
|
+
|
|
677
|
+
if (safeOrderDirection === "DESC") {
|
|
678
|
+
query = query.where((eb) =>
|
|
679
|
+
eb.or([
|
|
680
|
+
eb(dbField as any, "<", orderValue),
|
|
681
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
|
|
682
|
+
]),
|
|
683
|
+
);
|
|
684
|
+
} else {
|
|
685
|
+
query = query.where((eb) =>
|
|
686
|
+
eb.or([
|
|
687
|
+
eb(dbField as any, ">", orderValue),
|
|
688
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
|
|
689
|
+
]),
|
|
690
|
+
);
|
|
695
691
|
}
|
|
696
692
|
}
|
|
697
693
|
|
|
@@ -921,8 +917,14 @@ export class ContentRepository {
|
|
|
921
917
|
* Syncs the draft revision's data into the content table columns so the
|
|
922
918
|
* content table always reflects the published version.
|
|
923
919
|
* If no draft revision exists, creates one from current data and publishes it.
|
|
920
|
+
*
|
|
921
|
+
* `publishedAt` (optional) overrides the publication timestamp. If omitted,
|
|
922
|
+
* the existing `published_at` is preserved (idempotent re-publish keeps the
|
|
923
|
+
* original date) and falls back to the current time on first publish. Pass
|
|
924
|
+
* an explicit value to backdate a publish (e.g. when migrating content from
|
|
925
|
+
* another CMS).
|
|
924
926
|
*/
|
|
925
|
-
async publish(type: string, id: string): Promise<ContentItem> {
|
|
927
|
+
async publish(type: string, id: string, publishedAt?: string): Promise<ContentItem> {
|
|
926
928
|
const tableName = getTableName(type);
|
|
927
929
|
const now = new Date().toISOString();
|
|
928
930
|
|
|
@@ -960,17 +962,35 @@ export class ContentRepository {
|
|
|
960
962
|
}
|
|
961
963
|
}
|
|
962
964
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
965
|
+
if (publishedAt !== undefined) {
|
|
966
|
+
// Caller supplied an explicit timestamp, so we overwrite published_at
|
|
967
|
+
// directly (used to backdate a publish, e.g. for content migrations).
|
|
968
|
+
await sql`
|
|
969
|
+
UPDATE ${sql.ref(tableName)}
|
|
970
|
+
SET live_revision_id = ${revisionToPublish},
|
|
971
|
+
draft_revision_id = NULL,
|
|
972
|
+
status = 'published',
|
|
973
|
+
scheduled_at = NULL,
|
|
974
|
+
published_at = ${publishedAt},
|
|
975
|
+
updated_at = ${now}
|
|
976
|
+
WHERE id = ${id}
|
|
977
|
+
AND deleted_at IS NULL
|
|
978
|
+
`.execute(this.db);
|
|
979
|
+
} else {
|
|
980
|
+
// No timestamp supplied — preserve existing published_at on
|
|
981
|
+
// idempotent re-publish, fall back to `now` on first publish.
|
|
982
|
+
await sql`
|
|
983
|
+
UPDATE ${sql.ref(tableName)}
|
|
984
|
+
SET live_revision_id = ${revisionToPublish},
|
|
985
|
+
draft_revision_id = NULL,
|
|
986
|
+
status = 'published',
|
|
987
|
+
scheduled_at = NULL,
|
|
988
|
+
published_at = COALESCE(published_at, ${now}),
|
|
989
|
+
updated_at = ${now}
|
|
990
|
+
WHERE id = ${id}
|
|
991
|
+
AND deleted_at IS NULL
|
|
992
|
+
`.execute(this.db);
|
|
993
|
+
}
|
|
974
994
|
|
|
975
995
|
const updated = await this.findById(type, id);
|
|
976
996
|
if (!updated) {
|
|
@@ -1018,6 +1038,7 @@ export class ContentRepository {
|
|
|
1018
1038
|
UPDATE ${sql.ref(tableName)}
|
|
1019
1039
|
SET live_revision_id = NULL,
|
|
1020
1040
|
status = 'draft',
|
|
1041
|
+
published_at = NULL,
|
|
1021
1042
|
updated_at = ${now}
|
|
1022
1043
|
WHERE id = ${id}
|
|
1023
1044
|
AND deleted_at IS NULL
|
|
@@ -1198,7 +1219,10 @@ export class ContentRepository {
|
|
|
1198
1219
|
scheduledAt: "scheduled_at",
|
|
1199
1220
|
deletedAt: "deleted_at",
|
|
1200
1221
|
title: "title",
|
|
1222
|
+
name: "name",
|
|
1201
1223
|
slug: "slug",
|
|
1224
|
+
status: "status",
|
|
1225
|
+
locale: "locale",
|
|
1202
1226
|
};
|
|
1203
1227
|
|
|
1204
1228
|
const mapped = mapping[field];
|
|
@@ -27,4 +27,4 @@ export { RedirectRepository } from "./redirect.js";
|
|
|
27
27
|
export { BylineRepository } from "./byline.js";
|
|
28
28
|
export type { CreateBylineInput, UpdateBylineInput, ContentBylineInput } from "./byline.js";
|
|
29
29
|
export type * from "./types.js";
|
|
30
|
-
export { EmDashValidationError, encodeCursor, decodeCursor } from "./types.js";
|
|
30
|
+
export { EmDashValidationError, InvalidCursorError, encodeCursor, decodeCursor } from "./types.js";
|
|
@@ -202,20 +202,17 @@ export class MediaRepository {
|
|
|
202
202
|
.orderBy("id", "desc")
|
|
203
203
|
.limit(limit + 1);
|
|
204
204
|
|
|
205
|
-
// Handle cursor-based pagination
|
|
205
|
+
// Handle cursor-based pagination — throws on invalid cursor.
|
|
206
206
|
if (options.cursor) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
eb.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
]),
|
|
217
|
-
);
|
|
218
|
-
}
|
|
207
|
+
const { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);
|
|
208
|
+
|
|
209
|
+
// Keyset pagination: get items where (created_at, id) < cursor
|
|
210
|
+
query = query.where((eb) =>
|
|
211
|
+
eb.or([
|
|
212
|
+
eb("created_at", "<", createdAt),
|
|
213
|
+
eb.and([eb("created_at", "=", createdAt), eb("id", "<", cursorId)]),
|
|
214
|
+
]),
|
|
215
|
+
);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
if (options.mimeType) {
|