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.
Unified · Split
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { Context, Effect, Layer, Schema } from "effect"; |
| 2 | −import { makeUserStore } from "@executor/storage-postgres"; | |
| 2 | +import { makeUserStore } from "../services/user-store"; | |
| 3 | 3 | import type { DrizzleDb } from "../services/db"; |
| 4 | 4 | |
| 5 | 5 | // --------------------------------------------------------------------------- |
| @@ -3,9 +3,12 @@ · 2 unmodified lines | ||
| 3 | 3 | // --------------------------------------------------------------------------- |
| 4 | 4 | |
| 5 | 5 | 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"; | |
| 7 | 8 | import type { DrizzleDb } from "@executor/storage-postgres"; |
| 8 | 9 | |
| 10 | +const schema = { ...sharedSchema, ...cloudSchema }; | |
| 11 | + | |
| 9 | 12 | export type { DrizzleDb }; |
| 10 | 13 | |
| 11 | 14 | const MIGRATIONS_DIR = resolve( |
| @@ -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 | +}); | |
| @@ -5,7 +5,7 @@ · 4 unmodified lines | ||
| 5 | 5 | import { eq, and } from "drizzle-orm"; |
| 6 | 6 | |
| 7 | 7 | import { users, teams, teamMembers, invitations } from "./schema"; |
| 8 | −import type { DrizzleDb } from "./types"; | |
| 8 | +import type { DrizzleDb } from "@executor/storage-postgres"; | |
| 9 | 9 | |
| 10 | 10 | export type User = typeof users.$inferSelect; |
| 11 | 11 | export type Team = typeof teams.$inferSelect; |
| @@ -4,7 +4,8 @@ · 3 unmodified lines | ||
| 4 | 4 | "private": true, |
| 5 | 5 | "type": "module", |
| 6 | 6 | "exports": { |
| 7 | − ".": "./src/index.ts" | |
| 7 | + ".": "./src/index.ts", | |
| 8 | + "./schema": "./src/schema.ts" | |
| 8 | 9 | }, |
| 9 | 10 | "dependencies": { |
| 10 | 11 | "@effect/sql": "catalog:", |
| @@ -21,7 +21,6 @@ import { · 20 unmodified lines | ||
| 21 | 21 | |
| 22 | 22 | import { makePgConfig } from "./index"; |
| 23 | 23 | import { makePgKv } from "./pg-kv"; |
| 24 | −import { makeUserStore } from "./user-store"; | |
| 25 | 24 | import * 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 | − | |
| 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 | −}); | |
| @@ -29,11 +29,8 @@ export { makePgKv } from "./pg-kv"; · 28 unmodified lines | ||
| 29 | 29 | export { makePgToolRegistry } from "./tool-registry"; |
| 30 | 30 | export { makePgSecretStore } from "./secret-store"; |
| 31 | 31 | export { makePgPolicyEngine } from "./policy-engine"; |
| 32 | −export { makeUserStore } from "./user-store"; | |
| 33 | −export type { User, Team, TeamMember, Invitation } from "./user-store"; | |
| 34 | 32 | export { encrypt, decrypt } from "./crypto"; |
| 35 | 33 | export type { DrizzleDb } from "./types"; |
| 36 | −export * from "./schema"; | |
| 37 | 34 | |
| 38 | 35 | // --------------------------------------------------------------------------- |
| 39 | 36 | // Convenience: build a full ExecutorConfig from a Drizzle DB instance |
| @@ -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 | − | |
| 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 | − | |
| 61 | 22 | // --------------------------------------------------------------------------- |
| 62 | 23 | // Domain data — all team-scoped |
| 63 | 24 | // --------------------------------------------------------------------------- |