Replace proxy/layered row operations with native Drizzle backends and scoped row queries so control-plane services follow straightforward database flows.
Unified · Split
| @@ -37,14 +37,6 @@ const localAdminFallbackEnabled = (() => { · 36 unmodified lines | ||
| 37 | 37 | return isTruthy(configured); |
| 38 | 38 | })(); |
| 39 | 39 | |
| 40 | −const organizationMembershipsForAccount = ( | |
| 41 | − memberships: ReadonlyArray<OrganizationMembership>, | |
| 42 | − accountId: OrganizationMembership["accountId"], | |
| 43 | −): ReadonlyArray<OrganizationMembership> => | |
| 44 | − memberships.filter( | |
| 45 | − (membership) => membership.accountId === accountId, | |
| 46 | − ); | |
| 47 | − | |
| 48 | 40 | const workspaceMembershipsForAccount = ( |
| 49 | 41 | workspaces: ReadonlyArray<Workspace>, |
| 50 | 42 | accountId: OrganizationMembership["accountId"], |
| @@ -66,10 +58,20 @@ const resolveActorFromSnapshot = ( · 15 unmodified lines | ||
| 66 | 58 | Effect.gen(function* () { |
| 67 | 59 | const principal = yield* requirePrincipalFromHeaders(headers); |
| 68 | 60 | |
| 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( | |
| 73 | 75 | Effect.mapError( |
| 74 | 76 | (error) => |
| 75 | 77 | new ActorUnauthenticatedError({ |
| @@ -82,10 +84,7 @@ const resolveActorFromSnapshot = ( · 6 unmodified lines | ||
| 82 | 84 | return makeAllowAllActor(principal); |
| 83 | 85 | } |
| 84 | 86 | |
| 85 | − const organizationMemberships = organizationMembershipsForAccount( | |
| 86 | − memberships, | |
| 87 | − principal.accountId, | |
| 88 | − ); | |
| 87 | + const organizationMemberships = memberships; | |
| 89 | 88 | const workspaceMemberships = workspaceMembershipsForAccount( |
| 90 | 89 | workspaces, |
| 91 | 90 | principal.accountId, |
| @@ -106,10 +105,19 @@ export const PmActorLive = (rows: ActorRows) => · 14 unmodified lines | ||
| 106 | 105 | Effect.gen(function* () { |
| 107 | 106 | const principal = yield* requirePrincipalFromHeaders(input.headers); |
| 108 | 107 | |
| 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( | |
| 113 | 121 | Effect.mapError( |
| 114 | 122 | (error) => |
| 115 | 123 | new ActorUnauthenticatedError({ |
| @@ -122,10 +130,7 @@ export const PmActorLive = (rows: ActorRows) => · 6 unmodified lines | ||
| 122 | 130 | return makeAllowAllActor(principal); |
| 123 | 131 | } |
| 124 | 132 | |
| 125 | − const organizationMemberships = organizationMembershipsForAccount( | |
| 126 | − memberships, | |
| 127 | − principal.accountId, | |
| 128 | − ); | |
| 133 | + const organizationMemberships = memberships; | |
| 129 | 134 | |
| 130 | 135 | const workspace = workspaces.find((item) => item.id === input.workspaceId) ?? null; |
| 131 | 136 | |
| @@ -12,6 +12,7 @@ import { · 11 unmodified lines | ||
| 12 | 12 | } from "@executor-v2/management-api"; |
| 13 | 13 | import { type Approval } from "@executor-v2/schema"; |
| 14 | 14 | import * as Effect from "effect/Effect"; |
| 15 | +import * as Option from "effect/Option"; | |
| 15 | 16 | |
| 16 | 17 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 17 | 18 | |
| @@ -19,18 +20,6 @@ type ApprovalRows = Pick<SqlControlPlanePersistence["rows"], "approvals">; · 1 unmodified line | ||
| 19 | 20 | |
| 20 | 21 | const sourceStoreError = createSqlSourceStoreErrorMapper("approvals"); |
| 21 | 22 | |
| 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 | − | |
| 34 | 23 | const toPersistentApprovalStoreError = ( |
| 35 | 24 | operation: string, |
| 36 | 25 | message: string, |
| @@ -69,50 +58,45 @@ export const createPmPersistentToolApprovalPolicy = ( · 32 unmodified lines | ||
| 69 | 58 | ): ToolApprovalPolicy => { |
| 70 | 59 | const store: PersistentToolApprovalStore = { |
| 71 | 60 | 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( | |
| 73 | 68 | Effect.mapError((error) => |
| 74 | 69 | toPersistentApprovalStoreErrorFromRowStore("approvals.read", error), |
| 75 | 70 | ), |
| 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); | |
| 86 | 74 | }), |
| 87 | 75 | ), |
| 88 | 76 | |
| 89 | 77 | 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 | + }), | |
| 116 | 100 | }; |
| 117 | 101 | |
| 118 | 102 | return createPersistentToolApprovalPolicy({ |
| @@ -128,34 +112,25 @@ export const createPmApprovalsService = ( · 9 unmodified lines | ||
| 128 | 112 | makeControlPlaneApprovalsService({ |
| 129 | 113 | listApprovals: (workspaceId) => |
| 130 | 114 | Effect.gen(function* () { |
| 131 | − const approvals = yield* rows.approvals.list().pipe( | |
| 115 | + const approvals = yield* rows.approvals.listByWorkspaceId(workspaceId).pipe( | |
| 132 | 116 | Effect.mapError((error) => |
| 133 | 117 | sourceStoreError.fromRowStore("approvals.list", error), |
| 134 | 118 | ), |
| 135 | 119 | ); |
| 136 | 120 | |
| 137 | − const scopedApprovals = approvals.filter( | |
| 138 | − (approval) => approval.workspaceId === workspaceId, | |
| 139 | − ); | |
| 140 | − | |
| 141 | − return sortApprovals(scopedApprovals); | |
| 121 | + return approvals; | |
| 142 | 122 | }), |
| 143 | 123 | |
| 144 | 124 | resolveApproval: (input) => |
| 145 | 125 | Effect.gen(function* () { |
| 146 | − const approvals = yield* rows.approvals.list().pipe( | |
| 126 | + const approvalOption = yield* rows.approvals.getById(input.approvalId).pipe( | |
| 147 | 127 | Effect.mapError((error) => |
| 148 | − sourceStoreError.fromRowStore("approvals.resolve", error), | |
| 128 | + sourceStoreError.fromRowStore("approvals.get_by_id", error), | |
| 149 | 129 | ), |
| 150 | 130 | ); |
| 151 | 131 | |
| 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) { | |
| 159 | 134 | return yield* sourceStoreError.fromMessage( |
| 160 | 135 | "approvals.resolve", |
| 161 | 136 | "Approval not found", |
| @@ -163,7 +138,6 @@ export const createPmApprovalsService = ( · 1 unmodified line | ||
| 163 | 138 | ); |
| 164 | 139 | } |
| 165 | 140 | |
| 166 | − const approval = approvals[index]; | |
| 167 | 141 | if (approval.status !== "pending") { |
| 168 | 142 | return yield* sourceStoreError.fromMessage( |
| 169 | 143 | "approvals.resolve", |
| @@ -1,9 +1,4 @@ | ||
| 1 | 1 | import { SourceStoreError } from "@executor-v2/persistence-ports"; |
| 2 | −import { | |
| 3 | − type OrganizationId, | |
| 4 | − type Workspace, | |
| 5 | − type WorkspaceId, | |
| 6 | −} from "@executor-v2/schema"; | |
| 7 | 2 | |
| 8 | 3 | type RowStoreLikeError = { |
| 9 | 4 | message: string; |
| @@ -57,16 +52,3 @@ export const createSqlSourceStoreErrorMapper = ( · 47 unmodified lines | ||
| 57 | 52 | ), |
| 58 | 53 | }; |
| 59 | 54 | }; |
| 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 | −}; | |
| @@ -5,26 +5,19 @@ import { · 4 unmodified lines | ||
| 5 | 5 | sourceIdFromSourceKey, |
| 6 | 6 | type ResolveToolCredentials, |
| 7 | 7 | } 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"; | |
| 16 | 10 | import * as Effect from "effect/Effect"; |
| 17 | 11 | |
| 18 | −type RowReader<A> = { | |
| 19 | − list: () => Effect.Effect<ReadonlyArray<A>, RowStoreError>; | |
| 20 | −}; | |
| 21 | − | |
| 22 | 12 | 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">; | |
| 28 | 21 | }; |
| 29 | 22 | |
| 30 | 23 | const toCredentialResolverError = ( |
| @@ -180,33 +173,44 @@ export const createPmResolveToolCredentials = ( · 149 unmodified lines | ||
| 180 | 173 | }; |
| 181 | 174 | } |
| 182 | 175 | |
| 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( | |
| 190 | 186 | Effect.mapError((error) => |
| 191 | 187 | toCredentialResolverError( |
| 192 | − "read_credential_rows", | |
| 193 | − "Failed reading credential rows while resolving credentials", | |
| 188 | + "read_workspace", | |
| 189 | + "Failed reading workspace while resolving credentials", | |
| 194 | 190 | error.details ?? error.message, |
| 195 | 191 | ), |
| 196 | 192 | ), |
| 197 | 193 | ); |
| 198 | 194 | |
| 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) { | |
| 201 | 198 | return { |
| 202 | 199 | headers: {}, |
| 203 | 200 | }; |
| 204 | 201 | } |
| 205 | 202 | |
| 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 | + ); | |
| 210 | 214 | |
| 211 | 215 | const binding = bindings |
| 212 | 216 | .filter((candidate) => candidate.sourceId === sourceId) |
| @@ -247,9 +251,19 @@ export const createPmResolveToolCredentials = ( · 34 unmodified lines | ||
| 247 | 251 | }; |
| 248 | 252 | } |
| 249 | 253 | |
| 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 | + ), | |
| 252 | 262 | ); |
| 263 | + | |
| 264 | + const connection = connectionOption._tag === "Some" | |
| 265 | + ? connectionOption.value | |
| 266 | + : null; | |
| 253 | 267 | if (!connection || connection.status !== "active") { |
| 254 | 268 | return { |
| 255 | 269 | headers: {}, |
| @@ -261,9 +275,18 @@ export const createPmResolveToolCredentials = ( · 5 unmodified lines | ||
| 261 | 275 | ); |
| 262 | 276 | |
| 263 | 277 | 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; | |
| 267 | 290 | |
| 268 | 291 | const accessToken = normalizeString(oauthState?.accessTokenCiphertext); |
| 269 | 292 | const oauthHeaders = accessToken |
| @@ -275,9 +298,18 @@ export const createPmResolveToolCredentials = ( · 5 unmodified lines | ||
| 275 | 298 | }; |
| 276 | 299 | } |
| 277 | 300 | |
| 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; | |
| 281 | 313 | |
| 282 | 314 | if (!material) { |
| 283 | 315 | return { |
| @@ -9,14 +9,11 @@ import { · 8 unmodified lines | ||
| 9 | 9 | type OAuthState, |
| 10 | 10 | type SourceAuthBinding, |
| 11 | 11 | type SourceCredentialBinding, |
| 12 | − type WorkspaceId, | |
| 13 | 12 | } from "@executor-v2/schema"; |
| 14 | 13 | import * as Effect from "effect/Effect"; |
| 14 | +import * as Option from "effect/Option"; | |
| 15 | 15 | |
| 16 | −import { | |
| 17 | − createSqlSourceStoreErrorMapper, | |
| 18 | − resolveWorkspaceOrganizationId, | |
| 19 | −} from "./control-plane-row-helpers"; | |
| 16 | +import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | |
| 20 | 17 | import { |
| 21 | 18 | buildOAuthRefreshConfigFromPayload, |
| 22 | 19 | encodeOAuthRefreshConfig, |
| @@ -46,38 +43,42 @@ export const createPmCredentialsService = ( · 23 unmodified lines | ||
| 46 | 43 | makeControlPlaneCredentialsService({ |
| 47 | 44 | listCredentialBindings: (workspaceId) => |
| 48 | 45 | 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), | |
| 54 | 49 | ), |
| 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 | + ), | |
| 58 | 68 | ), |
| 59 | − ), | |
| 60 | − rows.workspaces.list().pipe( | |
| 69 | + rows.authConnections.listByOrganizationId(workspace.organizationId).pipe( | |
| 61 | 70 | Effect.mapError((error) => |
| 62 | − sourceStoreError.fromRowStore("credentials.workspaces.list", error), | |
| 71 | + sourceStoreError.fromRowStore("credentials.connections.list", error), | |
| 63 | 72 | ), |
| 64 | 73 | ), |
| 65 | 74 | ]); |
| 66 | 75 | |
| 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])); | |
| 74 | 77 | |
| 75 | 78 | const compatBindings: Array<SourceCredentialBinding> = []; |
| 76 | 79 | |
| 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); | |
| 81 | 82 | |
| 82 | 83 | if (!connection) { |
| 83 | 84 | continue; |
| @@ -91,33 +92,22 @@ export const createPmCredentialsService = ( · 7 unmodified lines | ||
| 91 | 92 | |
| 92 | 93 | upsertCredentialBinding: (input) => |
| 93 | 94 | 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), | |
| 119 | 98 | ), |
| 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; | |
| 121 | 111 | |
| 122 | 112 | if (input.payload.scopeType === "account" && input.payload.accountId === null) { |
| 123 | 113 | return yield* sourceStoreError.fromMessage( |
| @@ -140,11 +130,29 @@ export const createPmCredentialsService = ( · 16 unmodified lines | ||
| 140 | 130 | const requestedId = input.payload.id; |
| 141 | 131 | const requestedBindingId = requestedId as SourceAuthBinding["id"] | undefined; |
| 142 | 132 | |
| 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 | + } | |
| 148 | 156 | |
| 149 | 157 | const scopeWorkspaceId = |
| 150 | 158 | input.payload.scopeType === "workspace" ? input.workspaceId : null; |
| @@ -163,9 +171,15 @@ export const createPmCredentialsService = ( · 12 unmodified lines | ||
| 163 | 171 | ?? (`conn_${crypto.randomUUID()}` as AuthConnection["id"]) |
| 164 | 172 | ) as AuthConnection["id"]; |
| 165 | 173 | |
| 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); | |
| 169 | 183 | |
| 170 | 184 | if (existingConnection && existingConnection.organizationId !== organizationId) { |
| 171 | 185 | return yield* sourceStoreError.fromMessage( |
| @@ -228,9 +242,15 @@ export const createPmCredentialsService = ( · 56 unmodified lines | ||
| 228 | 242 | ); |
| 229 | 243 | |
| 230 | 244 | 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 | + | |
| 234 | 254 | const refreshConfig = buildOAuthRefreshConfigFromPayload( |
| 235 | 255 | input.payload, |
| 236 | 256 | parseOAuthRefreshConfig(existingOAuth?.refreshConfigJson ?? null), |
| @@ -282,9 +302,14 @@ export const createPmCredentialsService = ( · 45 unmodified lines | ||
| 282 | 302 | ), |
| 283 | 303 | ); |
| 284 | 304 | } 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); | |
| 288 | 313 | |
| 289 | 314 | const material: AuthMaterial = { |
| 290 | 315 | id: |
| @@ -312,30 +337,35 @@ export const createPmCredentialsService = ( · 21 unmodified lines | ||
| 312 | 337 | |
| 313 | 338 | removeCredentialBinding: (input) => |
| 314 | 339 | 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), | |
| 325 | 343 | ), |
| 326 | − ]); | |
| 344 | + ); | |
| 345 | + | |
| 346 | + const workspace = Option.getOrNull(workspaceOption); | |
| 347 | + if (workspace === null) { | |
| 348 | + return { | |
| 349 | + removed: false, | |
| 350 | + }; | |
| 351 | + } | |
| 327 | 352 | |
| 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), | |
| 335 | 358 | ), |
| 336 | − ); | |
| 359 | + ); | |
| 337 | 360 | |
| 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 | + ) { | |
| 339 | 369 | return { |
| 340 | 370 | removed: false, |
| 341 | 371 | }; |
| @@ -355,10 +385,15 @@ export const createPmCredentialsService = ( · 13 unmodified lines | ||
| 355 | 385 | }; |
| 356 | 386 | } |
| 357 | 387 | |
| 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); | |
| 362 | 397 | |
| 363 | 398 | if (!hasRemainingBindings) { |
| 364 | 399 | yield* Effect.all([ |
| @@ -109,10 +109,31 @@ const withPmProcess = (port: number, localDataDir: string) => · 108 unmodified lines | ||
| 109 | 109 | }), |
| 110 | 110 | ), |
| 111 | 111 | (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; | |
| 115 | 115 | } |
| 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 | + }); | |
| 116 | 137 | }), |
| 117 | 138 | ); |
| 118 | 139 | |
| @@ -59,7 +59,7 @@ const pmStateRootDir = process.env.PM_STATE_ROOT_DIR ?? ".executor-v2/pm-state"; · 58 unmodified lines | ||
| 59 | 59 | |
| 60 | 60 | const parsePort = (value: string | undefined): number => { |
| 61 | 61 | const parsed = Number.parseInt(value ?? "", 10); |
| 62 | − return Number.isFinite(parsed) && parsed > 0 ? parsed : 8787; | |
| 62 | + return Number.isFinite(parsed) && parsed > 0 ? parsed : 8788; | |
| 63 | 63 | }; |
| 64 | 64 | |
| 65 | 65 | const readConfiguredRuntimeKind = (value: string | undefined): string | undefined => { |
| @@ -102,17 +102,21 @@ const ensurePmBootstrap = ( · 36 unmodified lines | ||
| 102 | 102 | ) => |
| 103 | 103 | Effect.gen(function* () { |
| 104 | 104 | 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 | − ]); | |
| 111 | 105 | |
| 112 | 106 | const organizationId = "org_local" as OrganizationId; |
| 113 | 107 | const accountId = "acct_local" as AccountId; |
| 114 | 108 | |
| 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") { | |
| 116 | 120 | yield* persistence.rows.organizations.upsert({ |
| 117 | 121 | id: organizationId, |
| 118 | 122 | slug: organizationId, |
| @@ -124,11 +128,7 @@ const ensurePmBootstrap = ( · 5 unmodified lines | ||
| 124 | 128 | }); |
| 125 | 129 | } |
| 126 | 130 | |
| 127 | − if ( | |
| 128 | − memberships.find( | |
| 129 | − (item) => item.organizationId === organizationId && item.accountId === accountId, | |
| 130 | − ) === undefined | |
| 131 | − ) { | |
| 131 | + if (membershipOption._tag === "None") { | |
| 132 | 132 | yield* persistence.rows.organizationMemberships.upsert({ |
| 133 | 133 | id: "org_member_local" as OrganizationMemberId, |
| 134 | 134 | organizationId, |
| @@ -143,7 +143,7 @@ const ensurePmBootstrap = ( · 8 unmodified lines | ||
| 143 | 143 | }); |
| 144 | 144 | } |
| 145 | 145 | |
| 146 | − if (workspaces.find((item) => item.id === workspaceId) === undefined) { | |
| 146 | + if (workspaceOption._tag === "None") { | |
| 147 | 147 | yield* persistence.rows.workspaces.upsert({ |
| 148 | 148 | id: workspaceId, |
| 149 | 149 | organizationId, |
| @@ -8,6 +8,7 @@ import { · 7 unmodified lines | ||
| 8 | 8 | type OrganizationMembership, |
| 9 | 9 | } from "@executor-v2/schema"; |
| 10 | 10 | import * as Effect from "effect/Effect"; |
| 11 | +import * as Option from "effect/Option"; | |
| 11 | 12 | |
| 12 | 13 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 13 | 14 | |
| @@ -49,28 +50,20 @@ export const createPmOrganizationsService = ( · 35 unmodified lines | ||
| 49 | 50 | |
| 50 | 51 | upsertOrganization: (input) => |
| 51 | 52 | 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), | |
| 63 | 57 | ), |
| 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 | + ); | |
| 66 | 64 | |
| 67 | 65 | 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); | |
| 74 | 67 | |
| 75 | 68 | const nextOrganization: Organization = { |
| 76 | 69 | id: |
| @@ -94,13 +87,21 @@ export const createPmOrganizationsService = ( · 17 unmodified lines | ||
| 94 | 87 | ); |
| 95 | 88 | |
| 96 | 89 | 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 | + ); | |
| 102 | 103 | |
| 103 | − if (!existingMembership) { | |
| 104 | + if (Option.isNone(existingMembership)) { | |
| 104 | 105 | const membership: OrganizationMembership = { |
| 105 | 106 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], |
| 106 | 107 | organizationId: nextOrganization.id, |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | +import * as Option from "effect/Option"; | |
| 3 | 4 | import { type Approval, type WorkspaceId } from "@executor-v2/schema"; |
| 4 | 5 | |
| 5 | 6 | import { |
| @@ -15,6 +16,28 @@ describe("PM persistent approval policy", () => { · 9 unmodified lines | ||
| 15 | 16 | const approvalRows = { |
| 16 | 17 | approvals: { |
| 17 | 18 | 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 | + ), | |
| 18 | 41 | upsert: (approval: Approval) => |
| 19 | 42 | Effect.sync(() => { |
| 20 | 43 | const index = approvals.findIndex((item) => item.id === approval.id); |
| @@ -5,6 +5,7 @@ import { · 4 unmodified lines | ||
| 5 | 5 | } from "@executor-v2/management-api"; |
| 6 | 6 | import { type Policy } from "@executor-v2/schema"; |
| 7 | 7 | import * as Effect from "effect/Effect"; |
| 8 | +import * as Option from "effect/Option"; | |
| 8 | 9 | |
| 9 | 10 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 10 | 11 | |
| @@ -29,36 +30,36 @@ export const createPmPoliciesService = ( · 18 unmodified lines | ||
| 29 | 30 | makeControlPlanePoliciesService({ |
| 30 | 31 | listPolicies: (workspaceId) => |
| 31 | 32 | Effect.gen(function* () { |
| 32 | − const policies = yield* rows.policies.list().pipe( | |
| 33 | + const policies = yield* rows.policies.listByWorkspaceId(workspaceId).pipe( | |
| 33 | 34 | Effect.mapError((error) => |
| 34 | 35 | sourceStoreError.fromRowStore("policies.list", error), |
| 35 | 36 | ), |
| 36 | 37 | ); |
| 37 | 38 | |
| 38 | − return sortPolicies( | |
| 39 | − policies.filter((policy) => policy.workspaceId === workspaceId), | |
| 40 | − ); | |
| 39 | + return sortPolicies(policies); | |
| 41 | 40 | }), |
| 42 | 41 | |
| 43 | 42 | upsertPolicy: (input) => |
| 44 | 43 | Effect.gen(function* () { |
| 45 | − const policies = yield* rows.policies.list().pipe( | |
| 46 | − Effect.mapError((error) => | |
| 47 | − sourceStoreError.fromRowStore("policies.upsert", error), | |
| 48 | − ), | |
| 49 | − ); | |
| 50 | − | |
| 51 | 44 | const now = Date.now(); |
| 52 | 45 | const requestedId = input.payload.id; |
| 53 | 46 | |
| 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 | + } | |
| 62 | 63 | |
| 63 | 64 | const nextPolicy: Policy = { |
| 64 | 65 | id: existing?.id ?? (requestedId ?? (`pol_${crypto.randomUUID()}` as Policy["id"])), |
| @@ -80,17 +81,15 @@ export const createPmPoliciesService = ( · 15 unmodified lines | ||
| 80 | 81 | |
| 81 | 82 | removePolicy: (input) => |
| 82 | 83 | Effect.gen(function* () { |
| 83 | − const policies = yield* rows.policies.list().pipe( | |
| 84 | + const existingOption = yield* rows.policies.getById(input.policyId).pipe( | |
| 84 | 85 | Effect.mapError((error) => |
| 85 | − sourceStoreError.fromRowStore("policies.remove", error), | |
| 86 | + sourceStoreError.fromRowStore("policies.get_by_id", error), | |
| 86 | 87 | ), |
| 87 | 88 | ); |
| 88 | 89 | |
| 89 | − const existing = policies.find( | |
| 90 | − (policy) => policy.workspaceId === input.workspaceId && policy.id === input.policyId, | |
| 91 | − ); | |
| 90 | + const existing = Option.getOrNull(existingOption); | |
| 92 | 91 | |
| 93 | − if (!existing) { | |
| 92 | + if (!existing || existing.workspaceId !== input.workspaceId) { | |
| 94 | 93 | return { |
| 95 | 94 | removed: false, |
| 96 | 95 | }; |
| @@ -6,6 +6,7 @@ import { · 5 unmodified lines | ||
| 6 | 6 | type WorkspaceId, |
| 7 | 7 | } from "@executor-v2/schema"; |
| 8 | 8 | import * as Effect from "effect/Effect"; |
| 9 | +import * as Option from "effect/Option"; | |
| 9 | 10 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; |
| 10 | 11 | import { tmpdir } from "node:os"; |
| 11 | 12 | import * as path from "node:path"; |
| @@ -39,10 +40,32 @@ describe("PM storage service", () => { · 27 unmodified lines | ||
| 39 | 40 | const rows: StorageRows = { |
| 40 | 41 | workspaces: { |
| 41 | 42 | 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 | + ), | |
| 42 | 49 | upsert: () => Effect.void, |
| 43 | 50 | }, |
| 44 | 51 | storageInstances: { |
| 45 | 52 | 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 | + ), | |
| 46 | 69 | upsert: (storageInstance) => |
| 47 | 70 | Effect.sync(() => { |
| 48 | 71 | const index = storageInstances.findIndex((item) => item.id === storageInstance.id); |
| @@ -10,11 +10,9 @@ import { · 9 unmodified lines | ||
| 10 | 10 | type WorkspaceId, |
| 11 | 11 | } from "@executor-v2/schema"; |
| 12 | 12 | import * as Effect from "effect/Effect"; |
| 13 | +import * as Option from "effect/Option"; | |
| 13 | 14 | |
| 14 | −import { | |
| 15 | − createSqlSourceStoreErrorMapper, | |
| 16 | − resolveWorkspaceOrganizationId, | |
| 17 | −} from "./control-plane-row-helpers"; | |
| 15 | +import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | |
| 18 | 16 | import { |
| 19 | 17 | DEFAULT_EPHEMERAL_TTL_HOURS, |
| 20 | 18 | initializeStorageInstanceFiles, |
| @@ -100,7 +98,7 @@ const loadStorageScope = ( · 79 unmodified lines | ||
| 100 | 98 | input: { |
| 101 | 99 | workspaceId: WorkspaceId; |
| 102 | 100 | instancesOperation: string; |
| 103 | − workspacesOperation: string; | |
| 101 | + workspaceOperation: string; | |
| 104 | 102 | }, |
| 105 | 103 | ): Effect.Effect< |
| 106 | 104 | { |
| @@ -109,22 +107,33 @@ const loadStorageScope = ( · 2 unmodified lines | ||
| 109 | 107 | }, |
| 110 | 108 | SourceStoreError |
| 111 | 109 | > => |
| 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), | |
| 122 | 113 | ), |
| 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 | + }), | |
| 128 | 137 | ); |
| 129 | 138 | |
| 130 | 139 | export const createPmStorageService = ( |
| @@ -139,14 +148,10 @@ export const createPmStorageService = ( · 8 unmodified lines | ||
| 139 | 148 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 140 | 149 | workspaceId, |
| 141 | 150 | instancesOperation: "storage.list_instances", |
| 142 | − workspacesOperation: "storage.list_instances.workspaces", | |
| 151 | + workspaceOperation: "storage.list_instances.workspace", | |
| 143 | 152 | }); |
| 144 | 153 | |
| 145 | − return sortStorageInstances( | |
| 146 | − storageInstances.filter((instance) => | |
| 147 | − canAccessStorageInstance(instance, workspaceId, organizationId) | |
| 148 | − ), | |
| 149 | − ); | |
| 154 | + return sortStorageInstances(storageInstances); | |
| 150 | 155 | }), |
| 151 | 156 | |
| 152 | 157 | openStorageInstance: (input) => |
| @@ -159,14 +164,23 @@ export const createPmStorageService = ( · 6 unmodified lines | ||
| 159 | 164 | ); |
| 160 | 165 | } |
| 161 | 166 | |
| 162 | − const workspaces = yield* rows.workspaces.list().pipe( | |
| 167 | + const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe( | |
| 163 | 168 | Effect.mapError((error) => |
| 164 | − sourceStoreError.fromRowStore("storage.open.workspaces", error), | |
| 169 | + sourceStoreError.fromRowStore("storage.open.workspace", error), | |
| 165 | 170 | ), |
| 166 | 171 | ); |
| 167 | 172 | |
| 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 | + | |
| 168 | 182 | const now = Date.now(); |
| 169 | − const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId); | |
| 183 | + const organizationId = workspace.organizationId; | |
| 170 | 184 | const storageInstanceId = |
| 171 | 185 | `storage_${crypto.randomUUID()}` as StorageInstance["id"]; |
| 172 | 186 | const ttlHours = |
| @@ -231,7 +245,7 @@ export const createPmStorageService = ( · 58 unmodified lines | ||
| 231 | 245 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 232 | 246 | workspaceId: input.workspaceId, |
| 233 | 247 | instancesOperation: "storage.close.instances", |
| 234 | − workspacesOperation: "storage.close.workspaces", | |
| 248 | + workspaceOperation: "storage.close.workspace", | |
| 235 | 249 | }); |
| 236 | 250 | |
| 237 | 251 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -270,7 +284,7 @@ export const createPmStorageService = ( · 32 unmodified lines | ||
| 270 | 284 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 271 | 285 | workspaceId: input.workspaceId, |
| 272 | 286 | instancesOperation: "storage.remove.instances", |
| 273 | − workspacesOperation: "storage.remove.workspaces", | |
| 287 | + workspaceOperation: "storage.remove.workspace", | |
| 274 | 288 | }); |
| 275 | 289 | |
| 276 | 290 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -311,7 +325,7 @@ export const createPmStorageService = ( · 34 unmodified lines | ||
| 311 | 325 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 312 | 326 | workspaceId: input.workspaceId, |
| 313 | 327 | instancesOperation: "storage.list_directory.instances", |
| 314 | − workspacesOperation: "storage.list_directory.workspaces", | |
| 328 | + workspaceOperation: "storage.list_directory.workspace", | |
| 315 | 329 | }); |
| 316 | 330 | |
| 317 | 331 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -356,7 +370,7 @@ export const createPmStorageService = ( · 38 unmodified lines | ||
| 356 | 370 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 357 | 371 | workspaceId: input.workspaceId, |
| 358 | 372 | instancesOperation: "storage.read_file.instances", |
| 359 | − workspacesOperation: "storage.read_file.workspaces", | |
| 373 | + workspaceOperation: "storage.read_file.workspace", | |
| 360 | 374 | }); |
| 361 | 375 | |
| 362 | 376 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -397,7 +411,7 @@ export const createPmStorageService = ( · 34 unmodified lines | ||
| 397 | 411 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 398 | 412 | workspaceId: input.workspaceId, |
| 399 | 413 | instancesOperation: "storage.list_kv.instances", |
| 400 | − workspacesOperation: "storage.list_kv.workspaces", | |
| 414 | + workspaceOperation: "storage.list_kv.workspace", | |
| 401 | 415 | }); |
| 402 | 416 | |
| 403 | 417 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -438,7 +452,7 @@ export const createPmStorageService = ( · 34 unmodified lines | ||
| 438 | 452 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 439 | 453 | workspaceId: input.workspaceId, |
| 440 | 454 | instancesOperation: "storage.query_sql.instances", |
| 441 | − workspacesOperation: "storage.query_sql.workspaces", | |
| 455 | + workspaceOperation: "storage.query_sql.workspace", | |
| 442 | 456 | }); |
| 443 | 457 | |
| 444 | 458 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -4,24 +4,25 @@ import { · 3 unmodified lines | ||
| 4 | 4 | } from "@executor-v2/engine"; |
| 5 | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | 6 | import * as Effect from "effect/Effect"; |
| 7 | +import * as Option from "effect/Option"; | |
| 7 | 8 | |
| 8 | 9 | import { createPmResolveToolCredentials } from "./credential-resolver"; |
| 9 | 10 | |
| 10 | 11 | const emptyCredentialRows = { |
| 11 | 12 | workspaces: { |
| 12 | − list: () => Effect.succeed([]), | |
| 13 | + getById: () => Effect.succeed(Option.none()), | |
| 13 | 14 | }, |
| 14 | 15 | sourceAuthBindings: { |
| 15 | − list: () => Effect.succeed([]), | |
| 16 | + listByWorkspaceScope: () => Effect.succeed([]), | |
| 16 | 17 | }, |
| 17 | 18 | authConnections: { |
| 18 | − list: () => Effect.succeed([]), | |
| 19 | + getById: () => Effect.succeed(Option.none()), | |
| 19 | 20 | }, |
| 20 | 21 | authMaterials: { |
| 21 | − list: () => Effect.succeed([]), | |
| 22 | + getByConnectionId: () => Effect.succeed(Option.none()), | |
| 22 | 23 | }, |
| 23 | 24 | oauthStates: { |
| 24 | − list: () => Effect.succeed([]), | |
| 25 | + getByConnectionId: () => Effect.succeed(Option.none()), | |
| 25 | 26 | }, |
| 26 | 27 | }; |
| 27 | 28 | |
| @@ -5,6 +5,7 @@ import { · 4 unmodified lines | ||
| 5 | 5 | } from "@executor-v2/management-api"; |
| 6 | 6 | import { type Workspace } from "@executor-v2/schema"; |
| 7 | 7 | import * as Effect from "effect/Effect"; |
| 8 | +import * as Option from "effect/Option"; | |
| 8 | 9 | |
| 9 | 10 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 10 | 11 | |
| @@ -41,17 +42,16 @@ export const createPmWorkspacesService = ( · 30 unmodified lines | ||
| 41 | 42 | |
| 42 | 43 | upsertWorkspace: (input) => |
| 43 | 44 | 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>(); | |
| 49 | 52 | |
| 50 | 53 | 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); | |
| 55 | 55 | |
| 56 | 56 | const nextWorkspace: Workspace = { |
| 57 | 57 | id: existing?.id ?? (input.payload.id ?? (`ws_${crypto.randomUUID()}` as Workspace["id"])), |
| @@ -103,14 +103,17 @@ const ensurePrincipalProvisioned = ( · 102 unmodified lines | ||
| 103 | 103 | ) => |
| 104 | 104 | Effect.gen(function* () { |
| 105 | 105 | 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"]), | |
| 110 | 113 | persistence.rows.profile.get(), |
| 111 | 114 | ]); |
| 112 | 115 | |
| 113 | − if (organizations.find((organization) => organization.id === principal.organizationId) === undefined) { | |
| 116 | + if (organizationOption._tag === "None") { | |
| 114 | 117 | yield* persistence.rows.organizations.upsert({ |
| 115 | 118 | id: principal.organizationId as Organization["id"], |
| 116 | 119 | slug: principal.organizationId, |
| @@ -124,13 +127,7 @@ const ensurePrincipalProvisioned = ( · 7 unmodified lines | ||
| 124 | 127 | }); |
| 125 | 128 | } |
| 126 | 129 | |
| 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") { | |
| 134 | 131 | yield* persistence.rows.organizationMemberships.upsert({ |
| 135 | 132 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], |
| 136 | 133 | organizationId: principal.organizationId as OrganizationMembership["organizationId"], |
| @@ -145,7 +142,7 @@ const ensurePrincipalProvisioned = ( · 8 unmodified lines | ||
| 145 | 142 | }); |
| 146 | 143 | } |
| 147 | 144 | |
| 148 | − if (workspaces.find((workspace) => workspace.id === principal.workspaceId) === undefined) { | |
| 145 | + if (workspaceOption._tag === "None") { | |
| 149 | 146 | yield* persistence.rows.workspaces.upsert({ |
| 150 | 147 | id: principal.workspaceId as Workspace["id"], |
| 151 | 148 | organizationId: principal.organizationId as Workspace["organizationId"], |
| @@ -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 | +}); | |
| @@ -1,32 +1,15 @@ | ||
| 1 | 1 | import { |
| 2 | − type RowStoreError, | |
| 3 | 2 | type SourceStore, |
| 4 | 3 | type ToolArtifactStore, |
| 5 | 4 | } 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"; | |
| 19 | 5 | import * as Data from "effect/Data"; |
| 20 | 6 | import * as Effect from "effect/Effect"; |
| 21 | −import * as Option from "effect/Option"; | |
| 22 | 7 | import * as path from "node:path"; |
| 23 | 8 | |
| 24 | −import { createRowOperations } from "./row-operations"; | |
| 25 | −import { createRowsEffectApi } from "./rows-effect-api"; | |
| 9 | +import { createControlPlaneRows } from "./control-plane-rows"; | |
| 26 | 10 | import { |
| 11 | + createSqlRuntime, | |
| 27 | 12 | createDrizzleContext, |
| 28 | − createPGliteAdapter, | |
| 29 | − createPostgresAdapter, | |
| 30 | 13 | runMigrations, |
| 31 | 14 | type SqlBackend, |
| 32 | 15 | } from "./sql-internals"; |
| @@ -38,76 +21,13 @@ export type SqlControlPlanePersistenceOptions = { · 5 unmodified lines | ||
| 38 | 21 | postgresApplicationName?: string; |
| 39 | 22 | }; |
| 40 | 23 | |
| 24 | +export type SqlControlPlaneRows = ReturnType<typeof createControlPlaneRows>; | |
| 25 | + | |
| 41 | 26 | export type SqlControlPlanePersistence = { |
| 42 | 27 | backend: SqlBackend; |
| 43 | 28 | sourceStore: SourceStore; |
| 44 | 29 | 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; | |
| 111 | 31 | close: () => Promise<void>; |
| 112 | 32 | }; |
| 113 | 33 | |
| @@ -123,129 +43,43 @@ const trim = (value: string | undefined): string | undefined => { · 9 unmodified lines | ||
| 123 | 43 | return candidate && candidate.length > 0 ? candidate : undefined; |
| 124 | 44 | }; |
| 125 | 45 | |
| 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 | − | |
| 138 | 46 | export const makeSqlControlPlanePersistence = ( |
| 139 | 47 | options: SqlControlPlanePersistenceOptions, |
| 140 | 48 | ): Effect.Effect<SqlControlPlanePersistence, SqlPersistenceBootstrapError> => |
| 141 | 49 | Effect.tryPromise({ |
| 142 | 50 | try: async () => { |
| 143 | − const databaseUrl = trim(options.databaseUrl); | |
| 144 | 51 | const localDataDir = path.resolve( |
| 145 | 52 | options.localDataDir ?? ".executor-v2/control-plane-pgdata", |
| 146 | 53 | ); |
| 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 | + }); | |
| 151 | 59 | |
| 152 | − const adapter = | |
| 153 | − backend === "postgres" | |
| 154 | − ? await createPostgresAdapter(databaseUrl!, trim(options.postgresApplicationName)) | |
| 155 | − : await createPGliteAdapter(localDataDir); | |
| 60 | + await runMigrations(runtime); | |
| 156 | 61 | |
| 157 | − await runMigrations(adapter); | |
| 158 | − const drizzleContext = createDrizzleContext(adapter); | |
| 62 | + const drizzleContext = createDrizzleContext(runtime.db); | |
| 159 | 63 | const { db, tables } = drizzleContext; |
| 160 | 64 | |
| 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, | |
| 196 | 67 | db, |
| 197 | 68 | tables, |
| 198 | − writeLocked: <A>(run: () => Promise<A>) => withWriteLock(writeQueueRef, run), | |
| 199 | 69 | }); |
| 200 | 70 | |
| 201 | − const { sourceStore, toolArtifactStore } = createSourceAndArtifactStores({ | |
| 202 | − backend, | |
| 203 | − adapter, | |
| 71 | + const rows = createControlPlaneRows({ | |
| 72 | + backend: runtime.backend, | |
| 204 | 73 | db, |
| 205 | 74 | tables, |
| 206 | − writeLocked: <A>(run: () => Promise<A>) => withWriteLock(writeQueueRef, run), | |
| 207 | 75 | }); |
| 208 | 76 | |
| 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 | − | |
| 243 | 77 | return { |
| 244 | − backend, | |
| 78 | + backend: runtime.backend, | |
| 245 | 79 | sourceStore, |
| 246 | 80 | toolArtifactStore, |
| 247 | 81 | rows, |
| 248 | − close: () => adapter.close(), | |
| 82 | + close: () => runtime.close(), | |
| 249 | 83 | }; |
| 250 | 84 | }, |
| 251 | 85 | catch: (cause) => { |
| @@ -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 | −}; | |
| @@ -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 | −}; | |
| @@ -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 | −}); | |
| @@ -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 | −}); | |
| @@ -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 | −}); | |
| @@ -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 | −}); | |
| @@ -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 | − }); | |
| @@ -15,21 +15,15 @@ import { · 14 unmodified lines | ||
| 15 | 15 | } from "./persistence-errors"; |
| 16 | 16 | import { tableNames } from "./schema"; |
| 17 | 17 | import { |
| 18 | − createDrizzleContext, | |
| 19 | 18 | type DrizzleDb, |
| 20 | 19 | type DrizzleTables, |
| 21 | − type SqlAdapter, | |
| 22 | 20 | type SqlBackend, |
| 23 | 21 | } from "./sql-internals"; |
| 24 | 22 | |
| 25 | −type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>; | |
| 26 | − | |
| 27 | 23 | type CreateStoresInput = { |
| 28 | 24 | backend: SqlBackend; |
| 29 | − adapter: SqlAdapter; | |
| 30 | 25 | db: DrizzleDb; |
| 31 | 26 | tables: DrizzleTables; |
| 32 | − writeLocked: WriteLocked; | |
| 33 | 27 | }; |
| 34 | 28 | |
| 35 | 29 | const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`; |
| @@ -76,10 +70,8 @@ const toToolArtifact = ( · 40 unmodified lines | ||
| 76 | 70 | |
| 77 | 71 | export const createSourceAndArtifactStores = ({ |
| 78 | 72 | backend, |
| 79 | − adapter, | |
| 80 | 73 | db, |
| 81 | 74 | tables, |
| 82 | − writeLocked, | |
| 83 | 75 | }: CreateStoresInput): { |
| 84 | 76 | sourceStore: SourceStore; |
| 85 | 77 | toolArtifactStore: ToolArtifactStore; |
| @@ -124,46 +116,40 @@ export const createSourceAndArtifactStores = ({ · 38 unmodified lines | ||
| 124 | 116 | upsert: (source: Source) => |
| 125 | 117 | Effect.tryPromise({ |
| 126 | 118 | 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 | + }, | |
| 165 | 152 | }); |
| 166 | − }); | |
| 167 | 153 | }, |
| 168 | 154 | catch: (cause) => |
| 169 | 155 | toSourceStoreError(backend, "upsert", tableNames.sources, cause), |
| @@ -171,37 +157,33 @@ export const createSourceAndArtifactStores = ({ · 1 unmodified line | ||
| 171 | 157 | |
| 172 | 158 | removeById: (workspaceId: WorkspaceId, sourceId: SourceId) => |
| 173 | 159 | 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); | |
| 188 | 171 | |
| 189 | − if (existing.length === 0) { | |
| 190 | − return false; | |
| 191 | − } | |
| 172 | + if (existing.length === 0) { | |
| 173 | + return false; | |
| 174 | + } | |
| 192 | 175 | |
| 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 | + ); | |
| 201 | 184 | |
| 202 | − return true; | |
| 203 | − }) | |
| 204 | − ), | |
| 185 | + return true; | |
| 186 | + }, | |
| 205 | 187 | catch: (cause) => |
| 206 | 188 | toSourceStoreError(backend, "remove_by_id", tableNames.sources, cause), |
| 207 | 189 | }), |
| @@ -241,38 +223,32 @@ export const createSourceAndArtifactStores = ({ · 33 unmodified lines | ||
| 241 | 223 | upsert: (artifact: ToolArtifact) => |
| 242 | 224 | Effect.tryPromise({ |
| 243 | 225 | 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 | + }, | |
| 274 | 251 | }); |
| 275 | − }); | |
| 276 | 252 | }, |
| 277 | 253 | catch: (cause) => |
| 278 | 254 | toToolArtifactStoreError(backend, "upsert", tableNames.toolArtifacts, cause), |
| @@ -1,11 +1,13 @@ | ||
| 1 | 1 | 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"; | |
| 4 | 6 | import { existsSync } from "node:fs"; |
| 5 | 7 | import { mkdir } from "node:fs/promises"; |
| 6 | 8 | import * as path from "node:path"; |
| 7 | 9 | import { fileURLToPath } from "node:url"; |
| 8 | −import postgres, { type Sql } from "postgres"; | |
| 10 | +import postgres from "postgres"; | |
| 9 | 11 | |
| 10 | 12 | import { |
| 11 | 13 | approvalsTable, |
| @@ -26,160 +28,80 @@ import { · 14 unmodified lines | ||
| 26 | 28 | } from "./schema"; |
| 27 | 29 | |
| 28 | 30 | 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; | |
| 40 | 35 | }; |
| 41 | 36 | |
| 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, | |
| 48 | 53 | }; |
| 49 | 54 | |
| 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 }); | |
| 61 | 57 | |
| 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>; | |
| 70 | 60 | |
| 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; | |
| 80 | 63 | |
| 81 | − throw error; | |
| 82 | − } | |
| 83 | − }; | |
| 64 | +type SqlRuntime = { | |
| 65 | + backend: SqlBackend; | |
| 66 | + db: DrizzleDb; | |
| 67 | + close: () => Promise<void>; | |
| 68 | +}; | |
| 84 | 69 | |
| 85 | −export const createPGliteAdapter = async (localDataDir: string): Promise<SqlAdapter> => { | |
| 70 | +const createPGliteRuntime = async (localDataDir: string): Promise<SqlRuntime> => { | |
| 86 | 71 | const resolvedDataDir = path.resolve(localDataDir); |
| 87 | − await mkdir(path.dirname(resolvedDataDir), { recursive: true }); | |
| 72 | + await mkdir(resolvedDataDir, { recursive: true }); | |
| 88 | 73 | |
| 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); | |
| 105 | 76 | |
| 106 | 77 | return { |
| 107 | 78 | backend: "pglite", |
| 108 | − query, | |
| 109 | − execute, | |
| 110 | − transaction: makePGliteTransaction(execute, query), | |
| 79 | + db, | |
| 111 | 80 | close: async () => { |
| 112 | − await db.close(); | |
| 81 | + await client.close(); | |
| 113 | 82 | }, |
| 114 | 83 | }; |
| 115 | 84 | }; |
| 116 | 85 | |
| 117 | −export const createPostgresAdapter = async ( | |
| 86 | +const createPostgresRuntime = async ( | |
| 118 | 87 | databaseUrl: string, |
| 119 | 88 | applicationName: string | undefined, |
| 120 | −): Promise<SqlAdapter> => { | |
| 89 | +): Promise<SqlRuntime> => { | |
| 121 | 90 | const client = postgres(databaseUrl, { |
| 122 | 91 | prepare: false, |
| 123 | 92 | max: 10, |
| 124 | 93 | ...(applicationName ? { connection: { application_name: applicationName } } : {}), |
| 125 | 94 | }); |
| 126 | 95 | |
| 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); | |
| 135 | 97 | |
| 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 { | |
| 158 | 99 | 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, | |
| 177 | 101 | close: async () => { |
| 178 | 102 | await client.end({ timeout: 5 }); |
| 179 | 103 | }, |
| 180 | 104 | }; |
| 181 | − | |
| 182 | − return adapter; | |
| 183 | 105 | }; |
| 184 | 106 | |
| 185 | 107 | const resolveDrizzleMigrationsFolder = (): string => { |
| @@ -202,105 +124,37 @@ const resolveDrizzleMigrationsFolder = (): string => { · 16 unmodified lines | ||
| 202 | 124 | throw new Error("Unable to resolve drizzle migrations folder"); |
| 203 | 125 | }; |
| 204 | 126 | |
| 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 | − | |
| 267 | 127 | export type DrizzleContext = { |
| 268 | 128 | db: DrizzleDb; |
| 269 | 129 | tables: DrizzleTables; |
| 270 | 130 | }; |
| 271 | 131 | |
| 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, | |
| 292 | 134 | tables: drizzleSchema, |
| 293 | 135 | }); |
| 294 | 136 | |
| 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> => { | |
| 297 | 152 | const migrationsFolder = resolveDrizzleMigrationsFolder(); |
| 298 | 153 | |
| 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 }); | |
| 306 | 160 | }; |