diff --git a/scripts/fix-twins-post-topics.js b/scripts/fix-twins-post-topics.js new file mode 100644 index 0000000..ec9f639 --- /dev/null +++ b/scripts/fix-twins-post-topics.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +/** + * Fix: migrate-twins-post.js passed a bare documentId array for the `topics` + * many-to-many relation, which Strapi's document service silently mishandled + * (orphaned link rows with no twins_post_id). Reconnects each post to its + * topics using `{ connect: [...] }`, which links correctly. + * + * Topic slugs below are recovered from the original post frontmatter + * (twins-in-the-loop git history), since they were never persisted. + * + * Usage: node scripts/fix-twins-post-topics.js + */ + +const POST_TOPICS = { + 'ai-and-datacenter-conversations': ['ai', 'data-centers'], + 'arm-owns-the-datacenter': ['hardware', 'data-centers'], + 'arms-next-chapter': ['hardware'], + 'ask-me-about-the-weather': ['ai'], + 'i-invented-bare-metal': ['hardware'], + 'implications-of-geopolitical-decoupling': ['infrastructure'], + 'impossible-to-buy-datacenter-space': ['data-centers'], + 'live-coding-a-cli-in-rust': ['infrastructure'], + 'nobody-cares-about-gandalf': ['infrastructure'], + 'on-device-ai': ['ai', 'hardware'], + 'reading-500-incident-reports': ['infrastructure'], + 'reduced-deploy-times': ['infrastructure'], + 'simplify-then-add-lightness': ['hardware'], + 'taking-writing-lessons': ['ai'], + 'zero-downtime-schema-migrations': ['infrastructure'], +}; + +async function fixTopics(app) { + const topicsAttribute = app.db.metadata.get('api::twins-post.twins-post').attributes.topics; + const joinTableName = topicsAttribute?.joinTable?.name; + const postJoinColumn = topicsAttribute?.joinTable?.joinColumn?.name; + + if (!joinTableName || !postJoinColumn) { + throw new Error('Could not resolve twins-post topics join table metadata'); + } + + // Clear only the orphaned link rows left by the original migration. + await app.db.connection(joinTableName).whereNull(postJoinColumn).del(); + + for (const [slug, topicSlugs] of Object.entries(POST_TOPICS)) { + const topics = await app.documents('api::topic.topic').findMany({ + filters: { slug: { $in: topicSlugs } }, + }); + + const topicsBySlug = new Map(topics.map((topic) => [topic.slug, topic])); + const missingSlugs = topicSlugs.filter((s) => !topicsBySlug.has(s)); + if (missingSlugs.length) { + throw new Error(`Missing topic(s) for "${slug}": ${missingSlugs.join(', ')}`); + } + + const topicDocumentIds = topicSlugs.map((s) => topicsBySlug.get(s).documentId); + for (const status of ['draft', 'published']) { + const post = await app.documents('api::twins-post.twins-post').findFirst({ + filters: { slug }, + status, + }); + if (!post) { + console.warn(`No ${status} version found for "${slug}"`); + continue; + } + await app.documents('api::twins-post.twins-post').update({ + documentId: post.documentId, + data: { topics: { connect: topicDocumentIds } }, + status, + }); + } + console.log(`Linked "${slug}" to [${topicSlugs.join(', ')}]`); + } +} + +async function main() { + const { createStrapi, compileStrapi } = require('@strapi/strapi'); + + const appContext = await compileStrapi(); + const app = await createStrapi(appContext).load(); + + app.log.level = 'error'; + + await fixTopics(app); + await app.destroy(); + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/grant-twins-public-permissions.js b/scripts/grant-twins-public-permissions.js new file mode 100644 index 0000000..ff4fb5c --- /dev/null +++ b/scripts/grant-twins-public-permissions.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +/** + * Grant the public role read access to the specified content types. + * Idempotent: skips any action that's already granted. + * + * Usage: + * node scripts/grant-twins-public-permissions.js api::article.article api::category.category + */ + +const contentTypeUids = process.argv.slice(2); + +function getActions(app) { + if (contentTypeUids.length === 0) { + throw new Error( + 'Provide one or more content-type UIDs, e.g. api::article.article api::category.category' + ); + } + + return contentTypeUids.flatMap((uid) => { + if (!app.contentTypes[uid]) { + throw new Error(`Content type "${uid}" is not registered in this project`); + } + + return [`${uid}.find`, `${uid}.findOne`]; + }); +} + +async function grantPermissions(app) { + const publicRole = await app.query('plugin::users-permissions.role').findOne({ + where: { type: 'public' }, + }); + if (!publicRole) { + throw new Error('Public role not found'); + } + + for (const action of getActions(app)) { + const existing = await app.query('plugin::users-permissions.permission').findOne({ + where: { action, role: publicRole.id }, + }); + if (existing) { + console.log(`Skipping "${action}" (already granted)`); + continue; + } + await app.query('plugin::users-permissions.permission').create({ + data: { action, role: publicRole.id }, + }); + console.log(`Granted "${action}"`); + } +} + +async function main() { + const { createStrapi, compileStrapi } = require('@strapi/strapi'); + + const appContext = await compileStrapi(); + const app = await createStrapi(appContext).load(); + + app.log.level = 'error'; + + await grantPermissions(app); + await app.destroy(); + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/scripts/republish-twins-post.js b/scripts/republish-twins-post.js new file mode 100644 index 0000000..cac0200 --- /dev/null +++ b/scripts/republish-twins-post.js @@ -0,0 +1,53 @@ +#!/usr/bin/env node +/** + * Fix: migrate-twins-post.js created entries with status "published", but the + * published version ended up without its topics/blocks/cover relations + * (only the draft version got them). Republishing from the current draft + * copies its content into the published version. + * + * Usage: node scripts/republish-twins-post.js + */ + +const CONTENT_TYPE_UID = 'api::twins-post.twins-post'; + +async function republishAll(app) { + const drafts = await app.documents(CONTENT_TYPE_UID).findMany({ + status: 'draft', + }); + + for (const draft of drafts) { + const published = await app.documents(CONTENT_TYPE_UID).findFirst({ + documentId: draft.documentId, + status: 'published', + }); + + if (!published) { + console.log(`Skipping "${draft.slug}" (no published version to repair)`); + continue; + } + + await app.documents(CONTENT_TYPE_UID).publish({ + documentId: draft.documentId, + }); + console.log(`Republished "${draft.slug}"`); + } +} + +async function main() { + const { createStrapi, compileStrapi } = require('@strapi/strapi'); + + const appContext = await compileStrapi(); + const app = await createStrapi(appContext).load(); + + app.log.level = 'error'; + + await republishAll(app); + await app.destroy(); + + process.exit(0); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});