@coreframe/scripts 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/config.d.ts +18 -4
  2. package/dist/config.js +1 -3
  3. package/dist/coreframe.d.ts +243 -241
  4. package/dist/coreframe.js +1 -239
  5. package/dist/db/client.d.ts +26 -18
  6. package/dist/db/client.js +1 -29
  7. package/dist/db/config.d.ts +22 -19
  8. package/dist/db/config.js +1 -32
  9. package/dist/db/drizzle-kit.d.ts +6 -5
  10. package/dist/db/drizzle-kit.js +1 -21
  11. package/dist/db/ensure.d.ts +20 -2
  12. package/dist/db/ensure.js +1 -39
  13. package/dist/db/migrate-held.d.ts +16 -10
  14. package/dist/db/migrate-held.js +3 -112
  15. package/dist/db/migrate.d.ts +4 -2
  16. package/dist/db/migrate.js +1 -24
  17. package/dist/db/project.d.ts +19 -7
  18. package/dist/db/project.js +1 -9
  19. package/dist/db/reset.d.ts +4 -2
  20. package/dist/db/reset.js +1 -18
  21. package/dist/db/schema-push.d.ts +22 -0
  22. package/dist/db/schema-push.js +1 -0
  23. package/dist/db/seed.d.ts +6 -3
  24. package/dist/db/seed.js +1 -22
  25. package/dist/db/setup.d.ts +6 -3
  26. package/dist/db/setup.js +1 -14
  27. package/dist/deploy/check-migrations.d.ts +4 -2
  28. package/dist/deploy/check-migrations.js +1 -17
  29. package/dist/deploy/checks.d.ts +6 -2
  30. package/dist/deploy/checks.js +1 -27
  31. package/dist/deploy/config.d.ts +47 -44
  32. package/dist/deploy/config.js +1 -9
  33. package/dist/deploy/migrations.d.ts +21 -18
  34. package/dist/deploy/migrations.js +4 -203
  35. package/dist/deploy/pending-migrations.d.ts +9 -5
  36. package/dist/deploy/pending-migrations.js +1 -30
  37. package/dist/deploy/tauri-release.d.ts +93 -73
  38. package/dist/deploy/tauri-release.js +1 -512
  39. package/dist/deploy/version.d.ts +15 -13
  40. package/dist/deploy/version.js +1 -41
  41. package/dist/deploy.d.ts +16 -15
  42. package/dist/deploy.js +1 -246
  43. package/dist/dev.d.ts +23 -12
  44. package/dist/dev.js +1 -95
  45. package/dist/image-generator.d.ts +18 -2
  46. package/dist/image-generator.js +1 -59
  47. package/dist/project.d.ts +4 -1
  48. package/dist/project.js +1 -13
  49. package/dist/typecheck.d.ts +4 -2
  50. package/dist/typecheck.js +1 -44
  51. package/dist/upgrade/check-skill-current.d.ts +10 -5
  52. package/dist/upgrade/check-skill-current.js +1 -87
  53. package/dist/upgrade/ensure-template-remote.d.ts +10 -5
  54. package/dist/upgrade/ensure-template-remote.js +1 -56
  55. package/dist/upgrade/fast-forward-template-files.d.ts +8 -6
  56. package/dist/upgrade/fast-forward-template-files.js +1 -279
  57. package/package.json +7 -2
  58. package/config.test.ts +0 -15
  59. package/config.ts +0 -9
  60. package/coreframe.ts +0 -272
  61. package/db/client.ts +0 -72
  62. package/db/config.ts +0 -41
  63. package/db/drizzle-kit.test.ts +0 -27
  64. package/db/drizzle-kit.ts +0 -39
  65. package/db/ensure.ts +0 -67
  66. package/db/migrate-held.test.ts +0 -81
  67. package/db/migrate-held.ts +0 -166
  68. package/db/migrate.ts +0 -39
  69. package/db/project.ts +0 -28
  70. package/db/reset.ts +0 -24
  71. package/db/seed.ts +0 -38
  72. package/db/setup.ts +0 -18
  73. package/deploy/check-migrations.ts +0 -31
  74. package/deploy/checks.ts +0 -38
  75. package/deploy/config.test.ts +0 -24
  76. package/deploy/config.ts +0 -68
  77. package/deploy/migrations.test.ts +0 -110
  78. package/deploy/migrations.ts +0 -317
  79. package/deploy/pending-migrations.test.ts +0 -93
  80. package/deploy/pending-migrations.ts +0 -45
  81. package/deploy/tauri-release.test.ts +0 -196
  82. package/deploy/tauri-release.ts +0 -816
  83. package/deploy/version.test.ts +0 -29
  84. package/deploy/version.ts +0 -65
  85. package/deploy.test.ts +0 -101
  86. package/deploy.ts +0 -346
  87. package/dev.test.ts +0 -153
  88. package/dev.ts +0 -155
  89. package/image-generator.ts +0 -84
  90. package/project.ts +0 -6
  91. package/tsconfig.build.json +0 -12
  92. package/tsconfig.json +0 -20
  93. package/typecheck.ts +0 -55
  94. package/upgrade/check-skill-current.ts +0 -112
  95. package/upgrade/ensure-template-remote.ts +0 -79
  96. package/upgrade/fast-forward-template-files.ts +0 -408
