Files

93 lines
4.9 KiB
TypeScript

// The module's migration is its own file and exactly `schemaStatements()` over its two entities
// (decision AT-D20: the entity definitions are the one source of a column, so a hand-written column
// cannot drift from the schema the module validates its rows with). A change to an entity changes
// this snapshot, which is the moment a customer must see a new migration in review. The MariaDB
// folder is the same tables in the other dialect, generated by `mysqlSchemaStatements` (spec §5.3:
// the dialect differences are explicit, one numbered folder per dialect), and the MongoDB folder is
// the same collections and indexes as declarative commands, generated by `mongoSchemaCommands`.
import { readFileSync } from "node:fs"
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 { describe, expect, it } from "vitest"
import { NOTES_ENTITIES, notesModule } from "../index.js"
/** The statements for these tables alone: `schemaStatements` leads with the kernel's own two. */
function tablesOf(entities: Parameters<typeof schemaStatements>[0]): string {
const kernel = schemaStatements([]).length
return `${schemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for the MariaDB folder: `mysqlSchemaStatements` leads with the kernel's own two. */
function mysqlTablesOf(entities: Parameters<typeof mysqlSchemaStatements>[0]): string {
const kernel = mysqlSchemaStatements([]).length
return `${mysqlSchemaStatements(entities)
.slice(kernel)
.map((statement) => `${statement};`)
.join("\n\n")}\n`
}
/** The same for the MongoDB folder: `mongoSchemaCommands` leads with the kernel's own collections. */
function mongoCommandsOf(entities: Parameters<typeof mongoSchemaCommands>[0]): ReturnType<typeof mongoSchemaCommands> {
return mongoSchemaCommands(entities).slice(MONGO_KERNEL_COMMANDS.length)
}
describe("the migration of the notes module", () => {
it("is its own file, exactly its two tables", async () => {
await expect(tablesOf(NOTES_ENTITIES)).toMatchFileSnapshot("../migrations/0001_notes.sql")
})
it("is declared by the manifest, so the migrate job finds it, and every table is row-level secured", () => {
expect(notesModule.migrations).toEqual({
postgres: "migrations",
mariadb: "adapters/kysely/mysql/migrations",
mongodb: "adapters/mongo/migrations",
})
const sql = readFileSync(new URL("../migrations/0001_notes.sql", import.meta.url), "utf8")
for (const table of ["notes_note", "notes_attachment"]) {
expect(sql).toContain(`create table "${table}"`)
expect(sql).toContain(`alter table "${table}" force row level security`)
}
})
it("has the same two tables as MariaDB DDL, and no policies -- MariaDB has no row-level security", async () => {
const sql = readFileSync(new URL("../adapters/kysely/mysql/migrations/0001_notes.sql", import.meta.url), "utf8")
await expect(mysqlTablesOf(NOTES_ENTITIES)).toMatchFileSnapshot(
"../adapters/kysely/mysql/migrations/0001_notes.sql",
)
for (const table of ["notes_note", "notes_attachment"]) {
expect(sql).toContain(`create table \`${table}\``)
}
expect(sql).not.toContain("row level security")
})
it("has the same two collections as MongoDB commands, each index leading with org_id (AT3.2)", () => {
const file = new URL("../adapters/mongo/migrations/0001_notes.json", import.meta.url)
// The file is data (Biome keeps its JSON formatting canonical), so the pin is structural:
// exactly the commands `mongoSchemaCommands` generates for these entities, nothing else.
const commands = JSON.parse(readFileSync(file, "utf8")) as Array<Record<string, unknown>>
expect(commands).toEqual(mongoCommandsOf(NOTES_ENTITIES))
const created = commands
.filter((command) => "createCollection" in command)
.map((command) => command.createCollection)
expect(created).toEqual(["notes_attachment", "notes_note"])
// MongoDB has no row-level security: the wall is the repository predicate plus these
// org_id-leading indexes, and the contract suite of @conusai-kit/data-mongo is its proof.
const indexes = commands.filter((command) => "createIndex" in command)
for (const index of indexes) {
const keys = Object.keys((index.createIndex as { keys: Record<string, 1> }).keys)
expect(keys[0]).toBe("org_id")
}
// The validator is the entity's zod schema as $jsonSchema, carried by the file itself.
const note = commands.find((command) => command.createCollection === "notes_note") as {
validator: { $jsonSchema: { properties: Record<string, unknown>; required: string[] } }
}
expect(Object.keys(note.validator.$jsonSchema.properties)).toContain("org_id")
expect(note.validator.$jsonSchema.required).toContain("org_id")
})
})