import { readFile } from "node:fs/promises"; import vm from "node:vm"; // oxlint-disable-next-line typescript/no-extraneous-class -- Mirrors the PocketBase Field constructor API. class Field { constructor(options) { Object.assign(this, options); this.id = `field-${options.name}`; } } class Fields { constructor(fields = []) { this.values = fields.map((field) => (field instanceof Field ? field : new Field(field))); } getByName(name) { return this.values.find((field) => field.name === name); } add(field) { this.values.push(field); } removeById(id) { this.values = this.values.filter((field) => field.id !== id); } } class Collection { constructor(options) { Object.assign(this, options); this.id = options.id ?? options.name; this.fields = new Fields(options.fields); } addIndex() {} } let recordId = 0; class Record { constructor(collection) { this.collection = collection; this.id = `record-${++recordId}`; this.data = new Map(); } set(name, value) { this.data.set(name, value); } getString(name) { return String(this.data.get(name) ?? ""); } } const original = { passwordAuth: { enabled: true, identityFields: ["email"] }, oauth2: { enabled: true, providers: [{ name: "original", clientId: "before", clientSecret: "before-secret" }] }, rules: { listRule: "original-list", viewRule: "original-view", createRule: "original-create", updateRule: "original-update", deleteRule: "original-delete", }, }; const users = new Collection({ type: "auth", name: "users", fields: [{ type: "text", name: "existing" }] }); users.passwordAuth = structuredClone(original.passwordAuth); users.oauth2 = structuredClone(original.oauth2); Object.assign(users, original.rules); const collections = new Map([[users.id, users]]); const records = new Map(); const rawQueries = []; const app = { db() { return { newQuery(sql) { return { execute() { rawQueries.push(sql); }, }; }, }; }, findCollectionByNameOrId(name) { const collection = collections.get(name); if (!collection) throw new Error(`Missing collection ${name}`); return collection; }, findFirstRecordByFilter(collectionId) { const record = records.get(collectionId)?.[0]; if (!record) throw new Error(`Missing state record ${collectionId}`); return record; }, save(value) { if (value instanceof Collection) collections.set(value.id, value); else if (value instanceof Record) records.set(value.collection.id, [...(records.get(value.collection.id) ?? []), value]); }, delete(collection) { collections.delete(collection.id); records.delete(collection.id); }, }; let migration; const sandbox = { Collection, Record, TextField: Field, URLField: Field, FileField: Field, BoolField: Field, console, migrate: (up, down) => { migration = { up, down }; }, }; const source = await readFile("apps/workshop/pb_migrations/1784240000_workshop_widgets.js", "utf8"); vm.runInNewContext(source, sandbox); if (!migration) throw new Error("Workshop migration did not register"); migration.up(app); if (!collections.has("workshop_migration_state")) { throw new Error("Workshop migration did not capture state before applying changes"); } if ( !rawQueries.some( (query) => query.includes("CREATE TRIGGER submissions_revision_cas") && query.includes("NEW.expectedRevision != OLD.revision"), ) ) { throw new Error("Workshop migration did not install the atomic submission revision guard"); } migration.down(app); if (JSON.stringify(users.passwordAuth) !== JSON.stringify(original.passwordAuth)) throw new Error("Password authentication was not restored"); if (JSON.stringify(users.oauth2) !== JSON.stringify(original.oauth2)) throw new Error("OAuth settings were not restored"); for (const [name, value] of Object.entries(original.rules)) { if (users[name] !== value) throw new Error(`User rule ${name} was not restored`); } if (users.fields.values.map((field) => field.name).join(",") !== "existing") throw new Error("Workshop user fields were not removed on rollback"); for (const name of ["workshop_migration_state", "workshop_listings", "reports", "comments", "votes", "submissions"]) { if (collections.has(name)) throw new Error(`Rollback left collection ${name}`); } collections.clear(); records.clear(); rawQueries.length = 0; migration.up(app); if (!collections.has("users")) throw new Error("Fresh migration did not create the users collection"); migration.down(app); for (const name of [ "users", "workshop_migration_state", "workshop_listings", "reports", "comments", "votes", "submissions", ]) { if (collections.has(name)) throw new Error(`Fresh rollback left collection ${name}`); } console.log("Workshop migration up/down restoration passed");