@@ -1,166 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createHash } from "node:crypto"
3
- import { readFile } from "node:fs/promises"
4
-
5
- import { collectHeldMigrationFiles, formatMigrationList } from "../deploy/migrations.ts"
6
- import type { CoreframeDatabaseClient, CoreframeDatabaseDialect } from "./client.ts"
7
- import { createProjectDeployDatabase } from "./project.ts"
8
-
9
- const CONFIRM_FLAG = "--confirm=run-held-migrations"
10
- const HELD_HEADER_FIELDS = ["-- Held migration:", "-- Safe after:", "-- Verification:"] as const
11
-
12
- type MigrateHeldOptions = {
13
- argv?: string[]
14
- database?: CoreframeDatabaseClient
15
- env?: NodeJS.ProcessEnv
16
- }
17
-
18
- export async function migrateHeldMigrations({
19
- argv = process.argv.slice(2),
20
- database,
21
- env = process.env,
22
- }: MigrateHeldOptions = {}) {
23
- const migrations = await collectHeldMigrationFiles()
24
-
25
- console.log("Held migration files:")
26
- console.log(formatMigrationList(migrations))
27
-
28
- if (!argv.includes(CONFIRM_FLAG)) {
29
- throw new Error(`Held migrations require explicit confirmation. Re-run with ${CONFIRM_FLAG}.`)
30
- }
31
-
32
- if (migrations.length === 0) {
33
- console.log("No held migrations to run.")
34
- return
35
- }
36
-
37
- const activeDatabase = database ?? (await createProjectDeployDatabase({ env }))
38
-
39
- try {
40
- await ensureHeldMigrationJournal(activeDatabase)
41
-
42
- for (const migration of migrations) {
43
- const sql = await readFile(migration.path, "utf8")
44
- validateHeldMigrationHeader(sql, migration.relativePath)
45
- const sha256 = hashSql(sql)
46
- const applied = await activeDatabase.execute<{ sha256: string }>(
47
- heldMigrationSelectQuery(activeDatabase.dialect, migration.id),
48
- )
49
-
50
- if (applied.rows.length === 1) {
51
- if (applied.rows[0]?.sha256 !== sha256) {
52
- throw new Error(`Held migration ${migration.relativePath} changed after it was applied.`)
53
- }
54
-
55
- console.log(`Skipping already-applied held migration ${migration.relativePath}`)
56
- continue
57
- }
58
-
59
- console.log(`Applying held migration ${migration.relativePath}`)
60
- await runMigrationTransaction(activeDatabase, async (transactionDatabase) => {
61
- await transactionDatabase.execute(sql)
62
- await transactionDatabase.execute(
63
- heldMigrationInsertQuery(activeDatabase.dialect, migration.id, sha256),
64
- )
65
- })
66
- }
67
- } finally {
68
- if (!database) {
69
- await activeDatabase.close?.()
70
- }
71
- }
72
- }
73
-
74
- export function validateHeldMigrationHeader(sql: string, relativePath: string) {
75
- const missing = HELD_HEADER_FIELDS.filter((field) => !sql.includes(field))
76
-
77
- if (missing.length > 0) {
78
- throw new Error(
79
- `${relativePath} is missing held migration header fields: ${missing.join(", ")}`,
80
- )
81
- }
82
- }
83
-
84
- function hashSql(sql: string) {
85
- return createHash("sha256").update(sql).digest("hex")
86
- }
87
-
88
- async function ensureHeldMigrationJournal(database: CoreframeDatabaseClient) {
89
- for (const statement of heldMigrationJournalStatements(database.dialect)) {
90
- await database.execute(statement)
91
- }
92
- }
93
-
94
- function heldMigrationJournalStatements(dialect: CoreframeDatabaseDialect) {
95
- switch (dialect) {
96
- case "postgres":
97
- return [
98
- "create schema if not exists drizzle",
99
- `create table if not exists drizzle.__held_migrations (
100
- id text primary key,
101
- sha256 text not null,
102
- applied_at timestamptz not null default now()
103
- )`,
104
- ]
105
- case "sqlite":
106
- return [
107
- `create table if not exists __held_migrations (
108
- id text primary key,
109
- sha256 text not null,
110
- applied_at text not null default current_timestamp
111
- )`,
112
- ]
113
- }
114
- }
115
-
116
- export function heldMigrationSelectQuery(dialect: CoreframeDatabaseDialect, id: string) {
117
- return `select sha256 from ${heldMigrationJournalTable(dialect)} where id = ${sqlString(id)}`
118
- }
119
-
120
- export function heldMigrationInsertQuery(
121
- dialect: CoreframeDatabaseDialect,
122
- id: string,
123
- sha256: string,
124
- ) {
125
- return `insert into ${heldMigrationJournalTable(dialect)} (id, sha256) values (${sqlString(id)}, ${sqlString(sha256)})`
126
- }
127
-
128
- function heldMigrationJournalTable(dialect: CoreframeDatabaseDialect) {
129
- switch (dialect) {
130
- case "postgres":
131
- return "drizzle.__held_migrations"
132
- case "sqlite":
133
- return "__held_migrations"
134
- }
135
- }
136
-
137
- async function runMigrationTransaction(
138
- database: CoreframeDatabaseClient,
139
- run: (database: CoreframeDatabaseClient) => Promise<void>,
140
- ) {
141
- if (database.transaction) {
142
- await database.transaction(run)
143
- return
144
- }
145
-
146
- await database.execute("begin")
147
-
148
- try {
149
- await run(database)
150
- await database.execute("commit")
151
- } catch (error) {
152
- await database.execute("rollback")
153
- throw error
154
- }
155
- }
156
-
157
- function sqlString(value: string) {
158
- return `'${value.replaceAll("'", "''")}'`
159
- }
160
-
161
- if (import.meta.main) {
162
- migrateHeldMigrations().catch((error) => {
163
- console.error(error)
164
- process.exit(1)
165
- })
166
- }
package/db/migrate.ts DELETED
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
- import { access } from "node:fs/promises"
3
- import { resolve } from "node:path"
4
-
5
- import { migrate } from "drizzle-orm/pglite/migrator"
6
-
7
- import { importProjectModule } from "../project.ts"
8
- import { LOCAL_DB_DIR, MIGRATIONS_FOLDER } from "./config.ts"
9
-
10
- const migrationsFolder = resolve(MIGRATIONS_FOLDER)
11
-
12
- type LocalDbModule = {
13
- createLocalDb(dataDir?: string): {
14
- $client: {
15
- close(): Promise<void>
16
- }
17
- }
18
- }
19
-
20
- export async function migrateLocalDb() {
21
- await access(migrationsFolder)
22
- const { createLocalDb } = await importProjectModule<LocalDbModule>("src/db/local.ts")
23
-
24
- const db = createLocalDb(LOCAL_DB_DIR)
25
-
26
- try {
27
- await migrate(db as never, { migrationsFolder })
28
- console.log(`Applied local migrations from ${migrationsFolder}`)
29
- } finally {
30
- await db.$client.close()
31
- }
32
- }
33
-
34
- if (import.meta.main) {
35
- migrateLocalDb().catch((error) => {
36
- console.error(error)
37
- process.exit(1)
38
- })
39
- }
package/db/project.ts DELETED
@@ -1,28 +0,0 @@
1
- import { importProjectModule } from "../project.ts"
2
- import type { CoreframeDatabaseClient } from "./client.ts"
3
-
4
- const projectDatabaseConfigPath = "scripts/coreframe.db.ts"
5
-
6
- export type CreateProjectDatabaseOptions = {
7
- env?: NodeJS.ProcessEnv
8
- }
9
-
10
- export type ProjectDatabaseModule = {
11
- createDeployDatabase?(
12
- options?: CreateProjectDatabaseOptions,
13
- ): CoreframeDatabaseClient | Promise<CoreframeDatabaseClient>
14
- }
15
-
16
- export async function createProjectDeployDatabase(
17
- options: CreateProjectDatabaseOptions = {},
18
- ): Promise<CoreframeDatabaseClient> {
19
- const module = await importProjectModule<ProjectDatabaseModule>(projectDatabaseConfigPath)
20
-
21
- if (!module.createDeployDatabase) {
22
- throw new Error(
23
- `${projectDatabaseConfigPath} must export createDeployDatabase() for Coreframe database workflows.`,
24
- )
25
- }
26
-
27
- return module.createDeployDatabase(options)
28
- }
package/db/reset.ts DELETED
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env node
2
- import { rm } from "node:fs/promises"
3
- import { relative, resolve } from "node:path"
4
-
5
- import { LOCAL_DB_DIR } from "./config.ts"
6
-
7
- export async function resetLocalDb() {
8
- const dataDir = resolve(LOCAL_DB_DIR)
9
- const relativeDataDir = relative(process.cwd(), dataDir)
10
-
11
- if (relativeDataDir === "" || relativeDataDir.startsWith("..")) {
12
- throw new Error(`Refusing to reset local database outside the project: ${dataDir}`)
13
- }
14
-
15
- await rm(dataDir, { force: true, recursive: true })
16
- console.log(`Removed local PGlite data at ${dataDir}`)
17
- }
18
-
19
- if (import.meta.main) {
20
- resetLocalDb().catch((error) => {
21
- console.error(error)
22
- process.exit(1)
23
- })
24
- }
package/db/seed.ts DELETED
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- import { importProjectModule } from "../project.ts"
3
- import { getSeedProfile, LOCAL_DB_DIR } from "./config.ts"
4
- import type { SeedProfile } from "./config.ts"
5
-
6
- type LocalDbModule = {
7
- createLocalDb(dataDir?: string): {
8
- $client: {
9
- close(): Promise<void>
10
- }
11
- }
12
- }
13
-
14
- type SeedUsersModule = {
15
- seedUsers(db: unknown, profile: SeedProfile): Promise<void>
16
- }
17
-
18
- export async function seedLocalDb(profile: SeedProfile = getSeedProfile()) {
19
- const [{ createLocalDb }, { seedUsers }] = await Promise.all([
20
- importProjectModule<LocalDbModule>("src/db/local.ts"),
21
- importProjectModule<SeedUsersModule>("scripts/db/seeds/users.ts"),
22
- ])
23
- const db = createLocalDb(LOCAL_DB_DIR)
24
-
25
- try {
26
- await seedUsers(db, profile)
27
- console.log(`Seeded local database with "${profile}" profile`)
28
- } finally {
29
- await db.$client.close()
30
- }
31
- }
32
-
33
- if (import.meta.main) {
34
- seedLocalDb().catch((error) => {
35
- console.error(error)
36
- process.exit(1)
37
- })
38
- }
package/db/setup.ts DELETED
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env node
2
- import type { SeedProfile } from "./config.ts"
3
- import { migrateLocalDb } from "./migrate.ts"
4
- import { resetLocalDb } from "./reset.ts"
5
- import { seedLocalDb } from "./seed.ts"
6
-
7
- export async function setupLocalDb(profile: SeedProfile = "small") {
8
- await resetLocalDb()
9
- await migrateLocalDb()
10
- await seedLocalDb(profile)
11
- }
12
-
13
- if (import.meta.main) {
14
- setupLocalDb().catch((error) => {
15
- console.error(error)
16
- process.exit(1)
17
- })
18
- }
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- collectDeployMigrationFiles,
4
- formatMigrationList,
5
- formatMigrationScanFailures,
6
- scanDeployMigrationFiles,
7
- } from "./migrations.ts"
8
-
9
- export async function checkDeployMigrations() {
10
- const migrations = await collectDeployMigrationFiles()
11
-
12
- console.log("Deploy migration files:")
13
- console.log(formatMigrationList(migrations))
14
-
15
- const failures = await scanDeployMigrationFiles(migrations)
16
-
17
- if (failures.length > 0) {
18
- throw new Error(
19
- `Deploy migrations contain unsafe SQL. Move unsafe statements to held migrations.\n${formatMigrationScanFailures(failures)}`,
20
- )
21
- }
22
-
23
- console.log("Deploy migrations passed safety checks.")
24
- }
25
-
26
- if (import.meta.main) {
27
- checkDeployMigrations().catch((error) => {
28
- console.error(error)
29
- process.exit(1)
30
- })
31
- }
package/deploy/checks.ts DELETED
@@ -1,38 +0,0 @@
1
- import type { DeployContext, ResolvedDeployConfig } from "./config.ts"
2
-
3
- export async function runPostDeployChecks(config: ResolvedDeployConfig, context: DeployContext) {
4
- const productionUrl = context.productionUrl ?? config.productionUrl
5
-
6
- if (config.smokeChecks.length > 0 && !productionUrl) {
7
- console.log("Skipping HTTP smoke checks. Set DEPLOY_URL or productionUrl in deploy config.")
8
- }
9
-
10
- for (const check of config.smokeChecks) {
11
- if (!productionUrl && !check.url) {
12
- continue
13
- }
14
-
15
- const url = check.url ?? new URL(check.path ?? "/", productionUrl).toString()
16
- const response = await fetch(url)
17
-
18
- if (!statusMatches(response.status, check.expectedStatus ?? "2xx")) {
19
- throw new Error(`${check.name} smoke check failed: ${url} returned ${response.status}.`)
20
- }
21
-
22
- console.log(`${check.name} smoke check passed: ${response.status} ${url}`)
23
- }
24
-
25
- await config.afterDeploy?.({ ...context, productionUrl })
26
- }
27
-
28
- function statusMatches(status: number, expected: "2xx" | number | number[]) {
29
- if (expected === "2xx") {
30
- return status >= 200 && status < 300
31
- }
32
-
33
- if (Array.isArray(expected)) {
34
- return expected.includes(status)
35
- }
36
-
37
- return status === expected
38
- }
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
-
3
- import { resolveDeployConfig } from "./config.ts"
4
-
5
- describe("resolveDeployConfig", () => {
6
- it("uses a generic homepage smoke check by default", () => {
7
- expect(resolveDeployConfig({}, {}).smokeChecks).toEqual([{ name: "home", path: "/" }])
8
- })
9
-
10
- it("lets project config override smoke checks", () => {
11
- const config = resolveDeployConfig(
12
- { smokeChecks: [{ name: "api", path: "/api/health", expectedStatus: 204 }] },
13
- {},
14
- )
15
-
16
- expect(config.smokeChecks).toEqual([{ name: "api", path: "/api/health", expectedStatus: 204 }])
17
- })
18
-
19
- it("falls back to DEPLOY_URL for production checks", () => {
20
- expect(resolveDeployConfig({}, { DEPLOY_URL: "https://example.com" }).productionUrl).toBe(
21
- "https://example.com",
22
- )
23
- })
24
- })
package/deploy/config.ts DELETED
@@ -1,68 +0,0 @@
1
- export type DeployContext = {
2
- environment: string
3
- productionUrl?: string
4
- releaseName: string
5
- version: string
6
- }
7
-
8
- export const tauriReleasePlatforms = ["macos", "windows", "linux", "ios", "android"] as const
9
-
10
- export type TauriReleasePlatform = (typeof tauriReleasePlatforms)[number]
11
-
12
- export type TauriReleasePlatformConfig = {
13
- artifactGlobs?: string[]
14
- buildArgs?: string[]
15
- targetTriple?: string
16
- updatePlatform?: string
17
- }
18
-
19
- export type TauriReleaseConfig = {
20
- artifactDownloadDir?: string
21
- b2Bucket?: string
22
- latestManifestPath?: string
23
- localPlatforms?: TauriReleasePlatform[]
24
- platform?: Partial<Record<TauriReleasePlatform, TauriReleasePlatformConfig>>
25
- publicBaseUrl?: string
26
- publicPathPrefix?: string
27
- releaseNotesPath?: string
28
- releasePath?: string
29
- releaseUploadDir?: string
30
- workflowPlatforms?: TauriReleasePlatform[]
31
- workflowName?: string
32
- workflowPollIntervalMs?: number
33
- workflowRef?: string
34
- workflowTimeoutMs?: number
35
- }
36
-
37
- export type DeploySmokeCheck = {
38
- name: string
39
- path?: string
40
- url?: string
41
- expectedStatus?: "2xx" | number | number[]
42
- }
43
-
44
- export type DeployConfig = {
45
- afterDeploy?: (context: DeployContext) => Promise<void> | void
46
- productionUrl?: string
47
- smokeChecks?: DeploySmokeCheck[]
48
- tauriRelease?: TauriReleaseConfig
49
- }
50
-
51
- export type ResolvedDeployConfig = {
52
- afterDeploy?: (context: DeployContext) => Promise<void> | void
53
- productionUrl?: string
54
- smokeChecks: DeploySmokeCheck[]
55
- tauriRelease: TauriReleaseConfig
56
- }
57
-
58
- export function resolveDeployConfig(
59
- config: DeployConfig,
60
- env: NodeJS.ProcessEnv = process.env,
61
- ): ResolvedDeployConfig {
62
- return {
63
- afterDeploy: config.afterDeploy,
64
- productionUrl: config.productionUrl ?? env.DEPLOY_URL ?? env.PRODUCTION_URL,
65
- smokeChecks: config.smokeChecks ?? [{ name: "home", path: "/" }],
66
- tauriRelease: config.tauriRelease ?? {},
67
- }
68
- }
@@ -1,110 +0,0 @@
1
- import { mkdtemp, mkdir, writeFile } from "node:fs/promises"
2
- import { tmpdir } from "node:os"
3
- import { join } from "node:path"
4
-
5
- import { describe, expect, it } from "vitest"
6
-
7
- import {
8
- collectDeployMigrationFiles,
9
- scanDeployMigrationFiles,
10
- scanMigrationSql,
11
- } from "./migrations.ts"
12
-
13
- describe("scanMigrationSql", () => {
14
- it("allows additive schema created inside the same migration", () => {
15
- const risks = scanMigrationSql(`
16
- CREATE TABLE "users" (
17
- "id" uuid PRIMARY KEY,
18
- "email" text NOT NULL UNIQUE
19
- );
20
- CREATE TABLE "profiles" (
21
- "user_id" uuid PRIMARY KEY,
22
- "display_name" text DEFAULT '' NOT NULL
23
- );
24
- CREATE UNIQUE INDEX "profiles_user_id_uidx" ON "profiles" ("user_id");
25
- ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_users_id_fkey"
26
- FOREIGN KEY ("user_id") REFERENCES "users"("id");
27
- `)
28
-
29
- expect(risks).toEqual([])
30
- })
31
-
32
- it("blocks destructive and contract-style SQL", () => {
33
- const risks = scanMigrationSql(`
34
- ALTER TABLE "users" DROP COLUMN "name";
35
- ALTER TABLE "users" RENAME COLUMN "email" TO "primary_email";
36
- ALTER TABLE "users" ALTER COLUMN "active" SET NOT NULL;
37
- ALTER TABLE "users" ALTER COLUMN "created_at" TYPE text;
38
- UPDATE "users" SET "active" = false;
39
- DELETE FROM "users" WHERE "active" = false;
40
- DROP INDEX "users_email_idx";
41
- `)
42
-
43
- expect(risks.map((risk) => risk.code)).toEqual([
44
- "drop-column",
45
- "rename-table-or-column",
46
- "set-not-null",
47
- "rewrite-column-type",
48
- "broad-update",
49
- "broad-delete",
50
- "drop-schema-object",
51
- ])
52
- })
53
-
54
- it("blocks constraint enforcement on existing tables", () => {
55
- const risks = scanMigrationSql(`
56
- ALTER TABLE "users" ADD COLUMN "handle" text NOT NULL;
57
- ALTER TABLE "users" ADD CONSTRAINT "users_handle_unique" UNIQUE ("handle");
58
- CREATE UNIQUE INDEX "users_handle_idx" ON "users" ("handle");
59
- `)
60
-
61
- expect(risks.map((risk) => risk.code)).toEqual([
62
- "add-not-null-column",
63
- "add-constraint",
64
- "create-unique-index",
65
- ])
66
- })
67
-
68
- it("ignores unsafe words inside comments and string literals", () => {
69
- const risks = scanMigrationSql(`
70
- -- ALTER TABLE users DROP COLUMN name;
71
- CREATE TABLE "audit" (
72
- "id" uuid PRIMARY KEY,
73
- "message" text DEFAULT 'DROP TABLE users' NOT NULL
74
- );
75
- `)
76
-
77
- expect(risks).toEqual([])
78
- })
79
- })
80
-
81
- describe("collectDeployMigrationFiles", () => {
82
- it("collects Drizzle directory migrations in release order", async () => {
83
- const root = await mkdtemp(join(tmpdir(), "coreframe-migrations-"))
84
- await mkdir(join(root, "drizzle", "20260703120000_first"), { recursive: true })
85
- await mkdir(join(root, "drizzle", "20260703130000_second"), { recursive: true })
86
- await writeFile(join(root, "drizzle", "20260703130000_second", "migration.sql"), "select 2;")
87
- await writeFile(join(root, "drizzle", "20260703120000_first", "migration.sql"), "select 1;")
88
-
89
- const migrations = await collectDeployMigrationFiles(root)
90
-
91
- expect(migrations.map((migration) => migration.relativePath)).toEqual([
92
- "drizzle/20260703120000_first/migration.sql",
93
- "drizzle/20260703130000_second/migration.sql",
94
- ])
95
- })
96
-
97
- it("reports unsafe SQL from collected files", async () => {
98
- const root = await mkdtemp(join(tmpdir(), "coreframe-migration-scan-"))
99
- await mkdir(join(root, "drizzle", "20260703120000_drop_name"), { recursive: true })
100
- await writeFile(
101
- join(root, "drizzle", "20260703120000_drop_name", "migration.sql"),
102
- 'ALTER TABLE "users" DROP COLUMN "name";',
103
- )
104
-
105
- const failures = await scanDeployMigrationFiles(await collectDeployMigrationFiles(root))
106
-
107
- expect(failures).toHaveLength(1)
108
- expect(failures[0]?.risks[0]?.code).toBe("drop-column")
109
- })
110
- })