// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // Every table of a module the kit ships has a committed, forward-only migration, and each is exactly // `schemaStatements()` over that module's own entities (decision AT-D20: the entity definitions are // the one source of a column, so a hand-written column cannot drift from the schema a module validates // its rows with). A kit upgrade that changes a table changes a snapshot here, which is the moment a // customer must see a new migration in review. // // `0002` and `0003` are the grants the migrate job builds at run time (apps/worker/src/migrations.ts), // so the files of the kit's modules take the next numbers. import { readFileSync } from "node:fs" import { DOCUMENTS_ENTITIES, documentsModule } from "@app/modules/documents" import { buildRegistry } from "@conusai-kit/core" import { entitiesOf } from "@conusai-kit/data" import { schemaStatements } from "@conusai-kit/data-kysely" import { mysqlSchemaStatements } from "@conusai-kit/data-kysely/mysql" import { MONGO_KERNEL_COMMANDS, mongoSchemaCommands } from "@conusai-kit/data-mongo" import { filesModule } from "@conusai-kit/files" import { proposalsModule } from "@conusai-kit/proposals" import { tenancyModule } from "@conusai-kit/tenancy" import { describe, expect, it } from "vitest" import { SPDX_LINE } from "../scripts/licence.mjs" /** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */ function tablesOf(entities: Parameters[0]): string { const kernel = schemaStatements([]).length return `${schemaStatements(entities) .slice(kernel) .map((statement) => `${statement};`) .join("\n\n")}\n` } /** The same for a module's MariaDB folder (spec ยง5.3: one numbered SQL folder per dialect). */ function mysqlTablesOf(entities: Parameters[0]): string { const kernel = mysqlSchemaStatements([]).length return `${mysqlSchemaStatements(entities) .slice(kernel) .map((statement) => `${statement};`) .join("\n\n")}\n` } type MongoCommands = ReturnType /** The same for a module's MongoDB folder: `mongoSchemaCommands` leads with the kernel's own collections. */ function mongoCommandsOf(entities: Parameters[0]): MongoCommands { return mongoSchemaCommands(entities).slice(MONGO_KERNEL_COMMANDS.length) } const entitiesOfModule = (module: Parameters[0][number]) => entitiesOf(buildRegistry([tenancyModule, module]), "postgres").filter( (entity) => !entitiesOf(buildRegistry([tenancyModule]), "postgres").some((own) => own.table === entity.table), ) describe("the migrations of the modules the kit ships", () => { it("0004_files.sql creates the files table", async () => { await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(filesModule))}`).toMatchFileSnapshot( "../migrations/postgres/0004_files.sql", ) }) it("0005_proposals.sql creates the proposals table", async () => { await expect(`-- ${SPDX_LINE}\n${tablesOf(entitiesOfModule(proposalsModule))}`).toMatchFileSnapshot( "../migrations/postgres/0005_proposals.sql", ) }) }) describe("the migration of the documents module", () => { const migration = (name: string) => readFileSync(new URL(`../modules/documents/migrations/${name}`, import.meta.url), "utf8") // A migration that has run is never edited, so a column added later is a file of its own: the first // file stays what it was, the second adds the column, and together they are exactly the entities. it("0001 is its two tables as first shipped, and 0002 adds the one column since", () => { expect(migration("0002_skipped_pages.sql")).toBe( 'alter table "documents_document" add column "skipped_pages" jsonb;\n', ) const generated = tablesOf(DOCUMENTS_ENTITIES) expect(generated).toContain(' "skipped_pages" jsonb,\n') expect(generated.replace(' "skipped_pages" jsonb,\n', "")).toBe(migration("0001_documents.sql")) }) it("is declared by the manifest, so the migrate job finds it", () => { expect(documentsModule.migrations).toEqual({ postgres: "migrations", mariadb: "adapters/kysely/mysql/migrations", mongodb: "adapters/mongo/migrations", }) const sql = migration("0001_documents.sql") for (const table of ["documents_document", "documents_line"]) { expect(sql).toContain(`create table "${table}"`) expect(sql).toContain(`alter table "${table}" force row level security`) } }) it("ships the same migrations in the MariaDB dialect (AT3.1)", () => { const mysqlMigration = (name: string) => readFileSync(new URL(`../modules/documents/adapters/kysely/mysql/migrations/${name}`, import.meta.url), "utf8") expect(mysqlMigration("0002_skipped_pages.sql")).toBe( "alter table `documents_document` add column `skipped_pages` json;\n", ) const generated = mysqlTablesOf(DOCUMENTS_ENTITIES) expect(generated).toContain(" `skipped_pages` json,\n") expect(generated.replace(" `skipped_pages` json,\n", "")).toBe(mysqlMigration("0001_documents.sql")) }) it("ships the same migrations as MongoDB commands: 0001 the collections as first shipped, 0002 the added field (AT3.2)", () => { const mongoMigration = (name: string): MongoCommands => JSON.parse( readFileSync(new URL(`../modules/documents/adapters/mongo/migrations/${name}`, import.meta.url), "utf8"), ) const current = mongoCommandsOf(DOCUMENTS_ENTITIES) const createDocument = current.find( (command) => "createCollection" in command && command.createCollection === "documents_document", ) if (!createDocument || !("createCollection" in createDocument)) throw new Error("no createCollection for documents_document") // A migration that has run is never edited, so a field added later is a collMod of its own: // 0001's validator is the current one without skipped_pages, and 0002 carries the current one. const first = mongoMigration("0001_documents.json") const created = first.find( (command) => "createCollection" in command && command.createCollection === "documents_document", ) if (!created || !("createCollection" in created)) throw new Error("0001 does not create documents_document") expect(JSON.stringify(created.validator)).not.toContain("skipped_pages") const withoutSkippedPages = structuredClone(createDocument) const properties = withoutSkippedPages.validator.$jsonSchema.properties as Record expect(JSON.stringify(properties)).toContain("skipped_pages") delete properties.skipped_pages expect(JSON.stringify(created.validator)).toBe(JSON.stringify(withoutSkippedPages.validator)) // The other collection and every index are exactly the generated ones. const rest = (commands: MongoCommands) => JSON.stringify( commands.filter( (command) => !("createCollection" in command && command.createCollection === "documents_document"), ), ) expect(rest(first)).toBe(rest(current)) const second = mongoMigration("0002_skipped_pages.json") expect(second).toEqual([{ collMod: "documents_document", validator: createDocument.validator }]) }) })