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 | return isTruthy(configured); | 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 | const workspaceMembershipsForAccount = ( | 40 | const workspaceMembershipsForAccount = ( |
| 49 | workspaces: ReadonlyArray<Workspace>, | 41 | workspaces: ReadonlyArray<Workspace>, |
| 50 | accountId: OrganizationMembership["accountId"], | 42 | accountId: OrganizationMembership["accountId"], |
| @@ -66,10 +58,20 @@ const resolveActorFromSnapshot = ( · 15 unmodified lines | |||
| 66 | Effect.gen(function* () { | 58 | Effect.gen(function* () { |
| 67 | const principal = yield* requirePrincipalFromHeaders(headers); | 59 | const principal = yield* requirePrincipalFromHeaders(headers); |
| 68 | 60 | ||
| 69 | const [memberships, workspaces] = yield* Effect.all([ | 61 | const memberships = yield* rows.organizationMemberships |
| 70 | rows.organizationMemberships.list(), | 62 | .listByAccountId(principal.accountId) |
| 71 | rows.workspaces.list(), | 63 | .pipe( |
| 72 | ]).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 | Effect.mapError( | 75 | Effect.mapError( |
| 74 | (error) => | 76 | (error) => |
| 75 | new ActorUnauthenticatedError({ | 77 | new ActorUnauthenticatedError({ |
| @@ -82,10 +84,7 @@ const resolveActorFromSnapshot = ( · 6 unmodified lines | |||
| 82 | return makeAllowAllActor(principal); | 84 | return makeAllowAllActor(principal); |
| 83 | } | 85 | } |
| 84 | 86 | ||
| 85 | const organizationMemberships = organizationMembershipsForAccount( | 87 | const organizationMemberships = memberships; |
| 86 | memberships, | ||
| 87 | principal.accountId, | ||
| 88 | ); | ||
| 89 | const workspaceMemberships = workspaceMembershipsForAccount( | 88 | const workspaceMemberships = workspaceMembershipsForAccount( |
| 90 | workspaces, | 89 | workspaces, |
| 91 | principal.accountId, | 90 | principal.accountId, |
| @@ -106,10 +105,19 @@ export const PmActorLive = (rows: ActorRows) => · 14 unmodified lines | |||
| 106 | Effect.gen(function* () { | 105 | Effect.gen(function* () { |
| 107 | const principal = yield* requirePrincipalFromHeaders(input.headers); | 106 | const principal = yield* requirePrincipalFromHeaders(input.headers); |
| 108 | 107 | ||
| 109 | const [memberships, workspaces] = yield* Effect.all([ | 108 | const memberships = yield* rows.organizationMemberships |
| 110 | rows.organizationMemberships.list(), | 109 | .listByAccountId(principal.accountId) |
| 111 | rows.workspaces.list(), | 110 | .pipe( |
| 112 | ]).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 | Effect.mapError( | 121 | Effect.mapError( |
| 114 | (error) => | 122 | (error) => |
| 115 | new ActorUnauthenticatedError({ | 123 | new ActorUnauthenticatedError({ |
| @@ -122,10 +130,7 @@ export const PmActorLive = (rows: ActorRows) => · 6 unmodified lines | |||
| 122 | return makeAllowAllActor(principal); | 130 | return makeAllowAllActor(principal); |
| 123 | } | 131 | } |
| 124 | 132 | ||
| 125 | const organizationMemberships = organizationMembershipsForAccount( | 133 | const organizationMemberships = memberships; |
| 126 | memberships, | ||
| 127 | principal.accountId, | ||
| 128 | ); | ||
| 129 | 134 | ||
| 130 | const workspace = workspaces.find((item) => item.id === input.workspaceId) ?? null; | 135 | const workspace = workspaces.find((item) => item.id === input.workspaceId) ?? null; |
| 131 | 136 | ||
| @@ -12,6 +12,7 @@ import { · 11 unmodified lines | |||
| 12 | } from "@executor-v2/management-api"; | 12 | } from "@executor-v2/management-api"; |
| 13 | import { type Approval } from "@executor-v2/schema"; | 13 | import { type Approval } from "@executor-v2/schema"; |
| 14 | import * as Effect from "effect/Effect"; | 14 | import * as Effect from "effect/Effect"; |
| 15 | import * as Option from "effect/Option"; | ||
| 15 | 16 | ||
| 16 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | 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 | const sourceStoreError = createSqlSourceStoreErrorMapper("approvals"); | 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 | const toPersistentApprovalStoreError = ( | 23 | const toPersistentApprovalStoreError = ( |
| 35 | operation: string, | 24 | operation: string, |
| 36 | message: string, | 25 | message: string, |
| @@ -69,50 +58,45 @@ export const createPmPersistentToolApprovalPolicy = ( · 32 unmodified lines | |||
| 69 | ): ToolApprovalPolicy => { | 58 | ): ToolApprovalPolicy => { |
| 70 | const store: PersistentToolApprovalStore = { | 59 | const store: PersistentToolApprovalStore = { |
| 71 | findByRunAndCall: (input) => | 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 | Effect.mapError((error) => | 68 | Effect.mapError((error) => |
| 74 | toPersistentApprovalStoreErrorFromRowStore("approvals.read", error), | 69 | toPersistentApprovalStoreErrorFromRowStore("approvals.read", error), |
| 75 | ), | 70 | ), |
| 76 | Effect.flatMap((approvals) => { | 71 | Effect.flatMap((approvalOption) => { |
| 77 | const approval = | 72 | const approval = Option.getOrNull(approvalOption); |
| 78 | approvals.find( | 73 | return Effect.succeed(approval !== null ? toPersistentApprovalRecord(approval) : null); |
| 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); | ||
| 86 | }), | 74 | }), |
| 87 | ), | 75 | ), |
| 88 | 76 | ||
| 89 | createPending: (input) => | 77 | createPending: (input) => |
| 90 | rows.approvals.list().pipe( | 78 | Effect.gen(function* () { |
| 91 | Effect.mapError((error) => | 79 | const pendingApproval: Approval = { |
| 92 | toPersistentApprovalStoreErrorFromRowStore("approvals.read", error), | 80 | id: `apr_${crypto.randomUUID()}` as Approval["id"], |
| 93 | ), | 81 | workspaceId: input.workspaceId as Approval["workspaceId"], |
| 94 | Effect.flatMap(() => { | 82 | taskRunId: input.runId as Approval["taskRunId"], |
| 95 | const pendingApproval = { | 83 | callId: input.callId, |
| 96 | id: `apr_${crypto.randomUUID()}`, | 84 | toolPath: input.toolPath, |
| 97 | workspaceId: input.workspaceId, | 85 | status: "pending", |
| 98 | taskRunId: input.runId, | 86 | inputPreviewJson: input.inputPreviewJson, |
| 99 | callId: input.callId, | 87 | reason: null, |
| 100 | toolPath: input.toolPath, | 88 | requestedAt: Date.now(), |
| 101 | status: "pending", | 89 | resolvedAt: null, |
| 102 | inputPreviewJson: input.inputPreviewJson, | 90 | }; |
| 103 | reason: null, | 91 | |
| 104 | requestedAt: Date.now(), | 92 | yield* rows.approvals.upsert(pendingApproval).pipe( |
| 105 | resolvedAt: null, | 93 | Effect.mapError((error) => |
| 106 | } as Approval; | 94 | toPersistentApprovalStoreErrorFromRowStore("approvals.write", error), |
| 107 | 95 | ), | |
| 108 | return rows.approvals.upsert(pendingApproval).pipe( | 96 | ); |
| 109 | Effect.mapError((error) => | 97 | |
| 110 | toPersistentApprovalStoreErrorFromRowStore("approvals.write", error), | 98 | return toPersistentApprovalRecord(pendingApproval); |
| 111 | ), | 99 | }), |
| 112 | Effect.as(toPersistentApprovalRecord(pendingApproval)), | ||
| 113 | ); | ||
| 114 | }), | ||
| 115 | ), | ||
| 116 | }; | 100 | }; |
| 117 | 101 | ||
| 118 | return createPersistentToolApprovalPolicy({ | 102 | return createPersistentToolApprovalPolicy({ |
| @@ -128,34 +112,25 @@ export const createPmApprovalsService = ( · 9 unmodified lines | |||
| 128 | makeControlPlaneApprovalsService({ | 112 | makeControlPlaneApprovalsService({ |
| 129 | listApprovals: (workspaceId) => | 113 | listApprovals: (workspaceId) => |
| 130 | Effect.gen(function* () { | 114 | Effect.gen(function* () { |
| 131 | const approvals = yield* rows.approvals.list().pipe( | 115 | const approvals = yield* rows.approvals.listByWorkspaceId(workspaceId).pipe( |
| 132 | Effect.mapError((error) => | 116 | Effect.mapError((error) => |
| 133 | sourceStoreError.fromRowStore("approvals.list", error), | 117 | sourceStoreError.fromRowStore("approvals.list", error), |
| 134 | ), | 118 | ), |
| 135 | ); | 119 | ); |
| 136 | 120 | ||
| 137 | const scopedApprovals = approvals.filter( | 121 | return approvals; |
| 138 | (approval) => approval.workspaceId === workspaceId, | ||
| 139 | ); | ||
| 140 | |||
| 141 | return sortApprovals(scopedApprovals); | ||
| 142 | }), | 122 | }), |
| 143 | 123 | ||
| 144 | resolveApproval: (input) => | 124 | resolveApproval: (input) => |
| 145 | Effect.gen(function* () { | 125 | Effect.gen(function* () { |
| 146 | const approvals = yield* rows.approvals.list().pipe( | 126 | const approvalOption = yield* rows.approvals.getById(input.approvalId).pipe( |
| 147 | Effect.mapError((error) => | 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( | 132 | const approval = Option.getOrNull(approvalOption); |
| 153 | approvals, | 133 | if (approval === null || approval.workspaceId !== input.workspaceId) { |
| 154 | input.workspaceId, | ||
| 155 | input.approvalId, | ||
| 156 | ); | ||
| 157 | |||
| 158 | if (index < 0) { | ||
| 159 | return yield* sourceStoreError.fromMessage( | 134 | return yield* sourceStoreError.fromMessage( |
| 160 | "approvals.resolve", | 135 | "approvals.resolve", |
| 161 | "Approval not found", | 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 | if (approval.status !== "pending") { | 141 | if (approval.status !== "pending") { |
| 168 | return yield* sourceStoreError.fromMessage( | 142 | return yield* sourceStoreError.fromMessage( |
| 169 | "approvals.resolve", | 143 | "approvals.resolve", |
| @@ -1,9 +1,4 @@ | |||
| 1 | import { SourceStoreError } from "@executor-v2/persistence-ports"; | 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 | type RowStoreLikeError = { | 3 | type RowStoreLikeError = { |
| 9 | message: string; | 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 | sourceIdFromSourceKey, | 5 | sourceIdFromSourceKey, |
| 6 | type ResolveToolCredentials, | 6 | type ResolveToolCredentials, |
| 7 | } from "@executor-v2/engine"; | 7 | } from "@executor-v2/engine"; |
| 8 | import { type RowStoreError } from "@executor-v2/persistence-ports"; | 8 | import { type SqlControlPlanePersistence } from "@executor-v2/persistence-sql"; |
| 9 | import { | 9 | import { type OrganizationId, type WorkspaceId } from "@executor-v2/schema"; |
| 10 | type AuthConnection, | ||
| 11 | type AuthMaterial, | ||
| 12 | type OAuthState, | ||
| 13 | type SourceAuthBinding, | ||
| 14 | type Workspace, | ||
| 15 | } from "@executor-v2/schema"; | ||
| 16 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 17 | 11 | ||
| 18 | type RowReader<A> = { | ||
| 19 | list: () => Effect.Effect<ReadonlyArray<A>, RowStoreError>; | ||
| 20 | }; | ||
| 21 | |||
| 22 | type CredentialResolverRows = { | 12 | type CredentialResolverRows = { |
| 23 | workspaces: RowReader<Workspace>; | 13 | workspaces: Pick<SqlControlPlanePersistence["rows"]["workspaces"], "getById">; |
| 24 | sourceAuthBindings: RowReader<SourceAuthBinding>; | 14 | sourceAuthBindings: Pick< |
| 25 | authConnections: RowReader<AuthConnection>; | 15 | SqlControlPlanePersistence["rows"]["sourceAuthBindings"], |
| 26 | authMaterials: RowReader<AuthMaterial>; | 16 | "listByWorkspaceScope" |
| 27 | oauthStates: RowReader<OAuthState>; | 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 | const toCredentialResolverError = ( | 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([ | 176 | const sourceId = sourceIdFromSourceKey(context.sourceKey); |
| 184 | rows.workspaces.list(), | 177 | if (!sourceId) { |
| 185 | rows.sourceAuthBindings.list(), | 178 | return { |
| 186 | rows.authConnections.list(), | 179 | headers: {}, |
| 187 | rows.oauthStates.list(), | 180 | }; |
| 188 | rows.authMaterials.list(), | 181 | } |
| 189 | ]).pipe( | 182 | |
| 183 | const workspaceId = context.workspaceId as WorkspaceId; | ||
| 184 | |||
| 185 | const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe( | ||
| 190 | Effect.mapError((error) => | 186 | Effect.mapError((error) => |
| 191 | toCredentialResolverError( | 187 | toCredentialResolverError( |
| 192 | "read_credential_rows", | 188 | "read_workspace", |
| 193 | "Failed reading credential rows while resolving credentials", | 189 | "Failed reading workspace while resolving credentials", |
| 194 | error.details ?? error.message, | 190 | error.details ?? error.message, |
| 195 | ), | 191 | ), |
| 196 | ), | 192 | ), |
| 197 | ); | 193 | ); |
| 198 | 194 | ||
| 199 | const sourceId = sourceIdFromSourceKey(context.sourceKey); | 195 | const workspace = workspaceOption._tag === "Some" ? workspaceOption.value : null; |
| 200 | if (!sourceId) { | 196 | const organizationId = context.organizationId ?? workspace?.organizationId ?? null; |
| 197 | if (organizationId === null) { | ||
| 201 | return { | 198 | return { |
| 202 | headers: {}, | 199 | headers: {}, |
| 203 | }; | 200 | }; |
| 204 | } | 201 | } |
| 205 | 202 | ||
| 206 | const workspace = workspaces.find( | 203 | const bindings = yield* rows.sourceAuthBindings |
| 207 | (item) => item.id === context.workspaceId, | 204 | .listByWorkspaceScope(workspaceId, organizationId as OrganizationId) |
| 208 | ); | 205 | .pipe( |
| 209 | const organizationId = context.organizationId ?? workspace?.organizationId ?? null; | 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 | const binding = bindings | 215 | const binding = bindings |
| 212 | .filter((candidate) => candidate.sourceId === sourceId) | 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( | 254 | const connectionOption = yield* rows.authConnections.getById(binding.connectionId).pipe( |
| 251 | (candidate) => candidate.id === binding.connectionId, | 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 | if (!connection || connection.status !== "active") { | 267 | if (!connection || connection.status !== "active") { |
| 254 | return { | 268 | return { |
| 255 | headers: {}, | 269 | headers: {}, |
| @@ -261,9 +275,18 @@ export const createPmResolveToolCredentials = ( · 5 unmodified lines | |||
| 261 | ); | 275 | ); |
| 262 | 276 | ||
| 263 | if (connection.strategy === "oauth2") { | 277 | if (connection.strategy === "oauth2") { |
| 264 | const oauthState = oauthStates.find( | 278 | const oauthStateOption = yield* rows.oauthStates |
| 265 | (candidate) => candidate.connectionId === connection.id, | 279 | .getByConnectionId(connection.id) |
| 266 | ); | 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 | const accessToken = normalizeString(oauthState?.accessTokenCiphertext); | 291 | const accessToken = normalizeString(oauthState?.accessTokenCiphertext); |
| 269 | const oauthHeaders = accessToken | 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( | 301 | const materialOption = yield* rows.authMaterials |
| 279 | (candidate) => candidate.connectionId === connection.id, | 302 | .getByConnectionId(connection.id) |
| 280 | ); | 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 | if (!material) { | 314 | if (!material) { |
| 283 | return { | 315 | return { |
| @@ -9,14 +9,11 @@ import { · 8 unmodified lines | |||
| 9 | type OAuthState, | 9 | type OAuthState, |
| 10 | type SourceAuthBinding, | 10 | type SourceAuthBinding, |
| 11 | type SourceCredentialBinding, | 11 | type SourceCredentialBinding, |
| 12 | type WorkspaceId, | ||
| 13 | } from "@executor-v2/schema"; | 12 | } from "@executor-v2/schema"; |
| 14 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Option from "effect/Option"; | ||
| 15 | 15 | ||
| 16 | import { | 16 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 17 | createSqlSourceStoreErrorMapper, | ||
| 18 | resolveWorkspaceOrganizationId, | ||
| 19 | } from "./control-plane-row-helpers"; | ||
| 20 | import { | 17 | import { |
| 21 | buildOAuthRefreshConfigFromPayload, | 18 | buildOAuthRefreshConfigFromPayload, |
| 22 | encodeOAuthRefreshConfig, | 19 | encodeOAuthRefreshConfig, |
| @@ -46,38 +43,42 @@ export const createPmCredentialsService = ( · 23 unmodified lines | |||
| 46 | makeControlPlaneCredentialsService({ | 43 | makeControlPlaneCredentialsService({ |
| 47 | listCredentialBindings: (workspaceId) => | 44 | listCredentialBindings: (workspaceId) => |
| 48 | Effect.gen(function* () { | 45 | Effect.gen(function* () { |
| 49 | const [bindings, connections, workspaces] = yield* Effect.all([ | 46 | const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe( |
| 50 | rows.sourceAuthBindings.list().pipe( | 47 | Effect.mapError((error) => |
| 51 | Effect.mapError((error) => | 48 | sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", error), |
| 52 | sourceStoreError.fromRowStore("credentials.bindings.list", error), | ||
| 53 | ), | ||
| 54 | ), | 49 | ), |
| 55 | rows.authConnections.list().pipe( | 50 | ); |
| 56 | Effect.mapError((error) => | 51 | |
| 57 | sourceStoreError.fromRowStore("credentials.connections.list", error), | 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 | ), | 69 | rows.authConnections.listByOrganizationId(workspace.organizationId).pipe( |
| 60 | rows.workspaces.list().pipe( | ||
| 61 | Effect.mapError((error) => | 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); | 76 | const connectionById = new Map(connections.map((connection) => [connection.id, connection])); |
| 68 | |||
| 69 | const scopedBindings = bindings.filter( | ||
| 70 | (binding) => | ||
| 71 | binding.workspaceId === workspaceId | ||
| 72 | || (binding.workspaceId === null && binding.organizationId === organizationId), | ||
| 73 | ); | ||
| 74 | 77 | ||
| 75 | const compatBindings: Array<SourceCredentialBinding> = []; | 78 | const compatBindings: Array<SourceCredentialBinding> = []; |
| 76 | 79 | ||
| 77 | for (const binding of scopedBindings) { | 80 | for (const binding of bindings) { |
| 78 | const connection = connections.find( | 81 | const connection = connectionById.get(binding.connectionId); |
| 79 | (candidate) => candidate.id === binding.connectionId, | ||
| 80 | ); | ||
| 81 | 82 | ||
| 82 | if (!connection) { | 83 | if (!connection) { |
| 83 | continue; | 84 | continue; |
| @@ -91,33 +92,22 @@ export const createPmCredentialsService = ( · 7 unmodified lines | |||
| 91 | 92 | ||
| 92 | upsertCredentialBinding: (input) => | 93 | upsertCredentialBinding: (input) => |
| 93 | Effect.gen(function* () { | 94 | Effect.gen(function* () { |
| 94 | const [bindings, connections, materials, oauthStates, workspaces] = yield* Effect.all([ | 95 | const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe( |
| 95 | rows.sourceAuthBindings.list().pipe( | 96 | Effect.mapError((error) => |
| 96 | Effect.mapError((error) => | 97 | sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", 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 | ), | ||
| 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 | if (input.payload.scopeType === "account" && input.payload.accountId === null) { | 112 | if (input.payload.scopeType === "account" && input.payload.accountId === null) { |
| 123 | return yield* sourceStoreError.fromMessage( | 113 | return yield* sourceStoreError.fromMessage( |
| @@ -140,11 +130,29 @@ export const createPmCredentialsService = ( · 16 unmodified lines | |||
| 140 | const requestedId = input.payload.id; | 130 | const requestedId = input.payload.id; |
| 141 | const requestedBindingId = requestedId as SourceAuthBinding["id"] | undefined; | 131 | const requestedBindingId = requestedId as SourceAuthBinding["id"] | undefined; |
| 142 | 132 | ||
| 143 | const existingBinding = requestedBindingId | 133 | const existingBindingOption = requestedBindingId |
| 144 | ? bindings.find((binding) => binding.id === requestedBindingId) ?? null | 134 | ? yield* rows.sourceAuthBindings.getById(requestedBindingId).pipe( |
| 145 | : null; | 135 | Effect.mapError((error) => |
| 146 | 136 | sourceStoreError.fromRowStore("credentials.binding.get_by_id", error), | |
| 147 | const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId); | 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 | const scopeWorkspaceId = | 157 | const scopeWorkspaceId = |
| 150 | input.payload.scopeType === "workspace" ? input.workspaceId : null; | 158 | input.payload.scopeType === "workspace" ? input.workspaceId : null; |
| @@ -163,9 +171,15 @@ export const createPmCredentialsService = ( · 12 unmodified lines | |||
| 163 | ?? (`conn_${crypto.randomUUID()}` as AuthConnection["id"]) | 171 | ?? (`conn_${crypto.randomUUID()}` as AuthConnection["id"]) |
| 164 | ) as AuthConnection["id"]; | 172 | ) as AuthConnection["id"]; |
| 165 | 173 | ||
| 166 | const existingConnection = connections.find( | 174 | const existingConnectionOption = yield* rows.authConnections |
| 167 | (connection) => connection.id === requestedConnectionId, | 175 | .getById(requestedConnectionId) |
| 168 | ) ?? null; | 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 | if (existingConnection && existingConnection.organizationId !== organizationId) { | 184 | if (existingConnection && existingConnection.organizationId !== organizationId) { |
| 171 | return yield* sourceStoreError.fromMessage( | 185 | return yield* sourceStoreError.fromMessage( |
| @@ -228,9 +242,15 @@ export const createPmCredentialsService = ( · 56 unmodified lines | |||
| 228 | ); | 242 | ); |
| 229 | 243 | ||
| 230 | if (nextConnection.strategy === "oauth2") { | 244 | if (nextConnection.strategy === "oauth2") { |
| 231 | const existingOAuth = oauthStates.find( | 245 | const existingOAuthOption = yield* rows.oauthStates |
| 232 | (state) => state.connectionId === requestedConnectionId, | 246 | .getByConnectionId(requestedConnectionId) |
| 233 | ) ?? null; | 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 | const refreshConfig = buildOAuthRefreshConfigFromPayload( | 254 | const refreshConfig = buildOAuthRefreshConfigFromPayload( |
| 235 | input.payload, | 255 | input.payload, |
| 236 | parseOAuthRefreshConfig(existingOAuth?.refreshConfigJson ?? null), | 256 | parseOAuthRefreshConfig(existingOAuth?.refreshConfigJson ?? null), |
| @@ -282,9 +302,14 @@ export const createPmCredentialsService = ( · 45 unmodified lines | |||
| 282 | ), | 302 | ), |
| 283 | ); | 303 | ); |
| 284 | } else { | 304 | } else { |
| 285 | const existingMaterial = materials.find( | 305 | const existingMaterialOption = yield* rows.authMaterials |
| 286 | (material) => material.connectionId === requestedConnectionId, | 306 | .getByConnectionId(requestedConnectionId) |
| 287 | ) ?? null; | 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 | const material: AuthMaterial = { | 314 | const material: AuthMaterial = { |
| 290 | id: | 315 | id: |
| @@ -312,30 +337,35 @@ export const createPmCredentialsService = ( · 21 unmodified lines | |||
| 312 | 337 | ||
| 313 | removeCredentialBinding: (input) => | 338 | removeCredentialBinding: (input) => |
| 314 | Effect.gen(function* () { | 339 | Effect.gen(function* () { |
| 315 | const [bindings, workspaces] = yield* Effect.all([ | 340 | const workspaceOption = yield* rows.workspaces.getById(input.workspaceId).pipe( |
| 316 | rows.sourceAuthBindings.list().pipe( | 341 | Effect.mapError((error) => |
| 317 | Effect.mapError((error) => | 342 | sourceStoreError.fromRowStore("credentials.workspaces.get_by_id", 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 | ), | ||
| 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); | 353 | const bindingOption = yield* rows.sourceAuthBindings |
| 329 | const binding = bindings.find( | 354 | .getById(input.credentialBindingId) |
| 330 | (item) => | 355 | .pipe( |
| 331 | item.id === input.credentialBindingId | 356 | Effect.mapError((error) => |
| 332 | && ( | 357 | sourceStoreError.fromRowStore("credentials.binding.get_by_id", error), |
| 333 | item.workspaceId === input.workspaceId | ||
| 334 | || (item.workspaceId === null && item.organizationId === organizationId) | ||
| 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 | return { | 369 | return { |
| 340 | removed: false, | 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( | 388 | const remainingBindings = yield* rows.sourceAuthBindings |
| 359 | (candidate) => | 389 | .listByConnectionId(binding.connectionId) |
| 360 | candidate.id !== binding.id && candidate.connectionId === binding.connectionId, | 390 | .pipe( |
| 361 | ); | 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 | if (!hasRemainingBindings) { | 398 | if (!hasRemainingBindings) { |
| 364 | yield* Effect.all([ | 399 | yield* Effect.all([ |
| @@ -109,10 +109,31 @@ const withPmProcess = (port: number, localDataDir: string) => · 108 unmodified lines | |||
| 109 | }), | 109 | }), |
| 110 | ), | 110 | ), |
| 111 | (child) => | 111 | (child) => |
| 112 | Effect.sync(() => { | 112 | Effect.promise(async () => { |
| 113 | if (!child.killed) { | 113 | if (child.exitCode !== null || child.killed) { |
| 114 | child.kill("SIGTERM"); | 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 | const parsePort = (value: string | undefined): number => { | 60 | const parsePort = (value: string | undefined): number => { |
| 61 | const parsed = Number.parseInt(value ?? "", 10); | 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 | const readConfiguredRuntimeKind = (value: string | undefined): string | undefined => { | 65 | const readConfiguredRuntimeKind = (value: string | undefined): string | undefined => { |
| @@ -102,17 +102,21 @@ const ensurePmBootstrap = ( · 36 unmodified lines | |||
| 102 | ) => | 102 | ) => |
| 103 | Effect.gen(function* () { | 103 | Effect.gen(function* () { |
| 104 | const now = Date.now(); | 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 | const organizationId = "org_local" as OrganizationId; | 106 | const organizationId = "org_local" as OrganizationId; |
| 113 | const accountId = "acct_local" as AccountId; | 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 | yield* persistence.rows.organizations.upsert({ | 120 | yield* persistence.rows.organizations.upsert({ |
| 117 | id: organizationId, | 121 | id: organizationId, |
| 118 | slug: organizationId, | 122 | slug: organizationId, |
| @@ -124,11 +128,7 @@ const ensurePmBootstrap = ( · 5 unmodified lines | |||
| 124 | }); | 128 | }); |
| 125 | } | 129 | } |
| 126 | 130 | ||
| 127 | if ( | 131 | if (membershipOption._tag === "None") { |
| 128 | memberships.find( | ||
| 129 | (item) => item.organizationId === organizationId && item.accountId === accountId, | ||
| 130 | ) === undefined | ||
| 131 | ) { | ||
| 132 | yield* persistence.rows.organizationMemberships.upsert({ | 132 | yield* persistence.rows.organizationMemberships.upsert({ |
| 133 | id: "org_member_local" as OrganizationMemberId, | 133 | id: "org_member_local" as OrganizationMemberId, |
| 134 | organizationId, | 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 | yield* persistence.rows.workspaces.upsert({ | 147 | yield* persistence.rows.workspaces.upsert({ |
| 148 | id: workspaceId, | 148 | id: workspaceId, |
| 149 | organizationId, | 149 | organizationId, |
| @@ -8,6 +8,7 @@ import { · 7 unmodified lines | |||
| 8 | type OrganizationMembership, | 8 | type OrganizationMembership, |
| 9 | } from "@executor-v2/schema"; | 9 | } from "@executor-v2/schema"; |
| 10 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Option from "effect/Option"; | ||
| 11 | 12 | ||
| 12 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | 13 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 13 | 14 | ||
| @@ -49,28 +50,20 @@ export const createPmOrganizationsService = ( · 35 unmodified lines | |||
| 49 | 50 | ||
| 50 | upsertOrganization: (input) => | 51 | upsertOrganization: (input) => |
| 51 | Effect.gen(function* () { | 52 | Effect.gen(function* () { |
| 52 | const organizations = yield* rows.organizations.list().pipe( | 53 | const existingOption = input.payload.id |
| 53 | Effect.mapError((error) => | 54 | ? yield* rows.organizations.getById(input.payload.id).pipe( |
| 54 | sourceStoreError.fromRowStore("organizations.upsert", error), | 55 | Effect.mapError((error) => |
| 55 | ), | 56 | sourceStoreError.fromRowStore("organizations.get_by_id", error), |
| 56 | ); | ||
| 57 | |||
| 58 | const memberships = yield* rows.organizationMemberships.list().pipe( | ||
| 59 | Effect.mapError((error) => | ||
| 60 | sourceStoreError.fromRowStore( | ||
| 61 | "organizations.memberships.list", | ||
| 62 | error, | ||
| 63 | ), | 57 | ), |
| 64 | ), | 58 | ) |
| 65 | ); | 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 | const now = Date.now(); | 65 | const now = Date.now(); |
| 68 | const existingIndex = input.payload.id | 66 | const existing = Option.getOrNull(existingOption); |
| 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; | ||
| 74 | 67 | ||
| 75 | const nextOrganization: Organization = { | 68 | const nextOrganization: Organization = { |
| 76 | id: | 69 | id: |
| @@ -94,13 +87,21 @@ export const createPmOrganizationsService = ( · 17 unmodified lines | |||
| 94 | ); | 87 | ); |
| 95 | 88 | ||
| 96 | if (existing === null && nextOrganization.createdByAccountId !== null) { | 89 | if (existing === null && nextOrganization.createdByAccountId !== null) { |
| 97 | const existingMembership = memberships.find( | 90 | const existingMembership = yield* rows.organizationMemberships |
| 98 | (membership) => | 91 | .getByOrganizationAndAccount( |
| 99 | membership.organizationId === nextOrganization.id | 92 | nextOrganization.id, |
| 100 | && membership.accountId === nextOrganization.createdByAccountId, | 93 | nextOrganization.createdByAccountId, |
| 101 | ); | 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 | const membership: OrganizationMembership = { | 105 | const membership: OrganizationMembership = { |
| 105 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], | 106 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], |
| 106 | organizationId: nextOrganization.id, | 107 | organizationId: nextOrganization.id, |
| @@ -1,5 +1,6 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | ||
| 3 | import { type Approval, type WorkspaceId } from "@executor-v2/schema"; | 4 | import { type Approval, type WorkspaceId } from "@executor-v2/schema"; |
| 4 | 5 | ||
| 5 | import { | 6 | import { |
| @@ -15,6 +16,28 @@ describe("PM persistent approval policy", () => { · 9 unmodified lines | |||
| 15 | const approvalRows = { | 16 | const approvalRows = { |
| 16 | approvals: { | 17 | approvals: { |
| 17 | list: () => Effect.succeed(approvals), | 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 | upsert: (approval: Approval) => | 41 | upsert: (approval: Approval) => |
| 19 | Effect.sync(() => { | 42 | Effect.sync(() => { |
| 20 | const index = approvals.findIndex((item) => item.id === approval.id); | 43 | const index = approvals.findIndex((item) => item.id === approval.id); |
| @@ -5,6 +5,7 @@ import { · 4 unmodified lines | |||
| 5 | } from "@executor-v2/management-api"; | 5 | } from "@executor-v2/management-api"; |
| 6 | import { type Policy } from "@executor-v2/schema"; | 6 | import { type Policy } from "@executor-v2/schema"; |
| 7 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Option from "effect/Option"; | ||
| 8 | 9 | ||
| 9 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | 10 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 10 | 11 | ||
| @@ -29,36 +30,36 @@ export const createPmPoliciesService = ( · 18 unmodified lines | |||
| 29 | makeControlPlanePoliciesService({ | 30 | makeControlPlanePoliciesService({ |
| 30 | listPolicies: (workspaceId) => | 31 | listPolicies: (workspaceId) => |
| 31 | Effect.gen(function* () { | 32 | Effect.gen(function* () { |
| 32 | const policies = yield* rows.policies.list().pipe( | 33 | const policies = yield* rows.policies.listByWorkspaceId(workspaceId).pipe( |
| 33 | Effect.mapError((error) => | 34 | Effect.mapError((error) => |
| 34 | sourceStoreError.fromRowStore("policies.list", error), | 35 | sourceStoreError.fromRowStore("policies.list", error), |
| 35 | ), | 36 | ), |
| 36 | ); | 37 | ); |
| 37 | 38 | ||
| 38 | return sortPolicies( | 39 | return sortPolicies(policies); |
| 39 | policies.filter((policy) => policy.workspaceId === workspaceId), | ||
| 40 | ); | ||
| 41 | }), | 40 | }), |
| 42 | 41 | ||
| 43 | upsertPolicy: (input) => | 42 | upsertPolicy: (input) => |
| 44 | Effect.gen(function* () { | 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 | const now = Date.now(); | 44 | const now = Date.now(); |
| 52 | const requestedId = input.payload.id; | 45 | const requestedId = input.payload.id; |
| 53 | 46 | ||
| 54 | const existingIndex = requestedId | 47 | const existingOption = requestedId |
| 55 | ? policies.findIndex( | 48 | ? yield* rows.policies.getById(requestedId).pipe( |
| 56 | (policy) => | 49 | Effect.mapError((error) => |
| 57 | policy.workspaceId === input.workspaceId && policy.id === requestedId, | 50 | sourceStoreError.fromRowStore("policies.get_by_id", error), |
| 58 | ) | 51 | ), |
| 59 | : -1; | 52 | ) |
| 60 | 53 | : Option.none<Policy>(); | |
| 61 | const existing = existingIndex >= 0 ? policies[existingIndex] : null; | 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 | const nextPolicy: Policy = { | 64 | const nextPolicy: Policy = { |
| 64 | id: existing?.id ?? (requestedId ?? (`pol_${crypto.randomUUID()}` as Policy["id"])), | 65 | id: existing?.id ?? (requestedId ?? (`pol_${crypto.randomUUID()}` as Policy["id"])), |
| @@ -80,17 +81,15 @@ export const createPmPoliciesService = ( · 15 unmodified lines | |||
| 80 | 81 | ||
| 81 | removePolicy: (input) => | 82 | removePolicy: (input) => |
| 82 | Effect.gen(function* () { | 83 | Effect.gen(function* () { |
| 83 | const policies = yield* rows.policies.list().pipe( | 84 | const existingOption = yield* rows.policies.getById(input.policyId).pipe( |
| 84 | Effect.mapError((error) => | 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 | const existing = Option.getOrNull(existingOption); |
| 90 | (policy) => policy.workspaceId === input.workspaceId && policy.id === input.policyId, | ||
| 91 | ); | ||
| 92 | 91 | ||
| 93 | if (!existing) { | 92 | if (!existing || existing.workspaceId !== input.workspaceId) { |
| 94 | return { | 93 | return { |
| 95 | removed: false, | 94 | removed: false, |
| 96 | }; | 95 | }; |
| @@ -6,6 +6,7 @@ import { · 5 unmodified lines | |||
| 6 | type WorkspaceId, | 6 | type WorkspaceId, |
| 7 | } from "@executor-v2/schema"; | 7 | } from "@executor-v2/schema"; |
| 8 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 9 | import * as Option from "effect/Option"; | ||
| 9 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; | 10 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; |
| 10 | import { tmpdir } from "node:os"; | 11 | import { tmpdir } from "node:os"; |
| 11 | import * as path from "node:path"; | 12 | import * as path from "node:path"; |
| @@ -39,10 +40,32 @@ describe("PM storage service", () => { · 27 unmodified lines | |||
| 39 | const rows: StorageRows = { | 40 | const rows: StorageRows = { |
| 40 | workspaces: { | 41 | workspaces: { |
| 41 | list: () => Effect.succeed([workspace]), | 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 | upsert: () => Effect.void, | 49 | upsert: () => Effect.void, |
| 43 | }, | 50 | }, |
| 44 | storageInstances: { | 51 | storageInstances: { |
| 45 | list: () => Effect.succeed(storageInstances), | 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 | upsert: (storageInstance) => | 69 | upsert: (storageInstance) => |
| 47 | Effect.sync(() => { | 70 | Effect.sync(() => { |
| 48 | const index = storageInstances.findIndex((item) => item.id === storageInstance.id); | 71 | const index = storageInstances.findIndex((item) => item.id === storageInstance.id); |
| @@ -10,11 +10,9 @@ import { · 9 unmodified lines | |||
| 10 | type WorkspaceId, | 10 | type WorkspaceId, |
| 11 | } from "@executor-v2/schema"; | 11 | } from "@executor-v2/schema"; |
| 12 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as Option from "effect/Option"; | ||
| 13 | 14 | ||
| 14 | import { | 15 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 15 | createSqlSourceStoreErrorMapper, | ||
| 16 | resolveWorkspaceOrganizationId, | ||
| 17 | } from "./control-plane-row-helpers"; | ||
| 18 | import { | 16 | import { |
| 19 | DEFAULT_EPHEMERAL_TTL_HOURS, | 17 | DEFAULT_EPHEMERAL_TTL_HOURS, |
| 20 | initializeStorageInstanceFiles, | 18 | initializeStorageInstanceFiles, |
| @@ -100,7 +98,7 @@ const loadStorageScope = ( · 79 unmodified lines | |||
| 100 | input: { | 98 | input: { |
| 101 | workspaceId: WorkspaceId; | 99 | workspaceId: WorkspaceId; |
| 102 | instancesOperation: string; | 100 | instancesOperation: string; |
| 103 | workspacesOperation: string; | 101 | workspaceOperation: string; |
| 104 | }, | 102 | }, |
| 105 | ): Effect.Effect< | 103 | ): Effect.Effect< |
| 106 | { | 104 | { |
| @@ -109,22 +107,33 @@ const loadStorageScope = ( · 2 unmodified lines | |||
| 109 | }, | 107 | }, |
| 110 | SourceStoreError | 108 | SourceStoreError |
| 111 | > => | 109 | > => |
| 112 | Effect.all([ | 110 | rows.workspaces.getById(input.workspaceId).pipe( |
| 113 | rows.storageInstances.list().pipe( | 111 | Effect.mapError((error) => |
| 114 | Effect.mapError((error) => | 112 | sourceStoreError.fromRowStore(input.workspaceOperation, 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 | ), | ||
| 122 | ), | 113 | ), |
| 123 | ]).pipe( | 114 | Effect.flatMap((workspaceOption) => { |
| 124 | Effect.map(([storageInstances, workspaces]) => ({ | 115 | const workspace = Option.getOrNull(workspaceOption); |
| 125 | storageInstances, | 116 | if (workspace === null) { |
| 126 | organizationId: resolveWorkspaceOrganizationId(workspaces, input.workspaceId), | 117 | return sourceStoreError.fromMessage( |
| 127 | })), | 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 | export const createPmStorageService = ( | 139 | export const createPmStorageService = ( |
| @@ -139,14 +148,10 @@ export const createPmStorageService = ( · 8 unmodified lines | |||
| 139 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 148 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 140 | workspaceId, | 149 | workspaceId, |
| 141 | instancesOperation: "storage.list_instances", | 150 | instancesOperation: "storage.list_instances", |
| 142 | workspacesOperation: "storage.list_instances.workspaces", | 151 | workspaceOperation: "storage.list_instances.workspace", |
| 143 | }); | 152 | }); |
| 144 | 153 | ||
| 145 | return sortStorageInstances( | 154 | return sortStorageInstances(storageInstances); |
| 146 | storageInstances.filter((instance) => | ||
| 147 | canAccessStorageInstance(instance, workspaceId, organizationId) | ||
| 148 | ), | ||
| 149 | ); | ||
| 150 | }), | 155 | }), |
| 151 | 156 | ||
| 152 | openStorageInstance: (input) => | 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 | Effect.mapError((error) => | 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 | const now = Date.now(); | 182 | const now = Date.now(); |
| 169 | const organizationId = resolveWorkspaceOrganizationId(workspaces, input.workspaceId); | 183 | const organizationId = workspace.organizationId; |
| 170 | const storageInstanceId = | 184 | const storageInstanceId = |
| 171 | `storage_${crypto.randomUUID()}` as StorageInstance["id"]; | 185 | `storage_${crypto.randomUUID()}` as StorageInstance["id"]; |
| 172 | const ttlHours = | 186 | const ttlHours = |
| @@ -231,7 +245,7 @@ export const createPmStorageService = ( · 58 unmodified lines | |||
| 231 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 245 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 232 | workspaceId: input.workspaceId, | 246 | workspaceId: input.workspaceId, |
| 233 | instancesOperation: "storage.close.instances", | 247 | instancesOperation: "storage.close.instances", |
| 234 | workspacesOperation: "storage.close.workspaces", | 248 | workspaceOperation: "storage.close.workspace", |
| 235 | }); | 249 | }); |
| 236 | 250 | ||
| 237 | const found = findStorageInstance(storageInstances, organizationId, { | 251 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -270,7 +284,7 @@ export const createPmStorageService = ( · 32 unmodified lines | |||
| 270 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 284 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 271 | workspaceId: input.workspaceId, | 285 | workspaceId: input.workspaceId, |
| 272 | instancesOperation: "storage.remove.instances", | 286 | instancesOperation: "storage.remove.instances", |
| 273 | workspacesOperation: "storage.remove.workspaces", | 287 | workspaceOperation: "storage.remove.workspace", |
| 274 | }); | 288 | }); |
| 275 | 289 | ||
| 276 | const found = findStorageInstance(storageInstances, organizationId, { | 290 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -311,7 +325,7 @@ export const createPmStorageService = ( · 34 unmodified lines | |||
| 311 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 325 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 312 | workspaceId: input.workspaceId, | 326 | workspaceId: input.workspaceId, |
| 313 | instancesOperation: "storage.list_directory.instances", | 327 | instancesOperation: "storage.list_directory.instances", |
| 314 | workspacesOperation: "storage.list_directory.workspaces", | 328 | workspaceOperation: "storage.list_directory.workspace", |
| 315 | }); | 329 | }); |
| 316 | 330 | ||
| 317 | const found = findStorageInstance(storageInstances, organizationId, { | 331 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -356,7 +370,7 @@ export const createPmStorageService = ( · 38 unmodified lines | |||
| 356 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 370 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 357 | workspaceId: input.workspaceId, | 371 | workspaceId: input.workspaceId, |
| 358 | instancesOperation: "storage.read_file.instances", | 372 | instancesOperation: "storage.read_file.instances", |
| 359 | workspacesOperation: "storage.read_file.workspaces", | 373 | workspaceOperation: "storage.read_file.workspace", |
| 360 | }); | 374 | }); |
| 361 | 375 | ||
| 362 | const found = findStorageInstance(storageInstances, organizationId, { | 376 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -397,7 +411,7 @@ export const createPmStorageService = ( · 34 unmodified lines | |||
| 397 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 411 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 398 | workspaceId: input.workspaceId, | 412 | workspaceId: input.workspaceId, |
| 399 | instancesOperation: "storage.list_kv.instances", | 413 | instancesOperation: "storage.list_kv.instances", |
| 400 | workspacesOperation: "storage.list_kv.workspaces", | 414 | workspaceOperation: "storage.list_kv.workspace", |
| 401 | }); | 415 | }); |
| 402 | 416 | ||
| 403 | const found = findStorageInstance(storageInstances, organizationId, { | 417 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -438,7 +452,7 @@ export const createPmStorageService = ( · 34 unmodified lines | |||
| 438 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { | 452 | const { storageInstances, organizationId } = yield* loadStorageScope(rows, { |
| 439 | workspaceId: input.workspaceId, | 453 | workspaceId: input.workspaceId, |
| 440 | instancesOperation: "storage.query_sql.instances", | 454 | instancesOperation: "storage.query_sql.instances", |
| 441 | workspacesOperation: "storage.query_sql.workspaces", | 455 | workspaceOperation: "storage.query_sql.workspace", |
| 442 | }); | 456 | }); |
| 443 | 457 | ||
| 444 | const found = findStorageInstance(storageInstances, organizationId, { | 458 | const found = findStorageInstance(storageInstances, organizationId, { |
| @@ -4,24 +4,25 @@ import { · 3 unmodified lines | |||
| 4 | } from "@executor-v2/engine"; | 4 | } from "@executor-v2/engine"; |
| 5 | import { describe, expect, it } from "@effect/vitest"; | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Option from "effect/Option"; | ||
| 7 | 8 | ||
| 8 | import { createPmResolveToolCredentials } from "./credential-resolver"; | 9 | import { createPmResolveToolCredentials } from "./credential-resolver"; |
| 9 | 10 | ||
| 10 | const emptyCredentialRows = { | 11 | const emptyCredentialRows = { |
| 11 | workspaces: { | 12 | workspaces: { |
| 12 | list: () => Effect.succeed([]), | 13 | getById: () => Effect.succeed(Option.none()), |
| 13 | }, | 14 | }, |
| 14 | sourceAuthBindings: { | 15 | sourceAuthBindings: { |
| 15 | list: () => Effect.succeed([]), | 16 | listByWorkspaceScope: () => Effect.succeed([]), |
| 16 | }, | 17 | }, |
| 17 | authConnections: { | 18 | authConnections: { |
| 18 | list: () => Effect.succeed([]), | 19 | getById: () => Effect.succeed(Option.none()), |
| 19 | }, | 20 | }, |
| 20 | authMaterials: { | 21 | authMaterials: { |
| 21 | list: () => Effect.succeed([]), | 22 | getByConnectionId: () => Effect.succeed(Option.none()), |
| 22 | }, | 23 | }, |
| 23 | oauthStates: { | 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 | } from "@executor-v2/management-api"; | 5 | } from "@executor-v2/management-api"; |
| 6 | import { type Workspace } from "@executor-v2/schema"; | 6 | import { type Workspace } from "@executor-v2/schema"; |
| 7 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as Option from "effect/Option"; | ||
| 8 | 9 | ||
| 9 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; | 10 | import { createSqlSourceStoreErrorMapper } from "./control-plane-row-helpers"; |
| 10 | 11 | ||
| @@ -41,17 +42,16 @@ export const createPmWorkspacesService = ( · 30 unmodified lines | |||
| 41 | 42 | ||
| 42 | upsertWorkspace: (input) => | 43 | upsertWorkspace: (input) => |
| 43 | Effect.gen(function* () { | 44 | Effect.gen(function* () { |
| 44 | const workspaces = yield* rows.workspaces.list().pipe( | 45 | const existingOption = input.payload.id |
| 45 | Effect.mapError((error) => | 46 | ? yield* rows.workspaces.getById(input.payload.id).pipe( |
| 46 | sourceStoreError.fromRowStore("workspaces.upsert", error), | 47 | Effect.mapError((error) => |
| 47 | ), | 48 | sourceStoreError.fromRowStore("workspaces.get_by_id", error), |
| 48 | ); | 49 | ), |
| 50 | ) | ||
| 51 | : Option.none<Workspace>(); | ||
| 49 | 52 | ||
| 50 | const now = Date.now(); | 53 | const now = Date.now(); |
| 51 | const existingIndex = input.payload.id | 54 | const existing = Option.getOrNull(existingOption); |
| 52 | ? workspaces.findIndex((workspace) => workspace.id === input.payload.id) | ||
| 53 | : -1; | ||
| 54 | const existing = existingIndex >= 0 ? workspaces[existingIndex] : null; | ||
| 55 | 55 | ||
| 56 | const nextWorkspace: Workspace = { | 56 | const nextWorkspace: Workspace = { |
| 57 | id: existing?.id ?? (input.payload.id ?? (`ws_${crypto.randomUUID()}` as Workspace["id"])), | 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 | Effect.gen(function* () { | 104 | Effect.gen(function* () { |
| 105 | const now = Date.now(); | 105 | const now = Date.now(); |
| 106 | const [organizations, memberships, workspaces, profileOption] = yield* Effect.all([ | 106 | const [organizationOption, membershipOption, workspaceOption, profileOption] = yield* Effect.all([ |
| 107 | persistence.rows.organizations.list(), | 107 | persistence.rows.organizations.getById(principal.organizationId as Organization["id"]), |
| 108 | persistence.rows.organizationMemberships.list(), | 108 | persistence.rows.organizationMemberships.getByOrganizationAndAccount( |
| 109 | persistence.rows.workspaces.list(), | 109 | principal.organizationId as OrganizationMembership["organizationId"], |
| 110 | principal.accountId as OrganizationMembership["accountId"], | ||
| 111 | ), | ||
| 112 | persistence.rows.workspaces.getById(principal.workspaceId as Workspace["id"]), | ||
| 110 | persistence.rows.profile.get(), | 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 | yield* persistence.rows.organizations.upsert({ | 117 | yield* persistence.rows.organizations.upsert({ |
| 115 | id: principal.organizationId as Organization["id"], | 118 | id: principal.organizationId as Organization["id"], |
| 116 | slug: principal.organizationId, | 119 | slug: principal.organizationId, |
| @@ -124,13 +127,7 @@ const ensurePrincipalProvisioned = ( · 7 unmodified lines | |||
| 124 | }); | 127 | }); |
| 125 | } | 128 | } |
| 126 | 129 | ||
| 127 | if ( | 130 | if (membershipOption._tag === "None") { |
| 128 | memberships.find( | ||
| 129 | (membership) => | ||
| 130 | membership.organizationId === principal.organizationId | ||
| 131 | && membership.accountId === principal.accountId, | ||
| 132 | ) === undefined | ||
| 133 | ) { | ||
| 134 | yield* persistence.rows.organizationMemberships.upsert({ | 131 | yield* persistence.rows.organizationMemberships.upsert({ |
| 135 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], | 132 | id: `org_member_${crypto.randomUUID()}` as OrganizationMembership["id"], |
| 136 | organizationId: principal.organizationId as OrganizationMembership["organizationId"], | 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 | yield* persistence.rows.workspaces.upsert({ | 146 | yield* persistence.rows.workspaces.upsert({ |
| 150 | id: principal.workspaceId as Workspace["id"], | 147 | id: principal.workspaceId as Workspace["id"], |
| 151 | organizationId: principal.organizationId as Workspace["organizationId"], | 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 | import { | 1 | import { |
| 2 | type RowStoreError, | ||
| 3 | type SourceStore, | 2 | type SourceStore, |
| 4 | type ToolArtifactStore, | 3 | type ToolArtifactStore, |
| 5 | } from "@executor-v2/persistence-ports"; | 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 | import * as Data from "effect/Data"; | 5 | import * as Data from "effect/Data"; |
| 20 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 21 | import * as Option from "effect/Option"; | ||
| 22 | import * as path from "node:path"; | 7 | import * as path from "node:path"; |
| 23 | 8 | ||
| 24 | import { createRowOperations } from "./row-operations"; | 9 | import { createControlPlaneRows } from "./control-plane-rows"; |
| 25 | import { createRowsEffectApi } from "./rows-effect-api"; | ||
| 26 | import { | 10 | import { |
| 11 | createSqlRuntime, | ||
| 27 | createDrizzleContext, | 12 | createDrizzleContext, |
| 28 | createPGliteAdapter, | ||
| 29 | createPostgresAdapter, | ||
| 30 | runMigrations, | 13 | runMigrations, |
| 31 | type SqlBackend, | 14 | type SqlBackend, |
| 32 | } from "./sql-internals"; | 15 | } from "./sql-internals"; |
| @@ -38,76 +21,13 @@ export type SqlControlPlanePersistenceOptions = { · 5 unmodified lines | |||
| 38 | postgresApplicationName?: string; | 21 | postgresApplicationName?: string; |
| 39 | }; | 22 | }; |
| 40 | 23 | ||
| 24 | export type SqlControlPlaneRows = ReturnType<typeof createControlPlaneRows>; | ||
| 25 | |||
| 41 | export type SqlControlPlanePersistence = { | 26 | export type SqlControlPlanePersistence = { |
| 42 | backend: SqlBackend; | 27 | backend: SqlBackend; |
| 43 | sourceStore: SourceStore; | 28 | sourceStore: SourceStore; |
| 44 | toolArtifactStore: ToolArtifactStore; | 29 | toolArtifactStore: ToolArtifactStore; |
| 45 | rows: { | 30 | rows: SqlControlPlaneRows; |
| 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 | }; | ||
| 111 | close: () => Promise<void>; | 31 | close: () => Promise<void>; |
| 112 | }; | 32 | }; |
| 113 | 33 | ||
| @@ -123,129 +43,43 @@ const trim = (value: string | undefined): string | undefined => { · 9 unmodified lines | |||
| 123 | return candidate && candidate.length > 0 ? candidate : undefined; | 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 | export const makeSqlControlPlanePersistence = ( | 46 | export const makeSqlControlPlanePersistence = ( |
| 139 | options: SqlControlPlanePersistenceOptions, | 47 | options: SqlControlPlanePersistenceOptions, |
| 140 | ): Effect.Effect<SqlControlPlanePersistence, SqlPersistenceBootstrapError> => | 48 | ): Effect.Effect<SqlControlPlanePersistence, SqlPersistenceBootstrapError> => |
| 141 | Effect.tryPromise({ | 49 | Effect.tryPromise({ |
| 142 | try: async () => { | 50 | try: async () => { |
| 143 | const databaseUrl = trim(options.databaseUrl); | ||
| 144 | const localDataDir = path.resolve( | 51 | const localDataDir = path.resolve( |
| 145 | options.localDataDir ?? ".executor-v2/control-plane-pgdata", | 52 | options.localDataDir ?? ".executor-v2/control-plane-pgdata", |
| 146 | ); | 53 | ); |
| 147 | const backend: SqlBackend = | 54 | const runtime = await createSqlRuntime({ |
| 148 | databaseUrl && (databaseUrl.startsWith("postgres://") || databaseUrl.startsWith("postgresql://")) | 55 | databaseUrl: trim(options.databaseUrl), |
| 149 | ? "postgres" | 56 | localDataDir, |
| 150 | : "pglite"; | 57 | postgresApplicationName: trim(options.postgresApplicationName), |
| 58 | }); | ||
| 151 | 59 | ||
| 152 | const adapter = | 60 | await runMigrations(runtime); |
| 153 | backend === "postgres" | ||
| 154 | ? await createPostgresAdapter(databaseUrl!, trim(options.postgresApplicationName)) | ||
| 155 | : await createPGliteAdapter(localDataDir); | ||
| 156 | 61 | ||
| 157 | await runMigrations(adapter); | 62 | const drizzleContext = createDrizzleContext(runtime.db); |
| 158 | const drizzleContext = createDrizzleContext(adapter); | ||
| 159 | const { db, tables } = drizzleContext; | 63 | const { db, tables } = drizzleContext; |
| 160 | 64 | ||
| 161 | const writeQueueRef = { | 65 | const { sourceStore, toolArtifactStore } = createSourceAndArtifactStores({ |
| 162 | current: Promise.resolve<void>(undefined), | 66 | backend: runtime.backend, |
| 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, | ||
| 196 | db, | 67 | db, |
| 197 | tables, | 68 | tables, |
| 198 | writeLocked: <A>(run: () => Promise<A>) => withWriteLock(writeQueueRef, run), | ||
| 199 | }); | 69 | }); |
| 200 | 70 | ||
| 201 | const { sourceStore, toolArtifactStore } = createSourceAndArtifactStores({ | 71 | const rows = createControlPlaneRows({ |
| 202 | backend, | 72 | backend: runtime.backend, |
| 203 | adapter, | ||
| 204 | db, | 73 | db, |
| 205 | tables, | 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 | return { | 77 | return { |
| 244 | backend, | 78 | backend: runtime.backend, |
| 245 | sourceStore, | 79 | sourceStore, |
| 246 | toolArtifactStore, | 80 | toolArtifactStore, |
| 247 | rows, | 81 | rows, |
| 248 | close: () => adapter.close(), | 82 | close: () => runtime.close(), |
| 249 | }; | 83 | }; |
| 250 | }, | 84 | }, |
| 251 | catch: (cause) => { | 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 | } from "./persistence-errors"; | 15 | } from "./persistence-errors"; |
| 16 | import { tableNames } from "./schema"; | 16 | import { tableNames } from "./schema"; |
| 17 | import { | 17 | import { |
| 18 | createDrizzleContext, | ||
| 19 | type DrizzleDb, | 18 | type DrizzleDb, |
| 20 | type DrizzleTables, | 19 | type DrizzleTables, |
| 21 | type SqlAdapter, | ||
| 22 | type SqlBackend, | 20 | type SqlBackend, |
| 23 | } from "./sql-internals"; | 21 | } from "./sql-internals"; |
| 24 | 22 | ||
| 25 | type WriteLocked = <A>(run: () => Promise<A>) => Promise<A>; | ||
| 26 | |||
| 27 | type CreateStoresInput = { | 23 | type CreateStoresInput = { |
| 28 | backend: SqlBackend; | 24 | backend: SqlBackend; |
| 29 | adapter: SqlAdapter; | ||
| 30 | db: DrizzleDb; | 25 | db: DrizzleDb; |
| 31 | tables: DrizzleTables; | 26 | tables: DrizzleTables; |
| 32 | writeLocked: WriteLocked; | ||
| 33 | }; | 27 | }; |
| 34 | 28 | ||
| 35 | const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`; | 29 | const sourceStoreKey = (source: Source): string => `${source.workspaceId}:${source.id}`; |
| @@ -76,10 +70,8 @@ const toToolArtifact = ( · 40 unmodified lines | |||
| 76 | 70 | ||
| 77 | export const createSourceAndArtifactStores = ({ | 71 | export const createSourceAndArtifactStores = ({ |
| 78 | backend, | 72 | backend, |
| 79 | adapter, | ||
| 80 | db, | 73 | db, |
| 81 | tables, | 74 | tables, |
| 82 | writeLocked, | ||
| 83 | }: CreateStoresInput): { | 75 | }: CreateStoresInput): { |
| 84 | sourceStore: SourceStore; | 76 | sourceStore: SourceStore; |
| 85 | toolArtifactStore: ToolArtifactStore; | 77 | toolArtifactStore: ToolArtifactStore; |
| @@ -124,46 +116,40 @@ export const createSourceAndArtifactStores = ({ · 38 unmodified lines | |||
| 124 | upsert: (source: Source) => | 116 | upsert: (source: Source) => |
| 125 | Effect.tryPromise({ | 117 | Effect.tryPromise({ |
| 126 | try: async () => { | 118 | try: async () => { |
| 127 | await writeLocked(async () => { | 119 | await db |
| 128 | await adapter.transaction(async (transaction) => { | 120 | .insert(tables.sourcesTable) |
| 129 | const transactionContext = createDrizzleContext(transaction); | 121 | .values({ |
| 130 | 122 | workspaceId: source.workspaceId, | |
| 131 | await transactionContext.db | 123 | sourceId: source.id, |
| 132 | .insert(transactionContext.tables.sourcesTable) | 124 | name: source.name, |
| 133 | .values({ | 125 | kind: source.kind, |
| 134 | workspaceId: source.workspaceId, | 126 | endpoint: source.endpoint, |
| 135 | sourceId: source.id, | 127 | status: source.status, |
| 136 | name: source.name, | 128 | enabled: source.enabled, |
| 137 | kind: source.kind, | 129 | configJson: source.configJson, |
| 138 | endpoint: source.endpoint, | 130 | sourceHash: source.sourceHash, |
| 139 | status: source.status, | 131 | lastError: source.lastError, |
| 140 | enabled: source.enabled, | 132 | createdAt: source.createdAt, |
| 141 | configJson: source.configJson, | 133 | updatedAt: source.updatedAt, |
| 142 | sourceHash: source.sourceHash, | 134 | }) |
| 143 | lastError: source.lastError, | 135 | .onConflictDoUpdate({ |
| 144 | createdAt: source.createdAt, | 136 | target: [ |
| 145 | updatedAt: source.updatedAt, | 137 | tables.sourcesTable.workspaceId, |
| 146 | }) | 138 | tables.sourcesTable.sourceId, |
| 147 | .onConflictDoUpdate({ | 139 | ], |
| 148 | target: [ | 140 | set: { |
| 149 | transactionContext.tables.sourcesTable.workspaceId, | 141 | name: source.name, |
| 150 | transactionContext.tables.sourcesTable.sourceId, | 142 | kind: source.kind, |
| 151 | ], | 143 | endpoint: source.endpoint, |
| 152 | set: { | 144 | status: source.status, |
| 153 | name: source.name, | 145 | enabled: source.enabled, |
| 154 | kind: source.kind, | 146 | configJson: source.configJson, |
| 155 | endpoint: source.endpoint, | 147 | sourceHash: source.sourceHash, |
| 156 | status: source.status, | 148 | lastError: source.lastError, |
| 157 | enabled: source.enabled, | 149 | createdAt: source.createdAt, |
| 158 | configJson: source.configJson, | 150 | updatedAt: source.updatedAt, |
| 159 | sourceHash: source.sourceHash, | 151 | }, |
| 160 | lastError: source.lastError, | ||
| 161 | createdAt: source.createdAt, | ||
| 162 | updatedAt: source.updatedAt, | ||
| 163 | }, | ||
| 164 | }); | ||
| 165 | }); | 152 | }); |
| 166 | }); | ||
| 167 | }, | 153 | }, |
| 168 | catch: (cause) => | 154 | catch: (cause) => |
| 169 | toSourceStoreError(backend, "upsert", tableNames.sources, cause), | 155 | toSourceStoreError(backend, "upsert", tableNames.sources, cause), |
| @@ -171,37 +157,33 @@ export const createSourceAndArtifactStores = ({ · 1 unmodified line | |||
| 171 | 157 | ||
| 172 | removeById: (workspaceId: WorkspaceId, sourceId: SourceId) => | 158 | removeById: (workspaceId: WorkspaceId, sourceId: SourceId) => |
| 173 | Effect.tryPromise({ | 159 | Effect.tryPromise({ |
| 174 | try: async () => | 160 | try: async () => { |
| 175 | writeLocked(async () => | 161 | const existing = await db |
| 176 | adapter.transaction(async (transaction) => { | 162 | .select({ sourceId: tables.sourcesTable.sourceId }) |
| 177 | const transactionContext = createDrizzleContext(transaction); | 163 | .from(tables.sourcesTable) |
| 178 | const existing = await transactionContext.db | 164 | .where( |
| 179 | .select({ sourceId: transactionContext.tables.sourcesTable.sourceId }) | 165 | and( |
| 180 | .from(transactionContext.tables.sourcesTable) | 166 | eq(tables.sourcesTable.workspaceId, workspaceId), |
| 181 | .where( | 167 | eq(tables.sourcesTable.sourceId, sourceId), |
| 182 | and( | 168 | ), |
| 183 | eq(transactionContext.tables.sourcesTable.workspaceId, workspaceId), | 169 | ) |
| 184 | eq(transactionContext.tables.sourcesTable.sourceId, sourceId), | 170 | .limit(1); |
| 185 | ), | ||
| 186 | ) | ||
| 187 | .limit(1); | ||
| 188 | 171 | ||
| 189 | if (existing.length === 0) { | 172 | if (existing.length === 0) { |
| 190 | return false; | 173 | return false; |
| 191 | } | 174 | } |
| 192 | 175 | ||
| 193 | await transactionContext.db | 176 | await db |
| 194 | .delete(transactionContext.tables.sourcesTable) | 177 | .delete(tables.sourcesTable) |
| 195 | .where( | 178 | .where( |
| 196 | and( | 179 | and( |
| 197 | eq(transactionContext.tables.sourcesTable.workspaceId, workspaceId), | 180 | eq(tables.sourcesTable.workspaceId, workspaceId), |
| 198 | eq(transactionContext.tables.sourcesTable.sourceId, sourceId), | 181 | eq(tables.sourcesTable.sourceId, sourceId), |
| 199 | ), | 182 | ), |
| 200 | ); | 183 | ); |
| 201 | 184 | ||
| 202 | return true; | 185 | return true; |
| 203 | }) | 186 | }, |
| 204 | ), | ||
| 205 | catch: (cause) => | 187 | catch: (cause) => |
| 206 | toSourceStoreError(backend, "remove_by_id", tableNames.sources, cause), | 188 | toSourceStoreError(backend, "remove_by_id", tableNames.sources, cause), |
| 207 | }), | 189 | }), |
| @@ -241,38 +223,32 @@ export const createSourceAndArtifactStores = ({ · 33 unmodified lines | |||
| 241 | upsert: (artifact: ToolArtifact) => | 223 | upsert: (artifact: ToolArtifact) => |
| 242 | Effect.tryPromise({ | 224 | Effect.tryPromise({ |
| 243 | try: async () => { | 225 | try: async () => { |
| 244 | await writeLocked(async () => { | 226 | await db |
| 245 | await adapter.transaction(async (transaction) => { | 227 | .insert(tables.toolArtifactsTable) |
| 246 | const transactionContext = createDrizzleContext(transaction); | 228 | .values({ |
| 247 | 229 | id: artifact.id, | |
| 248 | await transactionContext.db | 230 | workspaceId: artifact.workspaceId, |
| 249 | .insert(transactionContext.tables.toolArtifactsTable) | 231 | sourceId: artifact.sourceId, |
| 250 | .values({ | 232 | sourceHash: artifact.sourceHash, |
| 251 | id: artifact.id, | 233 | toolCount: artifact.toolCount, |
| 252 | workspaceId: artifact.workspaceId, | 234 | manifestJson: artifact.manifestJson, |
| 253 | sourceId: artifact.sourceId, | 235 | createdAt: artifact.createdAt, |
| 254 | sourceHash: artifact.sourceHash, | 236 | updatedAt: artifact.updatedAt, |
| 255 | toolCount: artifact.toolCount, | 237 | }) |
| 256 | manifestJson: artifact.manifestJson, | 238 | .onConflictDoUpdate({ |
| 257 | createdAt: artifact.createdAt, | 239 | target: [ |
| 258 | updatedAt: artifact.updatedAt, | 240 | tables.toolArtifactsTable.workspaceId, |
| 259 | }) | 241 | tables.toolArtifactsTable.sourceId, |
| 260 | .onConflictDoUpdate({ | 242 | ], |
| 261 | target: [ | 243 | set: { |
| 262 | transactionContext.tables.toolArtifactsTable.workspaceId, | 244 | id: artifact.id, |
| 263 | transactionContext.tables.toolArtifactsTable.sourceId, | 245 | sourceHash: artifact.sourceHash, |
| 264 | ], | 246 | toolCount: artifact.toolCount, |
| 265 | set: { | 247 | manifestJson: artifact.manifestJson, |
| 266 | id: artifact.id, | 248 | createdAt: artifact.createdAt, |
| 267 | sourceHash: artifact.sourceHash, | 249 | updatedAt: artifact.updatedAt, |
| 268 | toolCount: artifact.toolCount, | 250 | }, |
| 269 | manifestJson: artifact.manifestJson, | ||
| 270 | createdAt: artifact.createdAt, | ||
| 271 | updatedAt: artifact.updatedAt, | ||
| 272 | }, | ||
| 273 | }); | ||
| 274 | }); | 251 | }); |
| 275 | }); | ||
| 276 | }, | 252 | }, |
| 277 | catch: (cause) => | 253 | catch: (cause) => |
| 278 | toToolArtifactStoreError(backend, "upsert", tableNames.toolArtifacts, cause), | 254 | toToolArtifactStoreError(backend, "upsert", tableNames.toolArtifacts, cause), |
| @@ -1,11 +1,13 @@ | |||
| 1 | import { PGlite } from "@electric-sql/pglite"; | 1 | import { PGlite } from "@electric-sql/pglite"; |
| 2 | import { drizzle as drizzlePgProxy } from "drizzle-orm/pg-proxy"; | 2 | import { drizzle as drizzlePGlite } from "drizzle-orm/pglite"; |
| 3 | import { migrate as migratePgProxy } from "drizzle-orm/pg-proxy/migrator"; | 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 | import { existsSync } from "node:fs"; | 6 | import { existsSync } from "node:fs"; |
| 5 | import { mkdir } from "node:fs/promises"; | 7 | import { mkdir } from "node:fs/promises"; |
| 6 | import * as path from "node:path"; | 8 | import * as path from "node:path"; |
| 7 | import { fileURLToPath } from "node:url"; | 9 | import { fileURLToPath } from "node:url"; |
| 8 | import postgres, { type Sql } from "postgres"; | 10 | import postgres from "postgres"; |
| 9 | 11 | ||
| 10 | import { | 12 | import { |
| 11 | approvalsTable, | 13 | approvalsTable, |
| @@ -26,160 +28,80 @@ import { · 14 unmodified lines | |||
| 26 | } from "./schema"; | 28 | } from "./schema"; |
| 27 | 29 | ||
| 28 | export type SqlBackend = "pglite" | "postgres"; | 30 | export type SqlBackend = "pglite" | "postgres"; |
| 29 | type SqlRow = Record<string, unknown>; | 31 | type CreateSqlRuntimeOptions = { |
| 30 | 32 | databaseUrl?: string; | |
| 31 | export type SqlAdapter = { | 33 | localDataDir: string; |
| 32 | readonly backend: SqlBackend; | 34 | postgresApplicationName?: string; |
| 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>; | ||
| 40 | }; | 35 | }; |
| 41 | 36 | ||
| 42 | const withPostgresPlaceholders = (statement: string): string => { | 37 | const drizzleSchema = { |
| 43 | let index = 0; | 38 | profileTable, |
| 44 | return statement.replace(/\?/g, () => { | 39 | organizationsTable, |
| 45 | index += 1; | 40 | organizationMembershipsTable, |
| 46 | return `$${index}`; | 41 | workspacesTable, |
| 47 | }); | 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); | 55 | const createPGliteDb = (client: PGlite) => drizzlePGlite(client, { schema: drizzleSchema }); |
| 51 | 56 | const createPostgresDb = (client: postgres.Sql) => drizzlePostgres(client, { schema: drizzleSchema }); | |
| 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"); | ||
| 61 | 57 | ||
| 62 | try { | 58 | type PGliteDb = ReturnType<typeof createPGliteDb>; |
| 63 | const adapter: SqlAdapter = { | 59 | type PostgresDb = ReturnType<typeof createPostgresDb>; |
| 64 | backend: "pglite", | ||
| 65 | query, | ||
| 66 | execute, | ||
| 67 | transaction: async (nestedRun) => nestedRun(adapter), | ||
| 68 | close: async () => {}, | ||
| 69 | }; | ||
| 70 | 60 | ||
| 71 | const result = await run(adapter); | 61 | export type DrizzleDb = PGliteDb | PostgresDb; |
| 72 | await execute("COMMIT"); | 62 | export type DrizzleTables = typeof drizzleSchema; |
| 73 | return result; | ||
| 74 | } catch (error) { | ||
| 75 | try { | ||
| 76 | await execute("ROLLBACK"); | ||
| 77 | } catch { | ||
| 78 | // ignore rollback failure after original error | ||
| 79 | } | ||
| 80 | 63 | ||
| 81 | throw error; | 64 | type SqlRuntime = { |
| 82 | } | 65 | backend: SqlBackend; |
| 83 | }; | 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 | const resolvedDataDir = path.resolve(localDataDir); | 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); | 74 | const client = new PGlite(resolvedDataDir); |
| 90 | 75 | const db = createPGliteDb(client); | |
| 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 | }; | ||
| 105 | 76 | ||
| 106 | return { | 77 | return { |
| 107 | backend: "pglite", | 78 | backend: "pglite", |
| 108 | query, | 79 | db, |
| 109 | execute, | ||
| 110 | transaction: makePGliteTransaction(execute, query), | ||
| 111 | close: async () => { | 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 | databaseUrl: string, | 87 | databaseUrl: string, |
| 119 | applicationName: string | undefined, | 88 | applicationName: string | undefined, |
| 120 | ): Promise<SqlAdapter> => { | 89 | ): Promise<SqlRuntime> => { |
| 121 | const client = postgres(databaseUrl, { | 90 | const client = postgres(databaseUrl, { |
| 122 | prepare: false, | 91 | prepare: false, |
| 123 | max: 10, | 92 | max: 10, |
| 124 | ...(applicationName ? { connection: { application_name: applicationName } } : {}), | 93 | ...(applicationName ? { connection: { application_name: applicationName } } : {}), |
| 125 | }); | 94 | }); |
| 126 | 95 | ||
| 127 | type UnsafeRunner = { | 96 | const db = createPostgresDb(client); |
| 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>>; | ||
| 135 | 97 | ||
| 136 | const queryWith = async <TRow extends SqlRow = SqlRow>( | 98 | return { |
| 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 = { | ||
| 158 | backend: "postgres", | 99 | backend: "postgres", |
| 159 | query: (statement, args = []) => queryWith(client, statement, args), | 100 | db, |
| 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 | }, | ||
| 177 | close: async () => { | 101 | close: async () => { |
| 178 | await client.end({ timeout: 5 }); | 102 | await client.end({ timeout: 5 }); |
| 179 | }, | 103 | }, |
| 180 | }; | 104 | }; |
| 181 | |||
| 182 | return adapter; | ||
| 183 | }; | 105 | }; |
| 184 | 106 | ||
| 185 | const resolveDrizzleMigrationsFolder = (): string => { | 107 | const resolveDrizzleMigrationsFolder = (): string => { |
| @@ -202,105 +124,37 @@ const resolveDrizzleMigrationsFolder = (): string => { · 16 unmodified lines | |||
| 202 | throw new Error("Unable to resolve drizzle migrations folder"); | 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 | export type DrizzleContext = { | 127 | export type DrizzleContext = { |
| 268 | db: DrizzleDb; | 128 | db: DrizzleDb; |
| 269 | tables: DrizzleTables; | 129 | tables: DrizzleTables; |
| 270 | }; | 130 | }; |
| 271 | 131 | ||
| 272 | const createPostgresProxyDb = (adapter: SqlAdapter): PostgresDrizzleDb => | 132 | export const createDrizzleContext = (db: DrizzleDb): DrizzleContext => ({ |
| 273 | drizzlePgProxy( | 133 | db, |
| 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), | ||
| 292 | tables: drizzleSchema, | 134 | tables: drizzleSchema, |
| 293 | }); | 135 | }); |
| 294 | 136 | ||
| 295 | export const runMigrations = async (adapter: SqlAdapter): Promise<void> => { | 137 | export const createSqlRuntime = async ( |
| 296 | const migrationDb = createPostgresProxyDb(adapter); | 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 | const migrationsFolder = resolveDrizzleMigrationsFolder(); | 152 | const migrationsFolder = resolveDrizzleMigrationsFolder(); |
| 298 | 153 | ||
| 299 | await migratePgProxy( | 154 | if (runtime.backend === "pglite") { |
| 300 | migrationDb, | 155 | await migratePGlite(runtime.db as PGliteDb, { migrationsFolder }); |
| 301 | async (queries) => runMigrationQueries(adapter, queries), | 156 | return; |
| 302 | { | 157 | } |
| 303 | migrationsFolder, | 158 | |
| 304 | }, | 159 | await migratePostgres(runtime.db as PostgresDb, { migrationsFolder }); |
| 305 | ); | ||
| 306 | }; | 160 | }; |