emdash 0.9.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-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-BzltprvY.mjs → apply-UsrFuO7l.mjs} +156 -254
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +10 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +35 -34
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
- package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
- package/dist/{byline-BSaNL1w7.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-esI7ioa9.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-esI7ioa9.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-fTzxgMFJ.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-fTzxgMFJ.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-Da2-b-oA.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +251 -79
- 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 +1 -1
- package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-8lOYF0pr.mjs → content-C7G4QXkK.mjs} +14 -3
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-BFRaVcD6.d.mts → index-DjPMOfO0.d.mts} +82 -67
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +28 -27
- package/dist/{load-DDqMMvZL.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-Bx2_9-5e.mjs} +31 -6
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
- package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
- 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-BW32b4gi.mjs → media-D8FbNsl0.mjs} +2 -2
- package/dist/{media-BW32b4gi.mjs.map → media-D8FbNsl0.mjs.map} +1 -1
- package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
- package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
- 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-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
- package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
- package/dist/{query-Cg9ZKRQ0.mjs → query-Bo-msrmu.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-Bo-msrmu.mjs.map} +1 -1
- package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
- package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
- package/dist/{registry-Dw70ChxB.mjs → registry-Beb7wxFc.mjs} +5 -5
- package/dist/{registry-Dw70ChxB.mjs.map → registry-Beb7wxFc.mjs.map} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Clwe4Mme.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Clwe4Mme.d.mts.map} +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DMnlIkh4.mjs} +433 -138
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DkN-BqsS.mjs} +164 -92
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
- package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
- 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.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-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
- package/dist/{tokens-D7zMmWi2.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-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
- package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
- package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
- package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/{types-4fVtCIm0.mjs → types-CoO6mpV3.mjs} +1 -1
- package/dist/{types-4fVtCIm0.mjs.map → types-CoO6mpV3.mjs.map} +1 -1
- package/dist/{types-BuBIptGk.d.mts → types-D19uBYWn.d.mts} +149 -4
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-Dl1fgFjn.d.mts} +1 -1
- package/dist/{types-BSyXeCFW.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CrtWgIvl.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-CIOg5AR8.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-CIOg5AR8.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-Baqf0slj.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-BfQh_C_y.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-B1NYiEos.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-BNJDQBSZ.mjs} +8 -3
- package/dist/zod-generator-BNJDQBSZ.mjs.map +1 -0
- package/package.json +6 -6
- package/src/api/handlers/content.ts +11 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- 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/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/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 -6
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +10 -2
- package/src/i18n/resolve.ts +37 -0
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +77 -18
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +140 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/taxonomies/index.ts +230 -213
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-BzltprvY.mjs.map +0 -1
- package/dist/content-8lOYF0pr.mjs.map +0 -1
- package/dist/index-BFRaVcD6.d.mts.map +0 -1
- package/dist/loader-CKLbBnhK.mjs.map +0 -1
- package/dist/runner-C7ADox5q.mjs.map +0 -1
- package/dist/search-dOGEccMa.mjs.map +0 -1
- package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
- package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CrtWgIvl.d.mts.map +0 -1
- package/dist/validate-Baqf0slj.mjs.map +0 -1
- package/dist/validate-BfQh_C_y.d.mts.map +0 -1
- package/dist/version-DoxrVdYf.mjs +0 -7
- package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -36,6 +36,7 @@ import * as m032 from "./032_rate_limits.js";
|
|
|
36
36
|
import * as m033 from "./033_optimize_content_indexes.js";
|
|
37
37
|
import * as m034 from "./034_published_at_index.js";
|
|
38
38
|
import * as m035 from "./035_bounded_404_log.js";
|
|
39
|
+
import * as m036 from "./036_i18n_menus_and_taxonomies.js";
|
|
39
40
|
|
|
40
41
|
const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
41
42
|
"001_initial": m001,
|
|
@@ -72,6 +73,7 @@ const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
|
72
73
|
"033_optimize_content_indexes": m033,
|
|
73
74
|
"034_published_at_index": m034,
|
|
74
75
|
"035_bounded_404_log": m035,
|
|
76
|
+
"036_i18n_menus_and_taxonomies": m036,
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
/** Total number of registered migrations. Exported for use in tests. */
|
|
@@ -637,12 +637,23 @@ export class ContentRepository {
|
|
|
637
637
|
/**
|
|
638
638
|
* Permanently delete content (cannot be undone)
|
|
639
639
|
*/
|
|
640
|
+
/**
|
|
641
|
+
* Permanently delete a soft-deleted content row.
|
|
642
|
+
*
|
|
643
|
+
* Returns `true` only when a soft-deleted (trashed) row was removed.
|
|
644
|
+
* Returns `false` when no row exists OR when the row exists but is live —
|
|
645
|
+
* the caller is responsible for distinguishing these cases (typically via
|
|
646
|
+
* a follow-up `findByIdOrSlugIncludingTrashed` to surface NOT_FOUND vs
|
|
647
|
+
* NOT_TRASHED). The `AND deleted_at IS NOT NULL` clause is the safety net
|
|
648
|
+
* that prevents permanent delete from bypassing the trash workflow.
|
|
649
|
+
*/
|
|
640
650
|
async permanentDelete(type: string, id: string): Promise<boolean> {
|
|
641
651
|
const tableName = getTableName(type);
|
|
642
652
|
|
|
643
653
|
const result = await sql`
|
|
644
654
|
DELETE FROM ${sql.ref(tableName)}
|
|
645
655
|
WHERE id = ${id}
|
|
656
|
+
AND deleted_at IS NOT NULL
|
|
646
657
|
`.execute(this.db);
|
|
647
658
|
|
|
648
659
|
return (result.numAffectedRows ?? 0n) > 0n;
|