History

refactor(v2): simplify SQL persistence to direct repositories

Replace proxy/layered row operations with native Drizzle backends and scoped row queries so control-plane services follow straightforward database flows.

Rhys Sullivan committed 8 months ago

Commit f50778e1 parent a5d22a226 files · +1508 −2152Browse files

Unified · Split

26 files
  1. apps/pm/src/actor.ts +29 −24
  2. apps/pm/src/approvals-service.ts +39 −65
  3. apps/pm/src/control-plane-row-helpers.ts +0 −18
  4. apps/pm/src/credential-resolver.ts +72 −40
  5. apps/pm/src/credentials-service.ts +126 −91
  6. apps/pm/src/execute.e2e.test.ts +24 −3
  7. apps/pm/src/main.ts +14 −14
  8. apps/pm/src/organizations-service.ts +26 −25
  9. apps/pm/src/persistent-approval-policy.test.ts +23 −0
  10. apps/pm/src/policies-service.ts +23 −24
  11. apps/pm/src/storage-service.test.ts +23 −0
  12. apps/pm/src/storage-service.ts +49 −35
  13. apps/pm/src/tool-call-handler.test.ts +6 −5
  14. apps/pm/src/workspaces-service.ts +9 −9
  15. apps/web/lib/control-plane/server.ts +10 −13
  16. packages/persistence-sql/src/control-plane-rows.ts +865 −0
  17. packages/persistence-sql/src/index.ts +18 −184
  18. packages/persistence-sql/src/row-operations-auth.ts +0 −404
  19. packages/persistence-sql/src/row-operations-core.ts +0 −515
  20. packages/persistence-sql/src/row-operations.ts +0 −21
  21. packages/persistence-sql/src/rows-effect-api-auth.ts +0 −116
  22. packages/persistence-sql/src/rows-effect-api-core.ts +0 −157
  23. packages/persistence-sql/src/rows-effect-api.ts +0 −12
  24. packages/persistence-sql/src/rows-effect-helpers.ts +0 −55
  25. packages/persistence-sql/src/source-artifact-stores.ts +82 −106
  26. packages/persistence-sql/src/sql-internals.ts +70 −216
