import { describe, expect, it } from "@effect/vitest";import { Effect, Exit } from "effect";import { collectTables, type SqliteDataMigrationClient } from "@executor-js/sdk";import { createSqliteTestFumaDb } from "@executor-js/sdk/testing"; import { runSqliteEncryptedSecretsRepartition } from "./repartition-migration"; // The real plugin_storage schema (unique on tenant/owner/subject/plugin_id/// collection/key), so the insert-or-ignore + delete choreography is exercised// against the production shape.const makeDb = () => Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() })); type Row = { readonly rowId: string; readonly owner: string; readonly subject: string; readonly key: string; readonly data?: string; readonly pluginId?: string; readonly collection?: string;}; const insertRow = (client: SqliteDataMigrationClient, row: Row) => Effect.promise(() => client.execute({ sql: `INSERT INTO plugin_storage (row_id, tenant, owner, subject, plugin_id, collection, key, data, created_at, updated_at) VALUES (?, 'tenant-a', ?, ?, ?, ?, ?, ?, 1, 1)`, args: [ row.rowId, row.owner, row.subject, row.pluginId ?? "encryptedSecrets", row.collection ?? "secrets", row.key, JSON.stringify(row.data ?? "v1.iv.tag.ct"), ], }), ); const partitions = (client: SqliteDataMigrationClient) => Effect.promise(() => client.execute("SELECT owner, subject, key, data FROM plugin_storage ORDER BY key"), ).pipe( Effect.map((result) => // `data` is a json column; compare its raw stored text. result.rows.map((row) => ({ owner: row.owner, subject: row.subject, key: row.key, data: String(row.data), })), ), ); describe("runSqliteEncryptedSecretsRepartition", () => { it.effect("moves org-embedded rows out of a user partition, ciphertext intact", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "r1", owner: "user", subject: "user-123", key: "oauth:org:linear:main", }); yield* insertRow(db.client, { rowId: "r2", owner: "user", subject: "user-123", key: "oauth:org:linear:main:refresh", }); const moved = yield* runSqliteEncryptedSecretsRepartition(db.client); expect(moved).toBe(2); expect(yield* partitions(db.client)).toEqual([ { owner: "org", subject: "", key: "oauth:org:linear:main", data: '"v1.iv.tag.ct"' }, { owner: "org", subject: "", key: "oauth:org:linear:main:refresh", data: '"v1.iv.tag.ct"' }, ]); }), ); it.effect("leaves correctly filed rows, user-embedded ids, and unscoped ids alone", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "r1", owner: "org", subject: "", key: "oauth:org:linear:main", }); yield* insertRow(db.client, { rowId: "r2", owner: "user", subject: "user-123", key: "connection:user:notion:p:token", }); yield* insertRow(db.client, { rowId: "r3", owner: "user", subject: "user-123", key: "legacy-random-id", }); const moved = yield* runSqliteEncryptedSecretsRepartition(db.client); expect(moved).toBe(0); expect((yield* partitions(db.client)).map((row) => `${row.owner}/${row.key}`)).toEqual([ "user/connection:user:notion:p:token", "user/legacy-random-id", "org/oauth:org:linear:main", ]); }), ); it.effect("a post-fix org row wins over the mis-filed duplicate", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "old", owner: "user", subject: "user-123", key: "oauth:org:linear:main", data: "v1.old", }); yield* insertRow(db.client, { rowId: "new", owner: "org", subject: "", key: "oauth:org:linear:main", data: "v1.new", }); const moved = yield* runSqliteEncryptedSecretsRepartition(db.client); expect(moved).toBe(1); expect(yield* partitions(db.client)).toEqual([ { owner: "org", subject: "", key: "oauth:org:linear:main", data: '"v1.new"' }, ]); }), ); it.effect("does not touch other plugins' rows", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "r1", owner: "user", subject: "user-123", key: "oauth:org:slack:main", pluginId: "workosVault", collection: "metadata", }); const moved = yield* runSqliteEncryptedSecretsRepartition(db.client); expect(moved).toBe(0); expect((yield* partitions(db.client))[0]).toMatchObject({ owner: "user", subject: "user-123", }); }), ); it.effect("fails loudly on an unreadable row instead of skipping it", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "r1", owner: "user", subject: "user-123", key: "oauth:org:linear:main", }); const scripted: SqliteDataMigrationClient = { execute: (stmt) => { const sql = typeof stmt === "string" ? stmt : stmt.sql; if (sql.includes("SELECT row_id")) { return Promise.resolve({ rows: [{ row_id: null, owner: "user", key: null }] }); } return db.client.execute(stmt as never) as never; }, }; const exit = yield* Effect.exit(runSqliteEncryptedSecretsRepartition(scripted)); expect(Exit.isFailure(exit), "an unreadable row fails the migration").toBe(true); }), ); it.effect("is a no-op on a database without the table", () => Effect.gen(function* () { const empty: SqliteDataMigrationClient = { execute: (stmt) => { const sql = typeof stmt === "string" ? stmt : stmt.sql; if (sql.includes("sqlite_master")) return Promise.resolve({ rows: [] }); // oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: scripted fake for a promise-shaped client interface return Promise.reject(new Error(`unexpected statement: ${sql}`)); }, }; const moved = yield* runSqliteEncryptedSecretsRepartition(empty); expect(moved).toBe(0); }), ); it.effect("is idempotent", () => Effect.gen(function* () { const db = yield* makeDb(); yield* insertRow(db.client, { rowId: "r1", owner: "user", subject: "user-123", key: "oauth:org:linear:main", }); expect(yield* runSqliteEncryptedSecretsRepartition(db.client)).toBe(1); expect(yield* runSqliteEncryptedSecretsRepartition(db.client)).toBe(0); }), );});
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { collectTables, type SqliteDataMigrationClient } from "@executor-js/sdk";
import { createSqliteTestFumaDb } from "@executor-js/sdk/testing";
import { runSqliteEncryptedSecretsRepartition } from "./repartition-migration";
// The real plugin_storage schema (unique on tenant/owner/subject/plugin_id/
// collection/key), so the insert-or-ignore + delete choreography is exercised
// against the production shape.
const makeDb = () => Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() }));
type Row = {
readonly rowId: string;
readonly owner: string;
readonly subject: string;
readonly key: string;
readonly data?: string;
readonly pluginId?: string;
readonly collection?: string;
};
const insertRow = (client: SqliteDataMigrationClient, row: Row) =>
Effect.promise(() =>
client.execute({
sql: `INSERT INTO plugin_storage
(row_id, tenant, owner, subject, plugin_id, collection, key, data, created_at, updated_at)
VALUES (?, 'tenant-a', ?, ?, ?, ?, ?, ?, 1, 1)`,
args: [
row.rowId,
row.owner,
row.subject,
row.pluginId ?? "encryptedSecrets",
row.collection ?? "secrets",
row.key,
JSON.stringify(row.data ?? "v1.iv.tag.ct"),
],
}),
);
const partitions = (client: SqliteDataMigrationClient) =>
Effect.promise(() =>
client.execute("SELECT owner, subject, key, data FROM plugin_storage ORDER BY key"),
).pipe(
Effect.map((result) =>
// `data` is a json column; compare its raw stored text.
result.rows.map((row) => ({
owner: row.owner,
subject: row.subject,
key: row.key,
data: String(row.data),
})),
),
);
describe("runSqliteEncryptedSecretsRepartition", () => {
it.effect("moves org-embedded rows out of a user partition, ciphertext intact", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "r1",
owner: "user",
subject: "user-123",
key: "oauth:org:linear:main",
});
yield* insertRow(db.client, {
rowId: "r2",
owner: "user",
subject: "user-123",
key: "oauth:org:linear:main:refresh",
});
const moved = yield* runSqliteEncryptedSecretsRepartition(db.client);
expect(moved).toBe(2);
expect(yield* partitions(db.client)).toEqual([
{ owner: "org", subject: "", key: "oauth:org:linear:main", data: '"v1.iv.tag.ct"' },
{ owner: "org", subject: "", key: "oauth:org:linear:main:refresh", data: '"v1.iv.tag.ct"' },
]);
}),
);
it.effect("leaves correctly filed rows, user-embedded ids, and unscoped ids alone", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "r1",
owner: "org",
subject: "",
key: "oauth:org:linear:main",
});
yield* insertRow(db.client, {
rowId: "r2",
owner: "user",
subject: "user-123",
key: "connection:user:notion:p:token",
});
yield* insertRow(db.client, {
rowId: "r3",
owner: "user",
subject: "user-123",
key: "legacy-random-id",
});
const moved = yield* runSqliteEncryptedSecretsRepartition(db.client);
expect(moved).toBe(0);
expect((yield* partitions(db.client)).map((row) => `${row.owner}/${row.key}`)).toEqual([
"user/connection:user:notion:p:token",
"user/legacy-random-id",
"org/oauth:org:linear:main",
]);
}),
);
it.effect("a post-fix org row wins over the mis-filed duplicate", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "old",
owner: "user",
subject: "user-123",
key: "oauth:org:linear:main",
data: "v1.old",
});
yield* insertRow(db.client, {
rowId: "new",
owner: "org",
subject: "",
key: "oauth:org:linear:main",
data: "v1.new",
});
const moved = yield* runSqliteEncryptedSecretsRepartition(db.client);
expect(moved).toBe(1);
expect(yield* partitions(db.client)).toEqual([
{ owner: "org", subject: "", key: "oauth:org:linear:main", data: '"v1.new"' },
]);
}),
);
it.effect("does not touch other plugins' rows", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "r1",
owner: "user",
subject: "user-123",
key: "oauth:org:slack:main",
pluginId: "workosVault",
collection: "metadata",
});
const moved = yield* runSqliteEncryptedSecretsRepartition(db.client);
expect(moved).toBe(0);
expect((yield* partitions(db.client))[0]).toMatchObject({
owner: "user",
subject: "user-123",
});
}),
);
it.effect("fails loudly on an unreadable row instead of skipping it", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "r1",
owner: "user",
subject: "user-123",
key: "oauth:org:linear:main",
});
const scripted: SqliteDataMigrationClient = {
execute: (stmt) => {
const sql = typeof stmt === "string" ? stmt : stmt.sql;
if (sql.includes("SELECT row_id")) {
return Promise.resolve({ rows: [{ row_id: null, owner: "user", key: null }] });
}
return db.client.execute(stmt as never) as never;
},
};
const exit = yield* Effect.exit(runSqliteEncryptedSecretsRepartition(scripted));
expect(Exit.isFailure(exit), "an unreadable row fails the migration").toBe(true);
}),
);
it.effect("is a no-op on a database without the table", () =>
Effect.gen(function* () {
const empty: SqliteDataMigrationClient = {
execute: (stmt) => {
const sql = typeof stmt === "string" ? stmt : stmt.sql;
if (sql.includes("sqlite_master")) return Promise.resolve({ rows: [] });
// oxlint-disable-next-line executor/no-promise-reject, executor/no-error-constructor -- boundary: scripted fake for a promise-shaped client interface
return Promise.reject(new Error(`unexpected statement: ${sql}`));
},
};
const moved = yield* runSqliteEncryptedSecretsRepartition(empty);
expect(moved).toBe(0);
}),
);
it.effect("is idempotent", () =>
Effect.gen(function* () {
const db = yield* makeDb();
yield* insertRow(db.client, {
rowId: "r1",
owner: "user",
subject: "user-123",
key: "oauth:org:linear:main",
});
expect(yield* runSqliteEncryptedSecretsRepartition(db.client)).toBe(1);
expect(yield* runSqliteEncryptedSecretsRepartition(db.client)).toBe(0);
}),
);
});