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 @@
11 import { Context, Effect, Layer, Schema } from "effect";
2−import { makeUserStore } from "@executor/storage-postgres";
2+import { makeUserStore } from "../services/user-store";
33 import type { DrizzleDb } from "../services/db";
44
55 // ---------------------------------------------------------------------------
apps/cloud/src/services/db.ts+4−1
@@ -3,9 +3,12 @@ · 2 unmodified lines
33 // ---------------------------------------------------------------------------
44
55 import { resolve } from "node:path";
6−import * as schema from "@executor/storage-postgres";
6+import * as sharedSchema from "@executor/storage-postgres/schema";
7+import * as cloudSchema from "./schema";
78 import type { DrizzleDb } from "@executor/storage-postgres";
89
10+const schema = { ...sharedSchema, ...cloudSchema };
11+
912 export type { DrizzleDb };
1013
1114 const 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+
5+import {
6+ pgTable,
7+ text,
8+ timestamp,
9+ primaryKey,
10+} from "drizzle-orm/pg-core";
11+
12+export 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+
20+export 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+
26+export 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+
37+export 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
55 import { eq, and } from "drizzle-orm";
66
77 import { users, teams, teamMembers, invitations } from "./schema";
8−import type { DrizzleDb } from "./types";
8+import type { DrizzleDb } from "@executor/storage-postgres";
99
1010 export type User = typeof users.$inferSelect;
1111 export type Team = typeof teams.$inferSelect;
packages/core/storage-postgres/package.json+2−1
@@ -4,7 +4,8 @@ · 3 unmodified lines
44 "private": true,
55 "type": "module",
66 "exports": {
7− ".": "./src/index.ts"
7+ ".": "./src/index.ts",
8+ "./schema": "./src/schema.ts"
89 },
910 "dependencies": {
1011 "@effect/sql": "catalog:",
packages/core/storage-postgres/src/index.test.ts+0−55
@@ -21,7 +21,6 @@ import { · 20 unmodified lines
2121
2222 import { makePgConfig } from "./index";
2323 import { makePgKv } from "./pg-kv";
24−import { makeUserStore } from "./user-store";
2524 import * as schema from "./schema";
2625
2726 // ---------------------------------------------------------------------------
@@ -355,57 +354,3 @@ describe("Executor with Postgres storage", () => { · 327 unmodified lines
355354 );
356355 });
357356
358−// ---------------------------------------------------------------------------
359−// User Store (not part of Executor, tested directly)
360−// ---------------------------------------------------------------------------
361−
362−describe("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
2929 export { makePgToolRegistry } from "./tool-registry";
3030 export { makePgSecretStore } from "./secret-store";
3131 export { makePgPolicyEngine } from "./policy-engine";
32−export { makeUserStore } from "./user-store";
33−export type { User, Team, TeamMember, Invitation } from "./user-store";
3432 export { encrypt, decrypt } from "./crypto";
3533 export type { DrizzleDb } from "./types";
36−export * from "./schema";
3734
3835 // ---------------------------------------------------------------------------
3936 // 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
1919 },
2020 });
2121
22−// ---------------------------------------------------------------------------
23−// Identity & multi-tenancy
24−// ---------------------------------------------------------------------------
25−
26−export 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−
34−export 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−
40−export 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−
51−export 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−
6122 // ---------------------------------------------------------------------------
6223 // Domain data — all team-scoped
6324 // ---------------------------------------------------------------------------

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