@zeno-lib/db 0.0.1 → 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 CHANGED
@@ -1,23 +1,13 @@
1
1
  {
2
2
  "name": "@zeno-lib/db",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "scripts": {
6
- "dev": "pnpm exec supabase start",
7
- "stop": "pnpm exec supabase stop",
8
- "reset": "pnpm exec supabase db reset",
9
- "db:generate": "drizzle-kit generate",
10
- "db:migrate": "drizzle-kit migrate",
11
- "db:push": "drizzle-kit push",
12
- "db:studio": "drizzle-kit studio",
13
- "test": "vitest --run",
14
- "test:watch": "vitest watch",
15
- "types:check": "tsc --noEmit"
16
- },
17
5
  "exports": {
18
6
  ".": "./src/index.ts",
7
+ "./auth": "./src/auth-schema.ts",
19
8
  "./config": "./src/config.ts",
20
- "./schema": "./src/schema.ts"
9
+ "./schema": "./src/schema.ts",
10
+ "./triggers": "./src/triggers.ts"
21
11
  },
22
12
  "files": [
23
13
  "src"
@@ -38,14 +28,26 @@
38
28
  "devDependencies": {
39
29
  "@supabase/supabase-js": "2.106.1",
40
30
  "@types/node": "24.10.0",
41
- "@zeno-lib/test": "workspace:*",
42
- "@zeno-lib/typescript": "workspace:^",
43
- "dotenv": "16.4.7",
31
+ "dotenv": "17.4.2",
44
32
  "drizzle-kit": "1.0.0-rc.3",
45
33
  "drizzle-orm": "1.0.0-rc.3",
46
34
  "postgres": "3.4.7",
47
35
  "supabase": "2.84.1",
48
36
  "vite": "8.0.16",
49
- "vitest": "4.1.0"
37
+ "vitest": "4.1.0",
38
+ "@zeno-lib/test": "0.0.3",
39
+ "@zeno-lib/typescript": "^1.1.0"
40
+ },
41
+ "scripts": {
42
+ "dev": "pnpm exec supabase start",
43
+ "stop": "pnpm exec supabase stop",
44
+ "reset": "pnpm exec supabase db reset",
45
+ "db:generate": "drizzle-kit generate",
46
+ "db:migrate": "drizzle-kit migrate",
47
+ "db:push": "drizzle-kit push",
48
+ "db:studio": "drizzle-kit studio",
49
+ "test": "vitest --run",
50
+ "test:watch": "vitest watch",
51
+ "types:check": "tsc --noEmit"
50
52
  }
51
- }
53
+ }
@@ -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
+ })