@zeno-lib/db 0.1.0 → 0.3.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/package.json +5 -3
- package/src/auth-schema.test.ts +59 -0
- package/src/auth-schema.ts +94 -0
- package/src/casing.test.ts +582 -28
- package/src/config.ts +51 -2
- package/src/schema.test-d.ts +121 -0
- package/src/schema.ts +494 -43
- package/src/triggers.test.ts +131 -0
- package/src/triggers.ts +171 -0
package/src/config.ts
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
// https://orm.drizzle.team/docs/rls#migrations (entities.roles.provider: "supabase")
|
|
3
3
|
import { type Config, defineConfig } from "drizzle-kit"
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
|
|
7
|
+
* not already exclude. That provider covers eight names (`anon`,
|
|
8
|
+
* `authenticator`, `authenticated`, `service_role`, `supabase_auth_admin`,
|
|
9
|
+
* `supabase_storage_admin`, `dashboard_user`, `supabase_admin`); a real
|
|
10
|
+
* instance has these too, and drizzle-kit manages every role it finds but
|
|
11
|
+
* cannot see declared, which means a `DROP ROLE` for each.
|
|
12
|
+
*
|
|
13
|
+
* The alternative is every consumer keeping a generated `roles.ts` nobody
|
|
14
|
+
* reads, or its own copy of this list, which rots as Supabase adds roles. That
|
|
15
|
+
* is the argument for it living in a versioned dependency.
|
|
16
|
+
*
|
|
17
|
+
* Verified against Supabase CLI 2.84.1 / Postgres 17.
|
|
18
|
+
*/
|
|
19
|
+
export const supabaseManagedRoles = [
|
|
20
|
+
"pg_checkpoint",
|
|
21
|
+
"pg_database_owner",
|
|
22
|
+
"pg_execute_server_program",
|
|
23
|
+
"pg_monitor",
|
|
24
|
+
"pg_read_all_data",
|
|
25
|
+
"pg_read_all_settings",
|
|
26
|
+
"pg_read_all_stats",
|
|
27
|
+
"pg_read_server_files",
|
|
28
|
+
"pg_signal_backend",
|
|
29
|
+
"pg_stat_scan_tables",
|
|
30
|
+
"pg_write_all_data",
|
|
31
|
+
"pg_write_server_files",
|
|
32
|
+
"pgbouncer",
|
|
33
|
+
"postgres",
|
|
34
|
+
"supabase_functions_admin",
|
|
35
|
+
"supabase_read_only_user",
|
|
36
|
+
"supabase_realtime_admin",
|
|
37
|
+
"supabase_replication_admin",
|
|
38
|
+
] as const
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Schemas drizzle-kit is allowed to diff. Supabase owns `auth`, `storage`,
|
|
42
|
+
* `realtime` and the rest, and changes them on upgrades, so migrating them is
|
|
43
|
+
* never right. Add your own schemas here rather than dropping the filter.
|
|
44
|
+
*/
|
|
45
|
+
const DEFAULT_SCHEMA_FILTER = ["public"]
|
|
46
|
+
|
|
5
47
|
export function defineDrizzleConfig(
|
|
6
48
|
overrides: Partial<Config> = {}
|
|
7
49
|
): ReturnType<typeof defineConfig> {
|
|
@@ -16,13 +58,20 @@ export function defineDrizzleConfig(
|
|
|
16
58
|
dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
|
|
17
59
|
dialect: "postgresql",
|
|
18
60
|
// Tells drizzle-kit that Supabase's built-in roles (anon, authenticated,
|
|
19
|
-
// service_role, ...) already exist
|
|
61
|
+
// service_role, ...) already exist, so it neither creates nor drops them.
|
|
62
|
+
// `provider` covers eight of them; `supabaseManagedRoles` covers the rest.
|
|
63
|
+
// A caller's own excludes are kept.
|
|
20
64
|
entities: {
|
|
21
65
|
...entities,
|
|
22
|
-
roles: {
|
|
66
|
+
roles: {
|
|
67
|
+
...roleOverrides,
|
|
68
|
+
exclude: [...(roleOverrides.exclude ?? []), ...supabaseManagedRoles],
|
|
69
|
+
provider: "supabase",
|
|
70
|
+
},
|
|
23
71
|
},
|
|
24
72
|
out: "./supabase/migrations",
|
|
25
73
|
schema: "./src/schema.ts",
|
|
74
|
+
schemaFilter: DEFAULT_SCHEMA_FILTER,
|
|
26
75
|
...configOverrides,
|
|
27
76
|
} as Config)
|
|
28
77
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { expectTypeOf, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
auditColumns,
|
|
5
|
+
authorship,
|
|
6
|
+
authUserId,
|
|
7
|
+
primaryId,
|
|
8
|
+
sequentialPrimaryId,
|
|
9
|
+
table,
|
|
10
|
+
userId,
|
|
11
|
+
uuidPrimaryId,
|
|
12
|
+
} from "./schema.ts"
|
|
13
|
+
|
|
14
|
+
test("primaryId infers each kind, and falls back to sequential", () => {
|
|
15
|
+
const fallback = table("fallback", { id: primaryId() })
|
|
16
|
+
const sequential = table("sequential", { id: primaryId("sequential") })
|
|
17
|
+
const uuid = table("uuid", { id: primaryId("uuid") })
|
|
18
|
+
const assigned = table("assigned", { id: primaryId("assigned") })
|
|
19
|
+
|
|
20
|
+
// Sequential keys are `number`, not `bigint`, because PostgREST serialises
|
|
21
|
+
// to JSON numbers. The bare call is sequential, matching the dashboard.
|
|
22
|
+
expectTypeOf<
|
|
23
|
+
(typeof fallback)["$inferSelect"]["id"]
|
|
24
|
+
>().toEqualTypeOf<number>()
|
|
25
|
+
expectTypeOf<
|
|
26
|
+
(typeof sequential)["$inferSelect"]["id"]
|
|
27
|
+
>().toEqualTypeOf<number>()
|
|
28
|
+
expectTypeOf<(typeof uuid)["$inferSelect"]["id"]>().toEqualTypeOf<string>()
|
|
29
|
+
expectTypeOf<
|
|
30
|
+
(typeof assigned)["$inferSelect"]["id"]
|
|
31
|
+
>().toEqualTypeOf<string>()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("sequential options change the inferred JavaScript type", () => {
|
|
35
|
+
const int = table("int", { id: sequentialPrimaryId({ type: "integer" }) })
|
|
36
|
+
const big = table("big", { id: sequentialPrimaryId({ mode: "bigint" }) })
|
|
37
|
+
const always = table("always", {
|
|
38
|
+
id: sequentialPrimaryId({ generated: "always" }),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expectTypeOf<(typeof int)["$inferSelect"]["id"]>().toEqualTypeOf<number>()
|
|
42
|
+
expectTypeOf<(typeof big)["$inferSelect"]["id"]>().toEqualTypeOf<bigint>()
|
|
43
|
+
expectTypeOf<(typeof always)["$inferSelect"]["id"]>().toEqualTypeOf<number>()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("mode is rejected for integer, which has one JavaScript representation", () => {
|
|
47
|
+
// @ts-expect-error mode applies to bigint only
|
|
48
|
+
sequentialPrimaryId({ mode: "bigint", type: "integer" })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("dropping the random default makes a uuid key required on insert", () => {
|
|
52
|
+
const posts = table("posts", { id: primaryId("uuid") })
|
|
53
|
+
const profiles = table("profiles", {
|
|
54
|
+
id: uuidPrimaryId({ defaultRandom: false }),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// gen_random_uuid() supplies the id, so callers may omit it.
|
|
58
|
+
expectTypeOf<(typeof posts)["$inferInsert"]>().toEqualTypeOf<{
|
|
59
|
+
id?: string
|
|
60
|
+
}>()
|
|
61
|
+
// An auth.users mirror takes its id from the referenced row.
|
|
62
|
+
expectTypeOf<(typeof profiles)["$inferInsert"]>().toEqualTypeOf<{
|
|
63
|
+
id: string
|
|
64
|
+
}>()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("author columns are nullable unless asked to be required", () => {
|
|
68
|
+
const posts = table("posts", { ownerId: authUserId() })
|
|
69
|
+
const required = table("required", {
|
|
70
|
+
ownerId: authUserId({ notNull: true }),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expectTypeOf<(typeof posts)["$inferSelect"]["ownerId"]>().toEqualTypeOf<
|
|
74
|
+
string | null
|
|
75
|
+
>()
|
|
76
|
+
expectTypeOf<
|
|
77
|
+
(typeof required)["$inferSelect"]["ownerId"]
|
|
78
|
+
>().toEqualTypeOf<string>()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("authorship options reach both columns", () => {
|
|
82
|
+
const audited = table("audited", { ...auditColumns({ notNull: true }) })
|
|
83
|
+
const loose = table("loose", { ...authorship() })
|
|
84
|
+
|
|
85
|
+
expectTypeOf<
|
|
86
|
+
(typeof audited)["$inferSelect"]["createdBy"]
|
|
87
|
+
>().toEqualTypeOf<string>()
|
|
88
|
+
expectTypeOf<
|
|
89
|
+
(typeof audited)["$inferSelect"]["updatedBy"]
|
|
90
|
+
>().toEqualTypeOf<string>()
|
|
91
|
+
expectTypeOf<(typeof loose)["$inferSelect"]["createdBy"]>().toEqualTypeOf<
|
|
92
|
+
string | null
|
|
93
|
+
>()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("author columns default on insert, so they stay optional", () => {
|
|
97
|
+
const posts = table("posts", { ...authorship({ notNull: true }) })
|
|
98
|
+
|
|
99
|
+
expectTypeOf<(typeof posts)["$inferInsert"]>().toEqualTypeOf<{
|
|
100
|
+
createdBy?: string
|
|
101
|
+
updatedBy?: string
|
|
102
|
+
}>()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("userId narrows the same way against its own reference", () => {
|
|
106
|
+
const profiles = table("profiles", {
|
|
107
|
+
id: uuidPrimaryId({ defaultRandom: false }),
|
|
108
|
+
})
|
|
109
|
+
const posts = table("posts", {
|
|
110
|
+
ownerId: userId(() => profiles.id, { notNull: true }),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expectTypeOf<
|
|
114
|
+
(typeof posts)["$inferSelect"]["ownerId"]
|
|
115
|
+
>().toEqualTypeOf<string>()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test("reference actions are rejected when there is no reference", () => {
|
|
119
|
+
// @ts-expect-error actions are meaningless without a foreign key
|
|
120
|
+
authUserId({ actions: { onDelete: "cascade" }, reference: null })
|
|
121
|
+
})
|