diff --git a/apps/cms/database/migrations/2026.04.02T00.00.00.create-core-sync-states.ts b/apps/cms/database/migrations/2026.04.02T00.00.00.create-core-sync-states.ts new file mode 100644 index 00000000..7d76c763 --- /dev/null +++ b/apps/cms/database/migrations/2026.04.02T00.00.00.create-core-sync-states.ts @@ -0,0 +1,42 @@ +/** + * Create the core_sync_states table used by the core-sync service to track + * per-phase watermarks and last-run statistics. + * + * Previously this table was created at runtime by ensureSyncStateTable(). + * Moving it to a proper migration ensures the schema exists on boot, + * before any status endpoint queries it. + */ + +const TABLE = "core_sync_states" + +/* eslint-disable @typescript-eslint/no-explicit-any */ +export async function up(knex: any): Promise { + const exists = await knex.schema.hasTable(TABLE) + + if (!exists) { + await knex.schema.createTable(TABLE, (t: any) => { + t.string("phase").primary() + t.timestamp("last_synced_at").notNullable() + t.integer("created").defaultTo(0) + t.integer("updated").defaultTo(0) + t.integer("soft_deleted").defaultTo(0) + t.integer("errors").defaultTo(0) + }) + return + } + + // Table exists from the old runtime creation — add stats columns if missing + const hasCreated = await knex.schema.hasColumn(TABLE, "created") + if (!hasCreated) { + await knex.schema.alterTable(TABLE, (t: any) => { + t.integer("created").defaultTo(0) + t.integer("updated").defaultTo(0) + t.integer("soft_deleted").defaultTo(0) + t.integer("errors").defaultTo(0) + }) + } +} + +export async function down(knex: any): Promise { + await knex.schema.dropTableIfExists(TABLE) +} diff --git a/apps/cms/src/api/core-sync/services/strapi-helpers.ts b/apps/cms/src/api/core-sync/services/strapi-helpers.ts index 5ffe6972..023de215 100644 --- a/apps/cms/src/api/core-sync/services/strapi-helpers.ts +++ b/apps/cms/src/api/core-sync/services/strapi-helpers.ts @@ -156,37 +156,16 @@ export async function buildCoreIdMap( // --------------------------------------------------------------------------- const SYNC_STATE_TABLE = "core_sync_states" -let tableEnsured = false -/** Ensure the sync-state table exists with stats columns (idempotent, cached). */ -export async function ensureSyncStateTable(strapi: Core.Strapi): Promise { - if (tableEnsured) return - const knex = strapi.db.connection - const exists = await knex.schema.hasTable(SYNC_STATE_TABLE) - if (!exists) { - await knex.schema.createTable(SYNC_STATE_TABLE, (t) => { - t.string("phase").primary() - t.timestamp("last_synced_at").notNullable() - t.integer("created").defaultTo(0) - t.integer("updated").defaultTo(0) - t.integer("soft_deleted").defaultTo(0) - t.integer("errors").defaultTo(0) - }) - strapi.log.info(`[core-sync] Created ${SYNC_STATE_TABLE} table`) - } else { - // Migrate existing tables: add stats columns if missing - const hasCreated = await knex.schema.hasColumn(SYNC_STATE_TABLE, "created") - if (!hasCreated) { - await knex.schema.alterTable(SYNC_STATE_TABLE, (t) => { - t.integer("created").defaultTo(0) - t.integer("updated").defaultTo(0) - t.integer("soft_deleted").defaultTo(0) - t.integer("errors").defaultTo(0) - }) - strapi.log.info(`[core-sync] Added stats columns to ${SYNC_STATE_TABLE}`) - } - } - tableEnsured = true +/** + * No-op safety net. The table is now created by the database migration + * `2026.04.02T00.00.00.create-core-sync-states.ts` which runs on boot. + * Kept as a function signature so callers don't need to change. + */ +export async function ensureSyncStateTable( + _strapi: Core.Strapi, +): Promise { + // Migration handles table creation and column additions } /** Read the last successful sync timestamp for a phase (null = never synced). */ @@ -216,8 +195,6 @@ export async function getAllSyncTimes( strapi: Core.Strapi, ): Promise { const knex = strapi.db.connection - const exists = await knex.schema.hasTable(SYNC_STATE_TABLE) - if (!exists) return [] const rows = await knex(SYNC_STATE_TABLE) .select( "phase",