History

move cloud-specific identity schema out of storage-postgres

Extract users, teams, teamMembers, invitations tables and user-store into apps/cloud/src/services/ since they're cloud-only concerns. Add subpath export for schema (import from @executor/storage-postgres/schema) so the main entrypoint only exports functions and types.

Rhys Sullivan committed 7 months ago

Commit ab8e9041 parent ffcfabb8 files · +53 −101Browse files

Unified · Split

8 files
  1. apps/cloud/src/auth/context.ts +1 −1
  2. apps/cloud/src/services/db.ts +4 −1
  3. apps/cloud/src/services/schema.ts +45 −0
  4. apps/cloud/src/services/user-store.ts +1 −1
  5. packages/core/storage-postgres/package.json +2 −1
  6. packages/core/storage-postgres/src/index.test.ts +0 −55
  7. packages/core/storage-postgres/src/index.ts +0 −3
  8. packages/core/storage-postgres/src/schema.ts +0 −39
apps/cloud/src/auth/context.ts+1−1
@@ -1,5 +1,5 @@
1import { Context, Effect, Layer, Schema } from "effect";1import { Context, Effect, Layer, Schema } from "effect";
2import { makeUserStore } from "@executor/storage-postgres";2import { makeUserStore } from "../services/user-store";
3import type { DrizzleDb } from "../services/db";3import type { DrizzleDb } from "../services/db";
4 4
5// ---------------------------------------------------------------------------5// ---------------------------------------------------------------------------
apps/cloud/src/services/db.ts+4−1
@@ -3,9 +3,12 @@ · 2 unmodified lines
3// ---------------------------------------------------------------------------3// ---------------------------------------------------------------------------
4 4
5import { resolve } from "node:path";5import { resolve } from "node:path";
6import * as schema from "@executor/storage-postgres";6import * as sharedSchema from "@executor/storage-postgres/schema";
7import * as cloudSchema from "./schema";
7import type { DrizzleDb } from "@executor/storage-postgres";8import type { DrizzleDb } from "@executor/storage-postgres";
8 9
10const schema = { ...sharedSchema, ...cloudSchema };
11
9export type { DrizzleDb };12export type { DrizzleDb };
10 13
11const MIGRATIONS_DIR = resolve(14const MIGRATIONS_DIR = resolve(
apps/cloud/src/services/schema.ts+45−0
@@ -0,0 +1,45 @@
1// ---------------------------------------------------------------------------
2// Identity & multi-tenancy — cloud-specific tables
3// ---------------------------------------------------------------------------
4
5import {
6 pgTable,
7 text,
8 timestamp,
9 primaryKey,
10} from "drizzle-orm/pg-core";
11
12export const users = pgTable("users", {
13 id: text("id").primaryKey(),
14 email: text("email").notNull().unique(),
15 name: text("name"),
16 avatarUrl: text("avatar_url"),
17 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
18});
19
20export const teams = pgTable("teams", {
21 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
22 name: text("name").notNull(),
23 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
24});
25
26export const teamMembers = pgTable(
27 "team_members",
28 {
29 teamId: text("team_id").notNull(),
30 userId: text("user_id").notNull(),
31 role: text("role").notNull().$default(() => "member"),
32 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
33 },
34 (table) => [primaryKey({ columns: [table.teamId, table.userId] })],
35);
36
37export const invitations = pgTable("invitations", {
38 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
39 teamId: text("team_id").notNull(),
40 email: text("email").notNull(),
41 invitedBy: text("invited_by").notNull(),
42 status: text("status").notNull().$default(() => "pending"),
43 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
44 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
45});
apps/cloud/src/services/user-store.tsrenamed from packages/core/storage-postgres/src/user-store.ts+1−1
@@ -5,7 +5,7 @@ · 4 unmodified lines
5import { eq, and } from "drizzle-orm";5import { eq, and } from "drizzle-orm";
6 6
7import { users, teams, teamMembers, invitations } from "./schema";7import { users, teams, teamMembers, invitations } from "./schema";
8import type { DrizzleDb } from "./types";8import type { DrizzleDb } from "@executor/storage-postgres";
9 9
10export type User = typeof users.$inferSelect;10export type User = typeof users.$inferSelect;
11export type Team = typeof teams.$inferSelect;11export type Team = typeof teams.$inferSelect;
packages/core/storage-postgres/package.json+2−1
@@ -4,7 +4,8 @@ · 3 unmodified lines
4 "private": true,4 "private": true,
5 "type": "module",5 "type": "module",
6 "exports": {6 "exports": {
7 ".": "./src/index.ts"7 ".": "./src/index.ts",
8 "./schema": "./src/schema.ts"
8 },9 },
9 "dependencies": {10 "dependencies": {
10 "@effect/sql": "catalog:",11 "@effect/sql": "catalog:",
packages/core/storage-postgres/src/index.test.ts+0−55
@@ -21,7 +21,6 @@ import { · 20 unmodified lines
21 21
22import { makePgConfig } from "./index";22import { makePgConfig } from "./index";
23import { makePgKv } from "./pg-kv";23import { makePgKv } from "./pg-kv";
24import { makeUserStore } from "./user-store";
25import * as schema from "./schema";24import * as schema from "./schema";
26 25
27// ---------------------------------------------------------------------------26// ---------------------------------------------------------------------------
@@ -355,57 +354,3 @@ describe("Executor with Postgres storage", () => { · 327 unmodified lines
355 );354 );
356});355});
357 356
358// ---------------------------------------------------------------------------
359// User Store (not part of Executor, tested directly)
360// ---------------------------------------------------------------------------
361
362describe("UserStore", () => {
363 it("upsert and get user", async () => {
364 const store = makeUserStore(db);
365 const user = await store.upsertUser({
366 id: "user-1",
367 email: "test@example.com",
368 name: "Test User",
369 });
370 expect(user.id).toBe("user-1");
371
372 const fetched = await store.getUser("user-1");
373 expect(fetched!.name).toBe("Test User");
374
375 await store.upsertUser({ id: "user-1", email: "test@example.com", name: "Updated" });
376 expect((await store.getUser("user-1"))!.name).toBe("Updated");
377 });
378
379 it("create team and manage members", async () => {
380 const store = makeUserStore(db);
381 await store.upsertUser({ id: "u1", email: "a@example.com", name: "A" });
382 await store.upsertUser({ id: "u2", email: "b@example.com", name: "B" });
383
384 const team = await store.createTeam("My Team");
385 await store.addMember(team.id, "u1", "owner");
386 await store.addMember(team.id, "u2", "member");
387
388 expect(await store.listMembers(team.id)).toHaveLength(2);
389
390 const teams = await store.getTeamsForUser("u1");
391 expect(teams[0]!.teamName).toBe("My Team");
392
393 await store.removeMember(team.id, "u2");
394 expect(await store.listMembers(team.id)).toHaveLength(1);
395 });
396
397 it("invitations workflow", async () => {
398 const store = makeUserStore(db);
399 await store.upsertUser({ id: "u1", email: "owner@example.com" });
400 const team = await store.createTeam("Team");
401 await store.addMember(team.id, "u1", "owner");
402
403 const invitation = await store.createInvitation(team.id, "new@example.com", "u1");
404 expect(invitation.status).toBe("pending");
405 expect(await store.getPendingInvitations("new@example.com")).toHaveLength(1);
406
407 await store.acceptInvitation(invitation.id);
408 expect(await store.getPendingInvitations("new@example.com")).toHaveLength(0);
409 });
410
411});
packages/core/storage-postgres/src/index.ts+0−3
@@ -29,11 +29,8 @@ export { makePgKv } from "./pg-kv"; · 28 unmodified lines
29export { makePgToolRegistry } from "./tool-registry";29export { makePgToolRegistry } from "./tool-registry";
30export { makePgSecretStore } from "./secret-store";30export { makePgSecretStore } from "./secret-store";
31export { makePgPolicyEngine } from "./policy-engine";31export { makePgPolicyEngine } from "./policy-engine";
32export { makeUserStore } from "./user-store";
33export type { User, Team, TeamMember, Invitation } from "./user-store";
34export { encrypt, decrypt } from "./crypto";32export { encrypt, decrypt } from "./crypto";
35export type { DrizzleDb } from "./types";33export type { DrizzleDb } from "./types";
36export * from "./schema";
37 34
38// ---------------------------------------------------------------------------35// ---------------------------------------------------------------------------
39// Convenience: build a full ExecutorConfig from a Drizzle DB instance36// Convenience: build a full ExecutorConfig from a Drizzle DB instance
packages/core/storage-postgres/src/schema.ts+0−39
@@ -19,45 +19,6 @@ const bytea = customType<{ data: Buffer }>({ · 18 unmodified lines
19 },19 },
20});20});
21 21
22// ---------------------------------------------------------------------------
23// Identity & multi-tenancy
24// ---------------------------------------------------------------------------
25
26export const users = pgTable("users", {
27 id: text("id").primaryKey(),
28 email: text("email").notNull().unique(),
29 name: text("name"),
30 avatarUrl: text("avatar_url"),
31 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
32});
33
34export const teams = pgTable("teams", {
35 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
36 name: text("name").notNull(),
37 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
38});
39
40export const teamMembers = pgTable(
41 "team_members",
42 {
43 teamId: text("team_id").notNull(),
44 userId: text("user_id").notNull(),
45 role: text("role").notNull().$default(() => "member"),
46 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
47 },
48 (table) => [primaryKey({ columns: [table.teamId, table.userId] })],
49);
50
51export const invitations = pgTable("invitations", {
52 id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
53 teamId: text("team_id").notNull(),
54 email: text("email").notNull(),
55 invitedBy: text("invited_by").notNull(),
56 status: text("status").notNull().$default(() => "pending"),
57 createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
58 expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
59});
60
61// ---------------------------------------------------------------------------22// ---------------------------------------------------------------------------
62// Domain data — all team-scoped23// Domain data — all team-scoped
63// ---------------------------------------------------------------------------24// ---------------------------------------------------------------------------

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t