@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/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 — don't try to CREATE or DROP them.
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: { ...roleOverrides, provider: "supabase" },
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
+ })