apps/pm/src/actor.ts+29−24
@@ -37,14 +37,6 @@ const localAdminFallbackEnabled = (() => { · 36 unmodified lines
3737 return isTruthy(configured);
3838 })();
3939
40−const organizationMembershipsForAccount = (
41− memberships: ReadonlyArray<OrganizationMembership>,
42− accountId: OrganizationMembership["accountId"],
43−): ReadonlyArray<OrganizationMembership> =>
44− memberships.filter(
45− (membership) => membership.accountId === accountId,
46− );
47−
4840 const workspaceMembershipsForAccount = (
4941 workspaces: ReadonlyArray<Workspace>,
5042 accountId: OrganizationMembership["accountId"],
@@ -66,10 +58,20 @@ const resolveActorFromSnapshot = ( · 15 unmodified lines
6658 Effect.gen(function* () {
6759 const principal = yield* requirePrincipalFromHeaders(headers);
6860
69− const [memberships, workspaces] = yield* Effect.all([
70− rows.organizationMemberships.list(),
71− rows.workspaces.list(),
72− ]).pipe(
61+ const memberships = yield* rows.organizationMemberships
62+ .listByAccountId(principal.accountId)
63+ .pipe(
64+ Effect.mapError(
65+ (error) =>
66+ new ActorUnauthenticatedError({
67+ message: `Unable to read local auth state (${error.operation})`,
68+ }),
69+ ),
70+ );
71+
72+ const organizationIds = memberships.map((membership) => membership.organizationId);
73+
74+ const workspaces = yield* rows.workspaces.listByOrganizationIds(organizationIds).pipe(
7375 Effect.mapError(
7476 (error) =>
7577 new ActorUnauthenticatedError({
@@ -82,10 +84,7 @@ const resolveActorFromSnapshot = ( · 6 unmodified lines
8284 return makeAllowAllActor(principal);
8385 }
8486
85− const organizationMemberships = organizationMembershipsForAccount(
86− memberships,
87− principal.accountId,
88− );
87+ const organizationMemberships = memberships;
8988 const workspaceMemberships = workspaceMembershipsForAccount(
9089 workspaces,
9190 principal.accountId,
@@ -106,10 +105,19 @@ export const PmActorLive = (rows: ActorRows) => · 14 unmodified lines
106105 Effect.gen(function* () {
107106 const principal = yield* requirePrincipalFromHeaders(input.headers);
108107
109− const [memberships, workspaces] = yield* Effect.all([
110− rows.organizationMemberships.list(),
111− rows.workspaces.list(),
112− ]).pipe(
108+ const memberships = yield* rows.organizationMemberships
109+ .listByAccountId(principal.accountId)
110+ .pipe(
111+ Effect.mapError(
112+ (error) =>
113+ new ActorUnauthenticatedError({
114+ message: `Unable to read local auth state (${error.operation})`,
115+ }),
116+ ),
117+ );
118+
119+ const organizationIds = memberships.map((membership) => membership.organizationId);
120+ const workspaces = yield* rows.workspaces.listByOrganizationIds(organizationIds).pipe(
113121 Effect.mapError(
114122 (error) =>
115123 new ActorUnauthenticatedError({
@@ -122,10 +130,7 @@ export const PmActorLive = (rows: ActorRows) => · 6 unmodified lines
122130 return makeAllowAllActor(principal);
123131 }
124132
125− const organizationMemberships = organizationMembershipsForAccount(
126− memberships,
127− principal.accountId,
128− );
133+ const organizationMemberships = memberships;
129134
130135 const workspace = workspaces.find((item) => item.id === input.workspaceId) ?? null;
131136
apps/pm/src/approvals-service.ts+39−65
@@ -12,6 +12,7 @@ import { · 11 unmodified lines
1212 } from "@executor-v2/management-api";
1313 import { type Approval } from "@executor-v2/schema";
1414 import * as Effect from "effect/Effect";
15+import * as Option from "effect/Option";
1516
1617 import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
1718
@@ -19,18 +20,6 @@ type ApprovalRows = Pick<SqlControlPlanePersistence["rows"], "approvals">; · 1 unmodified line
1920
2021 const sourceStoreError = createSqlSourceStoreErrorMapper("approvals");
2122
22−const findApprovalIndex = (
23− approvals: ReadonlyArray<Approval>,
24− workspaceId: string,
25− approvalId: string,
26−): number =>
27− approvals.findIndex(
28− (approval) => approval.workspaceId === workspaceId && approval.id === approvalId,
29− );
30−
31−const sortApprovals = (approvals: ReadonlyArray<Approval>): Array<Approval> =>
32− [...approvals].sort((left, right) => right.requestedAt - left.requestedAt);
33−
3423 const toPersistentApprovalStoreError = (
3524 operation: string,
3625 message: string,
@@ -69,50 +58,45 @@ export const createPmPersistentToolApprovalPolicy = ( · 32 unmodified lines
6958 ): ToolApprovalPolicy => {
7059 const store: PersistentToolApprovalStore = {
7160 findByRunAndCall: (input) =>
72− rows.approvals.list().pipe(
61+ rows.approvals
62+ .findByRunAndCall(
63+ input.workspaceId as Approval["workspaceId"],
64+ input.runId as Approval["taskRunId"],
65+ input.callId,
66+ )
67+ .pipe(
7368 Effect.mapError((error) =>
7469 toPersistentApprovalStoreErrorFromRowStore("approvals.read", error),
7570 ),
76− Effect.flatMap((approvals) => {
77− const approval =
78− approvals.find(
79− (candidate) =>
80− candidate.workspaceId === input.workspaceId &&
81− candidate.taskRunId === input.runId &&
82− candidate.callId === input.callId,
83− ) ?? null;
84−
85− return Effect.succeed(approval ? toPersistentApprovalRecord(approval) : null);
71+ Effect.flatMap((approvalOption) => {
72+ const approval = Option.getOrNull(approvalOption);
73+ return Effect.succeed(approval !== null ? toPersistentApprovalRecord(approval) : null);
8674 }),
8775 ),
8876
8977 createPending: (input) =>
90− rows.approvals.list().pipe(
91− Effect.mapError((error) =>
92− toPersistentApprovalStoreErrorFromRowStore("approvals.read", error),
93− ),
94− Effect.flatMap(() => {
95− const pendingApproval = {
96− id: `apr_${crypto.randomUUID()}`,
97− workspaceId: input.workspaceId,
98− taskRunId: input.runId,
99− callId: input.callId,
100− toolPath: input.toolPath,
101− status: "pending",
102− inputPreviewJson: input.inputPreviewJson,
103− reason: null,
104− requestedAt: Date.now(),
105− resolvedAt: null,
106− } as Approval;
107−
108− return rows.approvals.upsert(pendingApproval).pipe(
109− Effect.mapError((error) =>
110− toPersistentApprovalStoreErrorFromRowStore("approvals.write", error),
111− ),
112− Effect.as(toPersistentApprovalRecord(pendingApproval)),
113− );
114− }),
115− ),
78+ Effect.gen(function* () {
79+ const pendingApproval: Approval = {
80+ id: `apr_${crypto.randomUUID()}` as Approval["id"],
81+ workspaceId: input.workspaceId as Approval["workspaceId"],
82+ taskRunId: input.runId as Approval["taskRunId"],
83+ callId: input.callId,
84+ toolPath: input.toolPath,
85+ status: "pending",
86+ inputPreviewJson: input.inputPreviewJson,
87+ reason: null,
88+ requestedAt: Date.now(),
89+ resolvedAt: null,
90+ };
91+
92+ yield* rows.approvals.upsert(pendingApproval).pipe(
93+ Effect.mapError((error) =>
94+ toPersistentApprovalStoreErrorFromRowStore("approvals.write", error),
95+ ),
96+ );
97+
98+ return toPersistentApprovalRecord(pendingApproval);
99+ }),
116100 };
117101
118102 return createPersistentToolApprovalPolicy({
@@ -128,34 +112,25 @@ export const createPmApprovalsService = ( · 9 unmodified lines
128112 makeControlPlaneApprovalsService({
129113 listApprovals: (workspaceId) =>
130114 Effect.gen(function* () {
131− const approvals = yield* rows.approvals.list().pipe(
115+ const approvals = yield* rows.approvals.listByWorkspaceId(workspaceId).pipe(
132116 Effect.mapError((error) =>
133117 sourceStoreError.fromRowStore("approvals.list", error),
134118 ),
135119 );
136120
137− const scopedApprovals = approvals.filter(
138− (approval) => approval.workspaceId === workspaceId,
139− );
140−
141− return sortApprovals(scopedApprovals);
121+ return approvals;
142122 }),
143123
144124 resolveApproval: (input) =>
145125 Effect.gen(function* () {
146− const approvals = yield* rows.approvals.list().pipe(
126+ const approvalOption = yield* rows.approvals.getById(input.approvalId).pipe(
147127 Effect.mapError((error) =>
148− sourceStoreError.fromRowStore("approvals.resolve", error),
128+ sourceStoreError.fromRowStore("approvals.get_by_id", error),
149129 ),
150130 );
151131
152− const index = findApprovalIndex(
153− approvals,
154− input.workspaceId,
155− input.approvalId,
156− );
157−
158− if (index < 0) {
132+ const approval = Option.getOrNull(approvalOption);
133+ if (approval === null || approval.workspaceId !== input.workspaceId) {
159134 return yield* sourceStoreError.fromMessage(
160135 "approvals.resolve",
161136 "Approval not found",
@@ -163,7 +138,6 @@ export const createPmApprovalsService = ( · 1 unmodified line
163138 );
164139 }
165140
166− const approval = approvals[index];
167141 if (approval.status !== "pending") {
168142 return yield* sourceStoreError.fromMessage(
169143 "approvals.resolve",
apps/pm/src/control-plane-row-helpers.ts+0−18
@@ -1,9 +1,4 @@
11 import { SourceStoreError } from "@executor-v2/persistence-ports";
2−import {
3− type OrganizationId,
4− type Workspace,
5− type WorkspaceId,
6−} from "@executor-v2/schema";
72
83 type RowStoreLikeError = {
94 message: string;
@@ -57,16 +52,3 @@ export const createSqlSourceStoreErrorMapper = ( · 47 unmodified lines
5752 ),
5853 };
5954 };
60−
61−export const resolveWorkspaceOrganizationId = (
62− workspaces: ReadonlyArray<Workspace>,
63− workspaceId: WorkspaceId,
64−): OrganizationId => {
65− const workspace = workspaces.find((item) => item.id === workspaceId);
66−
67− if (!workspace) {
68− throw new Error(`Workspace not found: ${workspaceId}`);
69− }
70−
71− return workspace.organizationId;
72−};
apps/pm/src/credential-resolver.ts+72−40
@@ -5,26 +5,19 @@ import { · 4 unmodified lines
55 sourceIdFromSourceKey,
66 type ResolveToolCredentials,
77 } from "@executor-v2/engine";
8−import { type RowStoreError } from "@executor-v2/persistence-ports";
9−import {
10− type AuthConnection,
11− type AuthMaterial,
12− type OAuthState,
13− type SourceAuthBinding,
14− type Workspace,
15−} from "@executor-v2/schema";
8+import { type SqlControlPlanePersistence } from "@executor-v2/persistence-sql";
9+import { type OrganizationId, type WorkspaceId } from "@executor-v2/schema";
1610 import * as Effect from "effect/Effect";
1711
18−type RowReader<A> = {
19− list: () => Effect.Effect<ReadonlyArray<A>, RowStoreError>;
20−};
21−
2212 type CredentialResolverRows = {
23− workspaces: RowReader<Workspace>;
24− sourceAuthBindings: RowReader<SourceAuthBinding>;
25− authConnections: RowReader<AuthConnection>;
26− authMaterials: RowReader<AuthMaterial>;
27− oauthStates: RowReader<OAuthState>;
13+ workspaces: Pick<SqlControlPlanePersistence["rows"]["workspaces"], "getById">;
14+ sourceAuthBindings: Pick<
15+ SqlControlPlanePersistence["rows"]["sourceAuthBindings"],
16+ "listByWorkspaceScope"
17+ >;
18+ authConnections: Pick<SqlControlPlanePersistence["rows"]["authConnections"], "getById">;
19+ authMaterials: Pick<SqlControlPlanePersistence["rows"]["authMaterials"], "getByConnectionId">;
20+ oauthStates: Pick<SqlControlPlanePersistence["rows"]["oauthStates"], "getByConnectionId">;
2821 };
2922
3023 const toCredentialResolverError = (
@@ -180,33 +173,44 @@ export const createPmResolveToolCredentials = ( · 149 unmodified lines
180173 };
181174 }
182175
183− const [workspaces, bindings, connections, oauthStates, materials] = yield* Effect.all([
184− rows.workspaces.list(),
185− rows.sourceAuthBindings.list(),
186− rows.authConnections.list(),
187− rows.oauthStates.list(),
188− rows.authMaterials.list(),
189− ]).pipe(
176+ const sourceId = sourceIdFromSourceKey(context.sourceKey);
177+ if (!sourceId) {
178+ return {
179+ headers: {},
180+ };
181+ }
182+
183+ const workspaceId = context.workspaceId as WorkspaceId;
184+
185+ const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe(
190186 Effect.mapError((error) =>
191187 toCredentialResolverError(
192− "read_credential_rows",
193− "Failed reading credential rows while resolving credentials",
188+ "read_workspace",
189+ "Failed reading workspace while resolving credentials",
194190 error.details ?? error.message,
195191 ),
196192 ),
197193 );
198194
199− const sourceId = sourceIdFromSourceKey(context.sourceKey);
200− if (!sourceId) {
195+ const workspace = workspaceOption._tag === "Some" ? workspaceOption.value : null;
196+ const organizationId = context.organizationId ?? workspace?.organizationId ?? null;
197+ if (organizationId === null) {
201198 return {
202199 headers: {},
203200 };
204201 }
205202
206− const workspace = workspaces.find(
207− (item) => item.id === context.workspaceId,
208− );
209− const organizationId = context.organizationId ?? workspace?.organizationId ?? null;
203+ const bindings = yield* rows.sourceAuthBindings
204+ .listByWorkspaceScope(workspaceId, organizationId as OrganizationId)
205+ .pipe(
206+ Effect.mapError((error) =>
207+ toCredentialResolverError(
208+ "read_bindings",
209+ "Failed reading credential bindings",
210+ error.details ?? error.message,
211+ ),
212+ ),
213+ );
210214
211215 const binding = bindings
212216 .filter((candidate) => candidate.sourceId === sourceId)
@@ -247,9 +251,19 @@ export const createPmResolveToolCredentials = ( · 34 unmodified lines
247251 };
248252 }
249253
250− const connection = connections.find(
251− (candidate) => candidate.id === binding.connectionId,
254+ const connectionOption = yield* rows.authConnections.getById(binding.connectionId).pipe(
255+ Effect.mapError((error) =>
256+ toCredentialResolverError(
257+ "read_connection",
258+ "Failed reading auth connection",
259+ error.details ?? error.message,
260+ ),
261+ ),
252262 );
263+
264+ const connection = connectionOption._tag === "Some"
265+ ? connectionOption.value
266+ : null;
253267 if (!connection || connection.status !== "active") {
254268 return {
255269 headers: {},
@@ -261,9 +275,18 @@ export const createPmResolveToolCredentials = ( · 5 unmodified lines
261275 );
262276
263277 if (connection.strategy === "oauth2") {
264− const oauthState = oauthStates.find(
265− (candidate) => candidate.connectionId === connection.id,
266− );
278+ const oauthStateOption = yield* rows.oauthStates
279+ .getByConnectionId(connection.id)
280+ .pipe(
281+ Effect.mapError((error) =>
282+ toCredentialResolverError(
283+ "read_oauth_state",
284+ "Failed reading oauth state",
285+ error.details ?? error.message,
286+ ),
287+ ),
288+ );
289+ const oauthState = oauthStateOption._tag === "Some" ? oauthStateOption.value : null;
267290
268291 const accessToken = normalizeString(oauthState?.accessTokenCiphertext);
269292 const oauthHeaders = accessToken
@@ -275,9 +298,18 @@ export const createPmResolveToolCredentials = ( · 5 unmodified lines
275298 };
276299 }
277300
278− const material = materials.find(
279− (candidate) => candidate.connectionId === connection.id,
280− );
301+ const materialOption = yield* rows.authMaterials
302+ .getByConnectionId(connection.id)
303+ .pipe(
304+ Effect.mapError((error) =>
305+ toCredentialResolverError(
306+ "read_auth_material",
307+ "Failed reading auth material",
308+ error.details ?? error.message,
309+ ),
310+ ),
311+ );
312+ const material = materialOption._tag === "Some" ? materialOption.value : null;
281313
282314 if (!material) {
283315 return {
apps/pm/src/credentials-service.ts+126−91
@@ -9,14 +9,11 @@ import { · 8 unmodified lines
99 type OAuthState,
1010 type SourceAuthBinding,
1111 type SourceCredentialBinding,
12− type WorkspaceId,
1312 } from "@executor-v2/schema";
1413 import * as Effect from "effect/Effect";
14+import * as Option from "effect/Option";
1515
16−import {
17− createSqlSourceStoreErrorMapper,
18− resolveWorkspaceOrganizationId,
19−} from "./control-plane-row-helpers";
16+import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
2017 import {
2118 buildOAuthRefreshConfigFromPayload,
2219 encodeOAuthRefreshConfig,
@@ -46,38 +43,42 @@ export const createPmCredentialsService = ( · 23 unmodified lines
4643 makeControlPlaneCredentialsService({
4744 listCredentialBindings: (workspaceId) =>
4845 Effect.gen(function* () {
49− const [bindings, connections, workspaces] = yield* Effect.all([
50− rows.sourceAuthBindings.list().pipe(
51− Effect.mapError((error) =>
52− sourceStoreError.fromRowStore("credentials.bindings.list", error),
53− ),
46+ const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe(
47+ Effect.mapError((error) =>
48+ sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", error),
5449 ),
55− rows.authConnections.list().pipe(
56− Effect.mapError((error) =>
57− sourceStoreError.fromRowStore("credentials.connections.list", error),
50+ );
51+
52+ const workspace = Option.getOrNull(workspaceOption);
53+ if (workspace === null) {
54+ return yield* sourceStoreError.fromMessage(
55+ "credentials.list",
56+ "Workspace not found",
57+ `workspace=${workspaceId}`,
58+ );
59+ }
60+
61+ const [bindings, connections] = yield* Effect.all([
62+ rows.sourceAuthBindings
63+ .listByWorkspaceScope(workspaceId, workspace.organizationId)
64+ .pipe(
65+ Effect.mapError((error) =>
66+ sourceStoreError.fromRowStore("credentials.bindings.list", error),
67+ ),
5868 ),
59− ),
60− rows.workspaces.list().pipe(
69+ rows.authConnections.listByOrganizationId(workspace.organizationId).pipe(
6170 Effect.mapError((error) =>
62− sourceStoreError.fromRowStore("credentials.workspaces.list", error),
71+ sourceStoreError.fromRowStore("credentials.connections.list", error),
6372 ),
6473 ),
6574 ]);
6675
67− const organizationId = resolveWorkspaceOrganizationId(workspaces, workspaceId);
68−
69− const scopedBindings = bindings.filter(
70− (binding) =>
71− binding.workspaceId === workspaceId
72− || (binding.workspaceId === null && binding.organizationId === organizationId),
73− );
76+ const connectionById = new Map(connections.map((connection) => [connection.id, connection]));
7477
7578 const compatBindings: Array<SourceCredentialBinding> = [];
7679
77− for (const binding of scopedBindings) {
78− const connection = connections.find(
79− (candidate) => candidate.id === binding.connectionId,
80− );
80+ for (const binding of bindings) {
81+ const connection = connectionById.get(binding.connectionId);
8182
8283 if (!connection) {
8384 continue;
@@ -91,33 +92,22 @@ export const createPmCredentialsService = ( · 7 unmodified lines
9192
9293 upsertCredentialBinding: (input) =>
9394 Effect.gen(function* () {
94− const [bindings, connections, materials, oauthStates, workspaces] = yield* Effect.all([
95− rows.sourceAuthBindings.list().pipe(
96− Effect.mapError((error) =>
97− sourceStoreError.fromRowStore("credentials.bindings.list", error),
98− ),
99− ),
100− rows.authConnections.list().pipe(
101− Effect.mapError((error) =>
102− sourceStoreError.fromRowStore("credentials.connections.list", error),
103− ),
104− ),
105− rows.authMaterials.list().pipe(
106− Effect.mapError((error) =>
107− sourceStoreError.fromRowStore("credentials.materials.list", error),
108− ),
109− ),
110− rows.oauthStates.list().pipe(
111− Effect.mapError((error) =>
112− sourceStoreError.fromRowStore("credentials.oauth_states.list", error),
113− ),
114− ),
115− rows.workspaces.list().pipe(
116− Effect.mapError((error) =>
117− sourceStoreError.fromRowStore("credentials.workspaces.list", error),
118− ),
95+ const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe(
96+ Effect.mapError((error) =>
97+ sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", error),
11998 ),
120− ]);
99+ );
100+
101+ const workspace = Option.getOrNull(workspaceOption);
102+ if (workspace === null) {
103+ return yield* sourceStoreError.fromMessage(
104+ "credentials.upsert",
105+ "Workspace not found",
106+ `workspace=${input.workspaceId}`,
107+ );
108+ }
109+
110+ const organizationId = workspace.organizationId;
121111
122112 if (input.payload.scopeType === "account" && input.payload.accountId === null) {
123113 return yield* sourceStoreError.fromMessage(
@@ -140,11 +130,29 @@ export const createPmCredentialsService = ( · 16 unmodified lines
140130 const requestedId = input.payload.id;
141131 const requestedBindingId = requestedId as SourceAuthBinding["id"] | undefined;
142132
143− const existingBinding = requestedBindingId
144− ? bindings.find((binding) => binding.id === requestedBindingId) ?? null
145− : null;
146−
147− const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId);
133+ const existingBindingOption = requestedBindingId
134+ ? yield* rows.sourceAuthBindings.getById(requestedBindingId).pipe(
135+ Effect.mapError((error) =>
136+ sourceStoreError.fromRowStore("credentials.binding.get_by_id", error),
137+ ),
138+ )
139+ : Option.none<SourceAuthBinding>();
140+
141+ const existingBinding = Option.getOrNull(existingBindingOption);
142+ if (
143+ existingBinding !== null
144+ && existingBinding.workspaceId !== input.workspaceId
145+ && (
146+ existingBinding.workspaceId !== null
147+ || existingBinding.organizationId !== organizationId
148+ )
149+ ) {
150+ return yield* sourceStoreError.fromMessage(
151+ "credentials.upsert",
152+ "Credential binding is outside workspace scope",
153+ `workspace=${input.workspaceId} binding=${requestedBindingId}`,
154+ );
155+ }
148156
149157 const scopeWorkspaceId =
150158 input.payload.scopeType === "workspace" ? input.workspaceId : null;
@@ -163,9 +171,15 @@ export const createPmCredentialsService = ( · 12 unmodified lines
163171 ?? (`conn_${crypto.randomUUID()}` as AuthConnection["id"])
164172 ) as AuthConnection["id"];
165173
166− const existingConnection = connections.find(
167− (connection) => connection.id === requestedConnectionId,
168− ) ?? null;
174+ const existingConnectionOption = yield* rows.authConnections
175+ .getById(requestedConnectionId)
176+ .pipe(
177+ Effect.mapError((error) =>
178+ sourceStoreError.fromRowStore("credentials.connection.get_by_id", error),
179+ ),
180+ );
181+
182+ const existingConnection = Option.getOrNull(existingConnectionOption);
169183
170184 if (existingConnection && existingConnection.organizationId !== organizationId) {
171185 return yield* sourceStoreError.fromMessage(
@@ -228,9 +242,15 @@ export const createPmCredentialsService = ( · 56 unmodified lines
228242 );
229243
230244 if (nextConnection.strategy === "oauth2") {
231− const existingOAuth = oauthStates.find(
232− (state) => state.connectionId === requestedConnectionId,
233− ) ?? null;
245+ const existingOAuthOption = yield* rows.oauthStates
246+ .getByConnectionId(requestedConnectionId)
247+ .pipe(
248+ Effect.mapError((error) =>
249+ sourceStoreError.fromRowStore("credentials.oauth_states.get_by_connection", error),
250+ ),
251+ );
252+ const existingOAuth = Option.getOrNull(existingOAuthOption);
253+
234254 const refreshConfig = buildOAuthRefreshConfigFromPayload(
235255 input.payload,
236256 parseOAuthRefreshConfig(existingOAuth?.refreshConfigJson ?? null),
@@ -282,9 +302,14 @@ export const createPmCredentialsService = ( · 45 unmodified lines
282302 ),
283303 );
284304 } else {
285− const existingMaterial = materials.find(
286− (material) => material.connectionId === requestedConnectionId,
287− ) ?? null;
305+ const existingMaterialOption = yield* rows.authMaterials
306+ .getByConnectionId(requestedConnectionId)
307+ .pipe(
308+ Effect.mapError((error) =>
309+ sourceStoreError.fromRowStore("credentials.materials.get_by_connection", error),
310+ ),
311+ );
312+ const existingMaterial = Option.getOrNull(existingMaterialOption);
288313
289314 const material: AuthMaterial = {
290315 id:
@@ -312,30 +337,35 @@ export const createPmCredentialsService = ( · 21 unmodified lines
312337
313338 removeCredentialBinding: (input) =>
314339 Effect.gen(function* () {
315− const [bindings, workspaces] = yield* Effect.all([
316− rows.sourceAuthBindings.list().pipe(
317− Effect.mapError((error) =>
318− sourceStoreError.fromRowStore("credentials.bindings.list", error),
319− ),
320− ),
321− rows.workspaces.list().pipe(
322− Effect.mapError((error) =>
323− sourceStoreError.fromRowStore("credentials.workspaces.list", error),
324− ),
340+ const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe(
341+ Effect.mapError((error) =>
342+ sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", error),
325343 ),
326− ]);
344+ );
345+
346+ const workspace = Option.getOrNull(workspaceOption);
347+ if (workspace === null) {
348+ return {
349+ removed: false,
350+ };
351+ }
327352
328− const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId);
329− const binding = bindings.find(
330− (item) =>
331− item.id === input.credentialBindingId
332− && (
333− item.workspaceId === input.workspaceId
334− || (item.workspaceId === null && item.organizationId === organizationId)
353+ const bindingOption = yield* rows.sourceAuthBindings
354+ .getById(input.credentialBindingId)
355+ .pipe(
356+ Effect.mapError((error) =>
357+ sourceStoreError.fromRowStore("credentials.binding.get_by_id", error),
335358 ),
336− );
359+ );
337360
338− if (!binding) {
361+ const binding = Option.getOrNull(bindingOption);
362+ if (
363+ binding === null
364+ || (
365+ binding.workspaceId !== input.workspaceId
366+ && (binding.workspaceId !== null || binding.organizationId !== workspace.organizationId)
367+ )
368+ ) {
339369 return {
340370 removed: false,
341371 };
@@ -355,10 +385,15 @@ export const createPmCredentialsService = ( · 13 unmodified lines
355385 };
356386 }
357387
358− const hasRemainingBindings = bindings.some(
359− (candidate) =>
360− candidate.id !== binding.id && candidate.connectionId === binding.connectionId,
361− );
388+ const remainingBindings = yield* rows.sourceAuthBindings
389+ .listByConnectionId(binding.connectionId)
390+ .pipe(
391+ Effect.mapError((error) =>
392+ sourceStoreError.fromRowStore("credentials.bindings.list_by_connection", error),
393+ ),
394+ );
395+
396+ const hasRemainingBindings = remainingBindings.some((candidate) => candidate.id !== binding.id);
362397
363398 if (!hasRemainingBindings) {
364399 yield* Effect.all([
apps/pm/src/execute.e2e.test.ts+24−3
@@ -109,10 +109,31 @@ const withPmProcess = (port: number, localDataDir: string) => · 108 unmodified lines
109109 }),
110110 ),
111111 (child) =>
112− Effect.sync(() => {
113− if (!child.killed) {
114− child.kill("SIGTERM");
112+ Effect.promise(async () => {
113+ if (child.exitCode !== null || child.killed) {
114+ return;
115115 }
116+
117+ child.kill("SIGTERM");
118+
119+ await new Promise<void>((resolve) => {
120+ const timeout = setTimeout(() => {
121+ if (child.exitCode === null && !child.killed) {
122+ child.kill("SIGKILL");
123+ }
124+ resolve();
125+ }, 2_000);
126+
127+ child.once("exit", () => {
128+ clearTimeout(timeout);
129+ resolve();
130+ });
131+
132+ child.once("error", () => {
133+ clearTimeout(timeout);
134+ resolve();
135+ });
136+ });
116137 }),
117138 );
118139
apps/pm/src/main.ts+14−14
@@ -59,7 +59,7 @@ const pmStateRootDir = process.env.PM_STATE_ROOT_DIR ?? ".executor-v2/pm-state"; · 58 unmodified lines
5959
6060 const parsePort = (value: string | undefined): number => {
6161 const parsed = Number.parseInt(value ?? "", 10);
62− return Number.isFinite(parsed) && parsed > 0 ? parsed : 8787;
62+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 8788;
6363 };
6464
6565 const readConfiguredRuntimeKind = (value: string | undefined): string | undefined => {
@@ -102,17 +102,21 @@ const ensurePmBootstrap = ( · 36 unmodified lines
102102 ) =>
103103 Effect.gen(function* () {
104104 const now = Date.now();
105− const [organizations, memberships, workspaces, profileOption] = yield* Effect.all([
106− persistence.rows.organizations.list(),
107− persistence.rows.organizationMemberships.list(),
108− persistence.rows.workspaces.list(),
109− persistence.rows.profile.get(),
110− ]);
111105
112106 const organizationId = "org_local" as OrganizationId;
113107 const accountId = "acct_local" as AccountId;
114108
115− if (organizations.find((item) => item.id === organizationId) === undefined) {
109+ const [organizationOption, membershipOption, workspaceOption, profileOption] = yield* Effect.all([
110+ persistence.rows.organizations.getById(organizationId),
111+ persistence.rows.organizationMemberships.getByOrganizationAndAccount(
112+ organizationId,
113+ accountId,
114+ ),
115+ persistence.rows.workspaces.getById(workspaceId),
116+ persistence.rows.profile.get(),
117+ ]);
118+
119+ if (organizationOption._tag === "None") {
116120 yield* persistence.rows.organizations.upsert({
117121 id: organizationId,
118122 slug: organizationId,
@@ -124,11 +128,7 @@ const ensurePmBootstrap = ( · 5 unmodified lines
124128 });
125129 }
126130
127− if (
128− memberships.find(
129− (item) => item.organizationId === organizationId && item.accountId === accountId,
130− ) === undefined
131− ) {
131+ if (membershipOption._tag === "None") {
132132 yield* persistence.rows.organizationMemberships.upsert({
133133 id: "org_member_local" as OrganizationMemberId,
134134 organizationId,
@@ -143,7 +143,7 @@ const ensurePmBootstrap = ( · 8 unmodified lines
143143 });
144144 }
145145
146− if (workspaces.find((item) => item.id === workspaceId) === undefined) {
146+ if (workspaceOption._tag === "None") {
147147 yield* persistence.rows.workspaces.upsert({
148148 id: workspaceId,
149149 organizationId,
apps/pm/src/organizations-service.ts+26−25
@@ -8,6 +8,7 @@ import { · 7 unmodified lines
88 type OrganizationMembership,
99 } from "@executor-v2/schema";
1010 import * as Effect from "effect/Effect";
11+import * as Option from "effect/Option";
1112
1213 import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
1314
@@ -49,28 +50,20 @@ export const createPmOrganizationsService = ( · 35 unmodified lines
4950
5051 upsertOrganization: (input) =>
5152 Effect.gen(function* () {
52− const organizations = yield* rows.organizations.list().pipe(
53− Effect.mapError((error) =>
54− sourceStoreError.fromRowStore("organizations.upsert", error),
55− ),
56− );
57−
58− const memberships = yield* rows.organizationMemberships.list().pipe(
59− Effect.mapError((error) =>
60− sourceStoreError.fromRowStore(
61− "organizations.memberships.list",
62− error,
53+ const existingOption = input.payload.id
54+ ? yield* rows.organizations.getById(input.payload.id).pipe(
55+ Effect.mapError((error) =>
56+ sourceStoreError.fromRowStore("organizations.get_by_id", error),
6357 ),
64− ),
65− );
58+ )
59+ : yield* rows.organizations.getBySlug(input.payload.slug).pipe(
60+ Effect.mapError((error) =>
61+ sourceStoreError.fromRowStore("organizations.get_by_slug", error),
62+ ),
63+ );
6664
6765 const now = Date.now();
68− const existingIndex = input.payload.id
69− ? organizations.findIndex((organization) => organization.id === input.payload.id)
70− : organizations.findIndex(
71− (organization) => organization.slug === input.payload.slug,
72− );
73− const existing = existingIndex >= 0 ? organizations[existingIndex] : null;
66+ const existing = Option.getOrNull(existingOption);
7467
7568 const nextOrganization: Organization = {
7669 id:
@@ -94,13 +87,21 @@ export const createPmOrganizationsService = ( · 17 unmodified lines
9487 );
9588
9689 if (existing === null && nextOrganization.createdByAccountId !== null) {
97− const existingMembership = memberships.find(
98− (membership) =>
99− membership.organizationId === nextOrganization.id
100− && membership.accountId === nextOrganization.createdByAccountId,
101− );
90+ const existingMembership = yield* rows.organizationMemberships
91+ .getByOrganizationAndAccount(
92+ nextOrganization.id,
93+ nextOrganization.createdByAccountId,
94+ )
95+ .pipe(
96+ Effect.mapError((error) =>
97+ sourceStoreError.fromRowStore(
98+ "organizations.membership_get",
99+ error,
100+ ),
101+ ),
102+ );
102103
103− if (!existingMembership) {
104+ if (Option.isNone(existingMembership)) {
104105 const membership: OrganizationMembership = {
105106 id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"],
106107 organizationId: nextOrganization.id,
apps/pm/src/persistent-approval-policy.test.ts+23−0
@@ -1,5 +1,6 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
3+import * as Option from "effect/Option";
34 import { type Approval, type WorkspaceId } from "@executor-v2/schema";
45
56 import {
@@ -15,6 +16,28 @@ describe("PM persistent approval policy", () => { · 9 unmodified lines
1516 const approvalRows = {
1617 approvals: {
1718 list: () => Effect.succeed(approvals),
19+ listByWorkspaceId: (workspaceId: WorkspaceId) =>
20+ Effect.succeed(approvals.filter((approval) => approval.workspaceId === workspaceId)),
21+ getById: (approvalId: Approval["id"]) =>
22+ Effect.succeed(
23+ Option.fromNullable(
24+ approvals.find((approval) => approval.id === approvalId) ?? null,
25+ ),
26+ ),
27+ findByRunAndCall: (
28+ workspaceId: Approval["workspaceId"],
29+ taskRunId: Approval["taskRunId"],
30+ callId: Approval["callId"],
31+ ) =>
32+ Effect.succeed(
33+ Option.fromNullable(
34+ approvals.find((approval) =>
35+ approval.workspaceId === workspaceId
36+ && approval.taskRunId === taskRunId
37+ && approval.callId === callId
38+ ) ?? null,
39+ ),
40+ ),
1841 upsert: (approval: Approval) =>
1942 Effect.sync(() => {
2043 const index = approvals.findIndex((item) => item.id === approval.id);
apps/pm/src/policies-service.ts+23−24
@@ -5,6 +5,7 @@ import { · 4 unmodified lines
55 } from "@executor-v2/management-api";
66 import { type Policy } from "@executor-v2/schema";
77 import * as Effect from "effect/Effect";
8+import * as Option from "effect/Option";
89
910 import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
1011
@@ -29,36 +30,36 @@ export const createPmPoliciesService = ( · 18 unmodified lines
2930 makeControlPlanePoliciesService({
3031 listPolicies: (workspaceId) =>
3132 Effect.gen(function* () {
32− const policies = yield* rows.policies.list().pipe(
33+ const policies = yield* rows.policies.listByWorkspaceId(workspaceId).pipe(
3334 Effect.mapError((error) =>
3435 sourceStoreError.fromRowStore("policies.list", error),
3536 ),
3637 );
3738
38− return sortPolicies(
39− policies.filter((policy) => policy.workspaceId === workspaceId),
40− );
39+ return sortPolicies(policies);
4140 }),
4241
4342 upsertPolicy: (input) =>
4443 Effect.gen(function* () {
45− const policies = yield* rows.policies.list().pipe(
46− Effect.mapError((error) =>
47− sourceStoreError.fromRowStore("policies.upsert", error),
48− ),
49− );
50−
5144 const now = Date.now();
5245 const requestedId = input.payload.id;
5346
54− const existingIndex = requestedId
55− ? policies.findIndex(
56− (policy) =>
57− policy.workspaceId === input.workspaceId && policy.id === requestedId,
58− )
59− : -1;
60−
61− const existing = existingIndex >= 0 ? policies[existingIndex] : null;
47+ const existingOption = requestedId
48+ ? yield* rows.policies.getById(requestedId).pipe(
49+ Effect.mapError((error) =>
50+ sourceStoreError.fromRowStore("policies.get_by_id", error),
51+ ),
52+ )
53+ : Option.none<Policy>();
54+
55+ const existing = Option.getOrNull(existingOption);
56+ if (existing !== null && existing.workspaceId !== input.workspaceId) {
57+ return yield* sourceStoreError.fromMessage(
58+ "policies.upsert",
59+ "Policy belongs to another workspace",
60+ `workspace=${input.workspaceId} policy=${requestedId}`,
61+ );
62+ }
6263
6364 const nextPolicy: Policy = {
6465 id: existing?.id ?? (requestedId ?? (`pol_${crypto.randomUUID()}` as Policy["id"])),
@@ -80,17 +81,15 @@ export const createPmPoliciesService = ( · 15 unmodified lines
8081
8182 removePolicy: (input) =>
8283 Effect.gen(function* () {
83− const policies = yield* rows.policies.list().pipe(
84+ const existingOption = yield* rows.policies.getById(input.policyId).pipe(
8485 Effect.mapError((error) =>
85− sourceStoreError.fromRowStore("policies.remove", error),
86+ sourceStoreError.fromRowStore("policies.get_by_id", error),
8687 ),
8788 );
8889
89− const existing = policies.find(
90− (policy) => policy.workspaceId === input.workspaceId && policy.id === input.policyId,
91− );
90+ const existing = Option.getOrNull(existingOption);
9291
93− if (!existing) {
92+ if (!existing || existing.workspaceId !== input.workspaceId) {
9493 return {
9594 removed: false,
9695 };
apps/pm/src/storage-service.test.ts+23−0
@@ -6,6 +6,7 @@ import { · 5 unmodified lines
66 type WorkspaceId,
77 } from "@executor-v2/schema";
88 import * as Effect from "effect/Effect";
9+import * as Option from "effect/Option";
910 import { mkdtemp, rm, writeFile } from "node:fs/promises";
1011 import { tmpdir } from "node:os";
1112 import * as path from "node:path";
@@ -39,10 +40,32 @@ describe("PM storage service", () => { · 27 unmodified lines
3940 const rows: StorageRows = {
4041 workspaces: {
4142 list: () => Effect.succeed([workspace]),
43+ getById: (id) =>
44+ Effect.succeed(id === workspace.id ? Option.some(workspace) : Option.none()),
45+ listByOrganizationIds: (organizationIds) =>
46+ Effect.succeed(
47+ organizationIds.includes(workspace.organizationId) ? [workspace] : [],
48+ ),
4249 upsert: () => Effect.void,
4350 },
4451 storageInstances: {
4552 list: () => Effect.succeed(storageInstances),
53+ getById: (storageInstanceId) =>
54+ Effect.succeed(
55+ Option.fromNullable(
56+ storageInstances.find((item) => item.id === storageInstanceId) ?? null,
57+ ),
58+ ),
59+ listByWorkspaceScope: (scopeWorkspaceId, organizationId) =>
60+ Effect.succeed(
61+ storageInstances.filter((instance) =>
62+ instance.workspaceId === scopeWorkspaceId
63+ || (
64+ instance.workspaceId === null
65+ && instance.organizationId === organizationId
66+ )
67+ ),
68+ ),
4669 upsert: (storageInstance) =>
4770 Effect.sync(() => {
4871 const index = storageInstances.findIndex((item) => item.id === storageInstance.id);
apps/pm/src/storage-service.ts+49−35
@@ -10,11 +10,9 @@ import { · 9 unmodified lines
1010 type WorkspaceId,
1111 } from "@executor-v2/schema";
1212 import * as Effect from "effect/Effect";
13+import * as Option from "effect/Option";
1314
14−import {
15− createSqlSourceStoreErrorMapper,
16− resolveWorkspaceOrganizationId,
17−} from "./control-plane-row-helpers";
15+import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
1816 import {
1917 DEFAULT_EPHEMERAL_TTL_HOURS,
2018 initializeStorageInstanceFiles,
@@ -100,7 +98,7 @@ const loadStorageScope = ( · 79 unmodified lines
10098 input: {
10199 workspaceId: WorkspaceId;
102100 instancesOperation: string;
103− workspacesOperation: string;
101+ workspaceOperation: string;
104102 },
105103 ): Effect.Effect<
106104 {
@@ -109,22 +107,33 @@ const loadStorageScope = ( · 2 unmodified lines
109107 },
110108 SourceStoreError
111109 > =>
112− Effect.all([
113− rows.storageInstances.list().pipe(
114− Effect.mapError((error) =>
115− sourceStoreError.fromRowStore(input.instancesOperation, error),
116− ),
117− ),
118− rows.workspaces.list().pipe(
119− Effect.mapError((error) =>
120− sourceStoreError.fromRowStore(input.workspacesOperation, error),
121− ),
110+ rows.workspaces.getById(input.workspaceId).pipe(
111+ Effect.mapError((error) =>
112+ sourceStoreError.fromRowStore(input.workspaceOperation, error),
122113 ),
123− ]).pipe(
124− Effect.map(([storageInstances, workspaces]) => ({
125− storageInstances,
126− organizationId: resolveWorkspaceOrganizationId(workspaces, input.workspaceId),
127− })),
114+ Effect.flatMap((workspaceOption) => {
115+ const workspace = Option.getOrNull(workspaceOption);
116+ if (workspace === null) {
117+ return sourceStoreError.fromMessage(
118+ input.workspaceOperation,
119+ "Workspace not found",
120+ `workspace=${input.workspaceId}`,
121+ );
122+ }
123+
124+ return rows.storageInstances.listByWorkspaceScope(
125+ input.workspaceId,
126+ workspace.organizationId,
127+ ).pipe(
128+ Effect.mapError((error) =>
129+ sourceStoreError.fromRowStore(input.instancesOperation, error),
130+ ),
131+ Effect.map((storageInstances) => ({
132+ storageInstances,
133+ organizationId: workspace.organizationId,
134+ })),
135+ );
136+ }),
128137 );
129138
130139 export const createPmStorageService = (
@@ -139,14 +148,10 @@ export const createPmStorageService = ( · 8 unmodified lines
139148 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
140149 workspaceId,
141150 instancesOperation: "storage.list_instances",
142− workspacesOperation: "storage.list_instances.workspaces",
151+ workspaceOperation: "storage.list_instances.workspace",
143152 });
144153
145− return sortStorageInstances(
146− storageInstances.filter((instance) =>
147− canAccessStorageInstance(instance, workspaceId, organizationId)
148− ),
149− );
154+ return sortStorageInstances(storageInstances);
150155 }),
151156
152157 openStorageInstance: (input) =>
@@ -159,14 +164,23 @@ export const createPmStorageService = ( · 6 unmodified lines
159164 );
160165 }
161166
162− const workspaces = yield* rows.workspaces.list().pipe(
167+ const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe(
163168 Effect.mapError((error) =>
164− sourceStoreError.fromRowStore("storage.open.workspaces", error),
169+ sourceStoreError.fromRowStore("storage.open.workspace", error),
165170 ),
166171 );
167172
173+ const workspace = Option.getOrNull(workspaceOption);
174+ if (workspace === null) {
175+ return yield* sourceStoreError.fromMessage(
176+ "storage.open",
177+ "Workspace not found",
178+ `workspace=${input.workspaceId}`,
179+ );
180+ }
181+
168182 const now = Date.now();
169− const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId);
183+ const organizationId = workspace.organizationId;
170184 const storageInstanceId =
171185 `storage_${crypto.randomUUID()}` as StorageInstance["id"];
172186 const ttlHours =
@@ -231,7 +245,7 @@ export const createPmStorageService = ( · 58 unmodified lines
231245 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
232246 workspaceId: input.workspaceId,
233247 instancesOperation: "storage.close.instances",
234− workspacesOperation: "storage.close.workspaces",
248+ workspaceOperation: "storage.close.workspace",
235249 });
236250
237251 const found = findStorageInstance(storageInstances, organizationId, {
@@ -270,7 +284,7 @@ export const createPmStorageService = ( · 32 unmodified lines
270284 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
271285 workspaceId: input.workspaceId,
272286 instancesOperation: "storage.remove.instances",
273− workspacesOperation: "storage.remove.workspaces",
287+ workspaceOperation: "storage.remove.workspace",
274288 });
275289
276290 const found = findStorageInstance(storageInstances, organizationId, {
@@ -311,7 +325,7 @@ export const createPmStorageService = ( · 34 unmodified lines
311325 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
312326 workspaceId: input.workspaceId,
313327 instancesOperation: "storage.list_directory.instances",
314− workspacesOperation: "storage.list_directory.workspaces",
328+ workspaceOperation: "storage.list_directory.workspace",
315329 });
316330
317331 const found = findStorageInstance(storageInstances, organizationId, {
@@ -356,7 +370,7 @@ export const createPmStorageService = ( · 38 unmodified lines
356370 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
357371 workspaceId: input.workspaceId,
358372 instancesOperation: "storage.read_file.instances",
359− workspacesOperation: "storage.read_file.workspaces",
373+ workspaceOperation: "storage.read_file.workspace",
360374 });
361375
362376 const found = findStorageInstance(storageInstances, organizationId, {
@@ -397,7 +411,7 @@ export const createPmStorageService = ( · 34 unmodified lines
397411 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
398412 workspaceId: input.workspaceId,
399413 instancesOperation: "storage.list_kv.instances",
400− workspacesOperation: "storage.list_kv.workspaces",
414+ workspaceOperation: "storage.list_kv.workspace",
401415 });
402416
403417 const found = findStorageInstance(storageInstances, organizationId, {
@@ -438,7 +452,7 @@ export const createPmStorageService = ( · 34 unmodified lines
438452 const { storageInstances, organizationId } = yield* loadStorageScope(rows, {
439453 workspaceId: input.workspaceId,
440454 instancesOperation: "storage.query_sql.instances",
441− workspacesOperation: "storage.query_sql.workspaces",
455+ workspaceOperation: "storage.query_sql.workspace",
442456 });
443457
444458 const found = findStorageInstance(storageInstances, organizationId, {
apps/pm/src/tool-call-handler.test.ts+6−5
@@ -4,24 +4,25 @@ import { · 3 unmodified lines
44 } from "@executor-v2/engine";
55 import { describe, expect, it } from "@effect/vitest";
66 import * as Effect from "effect/Effect";
7+import * as Option from "effect/Option";
78
89 import { createPmResolveToolCredentials } from "./credential-resolver";
910
1011 const emptyCredentialRows = {
1112 workspaces: {
12− list: () => Effect.succeed([]),
13+ getById: () => Effect.succeed(Option.none()),
1314 },
1415 sourceAuthBindings: {
15− list: () => Effect.succeed([]),
16+ listByWorkspaceScope: () => Effect.succeed([]),
1617 },
1718 authConnections: {
18− list: () => Effect.succeed([]),
19+ getById: () => Effect.succeed(Option.none()),
1920 },
2021 authMaterials: {
21− list: () => Effect.succeed([]),
22+ getByConnectionId: () => Effect.succeed(Option.none()),
2223 },
2324 oauthStates: {
24− list: () => Effect.succeed([]),
25+ getByConnectionId: () => Effect.succeed(Option.none()),
2526 },
2627 };
2728
apps/pm/src/workspaces-service.ts+9−9
@@ -5,6 +5,7 @@ import { · 4 unmodified lines
55 } from "@executor-v2/management-api";
66 import { type Workspace } from "@executor-v2/schema";
77 import * as Effect from "effect/Effect";
8+import * as Option from "effect/Option";
89
910 import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers";
1011
@@ -41,17 +42,16 @@ export const createPmWorkspacesService = ( · 30 unmodified lines
4142
4243 upsertWorkspace: (input) =>
4344 Effect.gen(function* () {
44− const workspaces = yield* rows.workspaces.list().pipe(
45− Effect.mapError((error) =>
46− sourceStoreError.fromRowStore("workspaces.upsert", error),
47− ),
48− );
45+ const existingOption = input.payload.id
46+ ? yield* rows.workspaces.getById(input.payload.id).pipe(
47+ Effect.mapError((error) =>
48+ sourceStoreError.fromRowStore("workspaces.get_by_id", error),
49+ ),
50+ )
51+ : Option.none<Workspace>();
4952
5053 const now = Date.now();
51− const existingIndex = input.payload.id
52− ? workspaces.findIndex((workspace) => workspace.id === input.payload.id)
53− : -1;
54− const existing = existingIndex >= 0 ? workspaces[existingIndex] : null;
54+ const existing = Option.getOrNull(existingOption);
5555
5656 const nextWorkspace: Workspace = {
5757 id: existing?.id ?? (input.payload.id ?? (`ws_${crypto.randomUUID()}` as Workspace["id"])),
apps/web/lib/control-plane/server.ts+10−13
@@ -103,14 +103,17 @@ const ensurePrincipalProvisioned = ( · 102 unmodified lines
103103 ) =>
104104 Effect.gen(function* () {
105105 const now = Date.now();
106− const [organizations, memberships, workspaces, profileOption] = yield* Effect.all([
107− persistence.rows.organizations.list(),
108− persistence.rows.organizationMemberships.list(),
109− persistence.rows.workspaces.list(),
106+ const [organizationOption, membershipOption, workspaceOption, profileOption] = yield* Effect.all([
107+ persistence.rows.organizations.getById(principal.organizationId as Organization["id"]),
108+ persistence.rows.organizationMemberships.getByOrganizationAndAccount(
109+ principal.organizationId as OrganizationMembership["organizationId"],
110+ principal.accountId as OrganizationMembership["accountId"],
111+ ),
112+ persistence.rows.workspaces.getById(principal.workspaceId as Workspace["id"]),
110113 persistence.rows.profile.get(),
111114 ]);
112115
113− if (organizations.find((organization) => organization.id === principal.organizationId) === undefined) {
116+ if (organizationOption._tag === "None") {
114117 yield* persistence.rows.organizations.upsert({
115118 id: principal.organizationId as Organization["id"],
116119 slug: principal.organizationId,
@@ -124,13 +127,7 @@ const ensurePrincipalProvisioned = ( · 7 unmodified lines
124127 });
125128 }
126129
127− if (
128− memberships.find(
129− (membership) =>
130− membership.organizationId === principal.organizationId
131− && membership.accountId === principal.accountId,
132− ) === undefined
133− ) {
130+ if (membershipOption._tag === "None") {
134131 yield* persistence.rows.organizationMemberships.upsert({
135132 id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"],
136133 organizationId: principal.organizationId as OrganizationMembership["organizationId"],
@@ -145,7 +142,7 @@ const ensurePrincipalProvisioned = ( · 8 unmodified lines
145142 });
146143 }
147144
148− if (workspaces.find((workspace) => workspace.id === principal.workspaceId) === undefined) {
145+ if (workspaceOption._tag === "None") {
149146 yield* persistence.rows.workspaces.upsert({
150147 id: principal.workspaceId as Workspace["id"],
151148 organizationId: principal.organizationId as Workspace["organizationId"],
packages/persistence-sql/src/control-plane-rows.ts+865−0
@@ -0,0 +1,865 @@
1+import {
2+ type Approval,
3+ type AuthConnection,
4+ type AuthMaterial,
5+ type OAuthState,
6+ type Organization,
7+ type OrganizationMembership,
8+ type Policy,
9+ type Profile,
10+ type SourceAuthBinding,
11+ type StorageInstance,
12+ type Workspace,
13+} from "@executor-v2/schema";
14+import * as Effect from "effect/Effect";
15+import * as Option from "effect/Option";
16+import { and, asc, desc, eq, inArray, isNull, or } from "drizzle-orm";
17+
18+import { toRowStoreError } from "./persistence-errors";
19+import { tableNames } from "./schema";
20+import {
21+ type DrizzleDb,
22+ type DrizzleTables,
23+ type SqlBackend,
24+} from "./sql-internals";
25+
26+type CreateControlPlaneRowsInput = {
27+ backend: SqlBackend;
28+ db: DrizzleDb;
29+ tables: DrizzleTables;
30+};
31+
32+const asDomain = <A>(value: unknown): A => value as A;
33+const asDomainArray = <A>(value: ReadonlyArray<unknown>): Array<A> => value as Array<A>;
34+
35+const rowEffect = <A>(
36+ backend: SqlBackend,
37+ operation: string,
38+ location: string,
39+ run: () => Promise<A>,
40+) =>
41+ Effect.tryPromise({
42+ try: run,
43+ catch: (cause) => toRowStoreError(backend, operation, location, cause),
44+ });
45+
46+export const createControlPlaneRows = ({
47+ backend,
48+ db,
49+ tables,
50+}: CreateControlPlaneRowsInput) => ({
51+ profile: {
52+ get: () =>
53+ rowEffect(backend, "rows.profile.get", tableNames.profile, async () => {
54+ const row = await db
55+ .select()
56+ .from(tables.profileTable)
57+ .orderBy(desc(tables.profileTable.updatedAt), asc(tables.profileTable.id))
58+ .limit(1);
59+
60+ return row[0] ? Option.some(asDomain<Profile>(row[0])) : Option.none<Profile>();
61+ }),
62+
63+ upsert: (profile: Profile) =>
64+ rowEffect(backend, "rows.profile.upsert", tableNames.profile, async () => {
65+ await db
66+ .insert(tables.profileTable)
67+ .values(profile)
68+ .onConflictDoUpdate({
69+ target: tables.profileTable.id,
70+ set: profile,
71+ });
72+ }),
73+ },
74+
75+ organizations: {
76+ list: () =>
77+ rowEffect(backend, "rows.organizations.list", tableNames.organizations, async () => {
78+ const rows = await db
79+ .select()
80+ .from(tables.organizationsTable)
81+ .orderBy(asc(tables.organizationsTable.updatedAt), asc(tables.organizationsTable.id));
82+
83+ return asDomainArray<Organization>(rows);
84+ }),
85+
86+ getById: (organizationId: Organization["id"]) =>
87+ rowEffect(backend, "rows.organizations.get_by_id", tableNames.organizations, async () => {
88+ const row = await db
89+ .select()
90+ .from(tables.organizationsTable)
91+ .where(eq(tables.organizationsTable.id, organizationId))
92+ .limit(1);
93+
94+ return row[0]
95+ ? Option.some(asDomain<Organization>(row[0]))
96+ : Option.none<Organization>();
97+ }),
98+
99+ getBySlug: (slug: Organization["slug"]) =>
100+ rowEffect(
101+ backend,
102+ "rows.organizations.get_by_slug",
103+ tableNames.organizations,
104+ async () => {
105+ const row = await db
106+ .select()
107+ .from(tables.organizationsTable)
108+ .where(eq(tables.organizationsTable.slug, slug))
109+ .limit(1);
110+
111+ return row[0]
112+ ? Option.some(asDomain<Organization>(row[0]))
113+ : Option.none<Organization>();
114+ },
115+ ),
116+
117+ upsert: (organization: Organization) =>
118+ rowEffect(backend, "rows.organizations.upsert", tableNames.organizations, async () => {
119+ await db
120+ .insert(tables.organizationsTable)
121+ .values(organization)
122+ .onConflictDoUpdate({
123+ target: tables.organizationsTable.id,
124+ set: organization,
125+ });
126+ }),
127+ },
128+
129+ organizationMemberships: {
130+ list: () =>
131+ rowEffect(
132+ backend,
133+ "rows.organization_memberships.list",
134+ tableNames.organizationMemberships,
135+ async () => {
136+ const rows = await db
137+ .select()
138+ .from(tables.organizationMembershipsTable)
139+ .orderBy(
140+ asc(tables.organizationMembershipsTable.updatedAt),
141+ asc(tables.organizationMembershipsTable.id),
142+ );
143+
144+ return asDomainArray<OrganizationMembership>(rows);
145+ },
146+ ),
147+
148+ listByOrganizationId: (organizationId: OrganizationMembership["organizationId"]) =>
149+ rowEffect(
150+ backend,
151+ "rows.organization_memberships.list_by_organization",
152+ tableNames.organizationMemberships,
153+ async () => {
154+ const rows = await db
155+ .select()
156+ .from(tables.organizationMembershipsTable)
157+ .where(eq(tables.organizationMembershipsTable.organizationId, organizationId))
158+ .orderBy(
159+ asc(tables.organizationMembershipsTable.updatedAt),
160+ asc(tables.organizationMembershipsTable.id),
161+ );
162+
163+ return asDomainArray<OrganizationMembership>(rows);
164+ },
165+ ),
166+
167+ listByAccountId: (accountId: OrganizationMembership["accountId"]) =>
168+ rowEffect(
169+ backend,
170+ "rows.organization_memberships.list_by_account",
171+ tableNames.organizationMemberships,
172+ async () => {
173+ const rows = await db
174+ .select()
175+ .from(tables.organizationMembershipsTable)
176+ .where(eq(tables.organizationMembershipsTable.accountId, accountId))
177+ .orderBy(
178+ asc(tables.organizationMembershipsTable.updatedAt),
179+ asc(tables.organizationMembershipsTable.id),
180+ );
181+
182+ return asDomainArray<OrganizationMembership>(rows);
183+ },
184+ ),
185+
186+ getByOrganizationAndAccount: (
187+ organizationId: OrganizationMembership["organizationId"],
188+ accountId: OrganizationMembership["accountId"],
189+ ) =>
190+ rowEffect(
191+ backend,
192+ "rows.organization_memberships.get_by_organization_and_account",
193+ tableNames.organizationMemberships,
194+ async () => {
195+ const row = await db
196+ .select()
197+ .from(tables.organizationMembershipsTable)
198+ .where(
199+ and(
200+ eq(tables.organizationMembershipsTable.organizationId, organizationId),
201+ eq(tables.organizationMembershipsTable.accountId, accountId),
202+ ),
203+ )
204+ .limit(1);
205+
206+ return row[0]
207+ ? Option.some(asDomain<OrganizationMembership>(row[0]))
208+ : Option.none<OrganizationMembership>();
209+ },
210+ ),
211+
212+ upsert: (membership: OrganizationMembership) =>
213+ rowEffect(
214+ backend,
215+ "rows.organization_memberships.upsert",
216+ tableNames.organizationMemberships,
217+ async () => {
218+ await db
219+ .insert(tables.organizationMembershipsTable)
220+ .values(membership)
221+ .onConflictDoUpdate({
222+ target: tables.organizationMembershipsTable.id,
223+ set: membership,
224+ });
225+ },
226+ ),
227+ },
228+
229+ workspaces: {
230+ list: () =>
231+ rowEffect(backend, "rows.workspaces.list", tableNames.workspaces, async () => {
232+ const rows = await db
233+ .select()
234+ .from(tables.workspacesTable)
235+ .orderBy(asc(tables.workspacesTable.updatedAt), asc(tables.workspacesTable.id));
236+
237+ return asDomainArray<Workspace>(rows);
238+ }),
239+
240+ getById: (workspaceId: Workspace["id"]) =>
241+ rowEffect(backend, "rows.workspaces.get_by_id", tableNames.workspaces, async () => {
242+ const row = await db
243+ .select()
244+ .from(tables.workspacesTable)
245+ .where(eq(tables.workspacesTable.id, workspaceId))
246+ .limit(1);
247+
248+ return row[0] ? Option.some(asDomain<Workspace>(row[0])) : Option.none<Workspace>();
249+ }),
250+
251+ listByOrganizationIds: (
252+ organizationIds: ReadonlyArray<Workspace["organizationId"]>,
253+ ) =>
254+ rowEffect(
255+ backend,
256+ "rows.workspaces.list_by_organization_ids",
257+ tableNames.workspaces,
258+ async () => {
259+ if (organizationIds.length === 0) {
260+ return [] as Array<Workspace>;
261+ }
262+
263+ const rows = await db
264+ .select()
265+ .from(tables.workspacesTable)
266+ .where(inArray(tables.workspacesTable.organizationId, [...organizationIds]))
267+ .orderBy(asc(tables.workspacesTable.updatedAt), asc(tables.workspacesTable.id));
268+
269+ return asDomainArray<Workspace>(rows);
270+ },
271+ ),
272+
273+ upsert: (workspace: Workspace) =>
274+ rowEffect(backend, "rows.workspaces.upsert", tableNames.workspaces, async () => {
275+ await db
276+ .insert(tables.workspacesTable)
277+ .values(workspace)
278+ .onConflictDoUpdate({
279+ target: tables.workspacesTable.id,
280+ set: workspace,
281+ });
282+ }),
283+ },
284+
285+ authConnections: {
286+ list: () =>
287+ rowEffect(
288+ backend,
289+ "rows.auth_connections.list",
290+ tableNames.authConnections,
291+ async () => {
292+ const rows = await db
293+ .select()
294+ .from(tables.authConnectionsTable)
295+ .orderBy(asc(tables.authConnectionsTable.updatedAt), asc(tables.authConnectionsTable.id));
296+
297+ return asDomainArray<AuthConnection>(rows);
298+ },
299+ ),
300+
301+ getById: (connectionId: AuthConnection["id"]) =>
302+ rowEffect(
303+ backend,
304+ "rows.auth_connections.get_by_id",
305+ tableNames.authConnections,
306+ async () => {
307+ const row = await db
308+ .select()
309+ .from(tables.authConnectionsTable)
310+ .where(eq(tables.authConnectionsTable.id, connectionId))
311+ .limit(1);
312+
313+ return row[0]
314+ ? Option.some(asDomain<AuthConnection>(row[0]))
315+ : Option.none<AuthConnection>();
316+ },
317+ ),
318+
319+ listByOrganizationId: (organizationId: AuthConnection["organizationId"]) =>
320+ rowEffect(
321+ backend,
322+ "rows.auth_connections.list_by_organization",
323+ tableNames.authConnections,
324+ async () => {
325+ const rows = await db
326+ .select()
327+ .from(tables.authConnectionsTable)
328+ .where(eq(tables.authConnectionsTable.organizationId, organizationId))
329+ .orderBy(asc(tables.authConnectionsTable.updatedAt), asc(tables.authConnectionsTable.id));
330+
331+ return asDomainArray<AuthConnection>(rows);
332+ },
333+ ),
334+
335+ upsert: (connection: AuthConnection) =>
336+ rowEffect(
337+ backend,
338+ "rows.auth_connections.upsert",
339+ tableNames.authConnections,
340+ async () => {
341+ await db
342+ .insert(tables.authConnectionsTable)
343+ .values(connection)
344+ .onConflictDoUpdate({
345+ target: tables.authConnectionsTable.id,
346+ set: connection,
347+ });
348+ },
349+ ),
350+
351+ removeById: (connectionId: AuthConnection["id"]) =>
352+ rowEffect(
353+ backend,
354+ "rows.auth_connections.remove",
355+ tableNames.authConnections,
356+ async () => {
357+ const row = await db
358+ .select({ id: tables.authConnectionsTable.id })
359+ .from(tables.authConnectionsTable)
360+ .where(eq(tables.authConnectionsTable.id, connectionId))
361+ .limit(1);
362+
363+ if (row.length === 0) {
364+ return false;
365+ }
366+
367+ await db
368+ .delete(tables.authConnectionsTable)
369+ .where(eq(tables.authConnectionsTable.id, connectionId));
370+
371+ return true;
372+ },
373+ ),
374+ },
375+
376+ sourceAuthBindings: {
377+ list: () =>
378+ rowEffect(
379+ backend,
380+ "rows.source_auth_bindings.list",
381+ tableNames.sourceAuthBindings,
382+ async () => {
383+ const rows = await db
384+ .select()
385+ .from(tables.sourceAuthBindingsTable)
386+ .orderBy(
387+ asc(tables.sourceAuthBindingsTable.updatedAt),
388+ asc(tables.sourceAuthBindingsTable.id),
389+ );
390+
391+ return asDomainArray<SourceAuthBinding>(rows);
392+ },
393+ ),
394+
395+ getById: (bindingId: SourceAuthBinding["id"]) =>
396+ rowEffect(
397+ backend,
398+ "rows.source_auth_bindings.get_by_id",
399+ tableNames.sourceAuthBindings,
400+ async () => {
401+ const row = await db
402+ .select()
403+ .from(tables.sourceAuthBindingsTable)
404+ .where(eq(tables.sourceAuthBindingsTable.id, bindingId))
405+ .limit(1);
406+
407+ return row[0]
408+ ? Option.some(asDomain<SourceAuthBinding>(row[0]))
409+ : Option.none<SourceAuthBinding>();
410+ },
411+ ),
412+
413+ listByConnectionId: (connectionId: SourceAuthBinding["connectionId"]) =>
414+ rowEffect(
415+ backend,
416+ "rows.source_auth_bindings.list_by_connection",
417+ tableNames.sourceAuthBindings,
418+ async () => {
419+ const rows = await db
420+ .select()
421+ .from(tables.sourceAuthBindingsTable)
422+ .where(eq(tables.sourceAuthBindingsTable.connectionId, connectionId))
423+ .orderBy(
424+ asc(tables.sourceAuthBindingsTable.updatedAt),
425+ asc(tables.sourceAuthBindingsTable.id),
426+ );
427+
428+ return asDomainArray<SourceAuthBinding>(rows);
429+ },
430+ ),
431+
432+ listByWorkspaceScope: (
433+ workspaceId: Workspace["id"],
434+ organizationId: Organization["id"],
435+ ) =>
436+ rowEffect(
437+ backend,
438+ "rows.source_auth_bindings.list_by_workspace_scope",
439+ tableNames.sourceAuthBindings,
440+ async () => {
441+ const rows = await db
442+ .select()
443+ .from(tables.sourceAuthBindingsTable)
444+ .where(
445+ or(
446+ eq(tables.sourceAuthBindingsTable.workspaceId, workspaceId),
447+ and(
448+ isNull(tables.sourceAuthBindingsTable.workspaceId),
449+ eq(tables.sourceAuthBindingsTable.organizationId, organizationId),
450+ ),
451+ ),
452+ )
453+ .orderBy(
454+ desc(tables.sourceAuthBindingsTable.updatedAt),
455+ desc(tables.sourceAuthBindingsTable.createdAt),
456+ );
457+
458+ return asDomainArray<SourceAuthBinding>(rows);
459+ },
460+ ),
461+
462+ upsert: (binding: SourceAuthBinding) =>
463+ rowEffect(
464+ backend,
465+ "rows.source_auth_bindings.upsert",
466+ tableNames.sourceAuthBindings,
467+ async () => {
468+ await db
469+ .insert(tables.sourceAuthBindingsTable)
470+ .values(binding)
471+ .onConflictDoUpdate({
472+ target: tables.sourceAuthBindingsTable.id,
473+ set: binding,
474+ });
475+ },
476+ ),
477+
478+ removeById: (bindingId: SourceAuthBinding["id"]) =>
479+ rowEffect(
480+ backend,
481+ "rows.source_auth_bindings.remove",
482+ tableNames.sourceAuthBindings,
483+ async () => {
484+ const row = await db
485+ .select({ id: tables.sourceAuthBindingsTable.id })
486+ .from(tables.sourceAuthBindingsTable)
487+ .where(eq(tables.sourceAuthBindingsTable.id, bindingId))
488+ .limit(1);
489+
490+ if (row.length === 0) {
491+ return false;
492+ }
493+
494+ await db
495+ .delete(tables.sourceAuthBindingsTable)
496+ .where(eq(tables.sourceAuthBindingsTable.id, bindingId));
497+
498+ return true;
499+ },
500+ ),
501+ },
502+
503+ authMaterials: {
504+ list: () =>
505+ rowEffect(backend, "rows.auth_materials.list", tableNames.authMaterials, async () => {
506+ const rows = await db
507+ .select()
508+ .from(tables.authMaterialsTable)
509+ .orderBy(asc(tables.authMaterialsTable.updatedAt), asc(tables.authMaterialsTable.id));
510+
511+ return asDomainArray<AuthMaterial>(rows);
512+ }),
513+
514+ getByConnectionId: (connectionId: AuthMaterial["connectionId"]) =>
515+ rowEffect(
516+ backend,
517+ "rows.auth_materials.get_by_connection",
518+ tableNames.authMaterials,
519+ async () => {
520+ const row = await db
521+ .select()
522+ .from(tables.authMaterialsTable)
523+ .where(eq(tables.authMaterialsTable.connectionId, connectionId))
524+ .limit(1);
525+
526+ return row[0]
527+ ? Option.some(asDomain<AuthMaterial>(row[0]))
528+ : Option.none<AuthMaterial>();
529+ },
530+ ),
531+
532+ upsert: (material: AuthMaterial) =>
533+ rowEffect(
534+ backend,
535+ "rows.auth_materials.upsert",
536+ tableNames.authMaterials,
537+ async () => {
538+ await db
539+ .insert(tables.authMaterialsTable)
540+ .values(material)
541+ .onConflictDoUpdate({
542+ target: tables.authMaterialsTable.id,
543+ set: material,
544+ });
545+ },
546+ ),
547+
548+ removeByConnectionId: (connectionId: AuthMaterial["connectionId"]) =>
549+ rowEffect(
550+ backend,
551+ "rows.auth_materials.remove_by_connection",
552+ tableNames.authMaterials,
553+ async () => {
554+ await db
555+ .delete(tables.authMaterialsTable)
556+ .where(eq(tables.authMaterialsTable.connectionId, connectionId));
557+ },
558+ ),
559+ },
560+
561+ oauthStates: {
562+ list: () =>
563+ rowEffect(backend, "rows.oauth_states.list", tableNames.oauthStates, async () => {
564+ const rows = await db
565+ .select()
566+ .from(tables.oauthStatesTable)
567+ .orderBy(asc(tables.oauthStatesTable.updatedAt), asc(tables.oauthStatesTable.id));
568+
569+ return asDomainArray<OAuthState>(rows);
570+ }),
571+
572+ getByConnectionId: (connectionId: OAuthState["connectionId"]) =>
573+ rowEffect(
574+ backend,
575+ "rows.oauth_states.get_by_connection",
576+ tableNames.oauthStates,
577+ async () => {
578+ const row = await db
579+ .select()
580+ .from(tables.oauthStatesTable)
581+ .where(eq(tables.oauthStatesTable.connectionId, connectionId))
582+ .limit(1);
583+
584+ return row[0]
585+ ? Option.some(asDomain<OAuthState>(row[0]))
586+ : Option.none<OAuthState>();
587+ },
588+ ),
589+
590+ upsert: (state: OAuthState) =>
591+ rowEffect(backend, "rows.oauth_states.upsert", tableNames.oauthStates, async () => {
592+ await db
593+ .insert(tables.oauthStatesTable)
594+ .values(state)
595+ .onConflictDoUpdate({
596+ target: tables.oauthStatesTable.id,
597+ set: state,
598+ });
599+ }),
600+
601+ removeByConnectionId: (connectionId: OAuthState["connectionId"]) =>
602+ rowEffect(
603+ backend,
604+ "rows.oauth_states.remove_by_connection",
605+ tableNames.oauthStates,
606+ async () => {
607+ await db
608+ .delete(tables.oauthStatesTable)
609+ .where(eq(tables.oauthStatesTable.connectionId, connectionId));
610+ },
611+ ),
612+ },
613+
614+ storageInstances: {
615+ list: () =>
616+ rowEffect(
617+ backend,
618+ "rows.storage_instances.list",
619+ tableNames.storageInstances,
620+ async () => {
621+ const rows = await db
622+ .select()
623+ .from(tables.storageInstancesTable)
624+ .orderBy(asc(tables.storageInstancesTable.updatedAt), asc(tables.storageInstancesTable.id));
625+
626+ return asDomainArray<StorageInstance>(rows);
627+ },
628+ ),
629+
630+ getById: (storageInstanceId: StorageInstance["id"]) =>
631+ rowEffect(
632+ backend,
633+ "rows.storage_instances.get_by_id",
634+ tableNames.storageInstances,
635+ async () => {
636+ const row = await db
637+ .select()
638+ .from(tables.storageInstancesTable)
639+ .where(eq(tables.storageInstancesTable.id, storageInstanceId))
640+ .limit(1);
641+
642+ return row[0]
643+ ? Option.some(asDomain<StorageInstance>(row[0]))
644+ : Option.none<StorageInstance>();
645+ },
646+ ),
647+
648+ listByWorkspaceScope: (
649+ workspaceId: Workspace["id"],
650+ organizationId: Organization["id"],
651+ ) =>
652+ rowEffect(
653+ backend,
654+ "rows.storage_instances.list_by_workspace_scope",
655+ tableNames.storageInstances,
656+ async () => {
657+ const rows = await db
658+ .select()
659+ .from(tables.storageInstancesTable)
660+ .where(
661+ or(
662+ eq(tables.storageInstancesTable.workspaceId, workspaceId),
663+ and(
664+ isNull(tables.storageInstancesTable.workspaceId),
665+ eq(tables.storageInstancesTable.organizationId, organizationId),
666+ ),
667+ ),
668+ )
669+ .orderBy(
670+ desc(tables.storageInstancesTable.updatedAt),
671+ asc(tables.storageInstancesTable.id),
672+ );
673+
674+ return asDomainArray<StorageInstance>(rows);
675+ },
676+ ),
677+
678+ upsert: (storageInstance: StorageInstance) =>
679+ rowEffect(
680+ backend,
681+ "rows.storage_instances.upsert",
682+ tableNames.storageInstances,
683+ async () => {
684+ await db
685+ .insert(tables.storageInstancesTable)
686+ .values(storageInstance)
687+ .onConflictDoUpdate({
688+ target: tables.storageInstancesTable.id,
689+ set: storageInstance,
690+ });
691+ },
692+ ),
693+
694+ removeById: (storageInstanceId: StorageInstance["id"]) =>
695+ rowEffect(
696+ backend,
697+ "rows.storage_instances.remove",
698+ tableNames.storageInstances,
699+ async () => {
700+ const row = await db
701+ .select({ id: tables.storageInstancesTable.id })
702+ .from(tables.storageInstancesTable)
703+ .where(eq(tables.storageInstancesTable.id, storageInstanceId))
704+ .limit(1);
705+
706+ if (row.length === 0) {
707+ return false;
708+ }
709+
710+ await db
711+ .delete(tables.storageInstancesTable)
712+ .where(eq(tables.storageInstancesTable.id, storageInstanceId));
713+
714+ return true;
715+ },
716+ ),
717+ },
718+
719+ policies: {
720+ list: () =>
721+ rowEffect(backend, "rows.policies.list", tableNames.policies, async () => {
722+ const rows = await db
723+ .select()
724+ .from(tables.policiesTable)
725+ .orderBy(asc(tables.policiesTable.updatedAt), asc(tables.policiesTable.id));
726+
727+ return asDomainArray<Policy>(rows);
728+ }),
729+
730+ listByWorkspaceId: (workspaceId: Policy["workspaceId"]) =>
731+ rowEffect(
732+ backend,
733+ "rows.policies.list_by_workspace",
734+ tableNames.policies,
735+ async () => {
736+ const rows = await db
737+ .select()
738+ .from(tables.policiesTable)
739+ .where(eq(tables.policiesTable.workspaceId, workspaceId))
740+ .orderBy(asc(tables.policiesTable.updatedAt), asc(tables.policiesTable.id));
741+
742+ return asDomainArray<Policy>(rows);
743+ },
744+ ),
745+
746+ getById: (policyId: Policy["id"]) =>
747+ rowEffect(backend, "rows.policies.get_by_id", tableNames.policies, async () => {
748+ const row = await db
749+ .select()
750+ .from(tables.policiesTable)
751+ .where(eq(tables.policiesTable.id, policyId))
752+ .limit(1);
753+
754+ return row[0] ? Option.some(asDomain<Policy>(row[0])) : Option.none<Policy>();
755+ }),
756+
757+ upsert: (policy: Policy) =>
758+ rowEffect(backend, "rows.policies.upsert", tableNames.policies, async () => {
759+ await db
760+ .insert(tables.policiesTable)
761+ .values(policy)
762+ .onConflictDoUpdate({
763+ target: tables.policiesTable.id,
764+ set: policy,
765+ });
766+ }),
767+
768+ removeById: (policyId: Policy["id"]) =>
769+ rowEffect(backend, "rows.policies.remove", tableNames.policies, async () => {
770+ const row = await db
771+ .select({ id: tables.policiesTable.id })
772+ .from(tables.policiesTable)
773+ .where(eq(tables.policiesTable.id, policyId))
774+ .limit(1);
775+
776+ if (row.length === 0) {
777+ return false;
778+ }
779+
780+ await db.delete(tables.policiesTable).where(eq(tables.policiesTable.id, policyId));
781+ return true;
782+ }),
783+ },
784+
785+ approvals: {
786+ list: () =>
787+ rowEffect(backend, "rows.approvals.list", tableNames.approvals, async () => {
788+ const rows = await db
789+ .select()
790+ .from(tables.approvalsTable)
791+ .orderBy(desc(tables.approvalsTable.requestedAt), desc(tables.approvalsTable.id));
792+
793+ return asDomainArray<Approval>(rows);
794+ }),
795+
796+ listByWorkspaceId: (workspaceId: Approval["workspaceId"]) =>
797+ rowEffect(
798+ backend,
799+ "rows.approvals.list_by_workspace",
800+ tableNames.approvals,
801+ async () => {
802+ const rows = await db
803+ .select()
804+ .from(tables.approvalsTable)
805+ .where(eq(tables.approvalsTable.workspaceId, workspaceId))
806+ .orderBy(desc(tables.approvalsTable.requestedAt), desc(tables.approvalsTable.id));
807+
808+ return asDomainArray<Approval>(rows);
809+ },
810+ ),
811+
812+ getById: (approvalId: Approval["id"]) =>
813+ rowEffect(backend, "rows.approvals.get_by_id", tableNames.approvals, async () => {
814+ const row = await db
815+ .select()
816+ .from(tables.approvalsTable)
817+ .where(eq(tables.approvalsTable.id, approvalId))
818+ .limit(1);
819+
820+ return row[0]
821+ ? Option.some(asDomain<Approval>(row[0]))
822+ : Option.none<Approval>();
823+ }),
824+
825+ findByRunAndCall: (
826+ workspaceId: Approval["workspaceId"],
827+ taskRunId: Approval["taskRunId"],
828+ callId: Approval["callId"],
829+ ) =>
830+ rowEffect(
831+ backend,
832+ "rows.approvals.find_by_run_and_call",
833+ tableNames.approvals,
834+ async () => {
835+ const row = await db
836+ .select()
837+ .from(tables.approvalsTable)
838+ .where(
839+ and(
840+ eq(tables.approvalsTable.workspaceId, workspaceId),
841+ eq(tables.approvalsTable.taskRunId, taskRunId),
842+ eq(tables.approvalsTable.callId, callId),
843+ ),
844+ )
845+ .orderBy(desc(tables.approvalsTable.requestedAt))
846+ .limit(1);
847+
848+ return row[0]
849+ ? Option.some(asDomain<Approval>(row[0]))
850+ : Option.none<Approval>();
851+ },
852+ ),
853+
854+ upsert: (approval: Approval) =>
855+ rowEffect(backend, "rows.approvals.upsert", tableNames.approvals, async () => {
856+ await db
857+ .insert(tables.approvalsTable)
858+ .values(approval)
859+ .onConflictDoUpdate({
860+ target: tables.approvalsTable.id,
861+ set: approval,
862+ });
863+ }),
864+ },
865+});
packages/persistence-sql/src/index.ts+18−184
@@ -1,32 +1,15 @@
11 import {
2− type RowStoreError,
32 type SourceStore,
43 type ToolArtifactStore,
54 } from "@executor-v2/persistence-ports";
6−import {
7− type Approval,
8− type AuthConnection,
9− type AuthMaterial,
10− type OAuthState,
11− type Organization,
12− type OrganizationMembership,
13− type Policy,
14− type Profile,
15− type SourceAuthBinding,
16− type StorageInstance,
17− type Workspace,
18−} from "@executor-v2/schema";
195 import * as Data from "effect/Data";
206 import * as Effect from "effect/Effect";
21−import * as Option from "effect/Option";
227 import * as path from "node:path";
238
24−import { createRowOperations } from "./row-operations";
25−import { createRowsEffectApi } from "./rows-effect-api";
9+import { createControlPlaneRows } from "./control-plane-rows";
2610 import {
11+ createSqlRuntime,
2712 createDrizzleContext,
28− createPGliteAdapter,
29− createPostgresAdapter,
3013 runMigrations,
3114 type SqlBackend,
3215 } from "./sql-internals";
@@ -38,76 +21,13 @@ export type SqlControlPlanePersistenceOptions = { · 5 unmodified lines
3821 postgresApplicationName?: string;
3922 };
4023
24+export type SqlControlPlaneRows = ReturnType<typeof createControlPlaneRows>;
25+
4126 export type SqlControlPlanePersistence = {
4227 backend: SqlBackend;
4328 sourceStore: SourceStore;
4429 toolArtifactStore: ToolArtifactStore;
45− rows: {
46− profile: {
47− get: () => Effect.Effect<Option.Option<Profile>, RowStoreError>;
48− upsert: (profile: Profile) => Effect.Effect<void, RowStoreError>;
49− };
50− organizations: {
51− list: () => Effect.Effect<ReadonlyArray<Organization>, RowStoreError>;
52− upsert: (organization: Organization) => Effect.Effect<void, RowStoreError>;
53− };
54− organizationMemberships: {
55− list: () => Effect.Effect<ReadonlyArray<OrganizationMembership>, RowStoreError>;
56− upsert: (
57− membership: OrganizationMembership,
58− ) => Effect.Effect<void, RowStoreError>;
59− };
60− workspaces: {
61− list: () => Effect.Effect<ReadonlyArray<Workspace>, RowStoreError>;
62− upsert: (workspace: Workspace) => Effect.Effect<void, RowStoreError>;
63− };
64− authConnections: {
65− list: () => Effect.Effect<ReadonlyArray<AuthConnection>, RowStoreError>;
66− upsert: (connection: AuthConnection) => Effect.Effect<void, RowStoreError>;
67− removeById: (
68− connectionId: AuthConnection["id"],
69− ) => Effect.Effect<boolean, RowStoreError>;
70− };
71− sourceAuthBindings: {
72− list: () => Effect.Effect<ReadonlyArray<SourceAuthBinding>, RowStoreError>;
73− upsert: (binding: SourceAuthBinding) => Effect.Effect<void, RowStoreError>;
74− removeById: (
75− bindingId: SourceAuthBinding["id"],
76− ) => Effect.Effect<boolean, RowStoreError>;
77− };
78− authMaterials: {
79− list: () => Effect.Effect<ReadonlyArray<AuthMaterial>, RowStoreError>;
80− upsert: (material: AuthMaterial) => Effect.Effect<void, RowStoreError>;
81− removeByConnectionId: (
82− connectionId: AuthMaterial["connectionId"],
83− ) => Effect.Effect<void, RowStoreError>;
84− };
85− oauthStates: {
86− list: () => Effect.Effect<ReadonlyArray<OAuthState>, RowStoreError>;
87− upsert: (state: OAuthState) => Effect.Effect<void, RowStoreError>;
88− removeByConnectionId: (
89− connectionId: OAuthState["connectionId"],
90− ) => Effect.Effect<void, RowStoreError>;
91− };
92− storageInstances: {
93− list: () => Effect.Effect<ReadonlyArray<StorageInstance>, RowStoreError>;
94− upsert: (instance: StorageInstance) => Effect.Effect<void, RowStoreError>;
95− removeById: (
96− storageInstanceId: StorageInstance["id"],
97− ) => Effect.Effect<boolean, RowStoreError>;
98− };
99− policies: {
100− list: () => Effect.Effect<ReadonlyArray<Policy>, RowStoreError>;
101− upsert: (policy: Policy) => Effect.Effect<void, RowStoreError>;
102− removeById: (
103− policyId: Policy["id"],
104− ) => Effect.Effect<boolean, RowStoreError>;
105− };
106− approvals: {
107− list: () => Effect.Effect<ReadonlyArray<Approval>, RowStoreError>;
108− upsert: (approval: Approval) => Effect.Effect<void, RowStoreError>;
109− };
110− };
30+ rows: SqlControlPlaneRows;
11131 close: () => Promise<void>;
11232 };
11333
@@ -123,129 +43,43 @@ const trim = (value: string | undefined): string | undefined => { · 9 unmodified lines
12343 return candidate && candidate.length > 0 ? candidate : undefined;
12444 };
12545
126−const withWriteLock = <A>(
127− queueRef: { current: Promise<void> },
128− run: () => Promise<A>,
129−): Promise<A> => {
130− const next = queueRef.current.then(run, run);
131− queueRef.current = next.then(
132− () => undefined,
133− () => undefined,
134− );
135− return next;
136−};
137−
13846 export const makeSqlControlPlanePersistence = (
13947 options: SqlControlPlanePersistenceOptions,
14048 ): Effect.Effect<SqlControlPlanePersistence, SqlPersistenceBootstrapError> =>
14149 Effect.tryPromise({
14250 try: async () => {
143− const databaseUrl = trim(options.databaseUrl);
14451 const localDataDir = path.resolve(
14552 options.localDataDir ?? ".executor-v2/control-plane-pgdata",
14653 );
147− const backend: SqlBackend =
148− databaseUrl && (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://"))
149− ? "postgres"
150− : "pglite";
54+ const runtime = await createSqlRuntime({
55+ databaseUrl: trim(options.databaseUrl),
56+ localDataDir,
57+ postgresApplicationName: trim(options.postgresApplicationName),
58+ });
15159
152− const adapter =
153− backend === "postgres"
154− ? await createPostgresAdapter(databaseUrl!, trim(options.postgresApplicationName))
155− : await createPGliteAdapter(localDataDir);
60+ await runMigrations(runtime);
15661
157− await runMigrations(adapter);
158− const drizzleContext = createDrizzleContext(adapter);
62+ const drizzleContext = createDrizzleContext(runtime.db);
15963 const { db, tables } = drizzleContext;
16064
161− const writeQueueRef = {
162− current: Promise.resolve<void>(undefined),
163− };
164−
165− const {
166− listOrganizationsRows,
167− upsertOrganizationRow,
168− listOrganizationMembershipRows,
169− upsertOrganizationMembershipRow,
170− listWorkspaceRows,
171− upsertWorkspaceRow,
172− listAuthConnectionRows,
173− upsertAuthConnectionRow,
174− removeAuthConnectionRowById,
175− listSourceAuthBindingRows,
176− upsertSourceAuthBindingRow,
177− removeSourceAuthBindingRowById,
178− listAuthMaterialRows,
179− upsertAuthMaterialRow,
180− removeAuthMaterialRowsByConnectionId,
181− listOAuthStateRows,
182− upsertOAuthStateRow,
183− removeOAuthStateRowsByConnectionId,
184− listStorageInstanceRows,
185− upsertStorageInstanceRow,
186− removeStorageInstanceRowById,
187− listPolicyRows,
188− upsertPolicyRow,
189− removePolicyRowById,
190− listApprovalRows,
191− upsertApprovalRow,
192− getProfileRow,
193− upsertProfileRow,
194− } = createRowOperations({
195− adapter,
65+ const { sourceStore, toolArtifactStore } = createSourceAndArtifactStores({
66+ backend: runtime.backend,
19667 db,
19768 tables,
198− writeLocked: <A>(run: () => Promise<A>) => withWriteLock(writeQueueRef, run),
19969 });
20070
201− const { sourceStore, toolArtifactStore } = createSourceAndArtifactStores({
202− backend,
203− adapter,
71+ const rows = createControlPlaneRows({
72+ backend: runtime.backend,
20473 db,
20574 tables,
206− writeLocked: <A>(run: () => Promise<A>) => withWriteLock(writeQueueRef, run),
20775 });
20876
209− const rows: SqlControlPlanePersistence["rows"] = createRowsEffectApi(
210− backend,
211− {
212− listOrganizationsRows,
213− upsertOrganizationRow,
214− listOrganizationMembershipRows,
215− upsertOrganizationMembershipRow,
216− listWorkspaceRows,
217− upsertWorkspaceRow,
218− listAuthConnectionRows,
219− upsertAuthConnectionRow,
220− removeAuthConnectionRowById,
221− listSourceAuthBindingRows,
222− upsertSourceAuthBindingRow,
223− removeSourceAuthBindingRowById,
224− listAuthMaterialRows,
225− upsertAuthMaterialRow,
226− removeAuthMaterialRowsByConnectionId,
227− listOAuthStateRows,
228− upsertOAuthStateRow,
229− removeOAuthStateRowsByConnectionId,
230− listStorageInstanceRows,
231− upsertStorageInstanceRow,
232− removeStorageInstanceRowById,
233− listPolicyRows,
234− upsertPolicyRow,
235− removePolicyRowById,
236− listApprovalRows,
237− upsertApprovalRow,
238− getProfileRow,
239− upsertProfileRow,
240− },
241− );
242−
24377 return {
244− backend,
78+ backend: runtime.backend,
24579 sourceStore,
24680 toolArtifactStore,
24781 rows,
248− close: () => adapter.close(),
82+ close: () => runtime.close(),
24983 };
25084 },
25185 catch: (cause) => {
packages/persistence-sql/src/row-operations-auth.ts+0−404
@@ -1,404 +0,0 @@
1−import {
2− type AuthConnection,
3− type AuthMaterial,
4− type OAuthState,
5− type SourceAuthBinding,
6−} from "@executor-v2/schema";
7−import { asc, eq } from "drizzle-orm";
8−
9−import {
10− createDrizzleContext,
11− type DrizzleDb,
12− type DrizzleTables,
13− type SqlAdapter,
14−} from "./sql-internals";
15−
16−type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>;
17−
18−type AuthOperationsInput = {
19− adapter: SqlAdapter;
20− db: DrizzleDb;
21− tables: DrizzleTables;
22− writeLocked: WriteLocked;
23−};
24−
25−const toAuthConnection = (
26− row: DrizzleTables["authConnectionsTable"]["$inferSelect"],
27−): AuthConnection => ({
28− id: row.id as AuthConnection["id"],
29− organizationId: row.organizationId as AuthConnection["organizationId"],
30− workspaceId: row.workspaceId as AuthConnection["workspaceId"],
31− accountId: row.accountId as AuthConnection["accountId"],
32− ownerType: row.ownerType as AuthConnection["ownerType"],
33− strategy: row.strategy as AuthConnection["strategy"],
34− displayName: row.displayName,
35− status: row.status as AuthConnection["status"],
36− statusReason: row.statusReason,
37− lastAuthErrorClass: row.lastAuthErrorClass,
38− metadataJson: row.metadataJson,
39− additionalHeadersJson: row.additionalHeadersJson,
40− createdByAccountId: row.createdByAccountId as AuthConnection["createdByAccountId"],
41− createdAt: row.createdAt,
42− updatedAt: row.updatedAt,
43− lastUsedAt: row.lastUsedAt,
44−});
45−
46−const toSourceAuthBinding = (
47− row: DrizzleTables["sourceAuthBindingsTable"]["$inferSelect"],
48−): SourceAuthBinding => ({
49− id: row.id as SourceAuthBinding["id"],
50− sourceId: row.sourceId as SourceAuthBinding["sourceId"],
51− connectionId: row.connectionId as SourceAuthBinding["connectionId"],
52− organizationId: row.organizationId as SourceAuthBinding["organizationId"],
53− workspaceId: row.workspaceId as SourceAuthBinding["workspaceId"],
54− accountId: row.accountId as SourceAuthBinding["accountId"],
55− scopeType: row.scopeType as SourceAuthBinding["scopeType"],
56− selector: row.selector,
57− enabled: row.enabled,
58− createdAt: row.createdAt,
59− updatedAt: row.updatedAt,
60−});
61−
62−const toAuthMaterial = (
63− row: DrizzleTables["authMaterialsTable"]["$inferSelect"],
64−): AuthMaterial => ({
65− id: row.id as AuthMaterial["id"],
66− connectionId: row.connectionId as AuthMaterial["connectionId"],
67− ciphertext: row.ciphertext,
68− keyVersion: row.keyVersion,
69− createdAt: row.createdAt,
70− updatedAt: row.updatedAt,
71−});
72−
73−const toOAuthState = (
74− row: DrizzleTables["oauthStatesTable"]["$inferSelect"],
75−): OAuthState => ({
76− id: row.id as OAuthState["id"],
77− connectionId: row.connectionId as OAuthState["connectionId"],
78− accessTokenCiphertext: row.accessTokenCiphertext,
79− refreshTokenCiphertext: row.refreshTokenCiphertext,
80− keyVersion: row.keyVersion,
81− expiresAt: row.expiresAt,
82− scope: row.scope,
83− tokenType: row.tokenType,
84− issuer: row.issuer,
85− refreshConfigJson: row.refreshConfigJson,
86− tokenVersion: row.tokenVersion,
87− leaseHolder: row.leaseHolder,
88− leaseExpiresAt: row.leaseExpiresAt,
89− leaseFence: row.leaseFence,
90− lastRefreshAt: row.lastRefreshAt,
91− lastRefreshErrorClass: row.lastRefreshErrorClass,
92− lastRefreshError: row.lastRefreshError,
93− reauthRequiredAt: row.reauthRequiredAt,
94− createdAt: row.createdAt,
95− updatedAt: row.updatedAt,
96−});
97−
98−export const createAuthRowOperations = ({
99− adapter,
100− db,
101− tables,
102− writeLocked,
103−}: AuthOperationsInput) => {
104− const listAuthConnectionRows = async (): Promise<Array<AuthConnection>> => {
105− const rows = await db.select().from(tables.authConnectionsTable).orderBy(
106− asc(tables.authConnectionsTable.updatedAt),
107− asc(tables.authConnectionsTable.id),
108− );
109−
110− return rows.map(toAuthConnection);
111− };
112−
113− const upsertAuthConnectionRow = async (connection: AuthConnection): Promise<void> => {
114− await writeLocked(async () => {
115− await adapter.transaction(async (transaction) => {
116− const transactionContext = createDrizzleContext(transaction);
117−
118− await transactionContext.db
119− .insert(transactionContext.tables.authConnectionsTable)
120− .values({
121− id: connection.id,
122− organizationId: connection.organizationId,
123− workspaceId: connection.workspaceId,
124− accountId: connection.accountId,
125− ownerType: connection.ownerType,
126− strategy: connection.strategy,
127− displayName: connection.displayName,
128− status: connection.status,
129− statusReason: connection.statusReason,
130− lastAuthErrorClass: connection.lastAuthErrorClass,
131− metadataJson: connection.metadataJson,
132− additionalHeadersJson: connection.additionalHeadersJson,
133− createdByAccountId: connection.createdByAccountId,
134− createdAt: connection.createdAt,
135− updatedAt: connection.updatedAt,
136− lastUsedAt: connection.lastUsedAt,
137− })
138− .onConflictDoUpdate({
139− target: transactionContext.tables.authConnectionsTable.id,
140− set: {
141− organizationId: connection.organizationId,
142− workspaceId: connection.workspaceId,
143− accountId: connection.accountId,
144− ownerType: connection.ownerType,
145− strategy: connection.strategy,
146− displayName: connection.displayName,
147− status: connection.status,
148− statusReason: connection.statusReason,
149− lastAuthErrorClass: connection.lastAuthErrorClass,
150− metadataJson: connection.metadataJson,
151− additionalHeadersJson: connection.additionalHeadersJson,
152− createdByAccountId: connection.createdByAccountId,
153− createdAt: connection.createdAt,
154− updatedAt: connection.updatedAt,
155− lastUsedAt: connection.lastUsedAt,
156− },
157− });
158− });
159− });
160− };
161−
162− const removeAuthConnectionRowById = async (
163− connectionId: AuthConnection["id"],
164− ): Promise<boolean> =>
165− writeLocked(async () =>
166− adapter.transaction(async (transaction) => {
167− const transactionContext = createDrizzleContext(transaction);
168− const existing = await transactionContext.db
169− .select({ id: transactionContext.tables.authConnectionsTable.id })
170− .from(transactionContext.tables.authConnectionsTable)
171− .where(eq(transactionContext.tables.authConnectionsTable.id, connectionId))
172− .limit(1);
173−
174− if (existing.length === 0) {
175− return false;
176− }
177−
178− await transactionContext.db
179− .delete(transactionContext.tables.authConnectionsTable)
180− .where(eq(transactionContext.tables.authConnectionsTable.id, connectionId));
181−
182− return true;
183− })
184− );
185−
186− const listSourceAuthBindingRows = async (): Promise<Array<SourceAuthBinding>> => {
187− const rows = await db.select().from(tables.sourceAuthBindingsTable).orderBy(
188− asc(tables.sourceAuthBindingsTable.updatedAt),
189− asc(tables.sourceAuthBindingsTable.id),
190− );
191−
192− return rows.map(toSourceAuthBinding);
193− };
194−
195− const upsertSourceAuthBindingRow = async (
196− binding: SourceAuthBinding,
197− ): Promise<void> => {
198− await writeLocked(async () => {
199− await adapter.transaction(async (transaction) => {
200− const transactionContext = createDrizzleContext(transaction);
201−
202− await transactionContext.db
203− .insert(transactionContext.tables.sourceAuthBindingsTable)
204− .values({
205− id: binding.id,
206− sourceId: binding.sourceId,
207− connectionId: binding.connectionId,
208− organizationId: binding.organizationId,
209− workspaceId: binding.workspaceId,
210− accountId: binding.accountId,
211− scopeType: binding.scopeType,
212− selector: binding.selector,
213− enabled: binding.enabled,
214− createdAt: binding.createdAt,
215− updatedAt: binding.updatedAt,
216− })
217− .onConflictDoUpdate({
218− target: transactionContext.tables.sourceAuthBindingsTable.id,
219− set: {
220− sourceId: binding.sourceId,
221− connectionId: binding.connectionId,
222− organizationId: binding.organizationId,
223− workspaceId: binding.workspaceId,
224− accountId: binding.accountId,
225− scopeType: binding.scopeType,
226− selector: binding.selector,
227− enabled: binding.enabled,
228− createdAt: binding.createdAt,
229− updatedAt: binding.updatedAt,
230− },
231− });
232− });
233− });
234− };
235−
236− const removeSourceAuthBindingRowById = async (
237− bindingId: SourceAuthBinding["id"],
238− ): Promise<boolean> =>
239− writeLocked(async () =>
240− adapter.transaction(async (transaction) => {
241− const transactionContext = createDrizzleContext(transaction);
242− const existing = await transactionContext.db
243− .select({ id: transactionContext.tables.sourceAuthBindingsTable.id })
244− .from(transactionContext.tables.sourceAuthBindingsTable)
245− .where(eq(transactionContext.tables.sourceAuthBindingsTable.id, bindingId))
246− .limit(1);
247−
248− if (existing.length === 0) {
249− return false;
250− }
251−
252− await transactionContext.db
253− .delete(transactionContext.tables.sourceAuthBindingsTable)
254− .where(eq(transactionContext.tables.sourceAuthBindingsTable.id, bindingId));
255−
256− return true;
257− })
258− );
259−
260− const listAuthMaterialRows = async (): Promise<Array<AuthMaterial>> => {
261− const rows = await db.select().from(tables.authMaterialsTable).orderBy(
262− asc(tables.authMaterialsTable.updatedAt),
263− asc(tables.authMaterialsTable.id),
264− );
265−
266− return rows.map(toAuthMaterial);
267− };
268−
269− const upsertAuthMaterialRow = async (material: AuthMaterial): Promise<void> => {
270− await writeLocked(async () => {
271− await adapter.transaction(async (transaction) => {
272− const transactionContext = createDrizzleContext(transaction);
273−
274− await transactionContext.db
275− .insert(transactionContext.tables.authMaterialsTable)
276− .values({
277− id: material.id,
278− connectionId: material.connectionId,
279− ciphertext: material.ciphertext,
280− keyVersion: material.keyVersion,
281− createdAt: material.createdAt,
282− updatedAt: material.updatedAt,
283− })
284− .onConflictDoUpdate({
285− target: transactionContext.tables.authMaterialsTable.id,
286− set: {
287− connectionId: material.connectionId,
288− ciphertext: material.ciphertext,
289− keyVersion: material.keyVersion,
290− createdAt: material.createdAt,
291− updatedAt: material.updatedAt,
292− },
293− });
294− });
295− });
296− };
297−
298− const removeAuthMaterialRowsByConnectionId = async (
299− connectionId: AuthMaterial["connectionId"],
300− ): Promise<void> => {
301− await writeLocked(async () => {
302− await adapter.transaction(async (transaction) => {
303− const transactionContext = createDrizzleContext(transaction);
304− await transactionContext.db
305− .delete(transactionContext.tables.authMaterialsTable)
306− .where(eq(transactionContext.tables.authMaterialsTable.connectionId, connectionId));
307− });
308− });
309− };
310−
311− const listOAuthStateRows = async (): Promise<Array<OAuthState>> => {
312− const rows = await db.select().from(tables.oauthStatesTable).orderBy(
313− asc(tables.oauthStatesTable.updatedAt),
314− asc(tables.oauthStatesTable.id),
315− );
316−
317− return rows.map(toOAuthState);
318− };
319−
320− const upsertOAuthStateRow = async (state: OAuthState): Promise<void> => {
321− await writeLocked(async () => {
322− await adapter.transaction(async (transaction) => {
323− const transactionContext = createDrizzleContext(transaction);
324−
325− await transactionContext.db
326− .insert(transactionContext.tables.oauthStatesTable)
327− .values({
328− id: state.id,
329− connectionId: state.connectionId,
330− accessTokenCiphertext: state.accessTokenCiphertext,
331− refreshTokenCiphertext: state.refreshTokenCiphertext,
332− keyVersion: state.keyVersion,
333− expiresAt: state.expiresAt,
334− scope: state.scope,
335− tokenType: state.tokenType,
336− issuer: state.issuer,
337− refreshConfigJson: state.refreshConfigJson,
338− tokenVersion: state.tokenVersion,
339− leaseHolder: state.leaseHolder,
340− leaseExpiresAt: state.leaseExpiresAt,
341− leaseFence: state.leaseFence,
342− lastRefreshAt: state.lastRefreshAt,
343− lastRefreshErrorClass: state.lastRefreshErrorClass,
344− lastRefreshError: state.lastRefreshError,
345− reauthRequiredAt: state.reauthRequiredAt,
346− createdAt: state.createdAt,
347− updatedAt: state.updatedAt,
348− })
349− .onConflictDoUpdate({
350− target: transactionContext.tables.oauthStatesTable.id,
351− set: {
352− connectionId: state.connectionId,
353− accessTokenCiphertext: state.accessTokenCiphertext,
354− refreshTokenCiphertext: state.refreshTokenCiphertext,
355− keyVersion: state.keyVersion,
356− expiresAt: state.expiresAt,
357− scope: state.scope,
358− tokenType: state.tokenType,
359− issuer: state.issuer,
360− refreshConfigJson: state.refreshConfigJson,
361− tokenVersion: state.tokenVersion,
362− leaseHolder: state.leaseHolder,
363− leaseExpiresAt: state.leaseExpiresAt,
364− leaseFence: state.leaseFence,
365− lastRefreshAt: state.lastRefreshAt,
366− lastRefreshErrorClass: state.lastRefreshErrorClass,
367− lastRefreshError: state.lastRefreshError,
368− reauthRequiredAt: state.reauthRequiredAt,
369− createdAt: state.createdAt,
370− updatedAt: state.updatedAt,
371− },
372− });
373− });
374− });
375− };
376−
377− const removeOAuthStateRowsByConnectionId = async (
378− connectionId: OAuthState["connectionId"],
379− ): Promise<void> => {
380− await writeLocked(async () => {
381− await adapter.transaction(async (transaction) => {
382− const transactionContext = createDrizzleContext(transaction);
383− await transactionContext.db
384− .delete(transactionContext.tables.oauthStatesTable)
385− .where(eq(transactionContext.tables.oauthStatesTable.connectionId, connectionId));
386− });
387− });
388− };
389−
390− return {
391− listAuthConnectionRows,
392− upsertAuthConnectionRow,
393− removeAuthConnectionRowById,
394− listSourceAuthBindingRows,
395− upsertSourceAuthBindingRow,
396− removeSourceAuthBindingRowById,
397− listAuthMaterialRows,
398− upsertAuthMaterialRow,
399− removeAuthMaterialRowsByConnectionId,
400− listOAuthStateRows,
401− upsertOAuthStateRow,
402− removeOAuthStateRowsByConnectionId,
403− };
404−};
packages/persistence-sql/src/row-operations-core.ts+0−515
@@ -1,515 +0,0 @@
1−import {
2− type Approval,
3− type Organization,
4− type OrganizationMembership,
5− type Policy,
6− type Profile,
7− type StorageInstance,
8− type Workspace,
9−} from "@executor-v2/schema";
10−import { asc, desc, eq } from "drizzle-orm";
11−
12−import {
13− createDrizzleContext,
14− type DrizzleDb,
15− type DrizzleTables,
16− type SqlAdapter,
17−} from "./sql-internals";
18−
19−type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>;
20−
21−type CoreOperationsInput = {
22− adapter: SqlAdapter;
23− db: DrizzleDb;
24− tables: DrizzleTables;
25− writeLocked: WriteLocked;
26−};
27−
28−const toOrganization = (
29− row: DrizzleTables["organizationsTable"]["$inferSelect"],
30−): Organization => ({
31− id: row.id as Organization["id"],
32− slug: row.slug,
33− name: row.name,
34− status: row.status as Organization["status"],
35− createdByAccountId: row.createdByAccountId as Organization["createdByAccountId"],
36− createdAt: row.createdAt,
37− updatedAt: row.updatedAt,
38−});
39−
40−const toOrganizationMembership = (
41− row: DrizzleTables["organizationMembershipsTable"]["$inferSelect"],
42−): OrganizationMembership => ({
43− id: row.id as OrganizationMembership["id"],
44− organizationId: row.organizationId as OrganizationMembership["organizationId"],
45− accountId: row.accountId as OrganizationMembership["accountId"],
46− role: row.role as OrganizationMembership["role"],
47− status: row.status as OrganizationMembership["status"],
48− billable: row.billable,
49− invitedByAccountId:
50− row.invitedByAccountId as OrganizationMembership["invitedByAccountId"],
51− joinedAt: row.joinedAt,
52− createdAt: row.createdAt,
53− updatedAt: row.updatedAt,
54−});
55−
56−const toWorkspace = (
57− row: DrizzleTables["workspacesTable"]["$inferSelect"],
58−): Workspace => ({
59− id: row.id as Workspace["id"],
60− organizationId: row.organizationId as Workspace["organizationId"],
61− name: row.name,
62− createdByAccountId: row.createdByAccountId as Workspace["createdByAccountId"],
63− createdAt: row.createdAt,
64− updatedAt: row.updatedAt,
65−});
66−
67−const toStorageInstance = (
68− row: DrizzleTables["storageInstancesTable"]["$inferSelect"],
69−): StorageInstance => ({
70− id: row.id as StorageInstance["id"],
71− scopeType: row.scopeType as StorageInstance["scopeType"],
72− durability: row.durability as StorageInstance["durability"],
73− status: row.status as StorageInstance["status"],
74− provider: row.provider as StorageInstance["provider"],
75− backendKey: row.backendKey,
76− organizationId: row.organizationId as StorageInstance["organizationId"],
77− workspaceId: row.workspaceId as StorageInstance["workspaceId"],
78− accountId: row.accountId as StorageInstance["accountId"],
79− createdByAccountId: row.createdByAccountId as StorageInstance["createdByAccountId"],
80− purpose: row.purpose,
81− sizeBytes: row.sizeBytes,
82− fileCount: row.fileCount,
83− createdAt: row.createdAt,
84− updatedAt: row.updatedAt,
85− lastSeenAt: row.lastSeenAt,
86− closedAt: row.closedAt,
87− expiresAt: row.expiresAt,
88−});
89−
90−const toPolicy = (
91− row: DrizzleTables["policiesTable"]["$inferSelect"],
92−): Policy => ({
93− id: row.id as Policy["id"],
94− workspaceId: row.workspaceId as Policy["workspaceId"],
95− toolPathPattern: row.toolPathPattern,
96− decision: row.decision as Policy["decision"],
97− createdAt: row.createdAt,
98− updatedAt: row.updatedAt,
99−});
100−
101−const toApproval = (
102− row: DrizzleTables["approvalsTable"]["$inferSelect"],
103−): Approval => ({
104− id: row.id as Approval["id"],
105− workspaceId: row.workspaceId as Approval["workspaceId"],
106− taskRunId: row.taskRunId as Approval["taskRunId"],
107− callId: row.callId,
108− toolPath: row.toolPath,
109− status: row.status as Approval["status"],
110− inputPreviewJson: row.inputPreviewJson,
111− reason: row.reason,
112− requestedAt: row.requestedAt,
113− resolvedAt: row.resolvedAt,
114−});
115−
116−const toProfile = (row: DrizzleTables["profileTable"]["$inferSelect"]): Profile => ({
117− id: row.id as Profile["id"],
118− defaultWorkspaceId: row.defaultWorkspaceId as Profile["defaultWorkspaceId"],
119− displayName: row.displayName,
120− runtimeMode: row.runtimeMode as Profile["runtimeMode"],
121− createdAt: row.createdAt,
122− updatedAt: row.updatedAt,
123−});
124−
125−export const createCoreRowOperations = ({
126− adapter,
127− db,
128− tables,
129− writeLocked,
130−}: CoreOperationsInput) => {
131− const listOrganizationsRows = async (): Promise<Array<Organization>> => {
132− const rows = await db.select().from(tables.organizationsTable).orderBy(
133− asc(tables.organizationsTable.updatedAt),
134− asc(tables.organizationsTable.id),
135− );
136−
137− return rows.map(toOrganization);
138− };
139−
140− const upsertOrganizationRow = async (organization: Organization): Promise<void> => {
141− await writeLocked(async () => {
142− await adapter.transaction(async (transaction) => {
143− const transactionContext = createDrizzleContext(transaction);
144−
145− await transactionContext.db
146− .insert(transactionContext.tables.organizationsTable)
147− .values({
148− id: organization.id,
149− slug: organization.slug,
150− name: organization.name,
151− status: organization.status,
152− createdByAccountId: organization.createdByAccountId,
153− createdAt: organization.createdAt,
154− updatedAt: organization.updatedAt,
155− })
156− .onConflictDoUpdate({
157− target: transactionContext.tables.organizationsTable.id,
158− set: {
159− slug: organization.slug,
160− name: organization.name,
161− status: organization.status,
162− createdByAccountId: organization.createdByAccountId,
163− createdAt: organization.createdAt,
164− updatedAt: organization.updatedAt,
165− },
166− });
167− });
168− });
169− };
170−
171− const listOrganizationMembershipRows = async (): Promise<
172− Array<OrganizationMembership>
173− > => {
174− const rows = await db.select().from(tables.organizationMembershipsTable).orderBy(
175− asc(tables.organizationMembershipsTable.updatedAt),
176− asc(tables.organizationMembershipsTable.id),
177− );
178−
179− return rows.map(toOrganizationMembership);
180− };
181−
182− const upsertOrganizationMembershipRow = async (
183− membership: OrganizationMembership,
184− ): Promise<void> => {
185− await writeLocked(async () => {
186− await adapter.transaction(async (transaction) => {
187− const transactionContext = createDrizzleContext(transaction);
188−
189− await transactionContext.db
190− .insert(transactionContext.tables.organizationMembershipsTable)
191− .values({
192− id: membership.id,
193− organizationId: membership.organizationId,
194− accountId: membership.accountId,
195− role: membership.role,
196− status: membership.status,
197− billable: membership.billable,
198− invitedByAccountId: membership.invitedByAccountId,
199− joinedAt: membership.joinedAt,
200− createdAt: membership.createdAt,
201− updatedAt: membership.updatedAt,
202− })
203− .onConflictDoUpdate({
204− target: transactionContext.tables.organizationMembershipsTable.id,
205− set: {
206− organizationId: membership.organizationId,
207− accountId: membership.accountId,
208− role: membership.role,
209− status: membership.status,
210− billable: membership.billable,
211− invitedByAccountId: membership.invitedByAccountId,
212− joinedAt: membership.joinedAt,
213− createdAt: membership.createdAt,
214− updatedAt: membership.updatedAt,
215− },
216− });
217− });
218− });
219− };
220−
221− const listWorkspaceRows = async (): Promise<Array<Workspace>> => {
222− const rows = await db.select().from(tables.workspacesTable).orderBy(
223− asc(tables.workspacesTable.updatedAt),
224− asc(tables.workspacesTable.id),
225− );
226−
227− return rows.map(toWorkspace);
228− };
229−
230− const upsertWorkspaceRow = async (workspace: Workspace): Promise<void> => {
231− await writeLocked(async () => {
232− await adapter.transaction(async (transaction) => {
233− const transactionContext = createDrizzleContext(transaction);
234−
235− await transactionContext.db
236− .insert(transactionContext.tables.workspacesTable)
237− .values({
238− id: workspace.id,
239− organizationId: workspace.organizationId,
240− name: workspace.name,
241− createdByAccountId: workspace.createdByAccountId,
242− createdAt: workspace.createdAt,
243− updatedAt: workspace.updatedAt,
244− })
245− .onConflictDoUpdate({
246− target: transactionContext.tables.workspacesTable.id,
247− set: {
248− organizationId: workspace.organizationId,
249− name: workspace.name,
250− createdByAccountId: workspace.createdByAccountId,
251− createdAt: workspace.createdAt,
252− updatedAt: workspace.updatedAt,
253− },
254− });
255− });
256− });
257− };
258−
259− const listStorageInstanceRows = async (): Promise<Array<StorageInstance>> => {
260− const rows = await db.select().from(tables.storageInstancesTable).orderBy(
261− asc(tables.storageInstancesTable.updatedAt),
262− asc(tables.storageInstancesTable.id),
263− );
264−
265− return rows.map(toStorageInstance);
266− };
267−
268− const upsertStorageInstanceRow = async (
269− storageInstance: StorageInstance,
270− ): Promise<void> => {
271− await writeLocked(async () => {
272− await adapter.transaction(async (transaction) => {
273− const transactionContext = createDrizzleContext(transaction);
274−
275− await transactionContext.db
276− .insert(transactionContext.tables.storageInstancesTable)
277− .values({
278− id: storageInstance.id,
279− scopeType: storageInstance.scopeType,
280− durability: storageInstance.durability,
281− status: storageInstance.status,
282− provider: storageInstance.provider,
283− backendKey: storageInstance.backendKey,
284− organizationId: storageInstance.organizationId,
285− workspaceId: storageInstance.workspaceId,
286− accountId: storageInstance.accountId,
287− createdByAccountId: storageInstance.createdByAccountId,
288− purpose: storageInstance.purpose,
289− sizeBytes: storageInstance.sizeBytes,
290− fileCount: storageInstance.fileCount,
291− createdAt: storageInstance.createdAt,
292− updatedAt: storageInstance.updatedAt,
293− lastSeenAt: storageInstance.lastSeenAt,
294− closedAt: storageInstance.closedAt,
295− expiresAt: storageInstance.expiresAt,
296− })
297− .onConflictDoUpdate({
298− target: transactionContext.tables.storageInstancesTable.id,
299− set: {
300− scopeType: storageInstance.scopeType,
301− durability: storageInstance.durability,
302− status: storageInstance.status,
303− provider: storageInstance.provider,
304− backendKey: storageInstance.backendKey,
305− organizationId: storageInstance.organizationId,
306− workspaceId: storageInstance.workspaceId,
307− accountId: storageInstance.accountId,
308− createdByAccountId: storageInstance.createdByAccountId,
309− purpose: storageInstance.purpose,
310− sizeBytes: storageInstance.sizeBytes,
311− fileCount: storageInstance.fileCount,
312− createdAt: storageInstance.createdAt,
313− updatedAt: storageInstance.updatedAt,
314− lastSeenAt: storageInstance.lastSeenAt,
315− closedAt: storageInstance.closedAt,
316− expiresAt: storageInstance.expiresAt,
317− },
318− });
319− });
320− });
321− };
322−
323− const removeStorageInstanceRowById = async (
324− storageInstanceId: StorageInstance["id"],
325− ): Promise<boolean> =>
326− writeLocked(async () =>
327− adapter.transaction(async (transaction) => {
328− const transactionContext = createDrizzleContext(transaction);
329− const existing = await transactionContext.db
330− .select({ id: transactionContext.tables.storageInstancesTable.id })
331− .from(transactionContext.tables.storageInstancesTable)
332− .where(eq(transactionContext.tables.storageInstancesTable.id, storageInstanceId))
333− .limit(1);
334−
335− if (existing.length === 0) {
336− return false;
337− }
338−
339− await transactionContext.db
340− .delete(transactionContext.tables.storageInstancesTable)
341− .where(eq(transactionContext.tables.storageInstancesTable.id, storageInstanceId));
342−
343− return true;
344− })
345− );
346−
347− const listPolicyRows = async (): Promise<Array<Policy>> => {
348− const rows = await db.select().from(tables.policiesTable).orderBy(
349− asc(tables.policiesTable.updatedAt),
350− asc(tables.policiesTable.id),
351− );
352−
353− return rows.map(toPolicy);
354− };
355−
356− const upsertPolicyRow = async (policy: Policy): Promise<void> => {
357− await writeLocked(async () => {
358− await adapter.transaction(async (transaction) => {
359− const transactionContext = createDrizzleContext(transaction);
360−
361− await transactionContext.db
362− .insert(transactionContext.tables.policiesTable)
363− .values({
364− id: policy.id,
365− workspaceId: policy.workspaceId,
366− toolPathPattern: policy.toolPathPattern,
367− decision: policy.decision,
368− createdAt: policy.createdAt,
369− updatedAt: policy.updatedAt,
370− })
371− .onConflictDoUpdate({
372− target: transactionContext.tables.policiesTable.id,
373− set: {
374− workspaceId: policy.workspaceId,
375− toolPathPattern: policy.toolPathPattern,
376− decision: policy.decision,
377− createdAt: policy.createdAt,
378− updatedAt: policy.updatedAt,
379− },
380− });
381− });
382− });
383− };
384−
385− const removePolicyRowById = async (policyId: Policy["id"]): Promise<boolean> =>
386− writeLocked(async () =>
387− adapter.transaction(async (transaction) => {
388− const transactionContext = createDrizzleContext(transaction);
389−
390− const existing = await transactionContext.db
391− .select({ id: transactionContext.tables.policiesTable.id })
392− .from(transactionContext.tables.policiesTable)
393− .where(eq(transactionContext.tables.policiesTable.id, policyId))
394− .limit(1);
395−
396− if (existing.length === 0) {
397− return false;
398− }
399−
400− await transactionContext.db
401− .delete(transactionContext.tables.policiesTable)
402− .where(eq(transactionContext.tables.policiesTable.id, policyId));
403−
404− return true;
405− })
406− );
407−
408− const listApprovalRows = async (): Promise<Array<Approval>> => {
409− const rows = await db.select().from(tables.approvalsTable).orderBy(
410− asc(tables.approvalsTable.requestedAt),
411− asc(tables.approvalsTable.id),
412− );
413−
414− return rows.map(toApproval);
415− };
416−
417− const upsertApprovalRow = async (approval: Approval): Promise<void> => {
418− await writeLocked(async () => {
419− await adapter.transaction(async (transaction) => {
420− const transactionContext = createDrizzleContext(transaction);
421−
422− await transactionContext.db
423− .insert(transactionContext.tables.approvalsTable)
424− .values({
425− id: approval.id,
426− workspaceId: approval.workspaceId,
427− taskRunId: approval.taskRunId,
428− callId: approval.callId,
429− toolPath: approval.toolPath,
430− status: approval.status,
431− inputPreviewJson: approval.inputPreviewJson,
432− reason: approval.reason,
433− requestedAt: approval.requestedAt,
434− resolvedAt: approval.resolvedAt,
435− })
436− .onConflictDoUpdate({
437− target: transactionContext.tables.approvalsTable.id,
438− set: {
439− workspaceId: approval.workspaceId,
440− taskRunId: approval.taskRunId,
441− callId: approval.callId,
442− toolPath: approval.toolPath,
443− status: approval.status,
444− inputPreviewJson: approval.inputPreviewJson,
445− reason: approval.reason,
446− requestedAt: approval.requestedAt,
447− resolvedAt: approval.resolvedAt,
448− },
449− });
450− });
451− });
452− };
453−
454− const getProfileRow = async (): Promise<Profile | null> => {
455− const rows = await db.select().from(tables.profileTable).orderBy(
456− desc(tables.profileTable.updatedAt),
457− asc(tables.profileTable.id),
458− ).limit(1);
459−
460− const row = rows[0];
461− if (!row) {
462− return null;
463− }
464−
465− return toProfile(row);
466− };
467−
468− const upsertProfileRow = async (profile: Profile): Promise<void> => {
469− await writeLocked(async () => {
470− await adapter.transaction(async (transaction) => {
471− const transactionContext = createDrizzleContext(transaction);
472−
473− await transactionContext.db
474− .insert(transactionContext.tables.profileTable)
475− .values({
476− id: profile.id,
477− defaultWorkspaceId: profile.defaultWorkspaceId,
478− displayName: profile.displayName,
479− runtimeMode: profile.runtimeMode,
480− createdAt: profile.createdAt,
481− updatedAt: profile.updatedAt,
482− })
483− .onConflictDoUpdate({
484− target: transactionContext.tables.profileTable.id,
485− set: {
486− defaultWorkspaceId: profile.defaultWorkspaceId,
487− displayName: profile.displayName,
488− runtimeMode: profile.runtimeMode,
489− createdAt: profile.createdAt,
490− updatedAt: profile.updatedAt,
491− },
492− });
493− });
494− });
495− };
496−
497− return {
498− listOrganizationsRows,
499− upsertOrganizationRow,
500− listOrganizationMembershipRows,
501− upsertOrganizationMembershipRow,
502− listWorkspaceRows,
503− upsertWorkspaceRow,
504− listStorageInstanceRows,
505− upsertStorageInstanceRow,
506− removeStorageInstanceRowById,
507− listPolicyRows,
508− upsertPolicyRow,
509− removePolicyRowById,
510− listApprovalRows,
511− upsertApprovalRow,
512− getProfileRow,
513− upsertProfileRow,
514− };
515−};
packages/persistence-sql/src/row-operations.ts+0−21
@@ -1,21 +0,0 @@
1−import { createAuthRowOperations } from "./row-operations-auth";
2−import { createCoreRowOperations } from "./row-operations-core";
3−import {
4− type DrizzleDb,
5− type DrizzleTables,
6− type SqlAdapter,
7−} from "./sql-internals";
8−
9−type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>;
10−
11−type CreateRowOperationsInput = {
12− adapter: SqlAdapter;
13− db: DrizzleDb;
14− tables: DrizzleTables;
15− writeLocked: WriteLocked;
16−};
17−
18−export const createRowOperations = (input: CreateRowOperationsInput) => ({
19− ...createCoreRowOperations(input),
20− ...createAuthRowOperations(input),
21−});
packages/persistence-sql/src/rows-effect-api-auth.ts+0−116
@@ -1,116 +0,0 @@
1−import {
2− type AuthConnection,
3− type AuthMaterial,
4− type OAuthState,
5− type SourceAuthBinding,
6−} from "@executor-v2/schema";
7−
8−import { tableNames } from "./schema";
9−import { type SqlBackend } from "./sql-internals";
10−import {
11− type RowOperations,
12− toBooleanEffect,
13− toListEffect,
14− toVoidEffect,
15−} from "./rows-effect-helpers";
16−
17−export const createAuthRowsEffectApi = (
18− backend: SqlBackend,
19− operations: RowOperations,
20−) => ({
21− authConnections: {
22− list: () =>
23− toListEffect<AuthConnection>(
24− backend,
25− "rows.authConnections.list",
26− tableNames.authConnections,
27− operations.listAuthConnectionRows,
28− ),
29− upsert: (connection: AuthConnection) =>
30− toVoidEffect(
31− backend,
32− "rows.authConnections.upsert",
33− tableNames.authConnections,
34− () => operations.upsertAuthConnectionRow(connection),
35− ),
36− removeById: (connectionId: AuthConnection["id"]) =>
37− toBooleanEffect(
38− backend,
39− "rows.authConnections.remove",
40− tableNames.authConnections,
41− () => operations.removeAuthConnectionRowById(connectionId),
42− ),
43− },
44−
45− sourceAuthBindings: {
46− list: () =>
47− toListEffect<SourceAuthBinding>(
48− backend,
49− "rows.sourceAuthBindings.list",
50− tableNames.sourceAuthBindings,
51− operations.listSourceAuthBindingRows,
52− ),
53− upsert: (binding: SourceAuthBinding) =>
54− toVoidEffect(
55− backend,
56− "rows.sourceAuthBindings.upsert",
57− tableNames.sourceAuthBindings,
58− () => operations.upsertSourceAuthBindingRow(binding),
59− ),
60− removeById: (bindingId: SourceAuthBinding["id"]) =>
61− toBooleanEffect(
62− backend,
63− "rows.sourceAuthBindings.remove",
64− tableNames.sourceAuthBindings,
65− () => operations.removeSourceAuthBindingRowById(bindingId),
66− ),
67− },
68−
69− authMaterials: {
70− list: () =>
71− toListEffect<AuthMaterial>(
72− backend,
73− "rows.authMaterials.list",
74− tableNames.authMaterials,
75− operations.listAuthMaterialRows,
76− ),
77− upsert: (material: AuthMaterial) =>
78− toVoidEffect(
79− backend,
80− "rows.authMaterials.upsert",
81− tableNames.authMaterials,
82− () => operations.upsertAuthMaterialRow(material),
83− ),
84− removeByConnectionId: (connectionId: AuthMaterial["connectionId"]) =>
85− toVoidEffect(
86− backend,
87− "rows.authMaterials.remove_by_connection",
88− tableNames.authMaterials,
89− () => operations.removeAuthMaterialRowsByConnectionId(connectionId),
90− ),
91− },
92−
93− oauthStates: {
94− list: () =>
95− toListEffect<OAuthState>(
96− backend,
97− "rows.oauthStates.list",
98− tableNames.oauthStates,
99− operations.listOAuthStateRows,
100− ),
101− upsert: (state: OAuthState) =>
102− toVoidEffect(
103− backend,
104− "rows.oauthStates.upsert",
105− tableNames.oauthStates,
106− () => operations.upsertOAuthStateRow(state),
107− ),
108− removeByConnectionId: (connectionId: OAuthState["connectionId"]) =>
109− toVoidEffect(
110− backend,
111− "rows.oauthStates.remove_by_connection",
112− tableNames.oauthStates,
113− () => operations.removeOAuthStateRowsByConnectionId(connectionId),
114− ),
115− },
116−});
packages/persistence-sql/src/rows-effect-api-core.ts+0−157
@@ -1,157 +0,0 @@
1−import {
2− type Approval,
3− type Organization,
4− type OrganizationMembership,
5− type Policy,
6− type Profile,
7− type StorageInstance,
8− type Workspace,
9−} from "@executor-v2/schema";
10−
11−import { tableNames } from "./schema";
12−import { type SqlBackend } from "./sql-internals";
13−import {
14− type RowOperations,
15− toBooleanEffect,
16− toListEffect,
17− toOptionEffect,
18− toVoidEffect,
19−} from "./rows-effect-helpers";
20−
21−export const createCoreRowsEffectApi = (
22− backend: SqlBackend,
23− operations: RowOperations,
24−) => ({
25− profile: {
26− get: () =>
27− toOptionEffect<Profile>(
28− backend,
29− "rows.profile.get",
30− tableNames.profile,
31− operations.getProfileRow,
32− ),
33− upsert: (profile: Profile) =>
34− toVoidEffect(
35− backend,
36− "rows.profile.upsert",
37− tableNames.profile,
38− () => operations.upsertProfileRow(profile),
39− ),
40− },
41−
42− organizations: {
43− list: () =>
44− toListEffect<Organization>(
45− backend,
46− "rows.organizations.list",
47− tableNames.organizations,
48− operations.listOrganizationsRows,
49− ),
50− upsert: (organization: Organization) =>
51− toVoidEffect(
52− backend,
53− "rows.organizations.upsert",
54− tableNames.organizations,
55− () => operations.upsertOrganizationRow(organization),
56− ),
57− },
58−
59− organizationMemberships: {
60− list: () =>
61− toListEffect<OrganizationMembership>(
62− backend,
63− "rows.organizationMemberships.list",
64− tableNames.organizationMemberships,
65− operations.listOrganizationMembershipRows,
66− ),
67− upsert: (membership: OrganizationMembership) =>
68− toVoidEffect(
69− backend,
70− "rows.organizationMemberships.upsert",
71− tableNames.organizationMemberships,
72− () => operations.upsertOrganizationMembershipRow(membership),
73− ),
74− },
75−
76− workspaces: {
77− list: () =>
78− toListEffect<Workspace>(
79− backend,
80− "rows.workspaces.list",
81− tableNames.workspaces,
82− operations.listWorkspaceRows,
83− ),
84− upsert: (workspace: Workspace) =>
85− toVoidEffect(
86− backend,
87− "rows.workspaces.upsert",
88− tableNames.workspaces,
89− () => operations.upsertWorkspaceRow(workspace),
90− ),
91− },
92−
93− storageInstances: {
94− list: () =>
95− toListEffect<StorageInstance>(
96− backend,
97− "rows.storageInstances.list",
98− tableNames.storageInstances,
99− operations.listStorageInstanceRows,
100− ),
101− upsert: (storageInstance: StorageInstance) =>
102− toVoidEffect(
103− backend,
104− "rows.storageInstances.upsert",
105− tableNames.storageInstances,
106− () => operations.upsertStorageInstanceRow(storageInstance),
107− ),
108− removeById: (storageInstanceId: StorageInstance["id"]) =>
109− toBooleanEffect(
110− backend,
111− "rows.storageInstances.remove",
112− tableNames.storageInstances,
113− () => operations.removeStorageInstanceRowById(storageInstanceId),
114− ),
115− },
116−
117− policies: {
118− list: () =>
119− toListEffect<Policy>(
120− backend,
121− "rows.policies.list",
122− tableNames.policies,
123− operations.listPolicyRows,
124− ),
125− upsert: (policy: Policy) =>
126− toVoidEffect(
127− backend,
128− "rows.policies.upsert",
129− tableNames.policies,
130− () => operations.upsertPolicyRow(policy),
131− ),
132− removeById: (policyId: Policy["id"]) =>
133− toBooleanEffect(
134− backend,
135− "rows.policies.remove",
136− tableNames.policies,
137− () => operations.removePolicyRowById(policyId),
138− ),
139− },
140−
141− approvals: {
142− list: () =>
143− toListEffect<Approval>(
144− backend,
145− "rows.approvals.list",
146− tableNames.approvals,
147− operations.listApprovalRows,
148− ),
149− upsert: (approval: Approval) =>
150− toVoidEffect(
151− backend,
152− "rows.approvals.upsert",
153− tableNames.approvals,
154− () => operations.upsertApprovalRow(approval),
155− ),
156− },
157−});
packages/persistence-sql/src/rows-effect-api.ts+0−12
@@ -1,12 +0,0 @@
1−import { createAuthRowsEffectApi } from "./rows-effect-api-auth";
2−import { createCoreRowsEffectApi } from "./rows-effect-api-core";
3−import { type RowOperations } from "./rows-effect-helpers";
4−import { type SqlBackend } from "./sql-internals";
5−
6−export const createRowsEffectApi = (
7− backend: SqlBackend,
8− operations: RowOperations,
9−) => ({
10− ...createCoreRowsEffectApi(backend, operations),
11− ...createAuthRowsEffectApi(backend, operations),
12−});
packages/persistence-sql/src/rows-effect-helpers.ts+0−55
@@ -1,55 +0,0 @@
1−import * as Effect from "effect/Effect";
2−import * as Option from "effect/Option";
3−
4−import { toRowStoreError } from "./persistence-errors";
5−import { createRowOperations } from "./row-operations";
6−import { type SqlBackend } from "./sql-internals";
7−
8−export type RowOperations = ReturnType<typeof createRowOperations>;
9−
10−export const toListEffect = <A>(
11− backend: SqlBackend,
12− operation: string,
13− location: string,
14− run: () => Promise<ReadonlyArray<A>>,
15−) =>
16− Effect.tryPromise({
17− try: run,
18− catch: (cause) => toRowStoreError(backend, operation, location, cause),
19− });
20−
21−export const toVoidEffect = (
22− backend: SqlBackend,
23− operation: string,
24− location: string,
25− run: () => Promise<void>,
26−) =>
27− Effect.tryPromise({
28− try: run,
29− catch: (cause) => toRowStoreError(backend, operation, location, cause),
30− });
31−
32−export const toBooleanEffect = (
33− backend: SqlBackend,
34− operation: string,
35− location: string,
36− run: () => Promise<boolean>,
37−) =>
38− Effect.tryPromise({
39− try: run,
40− catch: (cause) => toRowStoreError(backend, operation, location, cause),
41− });
42−
43−export const toOptionEffect = <A>(
44− backend: SqlBackend,
45− operation: string,
46− location: string,
47− run: () => Promise<A | null>,
48−) =>
49− Effect.tryPromise({
50− try: async () => {
51− const value = await run();
52− return value === null ? Option.none<A>() : Option.some(value);
53− },
54− catch: (cause) => toRowStoreError(backend, operation, location, cause),
55− });
packages/persistence-sql/src/source-artifact-stores.ts+82−106
@@ -15,21 +15,15 @@ import { · 14 unmodified lines
1515 } from "./persistence-errors";
1616 import { tableNames } from "./schema";
1717 import {
18− createDrizzleContext,
1918 type DrizzleDb,
2019 type DrizzleTables,
21− type SqlAdapter,
2220 type SqlBackend,
2321 } from "./sql-internals";
2422
25−type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>;
26−
2723 type CreateStoresInput = {
2824 backend: SqlBackend;
29− adapter: SqlAdapter;
3025 db: DrizzleDb;
3126 tables: DrizzleTables;
32− writeLocked: WriteLocked;
3327 };
3428
3529 const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`;
@@ -76,10 +70,8 @@ const toToolArtifact = ( · 40 unmodified lines
7670
7771 export const createSourceAndArtifactStores = ({
7872 backend,
79− adapter,
8073 db,
8174 tables,
82− writeLocked,
8375 }: CreateStoresInput): {
8476 sourceStore: SourceStore;
8577 toolArtifactStore: ToolArtifactStore;
@@ -124,46 +116,40 @@ export const createSourceAndArtifactStores = ({ · 38 unmodified lines
124116 upsert: (source: Source) =>
125117 Effect.tryPromise({
126118 try: async () => {
127− await writeLocked(async () => {
128− await adapter.transaction(async (transaction) => {
129− const transactionContext = createDrizzleContext(transaction);
130−
131− await transactionContext.db
132− .insert(transactionContext.tables.sourcesTable)
133− .values({
134− workspaceId: source.workspaceId,
135− sourceId: source.id,
136− name: source.name,
137− kind: source.kind,
138− endpoint: source.endpoint,
139− status: source.status,
140− enabled: source.enabled,
141− configJson: source.configJson,
142− sourceHash: source.sourceHash,
143− lastError: source.lastError,
144− createdAt: source.createdAt,
145− updatedAt: source.updatedAt,
146− })
147− .onConflictDoUpdate({
148− target: [
149− transactionContext.tables.sourcesTable.workspaceId,
150− transactionContext.tables.sourcesTable.sourceId,
151− ],
152− set: {
153− name: source.name,
154− kind: source.kind,
155− endpoint: source.endpoint,
156− status: source.status,
157− enabled: source.enabled,
158− configJson: source.configJson,
159− sourceHash: source.sourceHash,
160− lastError: source.lastError,
161− createdAt: source.createdAt,
162− updatedAt: source.updatedAt,
163− },
164− });
119+ await db
120+ .insert(tables.sourcesTable)
121+ .values({
122+ workspaceId: source.workspaceId,
123+ sourceId: source.id,
124+ name: source.name,
125+ kind: source.kind,
126+ endpoint: source.endpoint,
127+ status: source.status,
128+ enabled: source.enabled,
129+ configJson: source.configJson,
130+ sourceHash: source.sourceHash,
131+ lastError: source.lastError,
132+ createdAt: source.createdAt,
133+ updatedAt: source.updatedAt,
134+ })
135+ .onConflictDoUpdate({
136+ target: [
137+ tables.sourcesTable.workspaceId,
138+ tables.sourcesTable.sourceId,
139+ ],
140+ set: {
141+ name: source.name,
142+ kind: source.kind,
143+ endpoint: source.endpoint,
144+ status: source.status,
145+ enabled: source.enabled,
146+ configJson: source.configJson,
147+ sourceHash: source.sourceHash,
148+ lastError: source.lastError,
149+ createdAt: source.createdAt,
150+ updatedAt: source.updatedAt,
151+ },
165152 });
166− });
167153 },
168154 catch: (cause) =>
169155 toSourceStoreError(backend, "upsert", tableNames.sources, cause),
@@ -171,37 +157,33 @@ export const createSourceAndArtifactStores = ({ · 1 unmodified line
171157
172158 removeById: (workspaceId: WorkspaceId, sourceId: SourceId) =>
173159 Effect.tryPromise({
174− try: async () =>
175− writeLocked(async () =>
176− adapter.transaction(async (transaction) => {
177− const transactionContext = createDrizzleContext(transaction);
178− const existing = await transactionContext.db
179− .select({ sourceId: transactionContext.tables.sourcesTable.sourceId })
180− .from(transactionContext.tables.sourcesTable)
181− .where(
182− and(
183− eq(transactionContext.tables.sourcesTable.workspaceId, workspaceId),
184− eq(transactionContext.tables.sourcesTable.sourceId, sourceId),
185− ),
186− )
187− .limit(1);
160+ try: async () => {
161+ const existing = await db
162+ .select({ sourceId: tables.sourcesTable.sourceId })
163+ .from(tables.sourcesTable)
164+ .where(
165+ and(
166+ eq(tables.sourcesTable.workspaceId, workspaceId),
167+ eq(tables.sourcesTable.sourceId, sourceId),
168+ ),
169+ )
170+ .limit(1);
188171
189− if (existing.length === 0) {
190− return false;
191− }
172+ if (existing.length === 0) {
173+ return false;
174+ }
192175
193− await transactionContext.db
194− .delete(transactionContext.tables.sourcesTable)
195− .where(
196− and(
197− eq(transactionContext.tables.sourcesTable.workspaceId, workspaceId),
198− eq(transactionContext.tables.sourcesTable.sourceId, sourceId),
199− ),
200− );
176+ await db
177+ .delete(tables.sourcesTable)
178+ .where(
179+ and(
180+ eq(tables.sourcesTable.workspaceId, workspaceId),
181+ eq(tables.sourcesTable.sourceId, sourceId),
182+ ),
183+ );
201184
202− return true;
203− })
204− ),
185+ return true;
186+ },
205187 catch: (cause) =>
206188 toSourceStoreError(backend, "remove_by_id", tableNames.sources, cause),
207189 }),
@@ -241,38 +223,32 @@ export const createSourceAndArtifactStores = ({ · 33 unmodified lines
241223 upsert: (artifact: ToolArtifact) =>
242224 Effect.tryPromise({
243225 try: async () => {
244− await writeLocked(async () => {
245− await adapter.transaction(async (transaction) => {
246− const transactionContext = createDrizzleContext(transaction);
247−
248− await transactionContext.db
249− .insert(transactionContext.tables.toolArtifactsTable)
250− .values({
251− id: artifact.id,
252− workspaceId: artifact.workspaceId,
253− sourceId: artifact.sourceId,
254− sourceHash: artifact.sourceHash,
255− toolCount: artifact.toolCount,
256− manifestJson: artifact.manifestJson,
257− createdAt: artifact.createdAt,
258− updatedAt: artifact.updatedAt,
259− })
260− .onConflictDoUpdate({
261− target: [
262− transactionContext.tables.toolArtifactsTable.workspaceId,
263− transactionContext.tables.toolArtifactsTable.sourceId,
264− ],
265− set: {
266− id: artifact.id,
267− sourceHash: artifact.sourceHash,
268− toolCount: artifact.toolCount,
269− manifestJson: artifact.manifestJson,
270− createdAt: artifact.createdAt,
271− updatedAt: artifact.updatedAt,
272− },
273− });
226+ await db
227+ .insert(tables.toolArtifactsTable)
228+ .values({
229+ id: artifact.id,
230+ workspaceId: artifact.workspaceId,
231+ sourceId: artifact.sourceId,
232+ sourceHash: artifact.sourceHash,
233+ toolCount: artifact.toolCount,
234+ manifestJson: artifact.manifestJson,
235+ createdAt: artifact.createdAt,
236+ updatedAt: artifact.updatedAt,
237+ })
238+ .onConflictDoUpdate({
239+ target: [
240+ tables.toolArtifactsTable.workspaceId,
241+ tables.toolArtifactsTable.sourceId,
242+ ],
243+ set: {
244+ id: artifact.id,
245+ sourceHash: artifact.sourceHash,
246+ toolCount: artifact.toolCount,
247+ manifestJson: artifact.manifestJson,
248+ createdAt: artifact.createdAt,
249+ updatedAt: artifact.updatedAt,
250+ },
274251 });
275− });
276252 },
277253 catch: (cause) =>
278254 toToolArtifactStoreError(backend, "upsert", tableNames.toolArtifacts, cause),
packages/persistence-sql/src/sql-internals.ts+70−216
@@ -1,11 +1,13 @@
11 import { PGlite } from "@electric-sql/pglite";
2−import { drizzle as drizzlePgProxy } from "drizzle-orm/pg-proxy";
3−import { migrate as migratePgProxy } from "drizzle-orm/pg-proxy/migrator";
2+import { drizzle as drizzlePGlite } from "drizzle-orm/pglite";
3+import { migrate as migratePGlite } from "drizzle-orm/pglite/migrator";
4+import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
5+import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
46 import { existsSync } from "node:fs";
57 import { mkdir } from "node:fs/promises";
68 import * as path from "node:path";
79 import { fileURLToPath } from "node:url";
8−import postgres, { type Sql } from "postgres";
10+import postgres from "postgres";
911
1012 import {
1113 approvalsTable,
@@ -26,160 +28,80 @@ import { · 14 unmodified lines
2628 } from "./schema";
2729
2830 export type SqlBackend = "pglite" | "postgres";
29−type SqlRow = Record<string, unknown>;
30−
31−export type SqlAdapter = {
32− readonly backend: SqlBackend;
33− query: <TRow extends SqlRow = SqlRow>(
34− statement: string,
35− args?: ReadonlyArray<unknown>,
36− ) => Promise<Array<TRow>>;
37− execute: (statement: string, args?: ReadonlyArray<unknown>) => Promise<void>;
38− transaction: <A>(run: (transaction: SqlAdapter) => Promise<A>) => Promise<A>;
39− close: () => Promise<void>;
31+type CreateSqlRuntimeOptions = {
32+ databaseUrl?: string;
33+ localDataDir: string;
34+ postgresApplicationName?: string;
4035 };
4136
42−const withPostgresPlaceholders = (statement: string): string => {
43− let index = 0;
44− return statement.replace(/\?/g, () => {
45− index += 1;
46− return `$${index}`;
47− });
37+const drizzleSchema = {
38+ profileTable,
39+ organizationsTable,
40+ organizationMembershipsTable,
41+ workspacesTable,
42+ sourcesTable,
43+ toolArtifactsTable,
44+ authConnectionsTable,
45+ sourceAuthBindingsTable,
46+ authMaterialsTable,
47+ oauthStatesTable,
48+ policiesTable,
49+ approvalsTable,
50+ taskRunsTable,
51+ storageInstancesTable,
52+ syncStatesTable,
4853 };
4954
50−const toPostgresStatement = (statement: string): string => withPostgresPlaceholders(statement);
51−
52−const makePGliteTransaction = (
53− execute: (statement: string, args?: ReadonlyArray<unknown>) => Promise<void>,
54− query: <TRow extends SqlRow = SqlRow>(
55− statement: string,
56− args?: ReadonlyArray<unknown>,
57− ) => Promise<Array<TRow>>,
58−): SqlAdapter["transaction"] =>
59− async <A>(run: (transactionAdapter: SqlAdapter) => Promise<A>): Promise<A> => {
60− await execute("BEGIN");
55+const createPGliteDb = (client: PGlite) => drizzlePGlite(client, { schema: drizzleSchema });
56+const createPostgresDb = (client: postgres.Sql) => drizzlePostgres(client, { schema: drizzleSchema });
6157
62− try {
63− const adapter: SqlAdapter = {
64− backend: "pglite",
65− query,
66− execute,
67− transaction: async (nestedRun) => nestedRun(adapter),
68− close: async () => {},
69− };
58+type PGliteDb = ReturnType<typeof createPGliteDb>;
59+type PostgresDb = ReturnType<typeof createPostgresDb>;
7060
71− const result = await run(adapter);
72− await execute("COMMIT");
73− return result;
74− } catch (error) {
75− try {
76− await execute("ROLLBACK");
77− } catch {
78− // ignore rollback failure after original error
79− }
61+export type DrizzleDb = PGliteDb | PostgresDb;
62+export type DrizzleTables = typeof drizzleSchema;
8063
81− throw error;
82− }
83− };
64+type SqlRuntime = {
65+ backend: SqlBackend;
66+ db: DrizzleDb;
67+ close: () => Promise<void>;
68+};
8469
85−export const createPGliteAdapter = async (localDataDir: string): Promise<SqlAdapter> => {
70+const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => {
8671 const resolvedDataDir = path.resolve(localDataDir);
87− await mkdir(path.dirname(resolvedDataDir), { recursive: true });
72+ await mkdir(resolvedDataDir, { recursive: true });
8873
89− const db = new PGlite(resolvedDataDir);
90−
91− const query = async <TRow extends SqlRow = SqlRow>(
92− statement: string,
93− args: ReadonlyArray<unknown> = [],
94− ): Promise<Array<TRow>> => {
95− const result = await db.query(toPostgresStatement(statement), [...args]);
96− return result.rows as Array<TRow>;
97− };
98−
99− const execute = async (
100− statement: string,
101− args: ReadonlyArray<unknown> = [],
102− ): Promise<void> => {
103− await db.query(toPostgresStatement(statement), [...args]);
104− };
74+ const client = new PGlite(resolvedDataDir);
75+ const db = createPGliteDb(client);
10576
10677 return {
10778 backend: "pglite",
108− query,
109− execute,
110− transaction: makePGliteTransaction(execute, query),
79+ db,
11180 close: async () => {
112− await db.close();
81+ await client.close();
11382 },
11483 };
11584 };
11685
117−export const createPostgresAdapter = async (
86+const createPostgresRuntime = async (
11887 databaseUrl: string,
11988 applicationName: string | undefined,
120−): Promise<SqlAdapter> => {
89+): Promise<SqlRuntime> => {
12190 const client = postgres(databaseUrl, {
12291 prepare: false,
12392 max: 10,
12493 ...(applicationName ? { connection: { application_name: applicationName } } : {}),
12594 });
12695
127− type UnsafeRunner = {
128− unsafe: Sql["unsafe"];
129− };
130−
131− const toPostgresParams = (
132− args: ReadonlyArray<unknown>,
133− ): Array<postgres.ParameterOrJSON<never>> =>
134− args as unknown as Array<postgres.ParameterOrJSON<never>>;
96+ const db = createPostgresDb(client);
13597
136− const queryWith = async <TRow extends SqlRow = SqlRow>(
137− runner: UnsafeRunner,
138− statement: string,
139− args: ReadonlyArray<unknown> = [],
140− ): Promise<Array<TRow>> =>
141− (await runner.unsafe(
142− toPostgresStatement(statement),
143− toPostgresParams(args),
144− )) as unknown as Array<TRow>;
145−
146− const executeWith = async (
147− runner: UnsafeRunner,
148− statement: string,
149− args: ReadonlyArray<unknown> = [],
150− ): Promise<void> => {
151− await runner.unsafe(
152− toPostgresStatement(statement),
153− toPostgresParams(args),
154− );
155− };
156−
157− const adapter: SqlAdapter = {
98+ return {
15899 backend: "postgres",
159− query: (statement, args = []) => queryWith(client, statement, args),
160− execute: (statement, args = []) => executeWith(client, statement, args),
161− transaction: async <A>(run: (transaction: SqlAdapter) => Promise<A>) => {
162− const result = await client.begin(async (transactionClient) => {
163− const runner: UnsafeRunner = transactionClient;
164− const transactionAdapter: SqlAdapter = {
165− backend: "postgres",
166− query: (statement, args = []) => queryWith(runner, statement, args),
167− execute: (statement, args = []) => executeWith(runner, statement, args),
168− transaction: async (nestedRun) => nestedRun(transactionAdapter),
169− close: async () => {},
170− };
171−
172− return run(transactionAdapter);
173− });
174−
175− return result as A;
176− },
100+ db,
177101 close: async () => {
178102 await client.end({ timeout: 5 });
179103 },
180104 };
181−
182− return adapter;
183105 };
184106
185107 const resolveDrizzleMigrationsFolder = (): string => {
@@ -202,105 +124,37 @@ const resolveDrizzleMigrationsFolder = (): string => { · 16 unmodified lines
202124 throw new Error("Unable to resolve drizzle migrations folder");
203125 };
204126
205−const runMigrationQueries = async (
206− adapter: SqlAdapter,
207− queries: ReadonlyArray<string>,
208−): Promise<void> => {
209− for (const query of queries) {
210− const statement = query.trim();
211− if (statement.length === 0) {
212− continue;
213− }
214−
215− await adapter.execute(statement);
216− }
217−};
218−
219−const toProxyRow = (row: unknown): unknown => {
220− if (Array.isArray(row) || row === null || row === undefined) {
221− return row;
222− }
223−
224− if (typeof row === "object") {
225− return Object.values(row as Record<string, unknown>);
226− }
227−
228− return row;
229−};
230−
231−const normalizeProxyRows = (
232− method: "execute" | "all" | "values" | "get",
233− rows: ReadonlyArray<unknown>,
234−): Array<unknown> => {
235− if (method === "get") {
236− const first = rows[0];
237− return first === undefined ? [] : [toProxyRow(first)];
238− }
239−
240− return rows.map(toProxyRow);
241−};
242−
243−const drizzleSchema = {
244− profileTable,
245− organizationsTable,
246− organizationMembershipsTable,
247− workspacesTable,
248− sourcesTable,
249− toolArtifactsTable,
250− authConnectionsTable,
251− sourceAuthBindingsTable,
252− authMaterialsTable,
253− oauthStatesTable,
254− policiesTable,
255− approvalsTable,
256− taskRunsTable,
257− storageInstancesTable,
258− syncStatesTable,
259−};
260−
261−type DrizzleSchema = typeof drizzleSchema;
262−type PostgresDrizzleDb = ReturnType<typeof drizzlePgProxy<DrizzleSchema>>;
263−
264−export type DrizzleDb = PostgresDrizzleDb;
265−export type DrizzleTables = DrizzleSchema;
266−
267127 export type DrizzleContext = {
268128 db: DrizzleDb;
269129 tables: DrizzleTables;
270130 };
271131
272−const createPostgresProxyDb = (adapter: SqlAdapter): PostgresDrizzleDb =>
273− drizzlePgProxy(
274− async (statement, params, method) => {
275− if (method === "execute") {
276− await adapter.execute(statement, params);
277− return { rows: [] };
278− }
279−
280− const rows = await adapter.query(statement, params);
281− return {
282− rows: normalizeProxyRows(method, rows),
283− };
284− },
285− {
286− schema: drizzleSchema,
287− },
288− );
289−
290−export const createDrizzleContext = (adapter: SqlAdapter): DrizzleContext => ({
291− db: createPostgresProxyDb(adapter),
132+export const createDrizzleContext = (db: DrizzleDb): DrizzleContext => ({
133+ db,
292134 tables: drizzleSchema,
293135 });
294136
295−export const runMigrations = async (adapter: SqlAdapter): Promise<void> => {
296− const migrationDb = createPostgresProxyDb(adapter);
137+export const createSqlRuntime = async (
138+ options: CreateSqlRuntimeOptions,
139+): Promise<SqlRuntime> => {
140+ const databaseUrl = options.databaseUrl?.trim();
141+ if (
142+ databaseUrl
143+ && (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://"))
144+ ) {
145+ return createPostgresRuntime(databaseUrl, options.postgresApplicationName?.trim());
146+ }
147+
148+ return createPGliteRuntime(options.localDataDir);
149+};
150+
151+export const runMigrations = async (runtime: SqlRuntime): Promise<void> => {
297152 const migrationsFolder = resolveDrizzleMigrationsFolder();
298153
299− await migratePgProxy(
300− migrationDb,
301− async (queries) => runMigrationQueries(adapter, queries),
302− {
303− migrationsFolder,
304− },
305− );
154+ if (runtime.backend === "pglite") {
155+ await migratePGlite(runtime.db as PGliteDb, { migrationsFolder });
156+ return;
157+ }
158+
159+ await migratePostgres(runtime.db as PostgresDb, { migrationsFolder });
306160 };

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