@zeno-lib/db 0.1.0 → 0.2.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 +512 -28
- package/src/config.ts +51 -2
- package/src/schema.test-d.ts +121 -0
- package/src/schema.ts +429 -43
- package/src/triggers.test.ts +131 -0
- package/src/triggers.ts +171 -0
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
7
|
+
"./auth": "./src/auth-schema.ts",
|
|
7
8
|
"./config": "./src/config.ts",
|
|
8
|
-
"./schema": "./src/schema.ts"
|
|
9
|
+
"./schema": "./src/schema.ts",
|
|
10
|
+
"./triggers": "./src/triggers.ts"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"src"
|
|
@@ -26,7 +28,7 @@
|
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@supabase/supabase-js": "2.106.1",
|
|
28
30
|
"@types/node": "24.10.0",
|
|
29
|
-
"dotenv": "
|
|
31
|
+
"dotenv": "17.4.2",
|
|
30
32
|
"drizzle-kit": "1.0.0-rc.3",
|
|
31
33
|
"drizzle-orm": "1.0.0-rc.3",
|
|
32
34
|
"postgres": "3.4.7",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { generateDrizzleJson } from "drizzle-kit/api-postgres"
|
|
2
|
+
import { getTableColumns, getTableName } from "drizzle-orm"
|
|
3
|
+
import { getTableConfig } from "drizzle-orm/pg-core"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
import { authSchema, authUsers } from "./auth-schema.ts"
|
|
6
|
+
// biome-ignore lint/performance/noNamespaceImport: the point is what the whole barrel exports
|
|
7
|
+
import * as schema from "./schema.ts"
|
|
8
|
+
|
|
9
|
+
type DdlEntity = { entityType: string; schema?: string; name: string }
|
|
10
|
+
|
|
11
|
+
const ddlFor = async (exports: Record<string, unknown>) =>
|
|
12
|
+
((await generateDrizzleJson(exports)).ddl ?? []) as DdlEntity[]
|
|
13
|
+
|
|
14
|
+
describe("auth.users", () => {
|
|
15
|
+
it("ships every column Supabase has, so schemaFilter can include auth", () => {
|
|
16
|
+
// Pinned to Supabase CLI 2.84.1 / GoTrue v2.188.1. A bump that adds or
|
|
17
|
+
// removes a column should fail here, not silently drift: regenerate with
|
|
18
|
+
// the procedure in AGENTS.md.
|
|
19
|
+
expect(Object.keys(getTableColumns(authUsers))).toHaveLength(35)
|
|
20
|
+
expect(getTableName(authUsers)).toBe("users")
|
|
21
|
+
expect(getTableConfig(authUsers).schema).toBe("auth")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("marks the schema existing so nothing emits CREATE SCHEMA", async () => {
|
|
25
|
+
expect(authSchema.isExisting).toBe(true)
|
|
26
|
+
expect(
|
|
27
|
+
(await ddlFor({ authSchema, authUsers })).filter(
|
|
28
|
+
(entity) => entity.entityType === "schemas"
|
|
29
|
+
)
|
|
30
|
+
).toEqual([])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("stays out of the schema barrel, which is the only real protection", async () => {
|
|
34
|
+
// drizzle-kit `generate` applies no entity filter, so a table reachable
|
|
35
|
+
// from the files a consumer's `schema` glob reads becomes DDL regardless of
|
|
36
|
+
// `.existing()` or `schemaFilter`. `@zeno-lib/db/schema` is what a consumer
|
|
37
|
+
// re-exports, so auth.users must not be in it.
|
|
38
|
+
expect(schema).not.toHaveProperty("authUsers")
|
|
39
|
+
expect(schema).not.toHaveProperty("authSchema")
|
|
40
|
+
|
|
41
|
+
const authEntities = (await ddlFor(schema)).filter(
|
|
42
|
+
(entity) => entity.schema === "auth"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(authEntities).toEqual([])
|
|
46
|
+
// Scoped to `auth` on purpose: `realtimeMessages` is still re-exported from
|
|
47
|
+
// the barrel and still emits `CREATE TABLE "realtime"."messages"`, the same
|
|
48
|
+
// bug for a different table. Widen this to every Supabase-owned schema when
|
|
49
|
+
// #152 lands.
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("resolves author column foreign keys against this table", () => {
|
|
53
|
+
const posts = schema.table("posts", { ownerId: schema.authUserId() })
|
|
54
|
+
const [foreignKey] = getTableConfig(posts).foreignKeys
|
|
55
|
+
|
|
56
|
+
// One auth.users object, not two with the same name.
|
|
57
|
+
expect(foreignKey?.reference().foreignTable).toBe(authUsers)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Supabase's auth.users, generated with `drizzle-kit pull` against the local
|
|
2
|
+
// stack. Do not hand-edit; regenerate with the procedure in AGENTS.md.
|
|
3
|
+
//
|
|
4
|
+
// Pinned to: Supabase CLI 2.84.1, Postgres 17.6.1.095, GoTrue v2.188.1,
|
|
5
|
+
// auth.schema_migrations 20260302000000.
|
|
6
|
+
//
|
|
7
|
+
// drizzle-orm/supabase ships 8 of these 35 columns, so putting `auth` in
|
|
8
|
+
// `schemaFilter` made drizzle-kit want to drop the other 27, and anyone needing
|
|
9
|
+
// a foreign key into auth.users hand-wrote the table instead.
|
|
10
|
+
//
|
|
11
|
+
// This is deliberately NOT exported from `@zeno-lib/db/schema`. drizzle-kit
|
|
12
|
+
// `generate` applies no entity filter at all: it passes `() => true` and its
|
|
13
|
+
// config accepts neither `schemaFilter` nor `entities`, so neither
|
|
14
|
+
// `pgSchema("auth").existing()` below nor a `schemaFilter` in your drizzle
|
|
15
|
+
// config stops it emitting `CREATE TABLE "auth"."users"`. The only thing that
|
|
16
|
+
// does is the table never reaching the files your `schema` glob reads, which is
|
|
17
|
+
// why it lives behind its own entrypoint rather than in the barrel you
|
|
18
|
+
// re-export. Verified against drizzle-kit@1.0.0-rc.3.
|
|
19
|
+
//
|
|
20
|
+
// The auth enums are deliberately absent too. auth.users uses none of them (they
|
|
21
|
+
// belong to the mfa_*, oauth_* and sso_* tables), and drizzle-kit never filters
|
|
22
|
+
// enums, so an exported one is guaranteed DDL in every command.
|
|
23
|
+
//
|
|
24
|
+
// Indexes, unique constraints and checks are omitted for the same reason they
|
|
25
|
+
// cannot help: this table is never migrated from here. Columns are what a
|
|
26
|
+
// consumer needs for a foreign key and for typed reads.
|
|
27
|
+
import { sql } from "drizzle-orm"
|
|
28
|
+
import {
|
|
29
|
+
boolean,
|
|
30
|
+
jsonb,
|
|
31
|
+
pgSchema,
|
|
32
|
+
smallint,
|
|
33
|
+
text,
|
|
34
|
+
timestamp,
|
|
35
|
+
uuid,
|
|
36
|
+
varchar,
|
|
37
|
+
} from "drizzle-orm/pg-core"
|
|
38
|
+
|
|
39
|
+
// `.existing()` stops `CREATE SCHEMA "auth"` everywhere, and stops
|
|
40
|
+
// `drizzle-kit push` touching the schema. It does not bind `generate`.
|
|
41
|
+
export const authSchema = pgSchema("auth").existing()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Supabase's `auth.users`, in full.
|
|
45
|
+
*
|
|
46
|
+
* Reading it needs an RLS-bypassing client, and several columns hold credential
|
|
47
|
+
* material (`encryptedPassword`, the `*Token` columns). Select the columns you
|
|
48
|
+
* need rather than `select()`.
|
|
49
|
+
*/
|
|
50
|
+
export const authUsers = authSchema.table.withRLS("users", {
|
|
51
|
+
aud: varchar({ length: 255 }),
|
|
52
|
+
bannedUntil: timestamp("banned_until", { withTimezone: true }),
|
|
53
|
+
confirmationSentAt: timestamp("confirmation_sent_at", { withTimezone: true }),
|
|
54
|
+
confirmationToken: varchar("confirmation_token", { length: 255 }),
|
|
55
|
+
confirmedAt: timestamp("confirmed_at", {
|
|
56
|
+
withTimezone: true,
|
|
57
|
+
}).generatedAlwaysAs(sql`LEAST(email_confirmed_at, phone_confirmed_at)`),
|
|
58
|
+
createdAt: timestamp("created_at", { withTimezone: true }),
|
|
59
|
+
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
|
60
|
+
email: varchar({ length: 255 }),
|
|
61
|
+
emailChange: varchar("email_change", { length: 255 }),
|
|
62
|
+
emailChangeConfirmStatus: smallint("email_change_confirm_status").default(0),
|
|
63
|
+
emailChangeSentAt: timestamp("email_change_sent_at", { withTimezone: true }),
|
|
64
|
+
emailChangeTokenCurrent: varchar("email_change_token_current", {
|
|
65
|
+
length: 255,
|
|
66
|
+
}).default(""),
|
|
67
|
+
emailChangeTokenNew: varchar("email_change_token_new", { length: 255 }),
|
|
68
|
+
emailConfirmedAt: timestamp("email_confirmed_at", { withTimezone: true }),
|
|
69
|
+
encryptedPassword: varchar("encrypted_password", { length: 255 }),
|
|
70
|
+
id: uuid().primaryKey(),
|
|
71
|
+
instanceId: uuid("instance_id"),
|
|
72
|
+
invitedAt: timestamp("invited_at", { withTimezone: true }),
|
|
73
|
+
isAnonymous: boolean("is_anonymous").default(false).notNull(),
|
|
74
|
+
isSsoUser: boolean("is_sso_user").default(false).notNull(),
|
|
75
|
+
isSuperAdmin: boolean("is_super_admin"),
|
|
76
|
+
lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true }),
|
|
77
|
+
phone: text().default(sql`NULL`),
|
|
78
|
+
phoneChange: text("phone_change").default(""),
|
|
79
|
+
phoneChangeSentAt: timestamp("phone_change_sent_at", { withTimezone: true }),
|
|
80
|
+
phoneChangeToken: varchar("phone_change_token", { length: 255 }).default(""),
|
|
81
|
+
phoneConfirmedAt: timestamp("phone_confirmed_at", { withTimezone: true }),
|
|
82
|
+
rawAppMetaData: jsonb("raw_app_meta_data"),
|
|
83
|
+
rawUserMetaData: jsonb("raw_user_meta_data"),
|
|
84
|
+
reauthenticationSentAt: timestamp("reauthentication_sent_at", {
|
|
85
|
+
withTimezone: true,
|
|
86
|
+
}),
|
|
87
|
+
reauthenticationToken: varchar("reauthentication_token", {
|
|
88
|
+
length: 255,
|
|
89
|
+
}).default(""),
|
|
90
|
+
recoverySentAt: timestamp("recovery_sent_at", { withTimezone: true }),
|
|
91
|
+
recoveryToken: varchar("recovery_token", { length: 255 }),
|
|
92
|
+
role: varchar({ length: 255 }),
|
|
93
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
|
94
|
+
})
|