diff --git a/CLAUDE.md b/CLAUDE.md index d30a6a39a..e6b6a1e01 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -197,7 +197,7 @@ yield* db.transaction(Effect.gen(function* () { /* every statement in here joins ``` - `db.transaction(effect)` pins every statement inside `effect` (repositories included) to one connection through fiber context; there is no transaction handle to pass around. -- Schema changes: edit `src/tables`, then `bun run db:generate --name `. effect-orm does not model enums or unique constraints: write those in a `--custom` migration. +- Schema changes: edit `src/tables`, then `bun run db:generate --name `. Enums are `PG.enumType`, unique constraints `PG.unique` in `uniqueConstraints`; `generate` migrates both. What effect-orm does not model (check constraints, removing or reordering enum labels) goes in a `--custom` migration. ## Architecture Notes diff --git a/apps/electric-proxy/src/e2e/electric-proxy-filters.e2e.test.ts b/apps/electric-proxy/src/e2e/electric-proxy-filters.e2e.test.ts index d60e266e5..16793f996 100644 --- a/apps/electric-proxy/src/e2e/electric-proxy-filters.e2e.test.ts +++ b/apps/electric-proxy/src/e2e/electric-proxy-filters.e2e.test.ts @@ -1094,7 +1094,7 @@ const assertVisibilitySpec = (rows: Array>, spec: Visibi describe("electric-proxy filter E2E (real Electric)", () => { beforeAll(async () => { runShell("docker compose up -d postgres electric cache_redis") - runShell("bun run --cwd packages/db db:push", REPO_ROOT, { DATABASE_URL: DB_URL }) + runShell("bun run --cwd packages/db db:migrate", REPO_ROOT, { DATABASE_URL: DB_URL }) fixture = createFixture() seedFixture(fixture) diff --git a/bun.lock b/bun.lock index 8bd4e5bde..cda0bb2ab 100644 --- a/bun.lock +++ b/bun.lock @@ -538,7 +538,7 @@ "@effect/sql-pg": "catalog:effect", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", - "@maple-dev/effect-orm": "0.5.0", + "@maple-dev/effect-orm": "0.6.0", "effect": "catalog:effect", }, "devDependencies": { @@ -1392,7 +1392,7 @@ "@maple-dev/alchemy": ["@maple-dev/alchemy@0.2.0", "", { "dependencies": { "@maple-dev/effect-sdk": "0.10.0" }, "peerDependencies": { "alchemy": ">=2.0.0-beta.80 <3", "effect": "^4.0.0" } }, "sha512-lGjw1u0eAfNBKzp+IfcZHOJyqn2N3ivT0uGvrj8alK5vJsYwP1lI3y9A7rgTIyrgIsEswodTyUFduaZa9YjGkw=="], - "@maple-dev/effect-orm": ["@maple-dev/effect-orm@0.5.0", "", { "peerDependencies": { "effect": "^4.0.0" }, "bin": { "ch-bench": "dist/benchmark/bin.mjs", "effect-orm": "dist/kit/bin.mjs" } }, "sha512-DsjyJSHSX5xRhlVdTsPOci69MFNT6wuh0xXKnviYdsBgH7Il4zsB5v/95krbx9JSL2LKR0Q+5iBDZfdcPQLGCA=="], + "@maple-dev/effect-orm": ["@maple-dev/effect-orm@0.6.0", "", { "peerDependencies": { "effect": "^4.0.0" }, "bin": { "ch-bench": "dist/benchmark/bin.mjs", "effect-orm": "dist/kit/bin.mjs" } }, "sha512-JFYeKtiSxV20iLs18Wnbl+8dklV2QZUg0kwLqVDcfDGgrSwrRqJ5BMts3bB/aAMpO72KfL9XZ9nKYhbibXMqmw=="], "@maple-dev/effect-sdk": ["@maple-dev/effect-sdk@0.10.0", "", { "dependencies": { "std-env": "^4.0.0" }, "peerDependencies": { "effect": "^4.0.0" } }, "sha512-3MWyBqWPyiizAZItDk7Uk+vi69csziIJxCcaqW10aowVV1Tlcwao1EW6xLccAzvfkz43Y8WAiLzhf/Kr4oRkdQ=="], diff --git a/packages/db/migrations/20261012222436_effect_orm_baseline/migration.sql b/packages/db/migrations/20261012222436_effect_orm_baseline/migration.sql deleted file mode 100644 index 7a2967e06..000000000 --- a/packages/db/migrations/20261012222436_effect_orm_baseline/migration.sql +++ /dev/null @@ -1 +0,0 @@ --- Baseline: the schema as it stood when effect-orm took over this folder. Runs nothing. diff --git a/packages/db/migrations/20261012000000_initial_schema/migration.sql b/packages/db/migrations/20261013000436_initial_schema/migration.sql similarity index 86% rename from packages/db/migrations/20261012000000_initial_schema/migration.sql rename to packages/db/migrations/20261013000436_initial_schema/migration.sql index b580d9399..408b0d176 100644 --- a/packages/db/migrations/20261012000000_initial_schema/migration.sql +++ b/packages/db/migrations/20261013000436_initial_schema/migration.sql @@ -1,39 +1,58 @@ -CREATE TYPE public.attachment_status AS ENUM ('uploading', 'complete', 'failed'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "attachment_status" AS ENUM ('uploading', 'complete', 'failed'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.channel_type AS ENUM ('public', 'private', 'thread', 'direct', 'single'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "channel_type" AS ENUM ('public', 'private', 'thread', 'direct', 'single'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.chat_sync_connection_status AS ENUM ('active', 'paused', 'error', 'disabled'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "chat_sync_connection_status" AS ENUM ('active', 'paused', 'error', 'disabled'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.chat_sync_direction AS ENUM ('both', 'hazel_to_external', 'external_to_hazel'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "chat_sync_direction" AS ENUM ('both', 'hazel_to_external', 'external_to_hazel'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.chat_sync_receipt_source AS ENUM ('hazel', 'external'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "chat_sync_receipt_source" AS ENUM ('hazel', 'external'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.chat_sync_receipt_status AS ENUM ('processed', 'ignored', 'failed'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "chat_sync_receipt_status" AS ENUM ('processed', 'ignored', 'failed'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connect_conversation_channel_role AS ENUM ('host', 'guest'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connect_conversation_channel_role" AS ENUM ('host', 'guest'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connect_conversation_status AS ENUM ('active', 'disconnected'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connect_conversation_status" AS ENUM ('active', 'disconnected'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connect_invite_status AS ENUM ('pending', 'accepted', 'declined', 'revoked', 'expired'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connect_invite_status" AS ENUM ('pending', 'accepted', 'declined', 'revoked', 'expired'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connect_invite_target_kind AS ENUM ('slug', 'email'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connect_invite_target_kind" AS ENUM ('slug', 'email'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connection_level AS ENUM ('organization', 'user'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connection_level" AS ENUM ('organization', 'user'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.connection_status AS ENUM ('active', 'expired', 'revoked', 'error', 'suspended'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "connection_status" AS ENUM ('active', 'expired', 'revoked', 'error', 'suspended'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.integration_request_status AS ENUM ('pending', 'reviewed', 'planned', 'rejected'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "integration_request_status" AS ENUM ('pending', 'reviewed', 'planned', 'rejected'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.message_outbox_event_status AS ENUM ('pending', 'processing', 'processed', 'failed'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "message_outbox_event_status" AS ENUM ('pending', 'processing', 'processed', 'failed'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.message_outbox_event_type AS ENUM ('message_created', 'message_updated', 'message_deleted', 'reaction_created', 'reaction_deleted'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "message_outbox_event_type" AS ENUM ('message_created', 'message_updated', 'message_deleted', 'reaction_created', 'reaction_deleted'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.organization_role AS ENUM ('admin', 'member', 'owner'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "organization_role" AS ENUM ('admin', 'member', 'owner'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.user_presence_status_enum AS ENUM ('online', 'away', 'busy', 'dnd', 'offline'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "user_presence_status_enum" AS ENUM ('online', 'away', 'busy', 'dnd', 'offline'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint -CREATE TYPE public.user_type AS ENUM ('user', 'machine'); +-- metadata +DO $effect_orm$ BEGIN CREATE TYPE "user_type" AS ENUM ('user', 'machine'); EXCEPTION WHEN duplicate_object THEN NULL; END $effect_orm$; --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "attachments" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, @@ -43,12 +62,13 @@ CREATE TABLE IF NOT EXISTS "attachments" ( "fileSize" integer NOT NULL, "externalUrl" character varying(2048), "uploadedBy" uuid NOT NULL, - "status" public.attachment_status NOT NULL DEFAULT 'uploading', + "status" attachment_status NOT NULL DEFAULT 'uploading', "uploadedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, CONSTRAINT "attachments_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "bot_commands" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "botId" uuid NOT NULL, @@ -62,6 +82,7 @@ CREATE TABLE IF NOT EXISTS "bot_commands" ( CONSTRAINT "bot_commands_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "bot_installations" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "botId" uuid NOT NULL, @@ -71,6 +92,7 @@ CREATE TABLE IF NOT EXISTS "bot_installations" ( CONSTRAINT "bot_installations_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "bots" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "userId" uuid NOT NULL, @@ -91,6 +113,7 @@ CREATE TABLE IF NOT EXISTS "bots" ( CONSTRAINT "bots_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "channel_access" ( "userId" uuid NOT NULL, "channelId" uuid NOT NULL, @@ -100,6 +123,7 @@ CREATE TABLE IF NOT EXISTS "channel_access" ( CONSTRAINT "channel_access_user_channel_pk" PRIMARY KEY ("userId", "channelId") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "channel_members" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -115,6 +139,7 @@ CREATE TABLE IF NOT EXISTS "channel_members" ( CONSTRAINT "channel_members_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "channel_sections" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, @@ -126,6 +151,7 @@ CREATE TABLE IF NOT EXISTS "channel_sections" ( CONSTRAINT "channel_sections_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "channel_webhooks" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -145,11 +171,12 @@ CREATE TABLE IF NOT EXISTS "channel_webhooks" ( CONSTRAINT "channel_webhooks_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "channels" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" character varying(255) NOT NULL, "icon" character varying(32), - "type" public.channel_type NOT NULL, + "type" channel_type NOT NULL, "organizationId" uuid NOT NULL, "parentChannelId" uuid, "sectionId" uuid, @@ -159,13 +186,14 @@ CREATE TABLE IF NOT EXISTS "channels" ( CONSTRAINT "channels_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "chat_sync_channel_links" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "syncConnectionId" uuid NOT NULL, "hazelChannelId" uuid NOT NULL, "externalChannelId" character varying(255) NOT NULL, "externalChannelName" character varying(255), - "direction" public.chat_sync_direction NOT NULL DEFAULT 'both', + "direction" chat_sync_direction NOT NULL DEFAULT 'both', "isActive" boolean NOT NULL DEFAULT true, "settings" jsonb, "lastSyncedAt" timestamp with time zone, @@ -175,6 +203,7 @@ CREATE TABLE IF NOT EXISTS "chat_sync_channel_links" ( CONSTRAINT "chat_sync_channel_links_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "chat_sync_connections" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, @@ -182,7 +211,7 @@ CREATE TABLE IF NOT EXISTS "chat_sync_connections" ( "provider" character varying(50) NOT NULL, "externalWorkspaceId" character varying(255) NOT NULL, "externalWorkspaceName" character varying(255), - "status" public.chat_sync_connection_status NOT NULL DEFAULT 'active', + "status" chat_sync_connection_status NOT NULL DEFAULT 'active', "settings" jsonb, "metadata" jsonb, "errorMessage" text, @@ -194,27 +223,29 @@ CREATE TABLE IF NOT EXISTS "chat_sync_connections" ( CONSTRAINT "chat_sync_connections_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "chat_sync_event_receipts" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "syncConnectionId" uuid NOT NULL, "channelLinkId" uuid, - "source" public.chat_sync_receipt_source NOT NULL, + "source" chat_sync_receipt_source NOT NULL, "externalEventId" character varying(255), "dedupeKey" character varying(255) NOT NULL, "payloadHash" character varying(128), - "status" public.chat_sync_receipt_status NOT NULL DEFAULT 'processed', + "status" chat_sync_receipt_status NOT NULL DEFAULT 'processed', "errorMessage" text, "processedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT "chat_sync_event_receipts_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "chat_sync_message_links" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelLinkId" uuid NOT NULL, "hazelMessageId" uuid NOT NULL, "externalMessageId" character varying(255) NOT NULL, - "source" public.chat_sync_receipt_source NOT NULL DEFAULT 'hazel', + "source" chat_sync_receipt_source NOT NULL DEFAULT 'hazel', "rootHazelMessageId" uuid, "rootExternalMessageId" character varying(255), "hazelThreadChannelId" uuid, @@ -226,12 +257,13 @@ CREATE TABLE IF NOT EXISTS "chat_sync_message_links" ( CONSTRAINT "chat_sync_message_links_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "connect_conversation_channels" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "conversationId" uuid NOT NULL, "organizationId" uuid NOT NULL, "channelId" uuid NOT NULL, - "role" public.connect_conversation_channel_role NOT NULL, + "role" connect_conversation_channel_role NOT NULL, "allowGuestMemberAdds" boolean NOT NULL DEFAULT false, "isActive" boolean NOT NULL DEFAULT true, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), @@ -240,11 +272,12 @@ CREATE TABLE IF NOT EXISTS "connect_conversation_channels" ( CONSTRAINT "connect_conversation_channels_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "connect_conversations" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "hostOrganizationId" uuid NOT NULL, "hostChannelId" uuid NOT NULL, - "status" public.connect_conversation_status NOT NULL DEFAULT 'active', + "status" connect_conversation_status NOT NULL DEFAULT 'active', "settings" jsonb, "createdBy" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), @@ -253,15 +286,16 @@ CREATE TABLE IF NOT EXISTS "connect_conversations" ( CONSTRAINT "connect_conversations_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "connect_invites" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "conversationId" uuid NOT NULL, "hostOrganizationId" uuid NOT NULL, "hostChannelId" uuid NOT NULL, - "targetKind" public.connect_invite_target_kind NOT NULL, + "targetKind" connect_invite_target_kind NOT NULL, "targetValue" character varying(255) NOT NULL, "guestOrganizationId" uuid, - "status" public.connect_invite_status NOT NULL DEFAULT 'pending', + "status" connect_invite_status NOT NULL DEFAULT 'pending', "allowGuestMemberAdds" boolean NOT NULL DEFAULT false, "invitedBy" uuid NOT NULL, "acceptedBy" uuid, @@ -273,6 +307,7 @@ CREATE TABLE IF NOT EXISTS "connect_invites" ( CONSTRAINT "connect_invites_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "connect_participants" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "conversationId" uuid NOT NULL, @@ -287,6 +322,7 @@ CREATE TABLE IF NOT EXISTS "connect_participants" ( CONSTRAINT "connect_participants_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "custom_emojis" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, @@ -299,6 +335,7 @@ CREATE TABLE IF NOT EXISTS "custom_emojis" ( CONSTRAINT "custom_emojis_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "github_subscriptions" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -317,13 +354,14 @@ CREATE TABLE IF NOT EXISTS "github_subscriptions" ( CONSTRAINT "github_subscriptions_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "integration_connections" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "provider" character varying(50) NOT NULL, "organizationId" uuid NOT NULL, "userId" uuid, - "level" public.connection_level NOT NULL, - "status" public.connection_status NOT NULL DEFAULT 'active', + "level" connection_level NOT NULL, + "status" connection_status NOT NULL DEFAULT 'active', "externalAccountId" character varying(255), "externalAccountName" character varying(255), "connectedBy" uuid NOT NULL, @@ -337,6 +375,7 @@ CREATE TABLE IF NOT EXISTS "integration_connections" ( CONSTRAINT "integration_connections_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "integration_requests" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, @@ -344,12 +383,13 @@ CREATE TABLE IF NOT EXISTS "integration_requests" ( "integrationName" character varying(255) NOT NULL, "integrationUrl" character varying(500), "description" text, - "status" public.integration_request_status NOT NULL DEFAULT 'pending', + "status" integration_request_status NOT NULL DEFAULT 'pending', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), CONSTRAINT "integration_requests_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "integration_tokens" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "connectionId" uuid NOT NULL, @@ -368,14 +408,15 @@ CREATE TABLE IF NOT EXISTS "integration_tokens" ( CONSTRAINT "integration_tokens_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "message_outbox_events" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "sequence" bigint NOT NULL GENERATED ALWAYS AS IDENTITY, - "eventType" public.message_outbox_event_type NOT NULL, + "eventType" message_outbox_event_type NOT NULL, "aggregateId" uuid NOT NULL, "channelId" uuid NOT NULL, "payload" jsonb NOT NULL, - "status" public.message_outbox_event_status NOT NULL DEFAULT 'pending', + "status" message_outbox_event_status NOT NULL DEFAULT 'pending', "attemptCount" integer NOT NULL DEFAULT 0, "availableAt" timestamp with time zone NOT NULL DEFAULT now(), "lockedAt" timestamp with time zone, @@ -386,6 +427,7 @@ CREATE TABLE IF NOT EXISTS "message_outbox_events" ( CONSTRAINT "message_outbox_events_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "message_reactions" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "messageId" uuid NOT NULL, @@ -397,6 +439,7 @@ CREATE TABLE IF NOT EXISTS "message_reactions" ( CONSTRAINT "message_reactions_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "messages" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -412,6 +455,7 @@ CREATE TABLE IF NOT EXISTS "messages" ( CONSTRAINT "messages_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "notifications" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "memberId" uuid NOT NULL, @@ -424,11 +468,12 @@ CREATE TABLE IF NOT EXISTS "notifications" ( CONSTRAINT "notifications_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "organization_members" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "organizationId" uuid NOT NULL, "userId" uuid NOT NULL, - "role" public.organization_role NOT NULL DEFAULT 'member', + "role" organization_role NOT NULL DEFAULT 'member', "nickname" character varying(100), "joinedAt" timestamp with time zone NOT NULL DEFAULT now(), "invitedBy" uuid, @@ -438,6 +483,7 @@ CREATE TABLE IF NOT EXISTS "organization_members" ( CONSTRAINT "organization_members_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "organizations" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" character varying(255) NOT NULL, @@ -451,6 +497,7 @@ CREATE TABLE IF NOT EXISTS "organizations" ( CONSTRAINT "organizations_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "pinned_messages" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -460,6 +507,7 @@ CREATE TABLE IF NOT EXISTS "pinned_messages" ( CONSTRAINT "pinned_messages_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "rss_posted_items" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "subscriptionId" uuid NOT NULL, @@ -470,6 +518,7 @@ CREATE TABLE IF NOT EXISTS "rss_posted_items" ( CONSTRAINT "rss_posted_items_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "rss_subscriptions" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -492,6 +541,7 @@ CREATE TABLE IF NOT EXISTS "rss_subscriptions" ( CONSTRAINT "rss_subscriptions_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "typing_indicators" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "channelId" uuid NOT NULL, @@ -500,10 +550,11 @@ CREATE TABLE IF NOT EXISTS "typing_indicators" ( CONSTRAINT "typing_indicators_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "user_presence_status" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "userId" uuid NOT NULL, - "status" public.user_presence_status_enum NOT NULL DEFAULT 'online', + "status" user_presence_status_enum NOT NULL DEFAULT 'online', "customMessage" character varying(255), "statusEmoji" character varying(10), "statusExpiresAt" timestamp with time zone, @@ -514,6 +565,7 @@ CREATE TABLE IF NOT EXISTS "user_presence_status" ( CONSTRAINT "user_presence_status_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata CREATE TABLE IF NOT EXISTS "users" ( "id" uuid NOT NULL DEFAULT gen_random_uuid(), "externalId" character varying(255) NOT NULL, @@ -521,7 +573,7 @@ CREATE TABLE IF NOT EXISTS "users" ( "firstName" character varying(100) NOT NULL, "lastName" character varying(100) NOT NULL, "avatarUrl" text, - "userType" public.user_type NOT NULL DEFAULT 'user', + "userType" user_type NOT NULL DEFAULT 'user', "settings" jsonb, "isOnboarded" boolean NOT NULL DEFAULT false, "timezone" character varying(100), @@ -531,326 +583,487 @@ CREATE TABLE IF NOT EXISTS "users" ( CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); --> statement-breakpoint +-- metadata +ALTER TABLE "bots" ADD CONSTRAINT "bots_userId_unique" UNIQUE ("userId"); +--> statement-breakpoint +-- metadata +ALTER TABLE "integration_tokens" ADD CONSTRAINT "integration_tokens_connectionId_unique" UNIQUE ("connectionId"); +--> statement-breakpoint +-- metadata +ALTER TABLE "message_reactions" ADD CONSTRAINT "reactions_message_user_emoji_unique" UNIQUE ("messageId", "userId", "emoji"); +--> statement-breakpoint +-- metadata +ALTER TABLE "organization_members" ADD CONSTRAINT "org_members_org_user_unique" UNIQUE ("organizationId", "userId"); +--> statement-breakpoint +-- metadata +ALTER TABLE "organizations" ADD CONSTRAINT "organizations_slug_unique" UNIQUE ("slug"); +--> statement-breakpoint +-- metadata +ALTER TABLE "user_presence_status" ADD CONSTRAINT "user_presence_status_userId_unique" UNIQUE ("userId"); +--> statement-breakpoint +-- metadata +ALTER TABLE "users" ADD CONSTRAINT "users_externalId_unique" UNIQUE ("externalId"); +--> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_channel_id_idx" ON "attachments" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_deleted_at_idx" ON "attachments" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_message_id_idx" ON "attachments" USING btree ("messageId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_message_uploaded_at_idx" ON "attachments" USING btree ("messageId", "uploadedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_organization_id_idx" ON "attachments" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_status_idx" ON "attachments" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "attachments_uploaded_by_idx" ON "attachments" USING btree ("uploadedBy"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bot_commands_bot_id_idx" ON "bot_commands" USING btree ("botId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "bot_commands_bot_id_name_idx" ON "bot_commands" USING btree ("botId", "name"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bot_installations_bot_idx" ON "bot_installations" USING btree ("botId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "bot_installations_bot_org_unique" ON "bot_installations" USING btree ("botId", "organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bot_installations_org_idx" ON "bot_installations" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bots_created_by_idx" ON "bots" USING btree ("createdBy"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bots_deleted_at_idx" ON "bots" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bots_is_public_idx" ON "bots" USING btree ("isPublic"); --> statement-breakpoint -ALTER TABLE "bots" ADD CONSTRAINT "bots_userId_unique" UNIQUE ("userId"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "bots_user_id_idx" ON "bots" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_access_channel_id_idx" ON "channel_access" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_access_org_user_idx" ON "channel_access" USING btree ("organizationId", "userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_access_organization_id_idx" ON "channel_access" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_access_user_id_idx" ON "channel_access" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_members_channel_id_idx" ON "channel_members" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_members_channel_user_idx" ON "channel_members" USING btree ("channelId", "userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_members_user_id_idx" ON "channel_members" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_sections_deleted_at_idx" ON "channel_sections" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_sections_order_idx" ON "channel_sections" USING btree ("organizationId", "order"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_sections_organization_id_idx" ON "channel_sections" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_webhooks_bot_user_id_idx" ON "channel_webhooks" USING btree ("botUserId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_webhooks_channel_id_idx" ON "channel_webhooks" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_webhooks_deleted_at_idx" ON "channel_webhooks" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_webhooks_organization_id_idx" ON "channel_webhooks" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channel_webhooks_token_hash_idx" ON "channel_webhooks" USING btree ("tokenHash"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channels_deleted_at_idx" ON "channels" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channels_organization_id_idx" ON "channels" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channels_parent_channel_id_idx" ON "channels" USING btree ("parentChannelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channels_section_id_idx" ON "channels" USING btree ("sectionId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "channels_type_idx" ON "channels" USING btree ("type"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_link_connection_external_unique" ON "chat_sync_channel_links" USING btree ("syncConnectionId", "externalChannelId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_link_connection_hazel_unique" ON "chat_sync_channel_links" USING btree ("syncConnectionId", "hazelChannelId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_link_connection_idx" ON "chat_sync_channel_links" USING btree ("syncConnectionId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_link_deleted_at_idx" ON "chat_sync_channel_links" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_link_external_channel_idx" ON "chat_sync_channel_links" USING btree ("externalChannelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_link_hazel_channel_idx" ON "chat_sync_channel_links" USING btree ("hazelChannelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_conn_deleted_at_idx" ON "chat_sync_connections" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_conn_integration_idx" ON "chat_sync_connections" USING btree ("integrationConnectionId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_conn_integration_unique" ON "chat_sync_connections" USING btree ("integrationConnectionId") WHERE "deletedAt" IS NULL AND "integrationConnectionId" IS NOT NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_conn_org_idx" ON "chat_sync_connections" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_conn_org_provider_workspace_unique" ON "chat_sync_connections" USING btree ("organizationId", "provider", "externalWorkspaceId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_conn_provider_idx" ON "chat_sync_connections" USING btree ("provider"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_conn_status_idx" ON "chat_sync_connections" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_receipt_channel_link_idx" ON "chat_sync_event_receipts" USING btree ("channelLinkId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_receipt_connection_idx" ON "chat_sync_event_receipts" USING btree ("syncConnectionId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_receipt_dedupe_unique" ON "chat_sync_event_receipts" USING btree ("syncConnectionId", "source", "dedupeKey"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_receipt_processed_at_idx" ON "chat_sync_event_receipts" USING btree ("processedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_receipt_source_idx" ON "chat_sync_event_receipts" USING btree ("source"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_msg_link_channel_idx" ON "chat_sync_message_links" USING btree ("channelLinkId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_msg_link_deleted_at_idx" ON "chat_sync_message_links" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_msg_link_external_message_idx" ON "chat_sync_message_links" USING btree ("externalMessageId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_msg_link_external_unique" ON "chat_sync_message_links" USING btree ("channelLinkId", "externalMessageId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_msg_link_hazel_message_idx" ON "chat_sync_message_links" USING btree ("hazelMessageId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "chat_sync_msg_link_hazel_unique" ON "chat_sync_message_links" USING btree ("channelLinkId", "hazelMessageId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "chat_sync_msg_link_root_hazel_idx" ON "chat_sync_message_links" USING btree ("rootHazelMessageId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conv_channels_channel_idx" ON "connect_conversation_channels" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "connect_conv_channels_channel_unique" ON "connect_conversation_channels" USING btree ("channelId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conv_channels_conversation_idx" ON "connect_conversation_channels" USING btree ("conversationId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "connect_conv_channels_conversation_org_unique" ON "connect_conversation_channels" USING btree ("conversationId", "organizationId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conv_channels_deleted_at_idx" ON "connect_conversation_channels" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conv_channels_org_idx" ON "connect_conversation_channels" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conversations_deleted_at_idx" ON "connect_conversations" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conversations_host_channel_idx" ON "connect_conversations" USING btree ("hostChannelId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "connect_conversations_host_channel_unique" ON "connect_conversations" USING btree ("hostChannelId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conversations_host_org_idx" ON "connect_conversations" USING btree ("hostOrganizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_conversations_status_idx" ON "connect_conversations" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_conversation_idx" ON "connect_invites" USING btree ("conversationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_deleted_at_idx" ON "connect_invites" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_guest_org_idx" ON "connect_invites" USING btree ("guestOrganizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_host_org_idx" ON "connect_invites" USING btree ("hostOrganizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_status_idx" ON "connect_invites" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_invites_target_idx" ON "connect_invites" USING btree ("targetKind", "targetValue"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_participants_channel_idx" ON "connect_participants" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "connect_participants_channel_user_unique" ON "connect_participants" USING btree ("channelId", "userId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_participants_conversation_idx" ON "connect_participants" USING btree ("conversationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_participants_deleted_at_idx" ON "connect_participants" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "connect_participants_user_idx" ON "connect_participants" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "custom_emojis_deleted_at_idx" ON "custom_emojis" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "custom_emojis_name_idx" ON "custom_emojis" USING btree ("name"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "custom_emojis_org_name_unique_idx" ON "custom_emojis" USING btree ("organizationId", "name") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "custom_emojis_organization_id_idx" ON "custom_emojis" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "github_subscriptions_channel_id_idx" ON "github_subscriptions" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "github_subscriptions_channel_repo_unique" ON "github_subscriptions" USING btree ("channelId", "repositoryId") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "github_subscriptions_deleted_at_idx" ON "github_subscriptions" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "github_subscriptions_organization_id_idx" ON "github_subscriptions" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "github_subscriptions_repository_id_idx" ON "github_subscriptions" USING btree ("repositoryId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_conn_deleted_at_idx" ON "integration_connections" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_conn_github_installation_idx" ON "integration_connections" USING btree (("metadata" ->> 'installationId')); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_conn_org_idx" ON "integration_connections" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "int_conn_org_provider_external_user_unique" ON "integration_connections" USING btree ("organizationId", "provider", "externalAccountId") WHERE "deletedAt" IS NULL AND "level" = 'user' AND "externalAccountId" IS NOT NULL; --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "int_conn_org_user_provider_unique" ON "integration_connections" USING btree ("organizationId", "userId", "provider"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_conn_provider_idx" ON "integration_connections" USING btree ("provider"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_conn_status_idx" ON "integration_connections" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "integration_requests_created_at_idx" ON "integration_requests" USING btree ("createdAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "integration_requests_organization_id_idx" ON "integration_requests" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "integration_requests_status_idx" ON "integration_requests" USING btree ("status"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_tokens_connection_id_idx" ON "integration_tokens" USING btree ("connectionId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_tokens_expires_at_idx" ON "integration_tokens" USING btree ("expiresAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "int_tokens_key_version_idx" ON "integration_tokens" USING btree ("encryptionKeyVersion"); --> statement-breakpoint -ALTER TABLE "integration_tokens" ADD CONSTRAINT "integration_tokens_connectionId_unique" UNIQUE ("connectionId"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "message_outbox_events_channel_sequence_idx" ON "message_outbox_events" USING btree ("channelId", "sequence"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "message_outbox_events_created_at_idx" ON "message_outbox_events" USING btree ("createdAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "message_outbox_events_locked_at_idx" ON "message_outbox_events" USING btree ("lockedAt") WHERE "status" = 'processing'; --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "message_outbox_events_sequence_unique" ON "message_outbox_events" USING btree ("sequence"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "message_outbox_events_status_available_sequence_idx" ON "message_outbox_events" USING btree ("status", "availableAt", "sequence"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "reactions_channel_id_idx" ON "message_reactions" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "reactions_conversation_id_idx" ON "message_reactions" USING btree ("conversationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "reactions_message_id_idx" ON "message_reactions" USING btree ("messageId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "reactions_message_user_emoji_idx" ON "message_reactions" USING btree ("messageId", "userId", "emoji"); --> statement-breakpoint -ALTER TABLE "message_reactions" ADD CONSTRAINT "reactions_message_user_emoji_unique" UNIQUE ("messageId", "userId", "emoji"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "reactions_user_id_idx" ON "message_reactions" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_author_id_idx" ON "messages" USING btree ("authorId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_channel_id_idx" ON "messages" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_conversation_id_idx" ON "messages" USING btree ("conversationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_created_at_idx" ON "messages" USING btree ("createdAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_deleted_at_idx" ON "messages" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_reply_to_idx" ON "messages" USING btree ("replyToMessageId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "messages_thread_channel_idx" ON "messages" USING btree ("threadChannelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "notifications_member_created_id_idx" ON "notifications" USING btree ("memberId", "createdAt", "id"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "notifications_member_id_idx" ON "notifications" USING btree ("memberId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "notifications_message_channel_dedupe_idx" ON "notifications" USING btree ("memberId", "resourceId", "targetedResourceId") WHERE "resourceType" = 'message' AND "targetedResourceType" = 'channel'; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "notifications_read_at_idx" ON "notifications" USING btree ("readAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "notifications_resource_idx" ON "notifications" USING btree ("resourceId", "resourceType"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "notifications_targeted_resource_idx" ON "notifications" USING btree ("targetedResourceId", "targetedResourceType"); --> statement-breakpoint -ALTER TABLE "organization_members" ADD CONSTRAINT "org_members_org_user_unique" UNIQUE ("organizationId", "userId"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "org_members_organization_id_idx" ON "organization_members" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "org_members_user_id_idx" ON "organization_members" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "organizations_clerk_org_id_unique" ON "organizations" USING btree (("settings" ->> 'clerkOrganizationId')) WHERE "deletedAt" IS NULL AND "settings"->>'clerkOrganizationId' IS NOT NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "organizations_deleted_at_idx" ON "organizations" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "organizations_slug_idx" ON "organizations" USING btree ("slug"); --> statement-breakpoint -ALTER TABLE "organizations" ADD CONSTRAINT "organizations_slug_unique" UNIQUE ("slug"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "pinned_messages_channel_id_idx" ON "pinned_messages" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "pinned_messages_message_id_idx" ON "pinned_messages" USING btree ("messageId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "pinned_messages_unique_idx" ON "pinned_messages" USING btree ("channelId", "messageId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "rss_posted_items_sub_guid_unique" ON "rss_posted_items" USING btree ("subscriptionId", "itemGuid"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "rss_posted_items_subscription_id_idx" ON "rss_posted_items" USING btree ("subscriptionId"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "rss_subscriptions_channel_feed_unique" ON "rss_subscriptions" USING btree ("channelId", "feedUrl") WHERE "deletedAt" IS NULL; --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "rss_subscriptions_channel_id_idx" ON "rss_subscriptions" USING btree ("channelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "rss_subscriptions_deleted_at_idx" ON "rss_subscriptions" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "rss_subscriptions_organization_id_idx" ON "rss_subscriptions" USING btree ("organizationId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "rss_subscriptions_poll_idx" ON "rss_subscriptions" USING btree ("isEnabled", "lastFetchedAt", "deletedAt"); --> statement-breakpoint +-- metadata CREATE UNIQUE INDEX IF NOT EXISTS "typing_indicators_channel_member_unique" ON "typing_indicators" USING btree ("channelId", "memberId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "typing_indicators_channel_timestamp_idx" ON "typing_indicators" USING btree ("channelId", "lastTyped"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "typing_indicators_timestamp_idx" ON "typing_indicators" USING btree ("lastTyped"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "user_presence_status_active_channel_idx" ON "user_presence_status" USING btree ("activeChannelId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "user_presence_status_last_seen_idx" ON "user_presence_status" USING btree ("lastSeenAt"); --> statement-breakpoint -ALTER TABLE "user_presence_status" ADD CONSTRAINT "user_presence_status_userId_unique" UNIQUE ("userId"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "user_presence_status_user_id_idx" ON "user_presence_status" USING btree ("userId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "users_deleted_at_idx" ON "users" USING btree ("deletedAt"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" USING btree ("email"); --> statement-breakpoint -ALTER TABLE "users" ADD CONSTRAINT "users_externalId_unique" UNIQUE ("externalId"); ---> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "users_external_id_idx" ON "users" USING btree ("externalId"); --> statement-breakpoint +-- metadata CREATE INDEX IF NOT EXISTS "users_user_type_idx" ON "users" USING btree ("userType"); --> statement-breakpoint +-- metadata ALTER TABLE "organization_members" ADD CONSTRAINT "organization_members_organizationId_organizations_id_fk" FOREIGN KEY ("organizationId") REFERENCES "organizations" ("id") ON DELETE CASCADE; diff --git a/packages/db/migrations/20261012222436_effect_orm_baseline/snapshot.json b/packages/db/migrations/20261013000436_initial_schema/snapshot.json similarity index 96% rename from packages/db/migrations/20261012222436_effect_orm_baseline/snapshot.json rename to packages/db/migrations/20261013000436_initial_schema/snapshot.json index 8987a60e5..6e96fbbe3 100644 --- a/packages/db/migrations/20261012222436_effect_orm_baseline/snapshot.json +++ b/packages/db/migrations/20261013000436_initial_schema/snapshot.json @@ -1,9 +1,123 @@ { "version": "1", "dialect": "postgres", - "id": "9630633859de45b3a47113409704ffc3527838ed939973fc94d6e3fec6499f78", + "id": "22e156dde724cb523f05dc334f01094098ae3d0ff19a185a57d84316f63fecb8", "prevIds": ["0000000000000000000000000000000000000000000000000000000000000000"], "entities": [ + { + "kind": "enum", + "schema": null, + "name": "attachment_status", + "values": ["uploading", "complete", "failed"] + }, + { + "kind": "enum", + "schema": null, + "name": "channel_type", + "values": ["public", "private", "thread", "direct", "single"] + }, + { + "kind": "enum", + "schema": null, + "name": "chat_sync_connection_status", + "values": ["active", "paused", "error", "disabled"] + }, + { + "kind": "enum", + "schema": null, + "name": "chat_sync_direction", + "values": ["both", "hazel_to_external", "external_to_hazel"] + }, + { + "kind": "enum", + "schema": null, + "name": "chat_sync_receipt_source", + "values": ["hazel", "external"] + }, + { + "kind": "enum", + "schema": null, + "name": "chat_sync_receipt_status", + "values": ["processed", "ignored", "failed"] + }, + { + "kind": "enum", + "schema": null, + "name": "connect_conversation_channel_role", + "values": ["host", "guest"] + }, + { + "kind": "enum", + "schema": null, + "name": "connect_conversation_status", + "values": ["active", "disconnected"] + }, + { + "kind": "enum", + "schema": null, + "name": "connect_invite_status", + "values": ["pending", "accepted", "declined", "revoked", "expired"] + }, + { + "kind": "enum", + "schema": null, + "name": "connect_invite_target_kind", + "values": ["slug", "email"] + }, + { + "kind": "enum", + "schema": null, + "name": "connection_level", + "values": ["organization", "user"] + }, + { + "kind": "enum", + "schema": null, + "name": "connection_status", + "values": ["active", "expired", "revoked", "error", "suspended"] + }, + { + "kind": "enum", + "schema": null, + "name": "integration_request_status", + "values": ["pending", "reviewed", "planned", "rejected"] + }, + { + "kind": "enum", + "schema": null, + "name": "message_outbox_event_status", + "values": ["pending", "processing", "processed", "failed"] + }, + { + "kind": "enum", + "schema": null, + "name": "message_outbox_event_type", + "values": [ + "message_created", + "message_updated", + "message_deleted", + "reaction_created", + "reaction_deleted" + ] + }, + { + "kind": "enum", + "schema": null, + "name": "organization_role", + "values": ["admin", "member", "owner"] + }, + { + "kind": "enum", + "schema": null, + "name": "user_presence_status_enum", + "values": ["online", "away", "busy", "dnd", "offline"] + }, + { + "kind": "enum", + "schema": null, + "name": "user_type", + "values": ["user", "machine"] + }, { "kind": "table", "name": "attachments", @@ -361,7 +475,7 @@ "table": "attachments", "name": "status", "position": 8, - "type": "public.attachment_status", + "type": "attachment_status", "notNull": true, "default": "'uploading'", "identity": null @@ -1101,7 +1215,7 @@ "table": "channels", "name": "type", "position": 3, - "type": "public.channel_type", + "type": "channel_type", "notNull": true, "default": null, "identity": null @@ -1221,7 +1335,7 @@ "table": "chat_sync_channel_links", "name": "direction", "position": 5, - "type": "public.chat_sync_direction", + "type": "chat_sync_direction", "notNull": true, "default": "'both'", "identity": null @@ -1351,7 +1465,7 @@ "table": "chat_sync_connections", "name": "status", "position": 6, - "type": "public.chat_sync_connection_status", + "type": "chat_sync_connection_status", "notNull": true, "default": "'active'", "identity": null @@ -1471,7 +1585,7 @@ "table": "chat_sync_event_receipts", "name": "source", "position": 3, - "type": "public.chat_sync_receipt_source", + "type": "chat_sync_receipt_source", "notNull": true, "default": null, "identity": null @@ -1511,7 +1625,7 @@ "table": "chat_sync_event_receipts", "name": "status", "position": 7, - "type": "public.chat_sync_receipt_status", + "type": "chat_sync_receipt_status", "notNull": true, "default": "'processed'", "identity": null @@ -1591,7 +1705,7 @@ "table": "chat_sync_message_links", "name": "source", "position": 4, - "type": "public.chat_sync_receipt_source", + "type": "chat_sync_receipt_source", "notNull": true, "default": "'hazel'", "identity": null @@ -1721,7 +1835,7 @@ "table": "connect_conversation_channels", "name": "role", "position": 4, - "type": "public.connect_conversation_channel_role", + "type": "connect_conversation_channel_role", "notNull": true, "default": null, "identity": null @@ -1811,7 +1925,7 @@ "table": "connect_conversations", "name": "status", "position": 3, - "type": "public.connect_conversation_status", + "type": "connect_conversation_status", "notNull": true, "default": "'active'", "identity": null @@ -1911,7 +2025,7 @@ "table": "connect_invites", "name": "targetKind", "position": 4, - "type": "public.connect_invite_target_kind", + "type": "connect_invite_target_kind", "notNull": true, "default": null, "identity": null @@ -1941,7 +2055,7 @@ "table": "connect_invites", "name": "status", "position": 7, - "type": "public.connect_invite_status", + "type": "connect_invite_status", "notNull": true, "default": "'pending'", "identity": null @@ -2391,7 +2505,7 @@ "table": "integration_connections", "name": "level", "position": 4, - "type": "public.connection_level", + "type": "connection_level", "notNull": true, "default": null, "identity": null @@ -2401,7 +2515,7 @@ "table": "integration_connections", "name": "status", "position": 5, - "type": "public.connection_status", + "type": "connection_status", "notNull": true, "default": "'active'", "identity": null @@ -2571,7 +2685,7 @@ "table": "integration_requests", "name": "status", "position": 6, - "type": "public.integration_request_status", + "type": "integration_request_status", "notNull": true, "default": "'pending'", "identity": null @@ -2761,7 +2875,7 @@ "table": "message_outbox_events", "name": "eventType", "position": 2, - "type": "public.message_outbox_event_type", + "type": "message_outbox_event_type", "notNull": true, "default": null, "identity": null @@ -2801,7 +2915,7 @@ "table": "message_outbox_events", "name": "status", "position": 6, - "type": "public.message_outbox_event_status", + "type": "message_outbox_event_status", "notNull": true, "default": "'pending'", "identity": null @@ -3171,7 +3285,7 @@ "table": "organization_members", "name": "role", "position": 3, - "type": "public.organization_role", + "type": "organization_role", "notNull": true, "default": "'member'", "identity": null @@ -3681,7 +3795,7 @@ "table": "user_presence_status", "name": "status", "position": 2, - "type": "public.user_presence_status_enum", + "type": "user_presence_status_enum", "notNull": true, "default": "'online'", "identity": null @@ -3821,7 +3935,7 @@ "table": "users", "name": "userType", "position": 6, - "type": "public.user_type", + "type": "user_type", "notNull": true, "default": "'user'", "identity": null @@ -4021,15 +4135,6 @@ "columns": ["\"isPublic\""], "where": null }, - { - "kind": "index", - "table": "bots", - "name": "bots_userId_unique", - "unique": true, - "method": "btree", - "columns": ["\"userId\""], - "where": null - }, { "kind": "index", "table": "bots", @@ -4840,15 +4945,6 @@ "columns": ["\"encryptionKeyVersion\""], "where": null }, - { - "kind": "index", - "table": "integration_tokens", - "name": "integration_tokens_connectionId_unique", - "unique": true, - "method": "btree", - "columns": ["\"connectionId\""], - "where": null - }, { "kind": "index", "table": "message_outbox_events", @@ -4930,15 +5026,6 @@ "columns": ["\"messageId\"", "\"userId\"", "\"emoji\""], "where": null }, - { - "kind": "index", - "table": "message_reactions", - "name": "reactions_message_user_emoji_unique", - "unique": true, - "method": "btree", - "columns": ["\"messageId\"", "\"userId\"", "\"emoji\""], - "where": null - }, { "kind": "index", "table": "message_reactions", @@ -5065,15 +5152,6 @@ "columns": ["\"targetedResourceId\"", "\"targetedResourceType\""], "where": null }, - { - "kind": "index", - "table": "organization_members", - "name": "org_members_org_user_unique", - "unique": true, - "method": "btree", - "columns": ["\"organizationId\"", "\"userId\""], - "where": null - }, { "kind": "index", "table": "organization_members", @@ -5119,15 +5197,6 @@ "columns": ["\"slug\""], "where": null }, - { - "kind": "index", - "table": "organizations", - "name": "organizations_slug_unique", - "unique": true, - "method": "btree", - "columns": ["\"slug\""], - "where": null - }, { "kind": "index", "table": "pinned_messages", @@ -5263,15 +5332,6 @@ "columns": ["\"lastSeenAt\""], "where": null }, - { - "kind": "index", - "table": "user_presence_status", - "name": "user_presence_status_userId_unique", - "unique": true, - "method": "btree", - "columns": ["\"userId\""], - "where": null - }, { "kind": "index", "table": "user_presence_status", @@ -5299,15 +5359,6 @@ "columns": ["\"email\""], "where": null }, - { - "kind": "index", - "table": "users", - "name": "users_externalId_unique", - "unique": true, - "method": "btree", - "columns": ["\"externalId\""], - "where": null - }, { "kind": "index", "table": "users", @@ -5326,6 +5377,55 @@ "columns": ["\"userType\""], "where": null }, + { + "kind": "unique", + "table": "bots", + "name": "bots_userId_unique", + "columns": ["userId"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "integration_tokens", + "name": "integration_tokens_connectionId_unique", + "columns": ["connectionId"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "message_reactions", + "name": "reactions_message_user_emoji_unique", + "columns": ["messageId", "userId", "emoji"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "organization_members", + "name": "org_members_org_user_unique", + "columns": ["organizationId", "userId"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "organizations", + "name": "organizations_slug_unique", + "columns": ["slug"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "user_presence_status", + "name": "user_presence_status_userId_unique", + "columns": ["userId"], + "nullsNotDistinct": false + }, + { + "kind": "unique", + "table": "users", + "name": "users_externalId_unique", + "columns": ["externalId"], + "nullsNotDistinct": false + }, { "kind": "foreign_key", "table": "organization_members", diff --git a/packages/db/package.json b/packages/db/package.json index 49589aeea..c38bd24a2 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ "@effect/sql-pg": "catalog:effect", "@hazel/domain": "workspace:*", "@hazel/schema": "workspace:*", - "@maple-dev/effect-orm": "0.5.0", + "@maple-dev/effect-orm": "0.6.0", "effect": "catalog:effect" }, "devDependencies": { diff --git a/packages/db/scripts/render-ddl.ts b/packages/db/scripts/render-ddl.ts deleted file mode 100644 index d46076a94..000000000 --- a/packages/db/scripts/render-ddl.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Prints the DDL the effect-orm tables describe, enum types first: the body of the initial migration -// (`--> statement-breakpoint` between statements), and the schema tests create. -import * as S from "@maple-dev/effect-orm/schema" -import { Effect } from "effect" -import * as Tables from "../src/tables" - -const exported: ReadonlyArray = Object.values(Tables) -const enums = exported.filter( - (value): value is { enumName: string; enumValues: ReadonlyArray } => - typeof value === "object" && value !== null && "enumName" in value, -) -/** - * Unique keys `drizzle-kit push` created as UNIQUE constraints. effect-orm models them as unique - * indexes; the initial migration creates the constraints, so a fresh database matches one push built. - */ -const UNIQUE_CONSTRAINTS = new Set([ - "bots_userId_unique", - "integration_tokens_connectionId_unique", - "org_members_org_user_unique", - "organizations_slug_unique", - "reactions_message_user_emoji_unique", - "user_presence_status_userId_unique", - "users_externalId_unique", -]) - -const asConstraint = (statement: string): string => { - const match = /^CREATE UNIQUE INDEX IF NOT EXISTS "([^"]+)" ON "([^"]+)" USING btree \((.+)\);$/.exec( - statement, - ) - if (match === null || !UNIQUE_CONSTRAINTS.has(match[1]!)) return statement - return `ALTER TABLE "${match[2]}" ADD CONSTRAINT "${match[1]}" UNIQUE (${match[3]});` -} - -const statements = [ - ...enums.map( - (e) => `CREATE TYPE ${e.enumName} AS ENUM (${e.enumValues.map((v) => `'${v}'`).join(", ")});`, - ), - ...S.renderPgSchema(Effect.runSync(S.pgEntitiesOf(exported.filter(S.isSchemaObject)))).map((s) => - asConstraint(`${s};`), - ), -] -console.log(statements.join("\n--> statement-breakpoint\n")) diff --git a/packages/db/src/migrate.ts b/packages/db/src/migrate.ts index 31e36560a..1a6985c7c 100644 --- a/packages/db/src/migrate.ts +++ b/packages/db/src/migrate.ts @@ -1,10 +1,10 @@ /** * The bundled migrations and how to apply them. * - * `migrations/` holds one `_/migration.sql` per migration. The first, - * `initial_schema`, creates the schema as `drizzle-kit push` left it; `effect_orm_baseline` starts - * effect-orm's history there. Every later migration is written by `effect-orm generate` - * (`effect-orm.config.ts`), or by hand with `--custom` for what effect-orm does not model (enums). + * `migrations/` holds one `_/migration.sql` per migration, written by + * `effect-orm generate` (`effect-orm.config.ts`) from `src/tables`. The first, `initial_schema`, + * creates the schema exactly as `drizzle-kit push` left it; a database push built adopts it as + * applied (see {@link adoptPushedSchema}). */ import { existsSync, readdirSync, readFileSync } from "node:fs" import { dirname, join, resolve } from "node:path" @@ -16,8 +16,8 @@ import { postgresDialect } from "@maple-dev/effect-orm/postgres" import { Effect, Layer, Redacted, Schema } from "effect" import * as SqlClient from "effect/sql/SqlClient" -/** The migration a database `drizzle-kit push` built is adopted at. */ -export const BASELINE_MIGRATION = "20261012222436_effect_orm_baseline" +/** The migration a database `drizzle-kit push` built is adopted at: the schema push left. */ +export const BASELINE_MIGRATION = "20261013000436_initial_schema" export const migrationsFolder = (): string => resolve(dirname(fileURLToPath(import.meta.url)), "../migrations") diff --git a/packages/db/src/tables/attachments.ts b/packages/db/src/tables/attachments.ts index 8e44cb9da..7827ee3c4 100644 --- a/packages/db/src/tables/attachments.ts +++ b/packages/db/src/tables/attachments.ts @@ -1,8 +1,8 @@ import { AttachmentId, ChannelId, MessageId, OrganizationId, UserId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { now, timestamptz, uuid, varchar } from "./columns" -export const attachmentStatusEnum = pgEnum("attachment_status", ["uploading", "complete", "failed"]) +export const attachmentStatusEnum = PG.enumType("attachment_status", ["uploading", "complete", "failed"]) export const attachmentsTable = PG.table("attachments", { columns: { diff --git a/packages/db/src/tables/bots.ts b/packages/db/src/tables/bots.ts index c46df5f18..8708a8ead 100644 --- a/packages/db/src/tables/bots.ts +++ b/packages/db/src/tables/bots.ts @@ -25,8 +25,8 @@ export const botsTable = PG.table("bots", { deletedAt: PG.nullable(timestamptz), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("bots_userId_unique", ["userId"])], indexes: [ - PG.uniqueIndex("bots_userId_unique", ["userId"]), PG.index("bots_user_id_idx", ["userId"]), PG.index("bots_created_by_idx", ["createdBy"]), PG.index("bots_is_public_idx", ["isPublic"]), diff --git a/packages/db/src/tables/channels.ts b/packages/db/src/tables/channels.ts index 2bf5f9ea5..efd9dc06a 100644 --- a/packages/db/src/tables/channels.ts +++ b/packages/db/src/tables/channels.ts @@ -8,9 +8,15 @@ import { UserId, } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { now, timestamptz, uuid, varchar } from "./columns" -export const channelTypeEnum = pgEnum("channel_type", ["public", "private", "thread", "direct", "single"]) +export const channelTypeEnum = PG.enumType("channel_type", [ + "public", + "private", + "thread", + "direct", + "single", +]) export const channelMembersTable = PG.table("channel_members", { columns: { diff --git a/packages/db/src/tables/chat-sync.ts b/packages/db/src/tables/chat-sync.ts index 3586afbda..ce9ae075f 100644 --- a/packages/db/src/tables/chat-sync.ts +++ b/packages/db/src/tables/chat-sync.ts @@ -10,21 +10,21 @@ import { UserId, } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, uuid, varchar } from "./columns" -export const chatSyncConnectionStatusEnum = pgEnum("chat_sync_connection_status", [ +export const chatSyncConnectionStatusEnum = PG.enumType("chat_sync_connection_status", [ "active", "paused", "error", "disabled", ]) -export const chatSyncDirectionEnum = pgEnum("chat_sync_direction", [ +export const chatSyncDirectionEnum = PG.enumType("chat_sync_direction", [ "both", "hazel_to_external", "external_to_hazel", ]) -export const chatSyncReceiptSourceEnum = pgEnum("chat_sync_receipt_source", ["hazel", "external"]) -export const chatSyncReceiptStatusEnum = pgEnum("chat_sync_receipt_status", [ +export const chatSyncReceiptSourceEnum = PG.enumType("chat_sync_receipt_source", ["hazel", "external"]) +export const chatSyncReceiptStatusEnum = PG.enumType("chat_sync_receipt_status", [ "processed", "ignored", "failed", diff --git a/packages/db/src/tables/columns.ts b/packages/db/src/tables/columns.ts index 7c8c0e038..d238cc5df 100644 --- a/packages/db/src/tables/columns.ts +++ b/packages/db/src/tables/columns.ts @@ -43,21 +43,6 @@ export const typed = >(type: T): PG.PgType> => type as unknown as PG.PgType> -/** - * A Postgres enum type in `public`, read as the union of its labels. effect-orm does not model enums, - * so the `CREATE TYPE` (and any later `ALTER TYPE`) is written in a `--custom` migration. The name is - * schema-qualified so `effect-orm verify`, which rebuilds the tables in a scratch schema, still - * resolves it. - */ -export const pgEnum = ]>( - name: string, - values: Values, -) => - Object.assign(PG.custom(`public.${name}`, Schema.Literals(values)), { - enumName: `public.${name}`, - enumValues: values, - }) - /** `now()`, typed as the `Date` the timestamp columns hold. */ export const now = () => PG.sql(timestamptz)`now()` diff --git a/packages/db/src/tables/connect.ts b/packages/db/src/tables/connect.ts index c45ae2863..17a105170 100644 --- a/packages/db/src/tables/connect.ts +++ b/packages/db/src/tables/connect.ts @@ -8,21 +8,24 @@ import { UserId, } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, uuid, varchar } from "./columns" -export const connectConversationStatusEnum = pgEnum("connect_conversation_status", ["active", "disconnected"]) -export const connectConversationChannelRoleEnum = pgEnum("connect_conversation_channel_role", [ +export const connectConversationStatusEnum = PG.enumType("connect_conversation_status", [ + "active", + "disconnected", +]) +export const connectConversationChannelRoleEnum = PG.enumType("connect_conversation_channel_role", [ "host", "guest", ]) -export const connectInviteStatusEnum = pgEnum("connect_invite_status", [ +export const connectInviteStatusEnum = PG.enumType("connect_invite_status", [ "pending", "accepted", "declined", "revoked", "expired", ]) -export const connectInviteTargetKindEnum = pgEnum("connect_invite_target_kind", ["slug", "email"]) +export const connectInviteTargetKindEnum = PG.enumType("connect_invite_target_kind", ["slug", "email"]) export const connectConversationChannelsTable = PG.table("connect_conversation_channels", { columns: { diff --git a/packages/db/src/tables/index.ts b/packages/db/src/tables/index.ts index 10dd0fd9d..d3fc568d2 100644 --- a/packages/db/src/tables/index.ts +++ b/packages/db/src/tables/index.ts @@ -25,4 +25,4 @@ export * from "./rss-subscriptions" export * from "./typing-indicators" export * from "./user-presence-status" export * from "./users" -export { jsonb, now, pgEnum, timestamptz, typed, uuid, varchar } from "./columns" +export { jsonb, now, timestamptz, typed, uuid, varchar } from "./columns" diff --git a/packages/db/src/tables/integration-connections.ts b/packages/db/src/tables/integration-connections.ts index 81f8b4af1..4b7382d79 100644 --- a/packages/db/src/tables/integration-connections.ts +++ b/packages/db/src/tables/integration-connections.ts @@ -1,9 +1,9 @@ import { IntegrationConnectionId, OrganizationId, UserId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, typed, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, typed, uuid, varchar } from "./columns" -export const connectionLevelEnum = pgEnum("connection_level", ["organization", "user"]) -export const connectionStatusEnum = pgEnum("connection_status", [ +export const connectionLevelEnum = PG.enumType("connection_level", ["organization", "user"]) +export const connectionStatusEnum = PG.enumType("connection_status", [ "active", "expired", "revoked", diff --git a/packages/db/src/tables/integration-requests.ts b/packages/db/src/tables/integration-requests.ts index c924f49e7..f4ebc9598 100644 --- a/packages/db/src/tables/integration-requests.ts +++ b/packages/db/src/tables/integration-requests.ts @@ -1,8 +1,8 @@ import { IntegrationRequestId, OrganizationId, UserId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { now, timestamptz, uuid, varchar } from "./columns" -export const integrationRequestStatusEnum = pgEnum("integration_request_status", [ +export const integrationRequestStatusEnum = PG.enumType("integration_request_status", [ "pending", "reviewed", "planned", diff --git a/packages/db/src/tables/integration-tokens.ts b/packages/db/src/tables/integration-tokens.ts index 8fbe6785a..1165d05a0 100644 --- a/packages/db/src/tables/integration-tokens.ts +++ b/packages/db/src/tables/integration-tokens.ts @@ -20,8 +20,8 @@ export const integrationTokensTable = PG.table("integration_tokens", { updatedAt: PG.column(timestamptz, { defaultExpr: "now()" }), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("integration_tokens_connectionId_unique", ["connectionId"])], indexes: [ - PG.uniqueIndex("integration_tokens_connectionId_unique", ["connectionId"]), PG.index("int_tokens_connection_id_idx", ["connectionId"]), PG.index("int_tokens_expires_at_idx", ["expiresAt"]), PG.index("int_tokens_key_version_idx", ["encryptionKeyVersion"]), diff --git a/packages/db/src/tables/message-outbox.ts b/packages/db/src/tables/message-outbox.ts index a1d620ec0..2d73636ff 100644 --- a/packages/db/src/tables/message-outbox.ts +++ b/packages/db/src/tables/message-outbox.ts @@ -1,15 +1,15 @@ import { ChannelId, MessageOutboxEventId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, uuid, varchar } from "./columns" -export const messageOutboxEventTypeEnum = pgEnum("message_outbox_event_type", [ +export const messageOutboxEventTypeEnum = PG.enumType("message_outbox_event_type", [ "message_created", "message_updated", "message_deleted", "reaction_created", "reaction_deleted", ]) -export const messageOutboxEventStatusEnum = pgEnum("message_outbox_event_status", [ +export const messageOutboxEventStatusEnum = PG.enumType("message_outbox_event_status", [ "pending", "processing", "processed", diff --git a/packages/db/src/tables/messages.ts b/packages/db/src/tables/messages.ts index 19471bc0d..a051148bb 100644 --- a/packages/db/src/tables/messages.ts +++ b/packages/db/src/tables/messages.ts @@ -74,13 +74,13 @@ export const messageReactionsTable = PG.table("message_reactions", { createdAt: PG.column(timestamptz, { defaultExpr: "now()" }), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("reactions_message_user_emoji_unique", ["messageId", "userId", "emoji"])], indexes: [ PG.index("reactions_channel_id_idx", ["channelId"]), PG.index("reactions_conversation_id_idx", ["conversationId"]), PG.index("reactions_message_id_idx", ["messageId"]), PG.index("reactions_user_id_idx", ["userId"]), PG.index("reactions_message_user_emoji_idx", ["messageId", "userId", "emoji"]), - PG.uniqueIndex("reactions_message_user_emoji_unique", ["messageId", "userId", "emoji"]), ], }) export type MessageReaction = PG.SelectRowOf diff --git a/packages/db/src/tables/organizations.ts b/packages/db/src/tables/organizations.ts index 6c843d6b9..e24f8551b 100644 --- a/packages/db/src/tables/organizations.ts +++ b/packages/db/src/tables/organizations.ts @@ -1,8 +1,8 @@ import { OrganizationId, OrganizationMemberId, UserId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, uuid, varchar } from "./columns" -export const organizationRoleEnum = pgEnum("organization_role", ["admin", "member", "owner"]) +export const organizationRoleEnum = PG.enumType("organization_role", ["admin", "member", "owner"]) export const organizationsTable = PG.table("organizations", { columns: { @@ -17,8 +17,8 @@ export const organizationsTable = PG.table("organizations", { deletedAt: PG.nullable(timestamptz), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("organizations_slug_unique", ["slug"])], indexes: [ - PG.uniqueIndex("organizations_slug_unique", ["slug"]), PG.index("organizations_slug_idx", ["slug"]), PG.index("organizations_deleted_at_idx", ["deletedAt"]), PG.uniqueIndex( @@ -45,10 +45,10 @@ export const organizationMembersTable = PG.table("organization_members", { deletedAt: PG.nullable(timestamptz), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("org_members_org_user_unique", ["organizationId", "userId"])], indexes: [ PG.index("org_members_organization_id_idx", ["organizationId"]), PG.index("org_members_user_id_idx", ["userId"]), - PG.uniqueIndex("org_members_org_user_unique", ["organizationId", "userId"]), ], foreignKeys: [ PG.foreignKey({ diff --git a/packages/db/src/tables/user-presence-status.ts b/packages/db/src/tables/user-presence-status.ts index 76cb52f54..58ad7291d 100644 --- a/packages/db/src/tables/user-presence-status.ts +++ b/packages/db/src/tables/user-presence-status.ts @@ -1,8 +1,8 @@ import { ChannelId, UserId, UserPresenceStatusId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { now, timestamptz, uuid, varchar } from "./columns" -export const userPresenceStatusEnum = pgEnum("user_presence_status_enum", [ +export const userPresenceStatusEnum = PG.enumType("user_presence_status_enum", [ "online", "away", "busy", @@ -24,8 +24,8 @@ export const userPresenceStatusTable = PG.table("user_presence_status", { lastSeenAt: PG.column(timestamptz, { defaultExpr: "now()" }), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("user_presence_status_userId_unique", ["userId"])], indexes: [ - PG.uniqueIndex("user_presence_status_userId_unique", ["userId"]), PG.index("user_presence_status_user_id_idx", ["userId"]), PG.index("user_presence_status_active_channel_idx", ["activeChannelId"]), PG.index("user_presence_status_last_seen_idx", ["lastSeenAt"]), diff --git a/packages/db/src/tables/users.ts b/packages/db/src/tables/users.ts index e558ae074..0d17672cd 100644 --- a/packages/db/src/tables/users.ts +++ b/packages/db/src/tables/users.ts @@ -1,8 +1,8 @@ import { UserId } from "@hazel/schema" import * as PG from "@maple-dev/effect-orm/postgres" -import { jsonb, now, pgEnum, timestamptz, uuid, varchar } from "./columns" +import { jsonb, now, timestamptz, uuid, varchar } from "./columns" -export const userTypeEnum = pgEnum("user_type", ["user", "machine"]) +export const userTypeEnum = PG.enumType("user_type", ["user", "machine"]) export const usersTable = PG.table("users", { columns: { @@ -21,8 +21,8 @@ export const usersTable = PG.table("users", { deletedAt: PG.nullable(timestamptz), }, primaryKey: ["id"], + uniqueConstraints: [PG.unique("users_externalId_unique", ["externalId"])], indexes: [ - PG.uniqueIndex("users_externalId_unique", ["externalId"]), PG.index("users_external_id_idx", ["externalId"]), PG.index("users_email_idx", ["email"]), PG.index("users_user_type_idx", ["userType"]), diff --git a/packages/setup/src/commands/env.ts b/packages/setup/src/commands/env.ts index 09903e2e3..eaaa51cf6 100644 --- a/packages/setup/src/commands/env.ts +++ b/packages/setup/src/commands/env.ts @@ -410,7 +410,9 @@ export const envCommand = Command.make( yield* Console.log(pc.green("\n\u2705 Setup complete!")) yield* Console.log(pc.bold("Next steps:")) yield* Console.log(` 1. Run ${pc.cyan("`docker compose up -d`")} to start local services`) - yield* Console.log(` 2. Run ${pc.cyan("`bun run db:push`")} to initialize the database`) + yield* Console.log( + ` 2. Run ${pc.cyan("`bun run --cwd packages/db db:migrate`")} to initialize the database`, + ) yield* Console.log( ` 3. Run ${pc.cyan("`bun run dev`")} to start the core apps and bot gateway`, )