diff --git a/packages/db/migrations/0000_sloppy_stryfe.sql b/packages/db/migrations/0000_living_namora.sql similarity index 98% rename from packages/db/migrations/0000_sloppy_stryfe.sql rename to packages/db/migrations/0000_living_namora.sql index 10388f6..2025fd3 100644 --- a/packages/db/migrations/0000_sloppy_stryfe.sql +++ b/packages/db/migrations/0000_living_namora.sql @@ -1,5 +1,12 @@ +CREATE EXTENSION IF NOT EXISTS vector;--> statement-breakpoint +CREATE SCHEMA "chat"; +--> statement-breakpoint CREATE SCHEMA "pdf"; --> statement-breakpoint +CREATE SCHEMA "image"; +--> statement-breakpoint +CREATE SCHEMA "mesh"; +--> statement-breakpoint CREATE TYPE "public"."credit_transaction_type" AS ENUM('signup', 'purchase', 'usage', 'admin_grant', 'admin_deduct', 'refund', 'promo', 'referral', 'expiry');--> statement-breakpoint CREATE TYPE "public"."status" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid');--> statement-breakpoint CREATE TYPE "public"."plan" AS ENUM('free', 'premium', 'enterprise');--> statement-breakpoint diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json index 7e4f549..3e4de35 100644 --- a/packages/db/migrations/meta/0000_snapshot.json +++ b/packages/db/migrations/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f8d7bddc-1efc-4f4a-8ebb-e8278508656e", + "id": "a1b0192f-d300-48eb-8f1d-e76c65b030dc", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -2792,7 +2792,10 @@ } }, "schemas": { - "pdf": "pdf" + "chat": "chat", + "pdf": "pdf", + "image": "image", + "mesh": "mesh" }, "sequences": {}, "roles": {}, diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 0211a3c..a61b7fa 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1775335397081, - "tag": "0000_sloppy_stryfe", + "when": 1775336269295, + "tag": "0000_living_namora", "breakpoints": true } ] diff --git a/packages/db/src/schema/chat.ts b/packages/db/src/schema/chat.ts index 00b60c3..59ef694 100644 --- a/packages/db/src/schema/chat.ts +++ b/packages/db/src/schema/chat.ts @@ -7,15 +7,18 @@ import { createInsertSchema, createSelectSchema } from "../utils/drizzle-zod"; import { user } from "./auth"; -export const schema = pgSchema("chat"); +// Uniquely-named pgSchema export (not `schema`) so drizzle-kit can +// introspect it through the `export * from "./chat"` barrel. See +// mesh.ts for the full rationale. +export const chatSchema = pgSchema("chat"); -export const messageRoleEnum = schema.enum("role", [ +export const messageRoleEnum = chatSchema.enum("role", [ "system", "assistant", "user", ]); -export const chat = schema.table("chat", { +export const chat = chatSchema.table("chat", { id: text().primaryKey().notNull().$defaultFn(generateId), name: text(), userId: text() @@ -27,7 +30,7 @@ export const chat = schema.table("chat", { createdAt: timestamp().defaultNow(), }); -export const message = schema.table("message", { +export const message = chatSchema.table("message", { id: text().primaryKey().notNull().$defaultFn(generateId), chatId: text() .references(() => chat.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -40,7 +43,7 @@ export const messageRelations = relations(message, ({ many }) => ({ part: many(part), })); -export const part = schema.table("part", { +export const part = chatSchema.table("part", { id: text().primaryKey().notNull().$defaultFn(generateId), messageId: text() .references(() => message.id, { onDelete: "cascade", onUpdate: "cascade" }) diff --git a/packages/db/src/schema/image.ts b/packages/db/src/schema/image.ts index 67c33fa..9676c5a 100644 --- a/packages/db/src/schema/image.ts +++ b/packages/db/src/schema/image.ts @@ -7,16 +7,17 @@ import { createInsertSchema, createSelectSchema } from "../utils/drizzle-zod"; import { user } from "./auth"; -export const schema = pgSchema("image"); +// Uniquely-named pgSchema export — see mesh.ts for rationale. +export const imageSchema = pgSchema("image"); -export const aspectRatioEnum = schema.enum("aspect_ratio", [ +export const aspectRatioEnum = imageSchema.enum("aspect_ratio", [ "square", "standard", "landscape", "portrait", ]); -export const generation = schema.table("generation", { +export const generation = imageSchema.table("generation", { id: text().primaryKey().notNull().$defaultFn(generateId), prompt: text().notNull(), model: text().notNull(), @@ -36,7 +37,7 @@ export const generationRelations = relations(generation, ({ many }) => ({ image: many(image), })); -export const image = schema.table("image", { +export const image = imageSchema.table("image", { id: text().primaryKey().notNull().$defaultFn(generateId), generationId: text() .references(() => generation.id, { diff --git a/packages/db/src/schema/mesh.ts b/packages/db/src/schema/mesh.ts index 302559a..78a1d30 100644 --- a/packages/db/src/schema/mesh.ts +++ b/packages/db/src/schema/mesh.ts @@ -13,41 +13,54 @@ import { createInsertSchema, createSelectSchema } from "../utils/drizzle-zod"; import { user } from "./auth"; -export const schema = pgSchema("mesh"); +/** + * pgSchema namespace for all mesh/broker tables. + * + * Exported under a UNIQUE name (not generic `schema`) to avoid being + * shadowed by `export * from` barrel merging when another module + * (chat, image) exports its own `schema` pgSchema. Without this, the + * TS ambiguous-re-export rule silently drops the `schema` binding, + * drizzle-kit can't introspect the pgSchema, and `CREATE SCHEMA + * "mesh"` is never emitted in the generated migration — producing + * broken migrations for fresh databases. + * + * See: pdf.ts for the same pattern (pdfSchema). + */ +export const meshSchema = pgSchema("mesh"); -export const meshVisibilityEnum = schema.enum("visibility", [ +export const meshVisibilityEnum = meshSchema.enum("visibility", [ "private", "public", ]); -export const meshTransportEnum = schema.enum("transport", [ +export const meshTransportEnum = meshSchema.enum("transport", [ "managed", "tailscale", "self_hosted", ]); -export const meshTierEnum = schema.enum("tier", [ +export const meshTierEnum = meshSchema.enum("tier", [ "free", "pro", "team", "enterprise", ]); -export const meshRoleEnum = schema.enum("role", ["admin", "member"]); +export const meshRoleEnum = meshSchema.enum("role", ["admin", "member"]); -export const presenceStatusEnum = schema.enum("presence_status", [ +export const presenceStatusEnum = meshSchema.enum("presence_status", [ "idle", "working", "dnd", ]); -export const presenceStatusSourceEnum = schema.enum("presence_status_source", [ +export const presenceStatusSourceEnum = meshSchema.enum("presence_status_source", [ "hook", "manual", "jsonl", ]); -export const messagePriorityEnum = schema.enum("message_priority", [ +export const messagePriorityEnum = meshSchema.enum("message_priority", [ "now", "next", "low", @@ -58,7 +71,7 @@ export const messagePriorityEnum = schema.enum("message_priority", [ * other via the broker. Ownership is tied to a user; transport/tier * describe how it's hosted and billed. */ -export const mesh = schema.table("mesh", { +export const mesh = meshSchema.table("mesh", { id: text().primaryKey().notNull().$defaultFn(generateId), name: text().notNull(), slug: text().notNull().unique(), @@ -76,8 +89,15 @@ export const mesh = schema.table("mesh", { /** * A member is a peer that has joined a mesh. user_id is nullable to * allow anonymous/invite-only peers (identity is the ed25519 pubkey). + * + * Note on asymmetric naming: the DB table is `mesh.member` (short, + * lives in the `mesh` pgSchema) but the TS export is `meshMember`. + * This is deliberate — `auth.member` also exports a `member` binding, + * and the schema barrel uses `export *`, which would silently drop + * one of the two on collision. Unique TS name + short DB name is the + * cleanest trade-off. */ -export const meshMember = schema.table("member", { +export const meshMember = meshSchema.table("member", { id: text().primaryKey().notNull().$defaultFn(generateId), meshId: text() .references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -97,7 +117,7 @@ export const meshMember = schema.table("member", { /** * Invite tokens used to join a mesh via shareable URL. */ -export const invite = schema.table("invite", { +export const invite = meshSchema.table("invite", { id: text().primaryKey().notNull().$defaultFn(generateId), meshId: text() .references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -119,7 +139,7 @@ export const invite = schema.table("invite", { * payload between peers is E2E encrypted client-side (libsodium), so * the broker/DB only ever see ciphertext + routing events. */ -export const auditLog = schema.table("audit_log", { +export const auditLog = meshSchema.table("audit_log", { id: text().primaryKey().notNull().$defaultFn(generateId), meshId: text() .references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -137,7 +157,7 @@ export const auditLog = schema.table("audit_log", { * heartbeat/hook signal, closed out (disconnectedAt set) on disconnect. * Persisted so the broker can resume state after a restart. */ -export const presence = schema.table("presence", { +export const presence = meshSchema.table("presence", { id: text().primaryKey().notNull().$defaultFn(generateId), memberId: text() .references(() => meshMember.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -162,7 +182,7 @@ export const presence = schema.table("presence", { * pubkey (direct message), a channel (`#general`), a tag (`tag:admins`), * or a broadcast (`*`). Resolution happens in broker logic, not SQL. */ -export const messageQueue = schema.table("message_queue", { +export const messageQueue = meshSchema.table("message_queue", { id: text().primaryKey().notNull().$defaultFn(generateId), meshId: text() .references(() => mesh.id, { onDelete: "cascade", onUpdate: "cascade" }) @@ -187,7 +207,7 @@ export const messageQueue = schema.table("message_queue", { * Intentionally NOT linked to member/mesh via FK — the whole point is * that no member row exists yet when the hook fires. */ -export const pendingStatus = schema.table("pending_status", { +export const pendingStatus = meshSchema.table("pending_status", { id: text().primaryKey().notNull().$defaultFn(generateId), pid: integer().notNull(), cwd: text().notNull(),