import { Effect } from "effect"; import { DataMigrationError, type SqliteDataMigrationClient } from "@executor-js/sdk/core"; const MIGRATION_NAME = "2026-06-20-google-openapi-ownership"; const googleOpenApiCandidate = (alias?: string): string => { const column = (name: string) => (alias ? `${alias}.${name}` : name); return `${column("plugin_id")} = 'openapi' AND ${column("config")} IS NOT NULL AND json_valid(${column("config")}) AND json_type(${column("config")}, '$.googleDiscoveryUrls') = 'array'`; }; const execute = ( client: SqliteDataMigrationClient, stmt: string | { readonly sql: string; readonly args: readonly unknown[] }, ) => Effect.tryPromise({ try: () => client.execute(stmt), catch: (cause) => new DataMigrationError({ migration: MIGRATION_NAME, cause }), }); export const runSqliteGoogleOpenApiOwnershipMigration = ( client: SqliteDataMigrationClient, ): Effect.Effect => Effect.gen(function* () { const exists = yield* execute( client, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'integration'", ); if (exists.rows.length === 0) return 0; const count = yield* execute( client, `SELECT COUNT(*) AS count FROM integration WHERE ${googleOpenApiCandidate()}`, ); const moved = Number(count.rows[0]?.count ?? 0); if (moved === 0) return 0; const applyAll = Effect.gen(function* () { yield* execute( client, `INSERT OR IGNORE INTO blob (namespace, key, value, row_id, id) SELECT 'o:' || g.tenant || '/google', b.key, b.value, lower(hex(randomblob(16))), json_array('o:' || g.tenant || '/google', b.key) FROM integration g JOIN blob b ON b.namespace = 'o:' || g.tenant || '/openapi' AND b.key = 'spec/' || json_extract(g.config, '$.specHash') WHERE ${googleOpenApiCandidate("g")} AND json_extract(g.config, '$.specHash') IS NOT NULL AND json_extract(g.config, '$.specHash') <> ''`, ); yield* execute( client, `INSERT OR IGNORE INTO plugin_storage (tenant, owner, subject, plugin_id, collection, key, data, created_at, updated_at, row_id) SELECT ps.tenant, ps.owner, ps.subject, 'google', ps.collection, ps.key, ps.data, ps.created_at, ps.updated_at, lower(hex(randomblob(16))) FROM plugin_storage ps JOIN integration g ON g.tenant = ps.tenant WHERE ps.plugin_id = 'openapi' AND ${googleOpenApiCandidate("g")} AND ps.collection = 'operation' AND ( json_extract(ps.data, '$.integration') = g.slug OR ps.key LIKE g.slug || '.%' )`, ); yield* execute( client, `DELETE FROM plugin_storage WHERE plugin_id = 'openapi' AND collection = 'operation' AND EXISTS ( SELECT 1 FROM integration g WHERE g.tenant = plugin_storage.tenant AND ${googleOpenApiCandidate("g")} AND ( json_extract(plugin_storage.data, '$.integration') = g.slug OR plugin_storage.key LIKE g.slug || '.%' ) )`, ); yield* execute( client, `UPDATE tool SET plugin_id = 'google' WHERE plugin_id = 'openapi' AND EXISTS ( SELECT 1 FROM integration g WHERE g.tenant = tool.tenant AND ${googleOpenApiCandidate("g")} AND g.slug = tool.integration )`, ); yield* execute( client, `UPDATE definition SET plugin_id = 'google' WHERE plugin_id = 'openapi' AND EXISTS ( SELECT 1 FROM integration g WHERE g.tenant = definition.tenant AND ${googleOpenApiCandidate("g")} AND g.slug = definition.integration )`, ); yield* execute( client, `UPDATE integration SET plugin_id = 'google' WHERE ${googleOpenApiCandidate()}`, ); yield* execute(client, "COMMIT"); return moved; }); yield* execute(client, "BEGIN"); return yield* applyAll.pipe( Effect.tapError(() => execute(client, "ROLLBACK").pipe(Effect.ignore)), ); }); export const googleOpenApiOwnershipDataMigration = { name: MIGRATION_NAME, run: (client: SqliteDataMigrationClient) => runSqliteGoogleOpenApiOwnershipMigration(client).pipe(Effect.asVoid), };