emdash 0.8.0 → 0.10.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-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.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 +118 -4
- 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 +14 -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 +15 -10
- 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 +8 -5
- 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 +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- 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-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.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-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.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-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.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/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- 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-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- 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.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- 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]/permanent.ts +1 -1
- 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].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- 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/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- 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/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- 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 +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- 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/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- 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/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- 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/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.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-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.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();
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
import { getI18nConfig } from "../../i18n/config.js";
|
|
5
|
+
import { currentTimestamp, isSqlite } from "../dialect-helpers.js";
|
|
6
|
+
import { validateIdentifier } from "../validate.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* i18n for menus + taxonomies. Adds `locale` + `translation_group` to system
|
|
10
|
+
* tables and stores translation_groups (not row ids) in
|
|
11
|
+
* `_emdash_menu_items.reference_id` and `content_taxonomies.taxonomy_id`.
|
|
12
|
+
* Backfill locale and column DEFAULTs use the site's configured defaultLocale.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function getDefaultLocale(): string {
|
|
16
|
+
return getI18nConfig()?.defaultLocale ?? "en";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
20
|
+
const defaultLocale = getDefaultLocale();
|
|
21
|
+
|
|
22
|
+
if (isSqlite(db)) {
|
|
23
|
+
// FKs off: rebuilding `taxonomies` would CASCADE-wipe `content_taxonomies`.
|
|
24
|
+
await sql.raw(`PRAGMA foreign_keys = OFF`).execute(db);
|
|
25
|
+
try {
|
|
26
|
+
await rebuildMenus(db, defaultLocale);
|
|
27
|
+
await addItemColumns(db, defaultLocale);
|
|
28
|
+
await rebuildTaxonomies(db, defaultLocale);
|
|
29
|
+
await rebuildTaxonomyDefs(db, defaultLocale);
|
|
30
|
+
await rebuildContentTaxonomies(db);
|
|
31
|
+
await remapMenuItemRefs(db);
|
|
32
|
+
} finally {
|
|
33
|
+
await sql.raw(`PRAGMA foreign_keys = ON`).execute(db);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await pgWiden(db, "_emdash_menus", ["name"], ["name", "locale"], defaultLocale);
|
|
39
|
+
await pgWiden(db, "_emdash_menu_items", null, null, defaultLocale);
|
|
40
|
+
await pgWiden(db, "taxonomies", ["name", "slug"], ["name", "slug", "locale"], defaultLocale);
|
|
41
|
+
await pgWiden(db, "_emdash_taxonomy_defs", ["name"], ["name", "locale"], defaultLocale);
|
|
42
|
+
await pgRemapContentTaxonomies(db);
|
|
43
|
+
await remapMenuItemRefs(db);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function rebuildMenus(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
|
|
47
|
+
if (await hasColumn(db, "_emdash_menus", "locale")) return;
|
|
48
|
+
await sql.raw(`DROP TABLE IF EXISTS "_emdash_menus_new"`).execute(db);
|
|
49
|
+
|
|
50
|
+
await db.schema
|
|
51
|
+
.createTable("_emdash_menus_new")
|
|
52
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
53
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
54
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
55
|
+
.addColumn("created_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
56
|
+
.addColumn("updated_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
57
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo(defaultLocale))
|
|
58
|
+
.addColumn("translation_group", "text")
|
|
59
|
+
.addUniqueConstraint("_emdash_menus_name_locale_unique", ["name", "locale"])
|
|
60
|
+
.execute();
|
|
61
|
+
|
|
62
|
+
await sql`
|
|
63
|
+
INSERT INTO _emdash_menus_new (id, name, label, created_at, updated_at, locale, translation_group)
|
|
64
|
+
SELECT id, name, label, created_at, updated_at, ${defaultLocale}, id FROM _emdash_menus
|
|
65
|
+
`.execute(db);
|
|
66
|
+
|
|
67
|
+
await db.schema.dropTable("_emdash_menus").execute();
|
|
68
|
+
await sql`ALTER TABLE _emdash_menus_new RENAME TO _emdash_menus`.execute(db);
|
|
69
|
+
|
|
70
|
+
await db.schema
|
|
71
|
+
.createIndex("idx__emdash_menus_locale")
|
|
72
|
+
.on("_emdash_menus")
|
|
73
|
+
.column("locale")
|
|
74
|
+
.execute();
|
|
75
|
+
await db.schema
|
|
76
|
+
.createIndex("idx__emdash_menus_translation_group")
|
|
77
|
+
.on("_emdash_menus")
|
|
78
|
+
.column("translation_group")
|
|
79
|
+
.execute();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function addItemColumns(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
|
|
83
|
+
if (await hasColumn(db, "_emdash_menu_items", "locale")) return;
|
|
84
|
+
|
|
85
|
+
await db.schema
|
|
86
|
+
.alterTable("_emdash_menu_items")
|
|
87
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo(defaultLocale))
|
|
88
|
+
.execute();
|
|
89
|
+
await db.schema.alterTable("_emdash_menu_items").addColumn("translation_group", "text").execute();
|
|
90
|
+
|
|
91
|
+
await sql`UPDATE _emdash_menu_items SET translation_group = id`.execute(db);
|
|
92
|
+
|
|
93
|
+
await db.schema
|
|
94
|
+
.createIndex("idx__emdash_menu_items_locale")
|
|
95
|
+
.on("_emdash_menu_items")
|
|
96
|
+
.column("locale")
|
|
97
|
+
.execute();
|
|
98
|
+
await db.schema
|
|
99
|
+
.createIndex("idx__emdash_menu_items_translation_group")
|
|
100
|
+
.on("_emdash_menu_items")
|
|
101
|
+
.column("translation_group")
|
|
102
|
+
.execute();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function rebuildTaxonomies(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
|
|
106
|
+
if (await hasColumn(db, "taxonomies", "locale")) return;
|
|
107
|
+
await sql.raw(`DROP TABLE IF EXISTS "taxonomies_new"`).execute(db);
|
|
108
|
+
await sql`DROP INDEX IF EXISTS idx_taxonomies_name`.execute(db);
|
|
109
|
+
|
|
110
|
+
await db.schema
|
|
111
|
+
.createTable("taxonomies_new")
|
|
112
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
113
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
114
|
+
.addColumn("slug", "text", (c) => c.notNull())
|
|
115
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
116
|
+
.addColumn("parent_id", "text")
|
|
117
|
+
.addColumn("data", "text")
|
|
118
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo(defaultLocale))
|
|
119
|
+
.addColumn("translation_group", "text")
|
|
120
|
+
.addUniqueConstraint("taxonomies_name_slug_locale_unique", ["name", "slug", "locale"])
|
|
121
|
+
.addForeignKeyConstraint("taxonomies_parent_fk", ["parent_id"], "taxonomies", ["id"], (cb) =>
|
|
122
|
+
cb.onDelete("set null"),
|
|
123
|
+
)
|
|
124
|
+
.execute();
|
|
125
|
+
|
|
126
|
+
await sql`
|
|
127
|
+
INSERT INTO taxonomies_new (id, name, slug, label, parent_id, data, locale, translation_group)
|
|
128
|
+
SELECT id, name, slug, label, parent_id, data, ${defaultLocale}, id FROM taxonomies
|
|
129
|
+
`.execute(db);
|
|
130
|
+
|
|
131
|
+
await db.schema.dropTable("taxonomies").execute();
|
|
132
|
+
await sql`ALTER TABLE taxonomies_new RENAME TO taxonomies`.execute(db);
|
|
133
|
+
|
|
134
|
+
await db.schema.createIndex("idx_taxonomies_name").on("taxonomies").column("name").execute();
|
|
135
|
+
await db.schema.createIndex("idx_taxonomies_locale").on("taxonomies").column("locale").execute();
|
|
136
|
+
await db.schema
|
|
137
|
+
.createIndex("idx_taxonomies_translation_group")
|
|
138
|
+
.on("taxonomies")
|
|
139
|
+
.column("translation_group")
|
|
140
|
+
.execute();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function rebuildTaxonomyDefs(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
|
|
144
|
+
if (await hasColumn(db, "_emdash_taxonomy_defs", "locale")) return;
|
|
145
|
+
await sql.raw(`DROP TABLE IF EXISTS "_emdash_taxonomy_defs_new"`).execute(db);
|
|
146
|
+
|
|
147
|
+
await db.schema
|
|
148
|
+
.createTable("_emdash_taxonomy_defs_new")
|
|
149
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
150
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
151
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
152
|
+
.addColumn("label_singular", "text")
|
|
153
|
+
.addColumn("hierarchical", "integer", (c) => c.defaultTo(0))
|
|
154
|
+
.addColumn("collections", "text")
|
|
155
|
+
.addColumn("created_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
156
|
+
.addColumn("locale", "text", (c) => c.notNull().defaultTo(defaultLocale))
|
|
157
|
+
.addColumn("translation_group", "text")
|
|
158
|
+
.addUniqueConstraint("_emdash_taxonomy_defs_name_locale_unique", ["name", "locale"])
|
|
159
|
+
.execute();
|
|
160
|
+
|
|
161
|
+
await sql`
|
|
162
|
+
INSERT INTO _emdash_taxonomy_defs_new
|
|
163
|
+
(id, name, label, label_singular, hierarchical, collections, created_at, locale, translation_group)
|
|
164
|
+
SELECT id, name, label, label_singular, hierarchical, collections, created_at, ${defaultLocale}, id
|
|
165
|
+
FROM _emdash_taxonomy_defs
|
|
166
|
+
`.execute(db);
|
|
167
|
+
|
|
168
|
+
await db.schema.dropTable("_emdash_taxonomy_defs").execute();
|
|
169
|
+
await sql`ALTER TABLE _emdash_taxonomy_defs_new RENAME TO _emdash_taxonomy_defs`.execute(db);
|
|
170
|
+
|
|
171
|
+
await db.schema
|
|
172
|
+
.createIndex("idx__emdash_taxonomy_defs_locale")
|
|
173
|
+
.on("_emdash_taxonomy_defs")
|
|
174
|
+
.column("locale")
|
|
175
|
+
.execute();
|
|
176
|
+
await db.schema
|
|
177
|
+
.createIndex("idx__emdash_taxonomy_defs_translation_group")
|
|
178
|
+
.on("_emdash_taxonomy_defs")
|
|
179
|
+
.column("translation_group")
|
|
180
|
+
.execute();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function rebuildContentTaxonomies(db: Kysely<unknown>): Promise<void> {
|
|
184
|
+
// Drop the FK (taxonomy_id now points at translation_group, not a row id)
|
|
185
|
+
// and remap the values.
|
|
186
|
+
const fks = await sql<{ id: number }>`PRAGMA foreign_key_list(content_taxonomies)`.execute(db);
|
|
187
|
+
if (fks.rows.length === 0) return;
|
|
188
|
+
|
|
189
|
+
await sql.raw(`DROP TABLE IF EXISTS "content_taxonomies_new"`).execute(db);
|
|
190
|
+
await db.schema
|
|
191
|
+
.createTable("content_taxonomies_new")
|
|
192
|
+
.addColumn("collection", "text", (c) => c.notNull())
|
|
193
|
+
.addColumn("entry_id", "text", (c) => c.notNull())
|
|
194
|
+
.addColumn("taxonomy_id", "text", (c) => c.notNull())
|
|
195
|
+
.addPrimaryKeyConstraint("content_taxonomies_pk", ["collection", "entry_id", "taxonomy_id"])
|
|
196
|
+
.execute();
|
|
197
|
+
|
|
198
|
+
await sql`
|
|
199
|
+
INSERT OR IGNORE INTO content_taxonomies_new (collection, entry_id, taxonomy_id)
|
|
200
|
+
SELECT ct.collection, ct.entry_id, COALESCE(
|
|
201
|
+
(SELECT t.translation_group FROM taxonomies t WHERE t.id = ct.taxonomy_id),
|
|
202
|
+
ct.taxonomy_id
|
|
203
|
+
)
|
|
204
|
+
FROM content_taxonomies ct
|
|
205
|
+
`.execute(db);
|
|
206
|
+
|
|
207
|
+
await db.schema.dropTable("content_taxonomies").execute();
|
|
208
|
+
await sql`ALTER TABLE content_taxonomies_new RENAME TO content_taxonomies`.execute(db);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function remapMenuItemRefs(db: Kysely<unknown>): Promise<void> {
|
|
212
|
+
// Items with `reference_collection IS NULL` are left untouched — the
|
|
213
|
+
// runtime fallback in `menus/index.ts` resolves them by id.
|
|
214
|
+
const collections = await sql<{ slug: string }>`SELECT slug FROM _emdash_collections`.execute(db);
|
|
215
|
+
for (const { slug } of collections.rows) {
|
|
216
|
+
validateIdentifier(slug, "collection slug");
|
|
217
|
+
const ec = sql.ref(`ec_${slug}`);
|
|
218
|
+
await sql`
|
|
219
|
+
UPDATE _emdash_menu_items SET reference_id = (
|
|
220
|
+
SELECT translation_group FROM ${ec} WHERE ${ec}.id = _emdash_menu_items.reference_id
|
|
221
|
+
)
|
|
222
|
+
WHERE reference_collection = ${slug} AND reference_id IS NOT NULL
|
|
223
|
+
AND EXISTS (SELECT 1 FROM ${ec} WHERE ${ec}.id = _emdash_menu_items.reference_id)
|
|
224
|
+
`.execute(db);
|
|
225
|
+
}
|
|
226
|
+
await sql`
|
|
227
|
+
UPDATE _emdash_menu_items SET reference_id = (
|
|
228
|
+
SELECT translation_group FROM taxonomies WHERE taxonomies.id = _emdash_menu_items.reference_id
|
|
229
|
+
)
|
|
230
|
+
WHERE type = 'taxonomy' AND reference_id IS NOT NULL
|
|
231
|
+
AND EXISTS (SELECT 1 FROM taxonomies WHERE taxonomies.id = _emdash_menu_items.reference_id)
|
|
232
|
+
`.execute(db);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function pgWiden(
|
|
236
|
+
db: Kysely<unknown>,
|
|
237
|
+
table: string,
|
|
238
|
+
oldCols: string[] | null,
|
|
239
|
+
newCols: string[] | null,
|
|
240
|
+
defaultLocale: string,
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
validateSystemIdent(table);
|
|
243
|
+
const ref = sql.ref(table);
|
|
244
|
+
await sql`ALTER TABLE ${ref} ADD COLUMN IF NOT EXISTS locale TEXT NOT NULL DEFAULT ${sql.lit(defaultLocale)}`.execute(
|
|
245
|
+
db,
|
|
246
|
+
);
|
|
247
|
+
await sql`ALTER TABLE ${ref} ADD COLUMN IF NOT EXISTS translation_group TEXT`.execute(db);
|
|
248
|
+
await sql`UPDATE ${ref} SET translation_group = id WHERE translation_group IS NULL`.execute(db);
|
|
249
|
+
await sql`CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table}_locale`)} ON ${ref} (locale)`.execute(
|
|
250
|
+
db,
|
|
251
|
+
);
|
|
252
|
+
await sql`
|
|
253
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table}_translation_group`)} ON ${ref} (translation_group)
|
|
254
|
+
`.execute(db);
|
|
255
|
+
|
|
256
|
+
if (!oldCols || !newCols) return;
|
|
257
|
+
for (const c of [...oldCols, ...newCols]) validateSystemIdent(c);
|
|
258
|
+
const cons = await sql<{ conname: string }>`
|
|
259
|
+
SELECT conname FROM pg_constraint c
|
|
260
|
+
WHERE c.conrelid = ${table}::regclass AND c.contype = 'u'
|
|
261
|
+
AND array_length(c.conkey, 1) = ${oldCols.length}
|
|
262
|
+
AND (
|
|
263
|
+
SELECT array_agg(a.attname ORDER BY pos.ord)
|
|
264
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS pos(attnum, ord)
|
|
265
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = pos.attnum
|
|
266
|
+
)::text[] = ${oldCols}::text[]
|
|
267
|
+
`.execute(db);
|
|
268
|
+
for (const c of cons.rows) {
|
|
269
|
+
await sql`ALTER TABLE ${ref} DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
|
|
270
|
+
}
|
|
271
|
+
const cols = sql.join(
|
|
272
|
+
newCols.map((c) => sql.ref(c)),
|
|
273
|
+
sql`, `,
|
|
274
|
+
);
|
|
275
|
+
await sql`
|
|
276
|
+
ALTER TABLE ${ref}
|
|
277
|
+
ADD CONSTRAINT ${sql.ref(`${table}_${newCols.join("_")}_unique`)} UNIQUE (${cols})
|
|
278
|
+
`.execute(db);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function pgRemapContentTaxonomies(db: Kysely<unknown>): Promise<void> {
|
|
282
|
+
const fks = await sql<{ conname: string }>`
|
|
283
|
+
SELECT conname FROM pg_constraint
|
|
284
|
+
WHERE conrelid = 'content_taxonomies'::regclass AND contype = 'f'
|
|
285
|
+
`.execute(db);
|
|
286
|
+
for (const c of fks.rows) {
|
|
287
|
+
await sql`ALTER TABLE content_taxonomies DROP CONSTRAINT ${sql.ref(c.conname)}`.execute(db);
|
|
288
|
+
}
|
|
289
|
+
await sql`
|
|
290
|
+
UPDATE content_taxonomies SET taxonomy_id = t.translation_group
|
|
291
|
+
FROM taxonomies t WHERE t.id = content_taxonomies.taxonomy_id
|
|
292
|
+
`.execute(db);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function hasColumn(db: Kysely<unknown>, table: string, column: string): Promise<boolean> {
|
|
296
|
+
const rows = await sql<{ name: string }>`PRAGMA table_info(${sql.ref(table)})`.execute(db);
|
|
297
|
+
return rows.rows.some((r) => r.name === column);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const SYSTEM_IDENT = /^[_a-z][a-z0-9_]*$/;
|
|
301
|
+
function validateSystemIdent(name: string): void {
|
|
302
|
+
if (!SYSTEM_IDENT.test(name)) throw new Error(`Invalid identifier: "${name}"`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* down() is destructive on multi-locale installs (dropping `locale` collapses
|
|
307
|
+
* translated rows onto an ambiguous unique key). Refuse to run when any row
|
|
308
|
+
* sits at a locale other than the configured defaultLocale.
|
|
309
|
+
*/
|
|
310
|
+
async function assertSingleLocale(db: Kysely<unknown>, defaultLocale: string): Promise<void> {
|
|
311
|
+
const tables = ["_emdash_menus", "_emdash_menu_items", "taxonomies", "_emdash_taxonomy_defs"];
|
|
312
|
+
for (const table of tables) {
|
|
313
|
+
validateSystemIdent(table);
|
|
314
|
+
const result = await sql<{ count: number | string }>`
|
|
315
|
+
SELECT COUNT(*) AS count FROM ${sql.ref(table)} WHERE locale != ${defaultLocale}
|
|
316
|
+
`.execute(db);
|
|
317
|
+
const count = Number(result.rows[0]?.count ?? 0);
|
|
318
|
+
if (count > 0) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Cannot revert migration 036_i18n_menus_and_taxonomies: ` +
|
|
321
|
+
`${count} row(s) in "${table}" use a non-default locale ` +
|
|
322
|
+
`(defaultLocale="${defaultLocale}"). ` +
|
|
323
|
+
`Reverting would drop them silently. Export translations first ` +
|
|
324
|
+
`(or delete them) and re-run the rollback. ` +
|
|
325
|
+
`See packages/core/src/database/migrations/036_i18n_menus_and_taxonomies.ts.`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
332
|
+
const defaultLocale = getDefaultLocale();
|
|
333
|
+
await assertSingleLocale(db, defaultLocale);
|
|
334
|
+
|
|
335
|
+
const widenedTables = [
|
|
336
|
+
"_emdash_menus",
|
|
337
|
+
"_emdash_menu_items",
|
|
338
|
+
"taxonomies",
|
|
339
|
+
"_emdash_taxonomy_defs",
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
if (isSqlite(db)) {
|
|
343
|
+
// FKs off — same reason as up().
|
|
344
|
+
await sql.raw(`PRAGMA foreign_keys = OFF`).execute(db);
|
|
345
|
+
try {
|
|
346
|
+
// Indexes first: a locale index blocks DROP COLUMN on _emdash_menu_items.
|
|
347
|
+
for (const t of widenedTables) {
|
|
348
|
+
await sql.raw(`DROP INDEX IF EXISTS idx_${t}_locale`).execute(db);
|
|
349
|
+
await sql.raw(`DROP INDEX IF EXISTS idx_${t}_translation_group`).execute(db);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
await rebuildContentTaxonomiesDown(db, defaultLocale);
|
|
353
|
+
await rebuildMenusDown(db);
|
|
354
|
+
await rebuildMenuItemsDown(db);
|
|
355
|
+
await rebuildTaxonomiesDown(db);
|
|
356
|
+
await rebuildTaxonomyDefsDown(db);
|
|
357
|
+
} finally {
|
|
358
|
+
await sql.raw(`PRAGMA foreign_keys = ON`).execute(db);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const t of widenedTables) {
|
|
364
|
+
await sql.raw(`DROP INDEX IF EXISTS idx_${t}_locale`).execute(db);
|
|
365
|
+
await sql.raw(`DROP INDEX IF EXISTS idx_${t}_translation_group`).execute(db);
|
|
366
|
+
await sql.raw(`ALTER TABLE "${t}" DROP COLUMN IF EXISTS locale`).execute(db);
|
|
367
|
+
await sql.raw(`ALTER TABLE "${t}" DROP COLUMN IF EXISTS translation_group`).execute(db);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function rebuildContentTaxonomiesDown(
|
|
372
|
+
db: Kysely<unknown>,
|
|
373
|
+
defaultLocale: string,
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
await sql.raw(`DROP TABLE IF EXISTS "content_taxonomies_new"`).execute(db);
|
|
376
|
+
await db.schema
|
|
377
|
+
.createTable("content_taxonomies_new")
|
|
378
|
+
.addColumn("collection", "text", (c) => c.notNull())
|
|
379
|
+
.addColumn("entry_id", "text", (c) => c.notNull())
|
|
380
|
+
.addColumn("taxonomy_id", "text", (c) => c.notNull())
|
|
381
|
+
.addPrimaryKeyConstraint("content_taxonomies_pk", ["collection", "entry_id", "taxonomy_id"])
|
|
382
|
+
.addForeignKeyConstraint(
|
|
383
|
+
"content_taxonomies_taxonomy_fk",
|
|
384
|
+
["taxonomy_id"],
|
|
385
|
+
"taxonomies",
|
|
386
|
+
["id"],
|
|
387
|
+
(cb) => cb.onDelete("cascade"),
|
|
388
|
+
)
|
|
389
|
+
.execute();
|
|
390
|
+
|
|
391
|
+
// Map translation_group back to a row id (assertSingleLocale guarantees a 1:1 match).
|
|
392
|
+
await sql`
|
|
393
|
+
INSERT OR IGNORE INTO content_taxonomies_new (collection, entry_id, taxonomy_id)
|
|
394
|
+
SELECT ct.collection, ct.entry_id, COALESCE(
|
|
395
|
+
(SELECT t.id FROM taxonomies t WHERE t.translation_group = ct.taxonomy_id AND t.locale = ${defaultLocale}),
|
|
396
|
+
ct.taxonomy_id
|
|
397
|
+
)
|
|
398
|
+
FROM content_taxonomies ct
|
|
399
|
+
`.execute(db);
|
|
400
|
+
|
|
401
|
+
await db.schema.dropTable("content_taxonomies").execute();
|
|
402
|
+
await sql`ALTER TABLE content_taxonomies_new RENAME TO content_taxonomies`.execute(db);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function rebuildMenusDown(db: Kysely<unknown>): Promise<void> {
|
|
406
|
+
await sql.raw(`DROP TABLE IF EXISTS "_emdash_menus_old"`).execute(db);
|
|
407
|
+
await db.schema
|
|
408
|
+
.createTable("_emdash_menus_old")
|
|
409
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
410
|
+
.addColumn("name", "text", (c) => c.notNull().unique())
|
|
411
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
412
|
+
.addColumn("created_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
413
|
+
.addColumn("updated_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
414
|
+
.execute();
|
|
415
|
+
await sql`
|
|
416
|
+
INSERT INTO _emdash_menus_old (id, name, label, created_at, updated_at)
|
|
417
|
+
SELECT id, name, label, created_at, updated_at FROM _emdash_menus
|
|
418
|
+
`.execute(db);
|
|
419
|
+
await db.schema.dropTable("_emdash_menus").execute();
|
|
420
|
+
await sql`ALTER TABLE _emdash_menus_old RENAME TO _emdash_menus`.execute(db);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function rebuildMenuItemsDown(db: Kysely<unknown>): Promise<void> {
|
|
424
|
+
// No UNIQUE on (locale,…) here, so DROP COLUMN is enough.
|
|
425
|
+
await sql.raw(`ALTER TABLE _emdash_menu_items DROP COLUMN locale`).execute(db);
|
|
426
|
+
await sql.raw(`ALTER TABLE _emdash_menu_items DROP COLUMN translation_group`).execute(db);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function rebuildTaxonomiesDown(db: Kysely<unknown>): Promise<void> {
|
|
430
|
+
await sql.raw(`DROP TABLE IF EXISTS "taxonomies_old"`).execute(db);
|
|
431
|
+
await db.schema
|
|
432
|
+
.createTable("taxonomies_old")
|
|
433
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
434
|
+
.addColumn("name", "text", (c) => c.notNull())
|
|
435
|
+
.addColumn("slug", "text", (c) => c.notNull())
|
|
436
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
437
|
+
.addColumn("parent_id", "text")
|
|
438
|
+
.addColumn("data", "text")
|
|
439
|
+
.addUniqueConstraint("taxonomies_name_slug_unique", ["name", "slug"])
|
|
440
|
+
.addForeignKeyConstraint(
|
|
441
|
+
"taxonomies_parent_fk",
|
|
442
|
+
["parent_id"],
|
|
443
|
+
"taxonomies_old",
|
|
444
|
+
["id"],
|
|
445
|
+
(cb) => cb.onDelete("set null"),
|
|
446
|
+
)
|
|
447
|
+
.execute();
|
|
448
|
+
await sql`
|
|
449
|
+
INSERT INTO taxonomies_old (id, name, slug, label, parent_id, data)
|
|
450
|
+
SELECT id, name, slug, label, parent_id, data FROM taxonomies
|
|
451
|
+
`.execute(db);
|
|
452
|
+
await db.schema.dropTable("taxonomies").execute();
|
|
453
|
+
await sql`ALTER TABLE taxonomies_old RENAME TO taxonomies`.execute(db);
|
|
454
|
+
await db.schema.createIndex("idx_taxonomies_name").on("taxonomies").column("name").execute();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function rebuildTaxonomyDefsDown(db: Kysely<unknown>): Promise<void> {
|
|
458
|
+
await sql.raw(`DROP TABLE IF EXISTS "_emdash_taxonomy_defs_old"`).execute(db);
|
|
459
|
+
await db.schema
|
|
460
|
+
.createTable("_emdash_taxonomy_defs_old")
|
|
461
|
+
.addColumn("id", "text", (c) => c.primaryKey())
|
|
462
|
+
.addColumn("name", "text", (c) => c.notNull().unique())
|
|
463
|
+
.addColumn("label", "text", (c) => c.notNull())
|
|
464
|
+
.addColumn("label_singular", "text")
|
|
465
|
+
.addColumn("hierarchical", "integer", (c) => c.defaultTo(0))
|
|
466
|
+
.addColumn("collections", "text")
|
|
467
|
+
.addColumn("created_at", "text", (c) => c.defaultTo(currentTimestamp(db)))
|
|
468
|
+
.execute();
|
|
469
|
+
await sql`
|
|
470
|
+
INSERT INTO _emdash_taxonomy_defs_old
|
|
471
|
+
(id, name, label, label_singular, hierarchical, collections, created_at)
|
|
472
|
+
SELECT id, name, label, label_singular, hierarchical, collections, created_at
|
|
473
|
+
FROM _emdash_taxonomy_defs
|
|
474
|
+
`.execute(db);
|
|
475
|
+
await db.schema.dropTable("_emdash_taxonomy_defs").execute();
|
|
476
|
+
await sql`ALTER TABLE _emdash_taxonomy_defs_old RENAME TO _emdash_taxonomy_defs`.execute(db);
|
|
477
|
+
}
|