This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// 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")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user