@zeno-lib/db 0.6.0 → 0.7.0
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.
- package/dist/config.d.mts +8 -1
- package/dist/config.mjs +46 -2
- package/package.json +1 -2
- package/src/config.ts +15 -3
- package/src/move-generated-sql.test.ts +98 -0
- package/src/move-generated-sql.ts +80 -0
package/dist/config.d.mts
CHANGED
|
@@ -15,6 +15,13 @@ import { Config, defineConfig } from "drizzle-kit";
|
|
|
15
15
|
* Verified against Supabase CLI 2.84.1 / Postgres 17.
|
|
16
16
|
*/
|
|
17
17
|
declare const supabaseManagedRoles: readonly ["pg_checkpoint", "pg_database_owner", "pg_execute_server_program", "pg_monitor", "pg_read_all_data", "pg_read_all_settings", "pg_read_all_stats", "pg_read_server_files", "pg_signal_backend", "pg_stat_scan_tables", "pg_write_all_data", "pg_write_server_files", "pgbouncer", "postgres", "supabase_functions_admin", "supabase_read_only_user", "supabase_realtime_admin", "supabase_replication_admin"];
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Drizzle Kit config for migrations the Supabase CLI applies.
|
|
20
|
+
* Moves each generated `<name>/migration.sql` to `<name>.sql` when `drizzle-kit generate` exits.
|
|
21
|
+
* Set `moveGeneratedSql: false` if `drizzle-kit migrate` applies them instead.
|
|
22
|
+
*/
|
|
23
|
+
declare function defineDrizzleConfig(overrides?: Partial<Config> & {
|
|
24
|
+
moveGeneratedSql?: boolean;
|
|
25
|
+
}): ReturnType<typeof defineConfig>;
|
|
19
26
|
//#endregion
|
|
20
27
|
export { defineDrizzleConfig, supabaseManagedRoles };
|
package/dist/config.mjs
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { existsSync, readdirSync, renameSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
//#region src/move-generated-sql.ts
|
|
6
|
+
const GENERATED_SQL = "migration.sql";
|
|
7
|
+
/**
|
|
8
|
+
* Move each `<name>/migration.sql` to `<name>.sql`,
|
|
9
|
+
* because the Supabase CLI only reads `.sql` files at the top of the folder.
|
|
10
|
+
* Throws if a `<name>.sql` already exists.
|
|
11
|
+
*/
|
|
12
|
+
function moveGeneratedSql(options) {
|
|
13
|
+
const { migrationsDir } = options;
|
|
14
|
+
const names = existsSync(migrationsDir) ? readdirSync(migrationsDir).filter((name) => existsSync(join(migrationsDir, name, GENERATED_SQL))).sort() : [];
|
|
15
|
+
const taken = names.map((name) => join(migrationsDir, `${name}.sql`)).filter((target) => existsSync(target));
|
|
16
|
+
if (taken.length > 0) throw new Error(`Cannot move the generated SQL, because these files already exist: ${taken.join(", ")}`);
|
|
17
|
+
return { moved: names.map((name) => {
|
|
18
|
+
const target = join(migrationsDir, `${name}.sql`);
|
|
19
|
+
renameSync(join(migrationsDir, name, GENERATED_SQL), target);
|
|
20
|
+
return target;
|
|
21
|
+
}) };
|
|
22
|
+
}
|
|
23
|
+
/** Run `moveGeneratedSql` when `drizzle-kit generate` exits. */
|
|
24
|
+
function moveGeneratedSqlAfterGenerate(options) {
|
|
25
|
+
if (process.argv[2] !== "generate") return;
|
|
26
|
+
process.once("exit", (code) => {
|
|
27
|
+
if (code !== 0) return;
|
|
28
|
+
try {
|
|
29
|
+
const check = styleText("green", "✓", { stream: process.stderr });
|
|
30
|
+
for (const path of moveGeneratedSql(options).moved) console.error(`${check} Moved the generated SQL to ${path}`);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
33
|
+
console.error(styleText("red", `✗ ${message}`, { stream: process.stderr }));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
2
39
|
//#region src/config.ts
|
|
3
40
|
/**
|
|
4
41
|
* Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
|
|
@@ -40,10 +77,15 @@ const supabaseManagedRoles = [
|
|
|
40
77
|
* never right. Add your own schemas here rather than dropping the filter.
|
|
41
78
|
*/
|
|
42
79
|
const DEFAULT_SCHEMA_FILTER = ["public"];
|
|
80
|
+
/**
|
|
81
|
+
* Drizzle Kit config for migrations the Supabase CLI applies.
|
|
82
|
+
* Moves each generated `<name>/migration.sql` to `<name>.sql` when `drizzle-kit generate` exits.
|
|
83
|
+
* Set `moveGeneratedSql: false` if `drizzle-kit migrate` applies them instead.
|
|
84
|
+
*/
|
|
43
85
|
function defineDrizzleConfig(overrides = {}) {
|
|
44
|
-
const { entities, ...configOverrides } = overrides;
|
|
86
|
+
const { entities, moveGeneratedSql = true, ...configOverrides } = overrides;
|
|
45
87
|
const roleOverrides = typeof entities?.roles === "object" ? entities.roles : {};
|
|
46
|
-
|
|
88
|
+
const config = defineConfig({
|
|
47
89
|
dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
|
|
48
90
|
dialect: "postgresql",
|
|
49
91
|
entities: {
|
|
@@ -59,6 +101,8 @@ function defineDrizzleConfig(overrides = {}) {
|
|
|
59
101
|
schemaFilter: DEFAULT_SCHEMA_FILTER,
|
|
60
102
|
...configOverrides
|
|
61
103
|
});
|
|
104
|
+
if (moveGeneratedSql && config.out) moveGeneratedSqlAfterGenerate({ migrationsDir: config.out });
|
|
105
|
+
return config;
|
|
62
106
|
}
|
|
63
107
|
//#endregion
|
|
64
108
|
export { defineDrizzleConfig, supabaseManagedRoles };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
"stop": "pnpm exec supabase stop",
|
|
93
93
|
"reset": "pnpm exec supabase db reset",
|
|
94
94
|
"db:generate": "drizzle-kit generate",
|
|
95
|
-
"db:migrate": "drizzle-kit migrate",
|
|
96
95
|
"db:push": "drizzle-kit push",
|
|
97
96
|
"db:studio": "drizzle-kit studio",
|
|
98
97
|
"test": "vitest --run",
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// https://orm.drizzle.team/docs/drizzle-config-file
|
|
2
2
|
// https://orm.drizzle.team/docs/rls#migrations (entities.roles.provider: "supabase")
|
|
3
3
|
import { type Config, defineConfig } from "drizzle-kit"
|
|
4
|
+
import { moveGeneratedSqlAfterGenerate } from "./move-generated-sql.ts"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
|
|
@@ -44,17 +45,22 @@ export const supabaseManagedRoles = [
|
|
|
44
45
|
*/
|
|
45
46
|
const DEFAULT_SCHEMA_FILTER = ["public"]
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Drizzle Kit config for migrations the Supabase CLI applies.
|
|
50
|
+
* Moves each generated `<name>/migration.sql` to `<name>.sql` when `drizzle-kit generate` exits.
|
|
51
|
+
* Set `moveGeneratedSql: false` if `drizzle-kit migrate` applies them instead.
|
|
52
|
+
*/
|
|
47
53
|
export function defineDrizzleConfig(
|
|
48
|
-
overrides: Partial<Config> = {}
|
|
54
|
+
overrides: Partial<Config> & { moveGeneratedSql?: boolean } = {}
|
|
49
55
|
): ReturnType<typeof defineConfig> {
|
|
50
|
-
const { entities, ...configOverrides } = overrides
|
|
56
|
+
const { entities, moveGeneratedSql = true, ...configOverrides } = overrides
|
|
51
57
|
// `entities.roles` may be a boolean (`true`) in drizzle-kit config; in that
|
|
52
58
|
// form there are no role options to preserve, so we only merge the object
|
|
53
59
|
// form. The `provider: "supabase"` flag below is always enforced regardless.
|
|
54
60
|
const roleOverrides =
|
|
55
61
|
typeof entities?.roles === "object" ? entities.roles : {}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
const config = defineConfig({
|
|
58
64
|
dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
|
|
59
65
|
dialect: "postgresql",
|
|
60
66
|
// Tells drizzle-kit that Supabase's built-in roles (anon, authenticated,
|
|
@@ -74,4 +80,10 @@ export function defineDrizzleConfig(
|
|
|
74
80
|
schemaFilter: DEFAULT_SCHEMA_FILTER,
|
|
75
81
|
...configOverrides,
|
|
76
82
|
} as Config)
|
|
83
|
+
|
|
84
|
+
if (moveGeneratedSql && config.out) {
|
|
85
|
+
moveGeneratedSqlAfterGenerate({ migrationsDir: config.out })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return config
|
|
77
89
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs"
|
|
9
|
+
import { tmpdir } from "node:os"
|
|
10
|
+
import { join } from "node:path"
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
12
|
+
import { moveGeneratedSql } from "./move-generated-sql.ts"
|
|
13
|
+
|
|
14
|
+
const POSTS = "20260102030405_posts"
|
|
15
|
+
const COMMENTS = "20260102030406_comments"
|
|
16
|
+
|
|
17
|
+
let migrationsDir: string
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
migrationsDir = mkdtempSync(join(tmpdir(), "zeno-db-migrations-"))
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(migrationsDir, { force: true, recursive: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Write `<name>/migration.sql` and `<name>/snapshot.json`, as `drizzle-kit generate` does.
|
|
28
|
+
function generated(name: string): void {
|
|
29
|
+
mkdirSync(join(migrationsDir, name))
|
|
30
|
+
writeFileSync(join(migrationsDir, name, "migration.sql"), `-- ${name}`)
|
|
31
|
+
writeFileSync(join(migrationsDir, name, "snapshot.json"), "{}")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The migrations folder, sorted.
|
|
35
|
+
const files = () => readdirSync(migrationsDir).sort()
|
|
36
|
+
|
|
37
|
+
const read = (name: string) => readFileSync(join(migrationsDir, name), "utf-8")
|
|
38
|
+
|
|
39
|
+
describe("moveGeneratedSql", () => {
|
|
40
|
+
it("moves migration.sql next to its folder", () => {
|
|
41
|
+
generated(POSTS)
|
|
42
|
+
|
|
43
|
+
moveGeneratedSql({ migrationsDir })
|
|
44
|
+
|
|
45
|
+
expect(files()).toEqual([POSTS, `${POSTS}.sql`])
|
|
46
|
+
expect(readdirSync(join(migrationsDir, POSTS))).toEqual(["snapshot.json"])
|
|
47
|
+
expect(read(`${POSTS}.sql`)).toBe(`-- ${POSTS}`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("returns the new paths in name order", () => {
|
|
51
|
+
generated(COMMENTS)
|
|
52
|
+
generated(POSTS)
|
|
53
|
+
|
|
54
|
+
expect(moveGeneratedSql({ migrationsDir })).toEqual({
|
|
55
|
+
moved: [
|
|
56
|
+
join(migrationsDir, `${POSTS}.sql`),
|
|
57
|
+
join(migrationsDir, `${COMMENTS}.sql`),
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("moves nothing on a second run", () => {
|
|
63
|
+
generated(POSTS)
|
|
64
|
+
moveGeneratedSql({ migrationsDir })
|
|
65
|
+
|
|
66
|
+
expect(moveGeneratedSql({ migrationsDir })).toEqual({ moved: [] })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("leaves a hand-written .sql file alone", () => {
|
|
70
|
+
generated(POSTS)
|
|
71
|
+
writeFileSync(join(migrationsDir, "20260102030407_grants.sql"), "grant;")
|
|
72
|
+
|
|
73
|
+
moveGeneratedSql({ migrationsDir })
|
|
74
|
+
|
|
75
|
+
expect(files()).toEqual([
|
|
76
|
+
POSTS,
|
|
77
|
+
`${POSTS}.sql`,
|
|
78
|
+
"20260102030407_grants.sql",
|
|
79
|
+
])
|
|
80
|
+
expect(read("20260102030407_grants.sql")).toBe("grant;")
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it("throws before moving anything when a .sql file is in the way", () => {
|
|
84
|
+
generated(POSTS)
|
|
85
|
+
generated(COMMENTS)
|
|
86
|
+
writeFileSync(join(migrationsDir, `${COMMENTS}.sql`), "-- edited by hand")
|
|
87
|
+
|
|
88
|
+
expect(() => moveGeneratedSql({ migrationsDir })).toThrow(`${COMMENTS}.sql`)
|
|
89
|
+
expect(files()).toEqual([POSTS, COMMENTS, `${COMMENTS}.sql`])
|
|
90
|
+
expect(read(`${COMMENTS}.sql`)).toBe("-- edited by hand")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("returns nothing for a missing folder", () => {
|
|
94
|
+
expect(
|
|
95
|
+
moveGeneratedSql({ migrationsDir: join(migrationsDir, "missing") })
|
|
96
|
+
).toEqual({ moved: [] })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readdirSync, renameSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { styleText } from "node:util"
|
|
4
|
+
|
|
5
|
+
export type MoveGeneratedSqlOptions = {
|
|
6
|
+
/** drizzle-kit writes migrations to this folder. */
|
|
7
|
+
migrationsDir: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type MoveGeneratedSqlResult = {
|
|
11
|
+
/** The new paths of the SQL files, sorted by name. */
|
|
12
|
+
moved: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const GENERATED_SQL = "migration.sql"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Move each `<name>/migration.sql` to `<name>.sql`,
|
|
19
|
+
* because the Supabase CLI only reads `.sql` files at the top of the folder.
|
|
20
|
+
* Throws if a `<name>.sql` already exists.
|
|
21
|
+
*/
|
|
22
|
+
export function moveGeneratedSql(
|
|
23
|
+
options: MoveGeneratedSqlOptions
|
|
24
|
+
): MoveGeneratedSqlResult {
|
|
25
|
+
const { migrationsDir } = options
|
|
26
|
+
|
|
27
|
+
const names = existsSync(migrationsDir)
|
|
28
|
+
? readdirSync(migrationsDir)
|
|
29
|
+
.filter((name) => existsSync(join(migrationsDir, name, GENERATED_SQL)))
|
|
30
|
+
.sort()
|
|
31
|
+
: []
|
|
32
|
+
|
|
33
|
+
const taken = names
|
|
34
|
+
.map((name) => join(migrationsDir, `${name}.sql`))
|
|
35
|
+
.filter((target) => existsSync(target))
|
|
36
|
+
if (taken.length > 0) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Cannot move the generated SQL, because these files already exist: ${taken.join(", ")}`
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const moved = names.map((name) => {
|
|
43
|
+
const target = join(migrationsDir, `${name}.sql`)
|
|
44
|
+
renameSync(join(migrationsDir, name, GENERATED_SQL), target)
|
|
45
|
+
return target
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { moved }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Run `moveGeneratedSql` when `drizzle-kit generate` exits. */
|
|
52
|
+
export function moveGeneratedSqlAfterGenerate(
|
|
53
|
+
options: MoveGeneratedSqlOptions
|
|
54
|
+
): void {
|
|
55
|
+
// drizzle-kit loads the config for every command.
|
|
56
|
+
// Only `generate` writes a migration.
|
|
57
|
+
if (process.argv[2] !== "generate") {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.once("exit", (code) => {
|
|
62
|
+
if (code !== 0) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Print to stderr, so the JSON from `--output json` on stdout stays valid.
|
|
67
|
+
try {
|
|
68
|
+
const check = styleText("green", "✓", { stream: process.stderr })
|
|
69
|
+
for (const path of moveGeneratedSql(options).moved) {
|
|
70
|
+
console.error(`${check} Moved the generated SQL to ${path}`)
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
74
|
+
console.error(
|
|
75
|
+
styleText("red", `✗ ${message}`, { stream: process.stderr })
|
|
76
|
+
)
|
|
77
|
+
process.exitCode = 1
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|