diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92d2d46..22ba959 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,22 +27,6 @@ jobs: - run: bun run check - run: bun run build - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: "24" - - name: Install Chromium - run: bun run --cwd apps/web playwright install --with-deps chromium - - name: Run browser acceptance test - id: e2e - run: bun run test:e2e - - name: Upload Playwright failure report - uses: actions/upload-artifact@v4 - if: ${{ failure() && steps.e2e.outcome == 'failure' }} - with: - name: playwright-report - path: apps/web/playwright-report/ - retention-days: 7 deploy_preview: name: Deploy labeled PR preview diff --git a/.gitignore b/.gitignore index 5b984dc..66bd5ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ .turbo node_modules *.tsbuildinfo -playwright-report/ -test-results/ diff --git a/AGENTS.md b/AGENTS.md index 43e108b..cbc5a38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ ui → nothing - Remote functions own application calls from the browser. A `query` is the cache; a `form` is the mutation. Do not add a client-side state library. - Remote handlers invoke core capabilities directly. - Better Auth is the deliberate exception: its Svelte client calls the same-origin `/api/auth/*` routes directly so the provider owns its browser protocol and cookies. +- Better Auth owns organizations and memberships. Core owns application permissions; require the capability through `OrganizationAccess` and scope persistence by the explicit organization ID. - Effect Schema validates remote form input through Standard Schema. Remote handlers map expected failures to SvelteKit `error` or `invalid` results. - Svelte runes are limited to component-local presentation state. - Server layouts resolve principals required by their pages; remote handlers and endpoints own authorization for their operations. @@ -45,6 +46,6 @@ ui → nothing ## Tests -Test through public interfaces using substitute Layers, PGlite, and real HTTP or SvelteKit boundaries. Do not mock modules or use arbitrary sleeps. +Test through public interfaces using substitute Layers, PGlite, and provider HTTP handlers. Do not mock modules or use arbitrary sleeps. A change is complete only after type checks, tests, formatting, architecture checks, and dead-code checks pass. diff --git a/README.md b/README.md index 7a5dd4d..2dc422c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ alchemy.run.ts application Stack summary SvelteKit remote functions call application services directly. The Web Worker hosts Better Auth behind same-origin routes and projects authenticated principals into application operations. Independent API and CLI clients are added only when their concrete contracts and credentials are known. +The example flow is a shared todo list: sign up, create an organization, and add or +complete todos with its members. Better Auth owns organizations and memberships; +`TodoDirectory` owns application permissions and `TodoStore` owns organization-scoped persistence. + ## Development Bootstrap a checkout and authenticate the local Alchemy profile: @@ -79,25 +83,22 @@ single-line logfmt; production uses JSON. Normal `bun run dev` needs no collecto See [operation logging](skills/effect-forge/references/observability.md) for examples. +## Organization URLs + +Organization URLs use handles, for example `/organizations/analytical-engine/todos`. +Authorization and persistence use the stable organization ID. Handles can be changed +through Better Auth; changing one changes its URL without redirecting old links. + ## Validation ```sh -bun run test # tests only +bun run test # colocated tests bun run check # formatting, lint, types, and tests bun run build # production builds ``` -CI runs `check`, `build`, and the browser acceptance test. - -### Browser acceptance test - -With Docker running: - -```sh -bun run test:e2e -``` - -This runs the authentication journey against the built app and a disposable PostgreSQL database. +CI runs `check` and `build`. Colocated tests cover domain and application behavior, +provider HTTP handling, and SQL through PGlite. ### Database changes diff --git a/adapters/database-postgres/drizzle.config.ts b/adapters/database-postgres/drizzle.config.ts index c20f290..550a8f4 100644 --- a/adapters/database-postgres/drizzle.config.ts +++ b/adapters/database-postgres/drizzle.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./drizzle", - schema: ["./src/auth/schema.ts", "./src/workspace/schema.ts"], + schema: ["./src/auth/schema.ts", "./src/todo/schema.ts"], }); diff --git a/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql new file mode 100644 index 0000000..0f91675 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql @@ -0,0 +1,38 @@ +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY, + "organization_id" text NOT NULL, + "email" text NOT NULL, + "role" text, + "status" text DEFAULT 'pending' NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "inviter_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY, + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text DEFAULT 'member' NOT NULL, + "created_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY, + "name" text NOT NULL, + "slug" text NOT NULL UNIQUE, + "logo" text, + "created_at" timestamp NOT NULL, + "metadata" text +); +--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "active_organization_id" text;--> statement-breakpoint +CREATE INDEX "invitation_organizationId_idx" ON "invitation" ("organization_id");--> statement-breakpoint +CREATE INDEX "invitation_email_idx" ON "invitation" ("email");--> statement-breakpoint +CREATE INDEX "member_organizationId_idx" ON "member" ("organization_id");--> statement-breakpoint +CREATE INDEX "member_userId_idx" ON "member" ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "organization_slug_uidx" ON "organization" ("slug");--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fkey" FOREIGN KEY ("inviter_id") REFERENCES "user"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json new file mode 100644 index 0000000..c404145 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json @@ -0,0 +1,1300 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2a9c34eb-9235-4966-b3f6-cd293645b3c9", + "prevIds": ["f3b5b1c1-720f-4d11-9a56-780da28be2d0"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspace_members", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspaces", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspace_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "workspace_members_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["workspace_id"], + "schemaTo": "public", + "tableTo": "workspaces", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "workspace_members_workspace_id_workspaces_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["workspace_id", "user_id"], + "nameExplicit": false, + "name": "workspace_members_pkey", + "entityType": "pks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspaces_pkey", + "schema": "public", + "table": "workspaces", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["name"], + "nullsNotDistinct": false, + "name": "workspaces_name_unique", + "entityType": "uniques", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\"role\" in ('owner', 'admin', 'member')", + "name": "workspace_members_role_check", + "entityType": "checks", + "schema": "public", + "table": "workspace_members" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql new file mode 100644 index 0000000..04b33a1 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql @@ -0,0 +1,10 @@ +CREATE TABLE "todos" ( + "id" uuid PRIMARY KEY, + "organization_id" text NOT NULL, + "title" varchar(200) NOT NULL, + "completed" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "todos_organization_id_index" ON "todos" ("organization_id");--> statement-breakpoint +ALTER TABLE "todos" ADD CONSTRAINT "todos_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json new file mode 100644 index 0000000..29cbf30 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json @@ -0,0 +1,1413 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "b2323111-6852-49eb-ac65-d97a9b51df79", + "prevIds": ["2a9c34eb-9235-4966-b3f6-cd293645b3c9"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspace_members", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspaces", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "todos", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspace_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "workspace_members_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "todos_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["workspace_id"], + "schemaTo": "public", + "tableTo": "workspaces", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "workspace_members_workspace_id_workspaces_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "todos_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "todos" + }, + { + "columns": ["workspace_id", "user_id"], + "nameExplicit": false, + "name": "workspace_members_pkey", + "entityType": "pks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspaces_pkey", + "schema": "public", + "table": "workspaces", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "todos_pkey", + "schema": "public", + "table": "todos", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["name"], + "nullsNotDistinct": false, + "name": "workspaces_name_unique", + "entityType": "uniques", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\"role\" in ('owner', 'admin', 'member')", + "name": "workspace_members_role_check", + "entityType": "checks", + "schema": "public", + "table": "workspace_members" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql new file mode 100644 index 0000000..f5e08c9 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "workspace_members" DROP CONSTRAINT "workspace_members_workspace_id_workspaces_id_fkey";--> statement-breakpoint +DROP TABLE "workspace_members";--> statement-breakpoint +DROP TABLE "workspaces"; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json new file mode 100644 index 0000000..a3c28d5 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json @@ -0,0 +1,1270 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "24c5b99c-89ba-4bcd-bcf3-7034e36ca21e", + "prevIds": ["b2323111-6852-49eb-ac65-d97a9b51df79"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "todos", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "todos_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "todos_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "todos" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "todos_pkey", + "schema": "public", + "table": "todos", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/migration.sql b/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/migration.sql new file mode 100644 index 0000000..acc79ce --- /dev/null +++ b/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "todos" ADD COLUMN "description" varchar(2000) DEFAULT '' NOT NULL; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/snapshot.json b/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/snapshot.json new file mode 100644 index 0000000..93bbb0e --- /dev/null +++ b/adapters/database-postgres/drizzle/20260922220335_worthless_texas_twister/snapshot.json @@ -0,0 +1,1283 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "64dd74c0-70f3-40f0-bc6d-a1a37330a461", + "prevIds": ["24c5b99c-89ba-4bcd-bcf3-7034e36ca21e"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "todos", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(2000)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "''", + "generated": null, + "identity": null, + "name": "description", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "todos_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "todos_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "todos" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "todos_pkey", + "schema": "public", + "table": "todos", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/src/auth/schema.ts b/adapters/database-postgres/src/auth/schema.ts index f7806fc..9f0fddf 100644 --- a/adapters/database-postgres/src/auth/schema.ts +++ b/adapters/database-postgres/src/auth/schema.ts @@ -1,6 +1,15 @@ /** @effect-diagnostics globalDate:skip-file */ import { defineRelationsPart } from "drizzle-orm"; -import { pgTable, text, bigint, timestamp, boolean, integer, index } from "drizzle-orm/pg-core"; +import { + pgTable, + text, + bigint, + timestamp, + boolean, + integer, + index, + uniqueIndex, +} from "drizzle-orm/pg-core"; export const user = pgTable("user", { id: text("id").primaryKey(), @@ -30,6 +39,7 @@ export const session = pgTable( userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), + activeOrganizationId: text("active_organization_id"), }, (table) => [index("session_userId_idx").on(table.userId)], ); @@ -74,6 +84,60 @@ export const verification = pgTable( (table) => [index("verification_identifier_idx").on(table.identifier)], ); +export const organization = pgTable( + "organization", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + slug: text("slug").notNull().unique(), + logo: text("logo"), + createdAt: timestamp("created_at").notNull(), + metadata: text("metadata"), + }, + (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], +); + +export const member = pgTable( + "member", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + role: text("role").default("member").notNull(), + createdAt: timestamp("created_at").notNull(), + }, + (table) => [ + index("member_organizationId_idx").on(table.organizationId), + index("member_userId_idx").on(table.userId), + ], +); + +export const invitation = pgTable( + "invitation", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role"), + status: text("status").default("pending").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("invitation_organizationId_idx").on(table.organizationId), + index("invitation_email_idx").on(table.email), + ], +); + export const rateLimit = pgTable("rate_limit", { id: text("id").primaryKey(), key: text("key").notNull().unique(), @@ -82,7 +146,16 @@ export const rateLimit = pgTable("rate_limit", { }); export const authRelations = defineRelationsPart( - { user, session, account, verification, rateLimit }, + { + user, + session, + account, + verification, + organization, + member, + invitation, + rateLimit, + }, (r) => ({ user: { sessions: r.many.session({ @@ -93,6 +166,14 @@ export const authRelations = defineRelationsPart( from: r.user.id, to: r.account.userId, }), + members: r.many.member({ + from: r.user.id, + to: r.member.userId, + }), + invitations: r.many.invitation({ + from: r.user.id, + to: r.invitation.inviterId, + }), }, session: { user: r.one.user({ @@ -106,5 +187,35 @@ export const authRelations = defineRelationsPart( to: r.user.id, }), }, + organization: { + members: r.many.member({ + from: r.organization.id, + to: r.member.organizationId, + }), + invitations: r.many.invitation({ + from: r.organization.id, + to: r.invitation.organizationId, + }), + }, + member: { + organization: r.one.organization({ + from: r.member.organizationId, + to: r.organization.id, + }), + user: r.one.user({ + from: r.member.userId, + to: r.user.id, + }), + }, + invitation: { + organization: r.one.organization({ + from: r.invitation.organizationId, + to: r.organization.id, + }), + user: r.one.user({ + from: r.invitation.inviterId, + to: r.user.id, + }), + }, }), ); diff --git a/adapters/database-postgres/src/persistence-postgres.ts b/adapters/database-postgres/src/persistence-postgres.ts index 070e569..5321366 100644 --- a/adapters/database-postgres/src/persistence-postgres.ts +++ b/adapters/database-postgres/src/persistence-postgres.ts @@ -1,7 +1,7 @@ import { Layer } from "effect"; import { type CustomTypesConfig, types } from "pg"; import { Database } from "./internal/database.ts"; -import { WorkspaceStorePostgres } from "./workspace/workspace-store-postgres.ts"; +import { TodoStorePostgres } from "./todo/todo-store-postgres.ts"; const drizzleRawStringOids = new Set([1082, 1114, 1184, 1186, 1231, 1115, 1185, 1187, 1182]); @@ -12,6 +12,6 @@ export const typeParsers: CustomTypesConfig = { }; /** Provides every PostgreSQL-backed application persistence port. */ -export const layer = WorkspaceStorePostgres.layer.pipe(Layer.provide(Database.layer)); +export const layer = TodoStorePostgres.layer.pipe(Layer.provide(Database.layer)); export * as PersistencePostgres from "./persistence-postgres.ts"; diff --git a/adapters/database-postgres/src/test/persistence-pglite.ts b/adapters/database-postgres/src/test/persistence-pglite.ts index ae4e706..26b0221 100644 --- a/adapters/database-postgres/src/test/persistence-pglite.ts +++ b/adapters/database-postgres/src/test/persistence-pglite.ts @@ -4,7 +4,7 @@ import { migrate } from "drizzle-orm/effect-pglite/migrator"; import { Effect, Layer } from "effect"; import { Database } from "../internal/database.ts"; import { migrationConfig } from "../migrations.ts"; -import { WorkspaceStorePostgres } from "../workspace/workspace-store-postgres.ts"; +import { TodoStorePostgres } from "../todo/todo-store-postgres.ts"; /** Provides the migrated test database for adapter fixtures that insert prerequisite records. */ export const databaseLayer = Layer.effect( @@ -17,6 +17,6 @@ export const databaseLayer = Layer.effect( ).pipe(Layer.provide(PgliteClient.layer())); /** Provides every PostgreSQL persistence port through an isolated migrated PGlite database. */ -export const layer = WorkspaceStorePostgres.layer.pipe(Layer.provide(databaseLayer)); +export const layer = TodoStorePostgres.layer.pipe(Layer.provide(databaseLayer)); export * as PersistencePglite from "./persistence-pglite.ts"; diff --git a/adapters/database-postgres/src/todo/schema.ts b/adapters/database-postgres/src/todo/schema.ts new file mode 100644 index 0000000..0933c54 --- /dev/null +++ b/adapters/database-postgres/src/todo/schema.ts @@ -0,0 +1,17 @@ +import { boolean, index, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import { organization } from "../auth/schema.ts"; + +export const todos = pgTable( + "todos", + { + id: uuid("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + title: varchar("title", { length: 200 }).notNull(), + description: varchar("description", { length: 2000 }).notNull().default(""), + completed: boolean("completed").notNull().default(false), + createdAt: timestamp("created_at").notNull().defaultNow(), + }, + (table) => [index("todos_organization_id_index").on(table.organizationId)], +); diff --git a/adapters/database-postgres/src/todo/todo-store-postgres.test.ts b/adapters/database-postgres/src/todo/todo-store-postgres.test.ts new file mode 100644 index 0000000..b1bf29b --- /dev/null +++ b/adapters/database-postgres/src/todo/todo-store-postgres.test.ts @@ -0,0 +1,136 @@ +import { assert, describe, it } from "@effect/vitest"; +import { TodoStore } from "@effect-forge/core/todo-store"; +import { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, TodoDescription, TodoId, TodoTitle } from "@effect-forge/domain/todo"; +import { eq } from "drizzle-orm"; +import { DateTime, Effect, Layer } from "effect"; +import { organization } from "../auth/schema.ts"; +import { Database } from "../internal/database.ts"; +import { PersistencePglite } from "../test/persistence-pglite.ts"; +import { todos } from "./schema.ts"; + +const orgA = OrganizationId.make("org-a"); +const orgB = OrganizationId.make("org-b"); +const createdAt = DateTime.toDateUtc(DateTime.makeUnsafe(0)); +const todo = Todo.make({ + id: TodoId.make("550e8400-e29b-41d4-a716-446655440000"), + organizationId: orgA, + title: TodoTitle.make("Ship organizations"), + description: TodoDescription.make("Check membership isolation.\nShip together."), + completed: false, +}); +const sibling = Todo.make({ ...todo, id: TodoId.make("550e8400-e29b-41d4-a716-446655440001") }); +const foreign = Todo.make({ + ...todo, + id: TodoId.make("550e8400-e29b-41d4-a716-446655440002"), + organizationId: orgB, +}); +const layer = PersistencePglite.layer.pipe(Layer.provideMerge(PersistencePglite.databaseLayer)); +const fixture = Effect.gen(function* () { + const database = yield* Database.Service; + const store = yield* TodoStore.Service; + yield* database.insert(organization).values([ + { id: orgA, name: "A", slug: "a", createdAt }, + { id: orgB, name: "B", slug: "b", createdAt }, + ]); + return { database, store }; +}); + +describe("PostgreSQL TodoStore", () => { + it.effect("defaults omitted descriptions to empty text", () => + Effect.gen(function* () { + const { database, store } = yield* fixture; + yield* database.insert(todos).values({ + id: todo.id, + organizationId: orgA, + title: todo.title, + completed: false, + }); + assert.deepEqual(yield* store.list(orgA), [ + { ...todo, description: TodoDescription.make("") }, + ]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("persists creation, repeated completion, and reopening", () => + Effect.gen(function* () { + const { store } = yield* fixture; + assert.deepEqual(yield* store.create(todo), todo); + assert.deepEqual(yield* store.list(orgA), [todo]); + const done = { ...todo, completed: true }; + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), done); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), done); + assert.deepEqual(yield* store.list(orgA), [done]); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, false), todo); + assert.deepEqual(yield* store.list(orgA), [todo]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("lists only the requested organization's todos", () => + Effect.gen(function* () { + const { store } = yield* fixture; + yield* store.create(todo); + yield* store.create(foreign); + assert.deepEqual(yield* store.list(orgA), [todo]); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("updates only the requested todo within its organization", () => + Effect.gen(function* () { + const { store } = yield* fixture; + for (const row of [todo, sibling, foreign]) yield* store.create(row); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), { + ...todo, + completed: true, + }); + assert.sameDeepMembers( + [...(yield* store.list(orgA))], + [{ ...todo, completed: true }, sibling], + ); + assert.deepEqual(yield* store.list(orgB), [foreign]); + assert.deepEqual( + yield* store.setCompleted(orgA, foreign.id, true).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: orgA, id: foreign.id }), + ); + const missingId = TodoId.make("550e8400-e29b-41d4-a716-446655440003"); + assert.deepEqual( + yield* store.setCompleted(orgA, missingId, false).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: orgA, id: missingId }), + ); + assert.sameDeepMembers( + [...(yield* store.list(orgA))], + [{ ...todo, completed: true }, sibling], + ); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("cascades organization deletion without removing another organization's todos", () => + Effect.gen(function* () { + const { database, store } = yield* fixture; + yield* store.create(todo); + yield* store.create(foreign); + yield* database.delete(organization).where(eq(organization.id, orgA)); + assert.deepEqual(yield* store.list(orgA), []); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("translates a database constraint failure into PersistenceError", () => + Effect.gen(function* () { + const { store } = yield* fixture; + yield* store.create(todo); + assert.instanceOf(yield* store.create(todo).pipe(Effect.flip), TodoStore.PersistenceError); + assert.deepEqual(yield* store.list(orgA), [todo]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("rejects malformed persisted rows with PersistenceError", () => + Effect.gen(function* () { + const { database, store } = yield* fixture; + yield* database.insert(todos).values({ ...todo, title: "" }); + assert.instanceOf(yield* store.list(orgA).pipe(Effect.flip), TodoStore.PersistenceError); + }).pipe(Effect.provide(layer)), + ); +}); diff --git a/adapters/database-postgres/src/todo/todo-store-postgres.ts b/adapters/database-postgres/src/todo/todo-store-postgres.ts new file mode 100644 index 0000000..e0fab3b --- /dev/null +++ b/adapters/database-postgres/src/todo/todo-store-postgres.ts @@ -0,0 +1,63 @@ +import { TodoStore } from "@effect-forge/core/todo-store"; +import type { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, type TodoId } from "@effect-forge/domain/todo"; +import { and, asc, eq } from "drizzle-orm"; +import { Effect, Layer, Schema } from "effect"; +import { Database } from "../internal/database.ts"; +import { todos } from "./schema.ts"; + +const columns = { + id: todos.id, + organizationId: todos.organizationId, + title: todos.title, + description: todos.description, + completed: todos.completed, +}; + +export const layer = Layer.effect( + TodoStore.Service, + Effect.gen(function* () { + const database = yield* Database.Service; + const create = Effect.fn("TodoStorePostgres.create")((todo: Todo) => + database + .insert(todos) + .values(todo) + .pipe( + Effect.as(todo), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ), + ); + const list = Effect.fn("TodoStorePostgres.list")((organizationId: OrganizationId) => + database + .select(columns) + .from(todos) + .where(eq(todos.organizationId, organizationId)) + .orderBy(asc(todos.createdAt), asc(todos.id)) + .pipe( + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Todo))), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ), + ); + const setCompleted = Effect.fn("TodoStorePostgres.setCompleted")(function* ( + organizationId: OrganizationId, + id: TodoId, + completed: boolean, + ) { + const rows = yield* database + .update(todos) + .set({ completed }) + .where(and(eq(todos.organizationId, organizationId), eq(todos.id, id))) + .returning(columns) + .pipe( + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Todo))), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ); + const todo = rows[0]; + if (todo === undefined) return yield* new TodoStore.NotFound({ organizationId, id }); + return todo; + }); + return TodoStore.Service.of({ create, list, setCompleted }); + }), +); + +export * as TodoStorePostgres from "./todo-store-postgres.ts"; diff --git a/adapters/database-postgres/src/workspace/schema.ts b/adapters/database-postgres/src/workspace/schema.ts deleted file mode 100644 index 3aeee0b..0000000 --- a/adapters/database-postgres/src/workspace/schema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { sql } from "drizzle-orm"; -import { check, index, pgTable, primaryKey, unique, uuid, varchar } from "drizzle-orm/pg-core"; - -/** PostgreSQL representation of persisted workspaces. */ -export const workspaces = pgTable( - "workspaces", - { - id: uuid("id").primaryKey(), - name: varchar("name", { length: 100 }).notNull(), - }, - (table) => [unique("workspaces_name_unique").on(table.name)], -); - -/** PostgreSQL representation of a user's role in one workspace. */ -export const workspaceMembers = pgTable( - "workspace_members", - { - workspaceId: uuid("workspace_id") - .notNull() - .references(() => workspaces.id, { onDelete: "cascade" }), - userId: varchar("user_id", { length: 255 }).notNull(), - role: varchar("role", { length: 20 }).notNull(), - }, - (table) => [ - primaryKey({ columns: [table.workspaceId, table.userId] }), - index("workspace_members_user_id_index").on(table.userId), - check("workspace_members_role_check", sql`${table.role} in ('owner', 'admin', 'member')`), - ], -); diff --git a/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts b/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts deleted file mode 100644 index f2b60ae..0000000 --- a/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { assert, describe, it } from "@effect/vitest"; -import { WorkspaceStore } from "@effect-forge/core/workspace-store"; -import { UserId } from "@effect-forge/domain/identity"; -import { Workspace, WorkspaceId, WorkspaceName } from "@effect-forge/domain/workspace"; -import { Effect, Layer } from "effect"; -import { Database } from "../internal/database.ts"; -import { PersistencePglite } from "../test/persistence-pglite.ts"; -import { workspaceMembers, workspaces } from "./schema.ts"; - -const ownerId = UserId.make("better-auth-user-1"); -const otherId = UserId.make("better-auth-user-2"); -const workspace = Workspace.make({ - id: WorkspaceId.make("123e4567-e89b-42d3-a456-426614174000"), - name: WorkspaceName.make("Effect Forge"), -}); -const testLayer = Layer.merge(PersistencePglite.layer, PersistencePglite.databaseLayer); - -describe("PostgreSQL WorkspaceStore", () => { - it.layer(testLayer)("principal scope", (it) => { - it.effect("atomically creates the workspace and owner membership", () => - Effect.gen(function* () { - const store = yield* WorkspaceStore.Service; - yield* store.create(workspace, ownerId); - - assert.deepEqual(yield* store.list(ownerId), [workspace]); - assert.deepEqual(yield* store.list(otherId), []); - const database = yield* Database.Service; - assert.deepEqual(yield* database.select().from(workspaceMembers), [ - { workspaceId: workspace.id, userId: ownerId, role: "owner" }, - ]); - }), - ); - }); - - it.layer(testLayer)("name conflict", (it) => { - it.effect("returns NameTaken without creating another membership", () => - Effect.gen(function* () { - const store = yield* WorkspaceStore.Service; - yield* store.create(workspace, ownerId); - const duplicate = Workspace.make({ - id: WorkspaceId.make("123e4567-e89b-42d3-a456-426614174001"), - name: workspace.name, - }); - - assert.instanceOf( - yield* store.create(duplicate, ownerId).pipe(Effect.flip), - WorkspaceStore.NameTaken, - ); - - const database = yield* Database.Service; - assert.lengthOf(yield* database.select().from(workspaces), 1); - assert.lengthOf(yield* database.select().from(workspaceMembers), 1); - }), - ); - }); -}); diff --git a/adapters/database-postgres/src/workspace/workspace-store-postgres.ts b/adapters/database-postgres/src/workspace/workspace-store-postgres.ts deleted file mode 100644 index 473b779..0000000 --- a/adapters/database-postgres/src/workspace/workspace-store-postgres.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { WorkspaceStore } from "@effect-forge/core/workspace-store"; -import { UserId } from "@effect-forge/domain/identity"; -import { Workspace } from "@effect-forge/domain/workspace"; -import { WorkspaceRole } from "@effect-forge/domain/workspace-member"; -import { asc, eq } from "drizzle-orm"; -import { Effect, Layer, Schema } from "effect"; -import { Database } from "../internal/database.ts"; -import { workspaceMembers, workspaces } from "./schema.ts"; - -const ownerRole: WorkspaceRole = "owner"; - -/** Provides PostgreSQL-backed principal-scoped workspace persistence. */ -export const layer = Layer.effect( - WorkspaceStore.Service, - Effect.gen(function* () { - const database = yield* Database.Service; - - const create = Effect.fn("WorkspaceStorePostgres.create")( - (workspace: Workspace, ownerId: UserId) => - database - .transaction((transaction) => - Effect.gen(function* () { - const inserted = yield* transaction - .insert(workspaces) - .values(workspace) - .onConflictDoNothing({ target: workspaces.name }) - .returning({ id: workspaces.id }); - if (inserted.length === 0) { - return yield* new WorkspaceStore.NameTaken({ name: workspace.name }); - } - - yield* transaction.insert(workspaceMembers).values({ - workspaceId: workspace.id, - userId: ownerId, - role: ownerRole, - }); - return workspace; - }), - ) - .pipe( - Effect.mapError((cause) => - Schema.is(WorkspaceStore.NameTaken)(cause) - ? cause - : new WorkspaceStore.PersistenceError({ cause }), - ), - ), - ); - - const list = Effect.fn("WorkspaceStorePostgres.list")((userId: UserId) => - database - .select({ id: workspaces.id, name: workspaces.name }) - .from(workspaceMembers) - .innerJoin(workspaces, eq(workspaceMembers.workspaceId, workspaces.id)) - .where(eq(workspaceMembers.userId, userId)) - .orderBy(asc(workspaces.name)) - .pipe( - Effect.flatMap(Schema.decodeEffect(Schema.Array(Workspace))), - Effect.mapError((cause) => new WorkspaceStore.PersistenceError({ cause })), - ), - ); - - return WorkspaceStore.Service.of({ create, list }); - }), -); - -export * as WorkspaceStorePostgres from "./workspace-store-postgres.ts"; diff --git a/apps/web/package.json b/apps/web/package.json index 513c4f5..b10c7c0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,10 +9,9 @@ }, "scripts": { "build": "vite build", - "check-types": "svelte-kit sync && svelte-check --tsgo --tsconfig ./tsconfig.json", + "check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "prepare": "svelte-kit sync", - "test": "vitest run src", - "test:e2e": "playwright test" + "test": "vitest run src" }, "dependencies": { "@better-auth/drizzle-adapter": "catalog:", @@ -26,18 +25,17 @@ "better-auth": "catalog:", "drizzle-orm": "catalog:", "effect": "catalog:", - "pg": "catalog:" + "pg": "catalog:", + "svelte-sonner": "1.2.1" }, "devDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.77", "@cloudflare/workers-types": "5.20260827.1", "@effect/vitest": "catalog:", "@electric-sql/pglite": "catalog:", - "@playwright/test": "1.63.0", "@sveltejs/kit": "3.0.0-next.25", "@sveltejs/vite-plugin-svelte": "7.3.0", "@tailwindcss/vite": "catalog:", - "@testcontainers/postgresql": "12.1.0", "@types/pg": "catalog:", "@typescript/native": "npm:typescript@7.0.2", "svelte": "catalog:", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts deleted file mode 100644 index 08d4596..0000000 --- a/apps/web/playwright.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -export default defineConfig({ - testDir: "./tests", - forbidOnly: true, - reporter: [["list"], ["html", { open: "never" }]], - use: { - baseURL: "http://127.0.0.1:4173", - trace: "retain-on-failure", - }, - projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - webServer: { - command: "bun run build && node tests/server.ts", - url: "http://127.0.0.1:4173/sign-in", - reuseExistingServer: false, - timeout: 120_000, - gracefulShutdown: { signal: "SIGTERM", timeout: 30_000 }, - }, -}); diff --git a/apps/web/src/hooks.client.ts b/apps/web/src/hooks.client.ts new file mode 100644 index 0000000..7bbe195 --- /dev/null +++ b/apps/web/src/hooks.client.ts @@ -0,0 +1,8 @@ +import type { HandleClientError } from "@sveltejs/kit/hooks"; + +export const handleError: HandleClientError = ({ kind, error }) => { + if (kind !== "unknown") return; + // oxlint-disable-next-line effecttsgo/global-console -- client failures occur outside the server Effect runtime. + console.error(error); + return { message: "Something went wrong. Refresh before trying again." }; +}; diff --git a/apps/web/src/hooks.server.test.ts b/apps/web/src/hooks.server.test.ts new file mode 100644 index 0000000..3bbdbe6 --- /dev/null +++ b/apps/web/src/hooks.server.test.ts @@ -0,0 +1,57 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { Cause } from "effect"; +import type { RequestEvent } from "@sveltejs/kit"; +import { handleError } from "./hooks.server.ts"; + +// SAFETY: This local fixture supplies the real Request, the only event field read by handleError. +const eventFor = (request: Request) => ({ request }) as RequestEvent; + +afterEach(() => vi.restoreAllMocks()); + +describe("handleError", () => { + it("keeps pure request cancellation out of unexpected-error diagnostics", () => { + const report = vi.spyOn(console, "error").mockImplementation(() => {}); + const error = new Error("cancelled", { cause: Cause.interrupt() }); + const request = new Request("https://example.com/organizations", { + signal: AbortSignal.abort(), + }); + expect(handleError({ kind: "unknown", error, event: eventFor(request) })).toEqual({ + message: "Something went wrong. Refresh before trying again.", + }); + expect(report).not.toHaveBeenCalled(); + }); + + it.each([ + { + name: "defect during cancellation", + cause: Cause.combine(Cause.interrupt(), Cause.die(new Error("private defect"))), + }, + { + name: "unknown failure during cancellation", + cause: new Error("private error"), + }, + ])("reports $name without exposing diagnostics", ({ cause }) => { + const report = vi.spyOn(console, "error").mockImplementation(() => {}); + const error = new Error("private operation detail", { cause }); + const request = new Request("https://example.com/organizations", { + signal: AbortSignal.abort(), + }); + expect(handleError({ kind: "unknown", error, event: eventFor(request) })).toEqual({ + message: "Something went wrong. Refresh before trying again.", + }); + expect(report).toHaveBeenCalledExactlyOnceWith(error); + }); + + it("leaves deliberate application errors to SvelteKit", () => { + const report = vi.spyOn(console, "error").mockImplementation(() => {}); + const request = new Request("https://example.com/organizations"); + expect( + handleError({ + kind: "app", + error: { status: 403, message: "Access denied" }, + event: eventFor(request), + }), + ).toBeUndefined(); + expect(report).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index 8e4078f..7483f42 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -1,27 +1,27 @@ -import { decodeAuthOrigin } from "#lib/server/auth-origin.ts"; -import { decodeAuthSecret } from "#lib/server/auth-secret.ts"; import { WebRuntime } from "#lib/server/runtime.ts"; -import type { Handle } from "@sveltejs/kit/hooks"; -import { dev } from "$app/env"; +import type { Handle, HandleServerError } from "@sveltejs/kit/hooks"; +import { Cause } from "effect"; + +export const handleError = (({ kind, error, event }) => { + if (kind !== "unknown") return; + const cancelled = + event.request.signal.aborted && + error instanceof Error && + Cause.isCause(error.cause) && + Cause.hasInterruptsOnly(error.cause); + if (!cancelled) { + // oxlint-disable-next-line effecttsgo/global-console -- this boundary must report failures even when the Effect runtime is unavailable. + console.error(error); + } + return { message: "Something went wrong. Refresh before trying again." }; +}) satisfies HandleServerError; export const handle: Handle = ({ event, resolve }) => { const platform = event.platform; if (platform === undefined) { throw new Error("SvelteKit platform environment is unavailable"); } - const authOrigin = decodeAuthOrigin(platform.env.AUTH_ORIGIN); - const runtime = WebRuntime.make({ - baseURL: authOrigin.origin, - connectionString: platform.env.DATABASE.connectionString, - request: event.request, - routeId: event.route.id, - secret: decodeAuthSecret(platform.env.AUTH_SECRET), - stage: platform.env.DEPLOYMENT_ENVIRONMENT, - dev, - telemetry: { - endpoint: platform.env.OTEL_EXPORTER_OTLP_ENDPOINT, - }, - }); + const runtime = WebRuntime.make(event); event.locals.run = runtime.run; return resolve(event).finally(() => { diff --git a/apps/web/src/lib/features/auth/client.ts b/apps/web/src/lib/features/auth/client.ts index 3792cf4..38b7cd3 100644 --- a/apps/web/src/lib/features/auth/client.ts +++ b/apps/web/src/lib/features/auth/client.ts @@ -1,4 +1,5 @@ import { createAuthClient } from "better-auth/svelte"; +import { organizationClient } from "better-auth/client/plugins"; /** Better Auth's shared browser session and protocol client. */ -export const authClient = createAuthClient(); +export const authClient = createAuthClient({ plugins: [organizationClient()] }); diff --git a/apps/web/src/lib/features/auth/return-path.test.ts b/apps/web/src/lib/features/auth/return-path.test.ts index 57f60eb..f7dd6c9 100644 --- a/apps/web/src/lib/features/auth/return-path.test.ts +++ b/apps/web/src/lib/features/auth/return-path.test.ts @@ -5,7 +5,7 @@ import { fromURL, ReturnPath } from "./return-path.ts"; const isReturnPath = Schema.is(ReturnPath); describe("ReturnPath", () => { - it.each(["/workspaces", "/workspaces?tab=recent", "/workspaces#recent"])( + it.each(["/organizations", "/organizations?tab=recent", "/organizations#recent"])( "accepts the same-document path %s", (path) => expect(isReturnPath(path)).toBe(true), ); @@ -18,15 +18,15 @@ describe("ReturnPath", () => { "/\t/evil.example", "/\n/evil.example", "/\r/evil.example", - "/workspaces\u0000", + "/organizations\u0000", ])("rejects the external destination %s", (path) => expect(isReturnPath(path)).toBe(false)); it("reads a safe return path and rejects an external one", () => { - expect(fromURL(new URL("https://app.example/sign-in?returnTo=%2Fworkspaces%3Ftab%3Dnew"))).toBe( - "/workspaces?tab=new", - ); + expect( + fromURL(new URL("https://app.example/sign-in?returnTo=%2Forganizations%3Ftab%3Dnew")), + ).toBe("/organizations?tab=new"); expect( fromURL(new URL("https://app.example/sign-in?returnTo=https%3A%2F%2Fevil.example")), - ).toBe("/workspaces"); + ).toBe("/organizations"); }); }); diff --git a/apps/web/src/lib/features/auth/return-path.ts b/apps/web/src/lib/features/auth/return-path.ts index d926f89..ca3d8bd 100644 --- a/apps/web/src/lib/features/auth/return-path.ts +++ b/apps/web/src/lib/features/auth/return-path.ts @@ -13,5 +13,5 @@ export const fromURL = (url: { const candidate = url.searchParams.get("returnTo"); return candidate !== null && Schema.is(ReturnPath)(candidate) ? candidate - : ReturnPath.make("/workspaces"); + : ReturnPath.make("/organizations"); }; diff --git a/apps/web/src/lib/features/auth/sign-in-form.svelte b/apps/web/src/lib/features/auth/sign-in-form.svelte index 86d770c..d85d6a7 100644 --- a/apps/web/src/lib/features/auth/sign-in-form.svelte +++ b/apps/web/src/lib/features/auth/sign-in-form.svelte @@ -28,7 +28,9 @@ : 'We couldn’t sign you in right now. Please try again.'; return; } - } catch { + } catch (failure) { + // oxlint-disable-next-line effecttsgo/global-console -- Locally recovered failures do not reach Kit's error hook. + console.error(failure); message = 'We couldn’t sign you in right now. Please try again.'; return; } finally { @@ -50,7 +52,7 @@
Continue to your workspaces.
+Continue to your organizations.