Unified · Split
| @@ -15,11 +15,11 @@ import { · 14 unmodified lines | ||
| 15 | 15 | ExecutionIdSchema, |
| 16 | 16 | RuntimeExecutionResolverService, |
| 17 | 17 | createControlPlaneClient, |
| 18 | − createSqlControlPlaneRuntime, | |
| 18 | + createControlPlaneRuntime, | |
| 19 | 19 | type ControlPlaneClient, |
| 20 | 20 | type ExecutionEnvelope, |
| 21 | 21 | type ExecutionInteraction, |
| 22 | − type SqlControlPlaneRuntime, | |
| 22 | + type ControlPlaneRuntime, | |
| 23 | 23 | } from "@executor/control-plane"; |
| 24 | 24 | import type { ToolCatalog } from "@executor/codemode-core"; |
| 25 | 25 | |
| @@ -247,7 +247,7 @@ const formatCatalogUnavailableMessage = (cause: Cause.Cause<unknown>): string => · 221 unmodified lines | ||
| 247 | 247 | : `Current workspace catalog unavailable: ${message}`; |
| 248 | 248 | }; |
| 249 | 249 | |
| 250 | −const closeSqlRuntime = (runtime: SqlControlPlaneRuntime) => | |
| 250 | +const closeRuntime = (runtime: ControlPlaneRuntime) => | |
| 251 | 251 | Effect.tryPromise({ |
| 252 | 252 | try: () => runtime.close(), |
| 253 | 253 | catch: toError, |
| @@ -274,7 +274,7 @@ const buildRunWorkflowText = ( · 20 unmodified lines | ||
| 274 | 274 | |
| 275 | 275 | const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => |
| 276 | 276 | Effect.acquireUseRelease( |
| 277 | − createSqlControlPlaneRuntime({ | |
| 277 | + createControlPlaneRuntime({ | |
| 278 | 278 | localDataDir: DEFAULT_LOCAL_DATA_DIR, |
| 279 | 279 | }).pipe(Effect.mapError(toError)), |
| 280 | 280 | (runtime) => |
| @@ -293,7 +293,7 @@ const loadRunWorkflowText = (): Effect.Effect<string, Error, never> => · 12 unmodified lines | ||
| 293 | 293 | |
| 294 | 294 | return yield* buildRunWorkflowText(environment.catalog); |
| 295 | 295 | }), |
| 296 | − closeSqlRuntime, | |
| 296 | + closeRuntime, | |
| 297 | 297 | ).pipe( |
| 298 | 298 | Effect.catchAllCause((cause) => |
| 299 | 299 | Effect.succeed( |
| @@ -21,6 +21,7 @@ import { · 20 unmodified lines | ||
| 21 | 21 | createControlPlaneClient, |
| 22 | 22 | type ControlPlaneClient, |
| 23 | 23 | buildLocalSourceArtifact, |
| 24 | + deriveLocalInstallation, | |
| 24 | 25 | materializationFromMcpManifestEntries, |
| 25 | 26 | type ResolveExecutionEnvironment, |
| 26 | 27 | resolveLocalWorkspaceContext, |
| @@ -96,6 +97,7 @@ const writeConfiguredLocalMcpSource = (input: { · 69 unmodified lines | ||
| 96 | 97 | const context = await resolveLocalWorkspaceContext({ |
| 97 | 98 | workspaceRoot: input.workspaceRoot, |
| 98 | 99 | }); |
| 100 | + const installation = deriveLocalInstallation(context); | |
| 99 | 101 | |
| 100 | 102 | await writeProjectLocalExecutorConfig({ |
| 101 | 103 | context, |
| @@ -118,7 +120,7 @@ const writeConfiguredLocalMcpSource = (input: { · 16 unmodified lines | ||
| 118 | 120 | |
| 119 | 121 | const source = { |
| 120 | 122 | id: sourceId, |
| 121 | − workspaceId: "ws_local_server_test" as never, | |
| 123 | + workspaceId: installation.workspaceId, | |
| 122 | 124 | name: input.name ?? "Demo", |
| 123 | 125 | kind: "mcp" as const, |
| 124 | 126 | endpoint: input.endpoint, |
| @@ -1259,7 +1261,7 @@ describe("local-executor-server", () => { · 1134 unmodified lines | ||
| 1259 | 1261 | 15_000, |
| 1260 | 1262 | ); |
| 1261 | 1263 | |
| 1262 | − it.scoped("gates mutating OpenAPI tools by default and allows them via organization policy", () => | |
| 1264 | + it.scoped("gates mutating OpenAPI tools by default and allows them via workspace policy", () => | |
| 1263 | 1265 | Effect.gen(function* () { |
| 1264 | 1266 | const openApiServer = yield* Effect.acquireRelease( |
| 1265 | 1267 | Effect.promise(() => startMutatingOpenApiDemoServer()), |
| @@ -1331,19 +1333,17 @@ describe("local-executor-server", () => { · 65 unmodified lines | ||
| 1331 | 1333 | expect(approved.execution.status).toBe("completed"); |
| 1332 | 1334 | expect(openApiServer.createdBodies).toHaveLength(1); |
| 1333 | 1335 | |
| 1334 | − const policy = yield* client.policies.createOrganization({ | |
| 1336 | + const policy = yield* client.policies.create({ | |
| 1335 | 1337 | path: { |
| 1336 | − organizationId: installation.organizationId, | |
| 1338 | + workspaceId: installation.workspaceId, | |
| 1337 | 1339 | }, |
| 1338 | 1340 | payload: { |
| 1339 | − resourceType: "tool_path", | |
| 1340 | 1341 | resourcePattern: "dns.records.createRecord", |
| 1341 | − matchType: "exact", | |
| 1342 | 1342 | effect: "allow", |
| 1343 | 1343 | approvalMode: "auto", |
| 1344 | 1344 | }, |
| 1345 | 1345 | }); |
| 1346 | − expect(policy.scopeType).toBe("organization"); | |
| 1346 | + expect(policy.scopeType).toBe("workspace"); | |
| 1347 | 1347 | |
| 1348 | 1348 | const automatic = yield* client.executions.create({ |
| 1349 | 1349 | path: { |
| @@ -2,26 +2,20 @@ import { HttpApi, OpenApi } from "@effect/platform"; · 1 unmodified line | ||
| 2 | 2 | |
| 3 | 3 | import { ExecutionsApi } from "./executions/api"; |
| 4 | 4 | import { LocalApi } from "./local/api"; |
| 5 | −import { MembershipsApi } from "./memberships/api"; | |
| 6 | 5 | import { OAuthApi } from "./oauth/api"; |
| 7 | −import { OrganizationsApi } from "./organizations/api"; | |
| 8 | 6 | import { PoliciesApi } from "./policies/api"; |
| 9 | 7 | import { SourcesApi } from "./sources/api"; |
| 10 | −import { WorkspacesApi } from "./workspaces/api"; | |
| 11 | 8 | |
| 12 | 9 | export class ControlPlaneApi extends HttpApi.make("controlPlane") |
| 13 | 10 | .add(LocalApi) |
| 14 | 11 | .add(OAuthApi) |
| 15 | − .add(OrganizationsApi) | |
| 16 | − .add(MembershipsApi) | |
| 17 | − .add(WorkspacesApi) | |
| 18 | 12 | .add(SourcesApi) |
| 19 | 13 | .add(PoliciesApi) |
| 20 | 14 | .add(ExecutionsApi) |
| 21 | 15 | .annotateContext( |
| 22 | 16 | OpenApi.annotations({ |
| 23 | 17 | title: "Executor Control Plane API", |
| 24 | − description: "CRUD control plane for organizations, workspaces, sources, and policies", | |
| 18 | + description: "Local-first control plane for workspace sources, policies, auth, and execution", | |
| 25 | 19 | }), |
| 26 | 20 | ) {} |
| 27 | 21 | |
| @@ -8,19 +8,13 @@ import { · 7 unmodified lines | ||
| 8 | 8 | } from "./auth/actor-resolver"; |
| 9 | 9 | import { ControlPlaneExecutionsLive } from "./executions/http"; |
| 10 | 10 | import { ControlPlaneLocalLive } from "./local/http"; |
| 11 | −import { ControlPlaneMembershipsLive } from "./memberships/http"; | |
| 12 | 11 | import { ControlPlaneOAuthLive } from "./oauth/http"; |
| 13 | −import { ControlPlaneOrganizationsLive } from "./organizations/http"; | |
| 14 | 12 | import { ControlPlanePoliciesLive } from "./policies/http"; |
| 15 | 13 | import { ControlPlaneSourcesLive } from "./sources/http"; |
| 16 | −import { ControlPlaneWorkspacesLive } from "./workspaces/http"; | |
| 17 | 14 | |
| 18 | 15 | export const ControlPlaneApiLive = HttpApiBuilder.api(ControlPlaneApi).pipe( |
| 19 | 16 | Layer.provide(ControlPlaneLocalLive), |
| 20 | 17 | Layer.provide(ControlPlaneOAuthLive), |
| 21 | − Layer.provide(ControlPlaneOrganizationsLive), | |
| 22 | − Layer.provide(ControlPlaneMembershipsLive), | |
| 23 | − Layer.provide(ControlPlaneWorkspacesLive), | |
| 24 | 18 | Layer.provide(ControlPlaneSourcesLive), |
| 25 | 19 | Layer.provide(ControlPlanePoliciesLive), |
| 26 | 20 | Layer.provide(ControlPlaneExecutionsLive), |
| @@ -60,27 +60,6 @@ export { · 59 unmodified lines | ||
| 60 | 60 | type CompleteSourceOAuthResult, |
| 61 | 61 | } from "./oauth/api"; |
| 62 | 62 | |
| 63 | −export { | |
| 64 | − CreateOrganizationPayloadSchema, | |
| 65 | − UpdateOrganizationPayloadSchema, | |
| 66 | − type CreateOrganizationPayload, | |
| 67 | − type UpdateOrganizationPayload, | |
| 68 | −} from "./organizations/api"; | |
| 69 | − | |
| 70 | −export { | |
| 71 | − CreateMembershipPayloadSchema, | |
| 72 | − UpdateMembershipPayloadSchema, | |
| 73 | − type CreateMembershipPayload, | |
| 74 | − type UpdateMembershipPayload, | |
| 75 | −} from "./memberships/api"; | |
| 76 | − | |
| 77 | −export { | |
| 78 | − CreateWorkspacePayloadSchema, | |
| 79 | − UpdateWorkspacePayloadSchema, | |
| 80 | − type CreateWorkspacePayload, | |
| 81 | − type UpdateWorkspacePayload, | |
| 82 | −} from "./workspaces/api"; | |
| 83 | − | |
| 84 | 63 | export { |
| 85 | 64 | ConnectSourcePayloadSchema, |
| 86 | 65 | ConnectSourceResultSchema, |
| @@ -1,84 +0,0 @@ | ||
| 1 | −import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | |
| 2 | −import { | |
| 3 | − AccountIdSchema, | |
| 4 | − OrganizationIdSchema, | |
| 5 | − OrganizationMembershipInsertSchema, | |
| 6 | − OrganizationMembershipSchema, | |
| 7 | − OrganizationMembershipUpdateSchema, | |
| 8 | −} from "#schema"; | |
| 9 | −import * as Schema from "effect/Schema"; | |
| 10 | − | |
| 11 | −import { | |
| 12 | − ControlPlaneBadRequestError, | |
| 13 | − ControlPlaneForbiddenError, | |
| 14 | − ControlPlaneNotFoundError, | |
| 15 | − ControlPlaneStorageError, | |
| 16 | − ControlPlaneUnauthorizedError, | |
| 17 | −} from "../errors"; | |
| 18 | − | |
| 19 | −const createMembershipPayloadRequiredSchema = OrganizationMembershipInsertSchema.pipe( | |
| 20 | − Schema.pick("accountId", "role"), | |
| 21 | −); | |
| 22 | − | |
| 23 | −const createMembershipPayloadOptionalSchema = OrganizationMembershipInsertSchema.pipe( | |
| 24 | − Schema.pick("status", "billable", "invitedByAccountId"), | |
| 25 | − Schema.partialWith({ exact: true }), | |
| 26 | −); | |
| 27 | − | |
| 28 | −export const CreateMembershipPayloadSchema = Schema.extend( | |
| 29 | − createMembershipPayloadRequiredSchema, | |
| 30 | − createMembershipPayloadOptionalSchema, | |
| 31 | −); | |
| 32 | − | |
| 33 | −export type CreateMembershipPayload = typeof CreateMembershipPayloadSchema.Type; | |
| 34 | − | |
| 35 | −export const UpdateMembershipPayloadSchema = OrganizationMembershipUpdateSchema.pipe( | |
| 36 | − Schema.pick("role", "status", "billable"), | |
| 37 | − Schema.partialWith({ exact: true }), | |
| 38 | −); | |
| 39 | − | |
| 40 | −export type UpdateMembershipPayload = typeof UpdateMembershipPayloadSchema.Type; | |
| 41 | − | |
| 42 | −const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema); | |
| 43 | −const accountIdParam = HttpApiSchema.param("accountId", AccountIdSchema); | |
| 44 | − | |
| 45 | −export class MembershipsApi extends HttpApiGroup.make("memberships") | |
| 46 | − .add( | |
| 47 | − HttpApiEndpoint.get("list")`/organizations/${organizationIdParam}/memberships` | |
| 48 | − .addSuccess(Schema.Array(OrganizationMembershipSchema)) | |
| 49 | − .addError(ControlPlaneBadRequestError) | |
| 50 | − .addError(ControlPlaneUnauthorizedError) | |
| 51 | − .addError(ControlPlaneForbiddenError) | |
| 52 | − .addError(ControlPlaneNotFoundError) | |
| 53 | − .addError(ControlPlaneStorageError), | |
| 54 | − ) | |
| 55 | − .add( | |
| 56 | − HttpApiEndpoint.post("create")`/organizations/${organizationIdParam}/memberships` | |
| 57 | − .setPayload(CreateMembershipPayloadSchema) | |
| 58 | − .addSuccess(OrganizationMembershipSchema) | |
| 59 | − .addError(ControlPlaneBadRequestError) | |
| 60 | − .addError(ControlPlaneUnauthorizedError) | |
| 61 | − .addError(ControlPlaneForbiddenError) | |
| 62 | − .addError(ControlPlaneNotFoundError) | |
| 63 | − .addError(ControlPlaneStorageError), | |
| 64 | − ) | |
| 65 | − .add( | |
| 66 | − HttpApiEndpoint.patch("update")`/organizations/${organizationIdParam}/memberships/${accountIdParam}` | |
| 67 | − .setPayload(UpdateMembershipPayloadSchema) | |
| 68 | − .addSuccess(OrganizationMembershipSchema) | |
| 69 | − .addError(ControlPlaneBadRequestError) | |
| 70 | − .addError(ControlPlaneUnauthorizedError) | |
| 71 | − .addError(ControlPlaneForbiddenError) | |
| 72 | − .addError(ControlPlaneNotFoundError) | |
| 73 | − .addError(ControlPlaneStorageError), | |
| 74 | − ) | |
| 75 | − .add( | |
| 76 | − HttpApiEndpoint.del("remove")`/organizations/${organizationIdParam}/memberships/${accountIdParam}` | |
| 77 | − .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | |
| 78 | − .addError(ControlPlaneBadRequestError) | |
| 79 | − .addError(ControlPlaneUnauthorizedError) | |
| 80 | − .addError(ControlPlaneForbiddenError) | |
| 81 | − .addError(ControlPlaneNotFoundError) | |
| 82 | − .addError(ControlPlaneStorageError), | |
| 83 | − ) | |
| 84 | − .prefix("/v1") {} | |
| @@ -1,67 +0,0 @@ | ||
| 1 | −import { HttpApiBuilder } from "@effect/platform"; | |
| 2 | −import type { OrganizationId } from "#schema"; | |
| 3 | − | |
| 4 | −import { requirePermission, withPolicy } from "#domain"; | |
| 5 | −import { | |
| 6 | − createMembership, | |
| 7 | − listMemberships, | |
| 8 | − removeMembership, | |
| 9 | − updateMembership, | |
| 10 | −} from "../../runtime/organizations-operations"; | |
| 11 | − | |
| 12 | −import { ControlPlaneApi } from "../api"; | |
| 13 | −import { withRequestActor } from "../http-auth"; | |
| 14 | − | |
| 15 | −const requireReadMemberships = (organizationId: OrganizationId) => | |
| 16 | − requirePermission({ | |
| 17 | − permission: "memberships:read", | |
| 18 | − organizationId, | |
| 19 | − }); | |
| 20 | − | |
| 21 | −const requireWriteMemberships = (organizationId: OrganizationId) => | |
| 22 | − requirePermission({ | |
| 23 | − permission: "memberships:write", | |
| 24 | − organizationId, | |
| 25 | − }); | |
| 26 | − | |
| 27 | −export const ControlPlaneMembershipsLive = HttpApiBuilder.group( | |
| 28 | − ControlPlaneApi, | |
| 29 | − "memberships", | |
| 30 | − (handlers) => | |
| 31 | − handlers | |
| 32 | − .handle("list", ({ path }) => | |
| 33 | − withRequestActor("memberships.list", () => | |
| 34 | − withPolicy(requireReadMemberships(path.organizationId))( | |
| 35 | − listMemberships(path.organizationId), | |
| 36 | − ), | |
| 37 | − ), | |
| 38 | − ) | |
| 39 | − .handle("create", ({ path, payload }) => | |
| 40 | − withRequestActor("memberships.create", () => | |
| 41 | − withPolicy(requireWriteMemberships(path.organizationId))( | |
| 42 | − createMembership({ organizationId: path.organizationId, payload }), | |
| 43 | − ), | |
| 44 | − ), | |
| 45 | − ) | |
| 46 | − .handle("update", ({ path, payload }) => | |
| 47 | − withRequestActor("memberships.update", () => | |
| 48 | − withPolicy(requireWriteMemberships(path.organizationId))( | |
| 49 | − updateMembership({ | |
| 50 | − organizationId: path.organizationId, | |
| 51 | − accountId: path.accountId, | |
| 52 | − payload, | |
| 53 | − }), | |
| 54 | − ), | |
| 55 | − ), | |
| 56 | − ) | |
| 57 | − .handle("remove", ({ path }) => | |
| 58 | − withRequestActor("memberships.remove", () => | |
| 59 | − withPolicy(requireWriteMemberships(path.organizationId))( | |
| 60 | − removeMembership({ | |
| 61 | − organizationId: path.organizationId, | |
| 62 | − accountId: path.accountId, | |
| 63 | − }), | |
| 64 | − ), | |
| 65 | − ), | |
| 66 | − ), | |
| 67 | −); | |
| @@ -1,82 +0,0 @@ | ||
| 1 | −import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | |
| 2 | −import { | |
| 3 | − OrganizationIdSchema, | |
| 4 | − OrganizationStatusSchema, | |
| 5 | − OrganizationSchema, | |
| 6 | −} from "#schema"; | |
| 7 | −import * as Schema from "effect/Schema"; | |
| 8 | − | |
| 9 | −import { | |
| 10 | − ControlPlaneBadRequestError, | |
| 11 | − ControlPlaneForbiddenError, | |
| 12 | − ControlPlaneNotFoundError, | |
| 13 | − ControlPlaneStorageError, | |
| 14 | − ControlPlaneUnauthorizedError, | |
| 15 | −} from "../errors"; | |
| 16 | −import { | |
| 17 | − OptionalTrimmedNonEmptyStringSchema, | |
| 18 | − TrimmedNonEmptyStringSchema, | |
| 19 | −} from "../string-schemas"; | |
| 20 | − | |
| 21 | −export const CreateOrganizationPayloadSchema = Schema.Struct({ | |
| 22 | − name: TrimmedNonEmptyStringSchema, | |
| 23 | − slug: OptionalTrimmedNonEmptyStringSchema, | |
| 24 | −}); | |
| 25 | − | |
| 26 | −export type CreateOrganizationPayload = typeof CreateOrganizationPayloadSchema.Type; | |
| 27 | − | |
| 28 | −export const UpdateOrganizationPayloadSchema = Schema.Struct({ | |
| 29 | − name: OptionalTrimmedNonEmptyStringSchema, | |
| 30 | − status: Schema.optional(OrganizationStatusSchema), | |
| 31 | −}); | |
| 32 | − | |
| 33 | −export type UpdateOrganizationPayload = typeof UpdateOrganizationPayloadSchema.Type; | |
| 34 | − | |
| 35 | −const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema); | |
| 36 | − | |
| 37 | −export class OrganizationsApi extends HttpApiGroup.make("organizations") | |
| 38 | − .add( | |
| 39 | − HttpApiEndpoint.get("list")`/organizations` | |
| 40 | − .addSuccess(Schema.Array(OrganizationSchema)) | |
| 41 | − .addError(ControlPlaneBadRequestError) | |
| 42 | − .addError(ControlPlaneUnauthorizedError) | |
| 43 | − .addError(ControlPlaneForbiddenError) | |
| 44 | − .addError(ControlPlaneStorageError), | |
| 45 | − ) | |
| 46 | − .add( | |
| 47 | − HttpApiEndpoint.post("create")`/organizations` | |
| 48 | − .setPayload(CreateOrganizationPayloadSchema) | |
| 49 | − .addSuccess(OrganizationSchema) | |
| 50 | − .addError(ControlPlaneBadRequestError) | |
| 51 | − .addError(ControlPlaneUnauthorizedError) | |
| 52 | − .addError(ControlPlaneForbiddenError) | |
| 53 | − .addError(ControlPlaneStorageError), | |
| 54 | − ) | |
| 55 | − .add( | |
| 56 | − HttpApiEndpoint.get("get")`/organizations/${organizationIdParam}` | |
| 57 | − .addSuccess(OrganizationSchema) | |
| 58 | − .addError(ControlPlaneBadRequestError) | |
| 59 | − .addError(ControlPlaneUnauthorizedError) | |
| 60 | − .addError(ControlPlaneForbiddenError) | |
| 61 | − .addError(ControlPlaneNotFoundError) | |
| 62 | − .addError(ControlPlaneStorageError), | |
| 63 | − ) | |
| 64 | − .add( | |
| 65 | − HttpApiEndpoint.patch("update")`/organizations/${organizationIdParam}` | |
| 66 | − .setPayload(UpdateOrganizationPayloadSchema) | |
| 67 | − .addSuccess(OrganizationSchema) | |
| 68 | − .addError(ControlPlaneBadRequestError) | |
| 69 | − .addError(ControlPlaneUnauthorizedError) | |
| 70 | − .addError(ControlPlaneForbiddenError) | |
| 71 | − .addError(ControlPlaneNotFoundError) | |
| 72 | − .addError(ControlPlaneStorageError), | |
| 73 | − ) | |
| 74 | − .add( | |
| 75 | − HttpApiEndpoint.del("remove")`/organizations/${organizationIdParam}` | |
| 76 | − .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | |
| 77 | − .addError(ControlPlaneBadRequestError) | |
| 78 | − .addError(ControlPlaneUnauthorizedError) | |
| 79 | − .addError(ControlPlaneForbiddenError) | |
| 80 | − .addError(ControlPlaneStorageError), | |
| 81 | − ) | |
| 82 | − .prefix("/v1") {} | |
| @@ -1,75 +0,0 @@ | ||
| 1 | −import { HttpApiBuilder } from "@effect/platform"; | |
| 2 | −import type { OrganizationId } from "#schema"; | |
| 3 | − | |
| 4 | −import { requirePermission, withPolicy } from "#domain"; | |
| 5 | −import { | |
| 6 | − createOrganization, | |
| 7 | − getOrganization, | |
| 8 | − listOrganizations, | |
| 9 | − removeOrganization, | |
| 10 | − updateOrganization, | |
| 11 | −} from "../../runtime/organizations-operations"; | |
| 12 | − | |
| 13 | −import { ControlPlaneApi } from "../api"; | |
| 14 | −import { withRequestActor } from "../http-auth"; | |
| 15 | − | |
| 16 | −const requireReadOrganizations = requirePermission({ | |
| 17 | − permission: "organizations:read", | |
| 18 | −}); | |
| 19 | − | |
| 20 | −const requireManageOrganizations = (organizationId: OrganizationId) => | |
| 21 | − requirePermission({ | |
| 22 | − permission: "organizations:manage", | |
| 23 | − organizationId, | |
| 24 | − }); | |
| 25 | − | |
| 26 | −export const ControlPlaneOrganizationsLive = HttpApiBuilder.group( | |
| 27 | − ControlPlaneApi, | |
| 28 | − "organizations", | |
| 29 | − (handlers) => | |
| 30 | − handlers | |
| 31 | − .handle("list", () => | |
| 32 | − withRequestActor("organizations.list", (actor) => | |
| 33 | − withPolicy(requireReadOrganizations)( | |
| 34 | − listOrganizations({ | |
| 35 | − accountId: actor.principal.accountId, | |
| 36 | − }), | |
| 37 | − ), | |
| 38 | − ), | |
| 39 | − ) | |
| 40 | − .handle("create", ({ payload }) => | |
| 41 | − withRequestActor("organizations.create", (actor) => | |
| 42 | − createOrganization({ | |
| 43 | − payload, | |
| 44 | − createdByAccountId: actor.principal.accountId, | |
| 45 | − }), | |
| 46 | − ), | |
| 47 | − ) | |
| 48 | − .handle("get", ({ path }) => | |
| 49 | − withRequestActor("organizations.get", (actor) => | |
| 50 | − withPolicy(requireReadOrganizations)( | |
| 51 | − getOrganization({ | |
| 52 | − organizationId: path.organizationId, | |
| 53 | − accountId: actor.principal.accountId, | |
| 54 | − }), | |
| 55 | − ), | |
| 56 | − ), | |
| 57 | − ) | |
| 58 | − .handle("update", ({ path, payload }) => | |
| 59 | − withRequestActor("organizations.update", () => | |
| 60 | − withPolicy(requireManageOrganizations(path.organizationId))( | |
| 61 | − updateOrganization({ | |
| 62 | − organizationId: path.organizationId, | |
| 63 | − payload, | |
| 64 | − }), | |
| 65 | − ), | |
| 66 | − ), | |
| 67 | − ) | |
| 68 | − .handle("remove", ({ path }) => | |
| 69 | − withRequestActor("organizations.remove", () => | |
| 70 | − withPolicy(requireManageOrganizations(path.organizationId))( | |
| 71 | − removeOrganization({ organizationId: path.organizationId }), | |
| 72 | − ), | |
| 73 | − ), | |
| 74 | − ), | |
| 75 | −); | |
| @@ -5,38 +5,15 @@ import * as Schema from "effect/Schema"; · 4 unmodified lines | ||
| 5 | 5 | import { |
| 6 | 6 | CreateExecutionPayloadSchema, |
| 7 | 7 | } from "./executions/api"; |
| 8 | −import { | |
| 9 | − CreateOrganizationPayloadSchema, | |
| 10 | − UpdateOrganizationPayloadSchema, | |
| 11 | −} from "./organizations/api"; | |
| 12 | 8 | import { CreatePolicyPayloadSchema } from "./policies/api"; |
| 13 | 9 | import { |
| 14 | 10 | ConnectSourcePayloadSchema, |
| 15 | 11 | CreateSourcePayloadSchema, |
| 16 | 12 | UpdateSourcePayloadSchema, |
| 17 | 13 | } from "./sources/api"; |
| 18 | −import { CreateWorkspacePayloadSchema } from "./workspaces/api"; | |
| 19 | 14 | |
| 20 | 15 | describe("control-plane payload schemas", () => { |
| 21 | 16 | it("normalizes trimmed strings at decode time", () => { |
| 22 | − expect( | |
| 23 | − Schema.decodeUnknownSync(CreateOrganizationPayloadSchema)({ | |
| 24 | − name: " Acme ", | |
| 25 | − slug: " acme ", | |
| 26 | − }), | |
| 27 | − ).toEqual({ | |
| 28 | − name: "Acme", | |
| 29 | − slug: "acme", | |
| 30 | − }); | |
| 31 | − | |
| 32 | − expect( | |
| 33 | − Schema.decodeUnknownSync(CreateWorkspacePayloadSchema)({ | |
| 34 | − name: " Primary ", | |
| 35 | − }), | |
| 36 | − ).toEqual({ | |
| 37 | − name: "Primary", | |
| 38 | − }); | |
| 39 | − | |
| 40 | 17 | expect( |
| 41 | 18 | Schema.decodeUnknownSync(CreateSourcePayloadSchema)({ |
| 42 | 19 | name: " Github ", |
| @@ -101,18 +78,6 @@ describe("control-plane payload schemas", () => { · 58 unmodified lines | ||
| 101 | 78 | }); |
| 102 | 79 | |
| 103 | 80 | it("rejects blank strings for normalized string fields", () => { |
| 104 | − throws(() => | |
| 105 | − Schema.decodeUnknownSync(CreateOrganizationPayloadSchema)({ | |
| 106 | − name: " ", | |
| 107 | − }) | |
| 108 | − ); | |
| 109 | − | |
| 110 | − throws(() => | |
| 111 | − Schema.decodeUnknownSync(UpdateOrganizationPayloadSchema)({ | |
| 112 | − name: " ", | |
| 113 | − }) | |
| 114 | − ); | |
| 115 | − | |
| 116 | 81 | throws(() => |
| 117 | 82 | Schema.decodeUnknownSync(UpdateSourcePayloadSchema)({ |
| 118 | 83 | endpoint: " ", |
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; |
| 2 | 2 | import { |
| 3 | 3 | AccountIdSchema, |
| 4 | − OrganizationIdSchema, | |
| 5 | 4 | PolicyIdSchema, |
| 6 | 5 | PolicyApprovalModeSchema, |
| 7 | 6 | PolicyEffectSchema, |
| @@ -51,58 +50,10 @@ export const UpdatePolicyPayloadSchema = Schema.Struct({ · 43 unmodified lines | ||
| 51 | 50 | |
| 52 | 51 | export type UpdatePolicyPayload = typeof UpdatePolicyPayloadSchema.Type; |
| 53 | 52 | |
| 54 | −const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema); | |
| 55 | 53 | const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); |
| 56 | 54 | const policyIdParam = HttpApiSchema.param("policyId", PolicyIdSchema); |
| 57 | 55 | |
| 58 | 56 | export class PoliciesApi extends HttpApiGroup.make("policies") |
| 59 | − .add( | |
| 60 | − HttpApiEndpoint.get("listOrganization")`/organizations/${organizationIdParam}/policies` | |
| 61 | − .addSuccess(Schema.Array(PolicySchema)) | |
| 62 | − .addError(ControlPlaneBadRequestError) | |
| 63 | − .addError(ControlPlaneUnauthorizedError) | |
| 64 | − .addError(ControlPlaneForbiddenError) | |
| 65 | − .addError(ControlPlaneNotFoundError) | |
| 66 | − .addError(ControlPlaneStorageError), | |
| 67 | − ) | |
| 68 | − .add( | |
| 69 | − HttpApiEndpoint.post("createOrganization")`/organizations/${organizationIdParam}/policies` | |
| 70 | − .setPayload(CreatePolicyPayloadSchema) | |
| 71 | − .addSuccess(PolicySchema) | |
| 72 | − .addError(ControlPlaneBadRequestError) | |
| 73 | − .addError(ControlPlaneUnauthorizedError) | |
| 74 | − .addError(ControlPlaneForbiddenError) | |
| 75 | − .addError(ControlPlaneNotFoundError) | |
| 76 | − .addError(ControlPlaneStorageError), | |
| 77 | − ) | |
| 78 | − .add( | |
| 79 | − HttpApiEndpoint.get("getOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}` | |
| 80 | − .addSuccess(PolicySchema) | |
| 81 | − .addError(ControlPlaneBadRequestError) | |
| 82 | − .addError(ControlPlaneUnauthorizedError) | |
| 83 | − .addError(ControlPlaneForbiddenError) | |
| 84 | − .addError(ControlPlaneNotFoundError) | |
| 85 | − .addError(ControlPlaneStorageError), | |
| 86 | − ) | |
| 87 | − .add( | |
| 88 | − HttpApiEndpoint.patch("updateOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}` | |
| 89 | − .setPayload(UpdatePolicyPayloadSchema) | |
| 90 | − .addSuccess(PolicySchema) | |
| 91 | − .addError(ControlPlaneBadRequestError) | |
| 92 | − .addError(ControlPlaneUnauthorizedError) | |
| 93 | − .addError(ControlPlaneForbiddenError) | |
| 94 | − .addError(ControlPlaneNotFoundError) | |
| 95 | − .addError(ControlPlaneStorageError), | |
| 96 | − ) | |
| 97 | − .add( | |
| 98 | − HttpApiEndpoint.del("removeOrganization")`/organizations/${organizationIdParam}/policies/${policyIdParam}` | |
| 99 | − .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | |
| 100 | − .addError(ControlPlaneBadRequestError) | |
| 101 | − .addError(ControlPlaneUnauthorizedError) | |
| 102 | − .addError(ControlPlaneForbiddenError) | |
| 103 | − .addError(ControlPlaneNotFoundError) | |
| 104 | − .addError(ControlPlaneStorageError), | |
| 105 | − ) | |
| 106 | 57 | .add( |
| 107 | 58 | HttpApiEndpoint.get("list")`/workspaces/${workspaceIdParam}/policies` |
| 108 | 59 | .addSuccess(Schema.Array(PolicySchema)) |
| @@ -1,34 +1,17 @@ | ||
| 1 | 1 | import { HttpApiBuilder } from "@effect/platform"; |
| 2 | −import type { OrganizationId, WorkspaceId } from "#schema"; | |
| 2 | +import type { WorkspaceId } from "#schema"; | |
| 3 | 3 | |
| 4 | 4 | import { requirePermission, withPolicy } from "#domain"; |
| 5 | 5 | import { |
| 6 | − createOrganizationPolicy, | |
| 7 | 6 | createPolicy, |
| 8 | − getOrganizationPolicy, | |
| 9 | 7 | getPolicy, |
| 10 | − listOrganizationPolicies, | |
| 11 | 8 | listPolicies, |
| 12 | − removeOrganizationPolicy, | |
| 13 | 9 | removePolicy, |
| 14 | − updateOrganizationPolicy, | |
| 15 | 10 | updatePolicy, |
| 16 | 11 | } from "../../runtime/policies-operations"; |
| 17 | 12 | |
| 18 | 13 | import { ControlPlaneApi } from "../api"; |
| 19 | −import { withRequestActor, withWorkspaceRequestActor } from "../http-auth"; | |
| 20 | − | |
| 21 | −const requireReadOrganizationPolicies = (organizationId: OrganizationId) => | |
| 22 | − requirePermission({ | |
| 23 | − permission: "policies:read", | |
| 24 | − organizationId, | |
| 25 | − }); | |
| 26 | − | |
| 27 | −const requireWriteOrganizationPolicies = (organizationId: OrganizationId) => | |
| 28 | − requirePermission({ | |
| 29 | − permission: "policies:write", | |
| 30 | − organizationId, | |
| 31 | − }); | |
| 14 | +import { withWorkspaceRequestActor } from "../http-auth"; | |
| 32 | 15 | |
| 33 | 16 | const requireReadPolicies = (workspaceId: WorkspaceId) => |
| 34 | 17 | requirePermission({ |
| @@ -47,54 +30,6 @@ export const ControlPlanePoliciesLive = HttpApiBuilder.group( · 12 unmodified lines | ||
| 47 | 30 | "policies", |
| 48 | 31 | (handlers) => |
| 49 | 32 | handlers |
| 50 | − .handle("listOrganization", ({ path }) => | |
| 51 | − withRequestActor("policies.listOrganization", () => | |
| 52 | − withPolicy(requireReadOrganizationPolicies(path.organizationId))( | |
| 53 | − listOrganizationPolicies(path.organizationId), | |
| 54 | − ), | |
| 55 | − ), | |
| 56 | − ) | |
| 57 | − .handle("createOrganization", ({ path, payload }) => | |
| 58 | − withRequestActor("policies.createOrganization", () => | |
| 59 | − withPolicy(requireWriteOrganizationPolicies(path.organizationId))( | |
| 60 | − createOrganizationPolicy({ | |
| 61 | − organizationId: path.organizationId, | |
| 62 | − payload, | |
| 63 | − }), | |
| 64 | − ), | |
| 65 | − ), | |
| 66 | − ) | |
| 67 | − .handle("getOrganization", ({ path }) => | |
| 68 | − withRequestActor("policies.getOrganization", () => | |
| 69 | − withPolicy(requireReadOrganizationPolicies(path.organizationId))( | |
| 70 | − getOrganizationPolicy({ | |
| 71 | − organizationId: path.organizationId, | |
| 72 | − policyId: path.policyId, | |
| 73 | − }), | |
| 74 | − ), | |
| 75 | − ), | |
| 76 | − ) | |
| 77 | − .handle("updateOrganization", ({ path, payload }) => | |
| 78 | − withRequestActor("policies.updateOrganization", () => | |
| 79 | − withPolicy(requireWriteOrganizationPolicies(path.organizationId))( | |
| 80 | − updateOrganizationPolicy({ | |
| 81 | − organizationId: path.organizationId, | |
| 82 | − policyId: path.policyId, | |
| 83 | − payload, | |
| 84 | − }), | |
| 85 | − ), | |
| 86 | − ), | |
| 87 | − ) | |
| 88 | − .handle("removeOrganization", ({ path }) => | |
| 89 | − withRequestActor("policies.removeOrganization", () => | |
| 90 | − withPolicy(requireWriteOrganizationPolicies(path.organizationId))( | |
| 91 | − removeOrganizationPolicy({ | |
| 92 | − organizationId: path.organizationId, | |
| 93 | − policyId: path.policyId, | |
| 94 | − }), | |
| 95 | − ), | |
| 96 | − ), | |
| 97 | − ) | |
| 98 | 33 | .handle("list", ({ path }) => |
| 99 | 34 | withWorkspaceRequestActor("policies.list", path.workspaceId, () => |
| 100 | 35 | withPolicy(requireReadPolicies(path.workspaceId))( |
| @@ -1,83 +0,0 @@ | ||
| 1 | −import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "@effect/platform"; | |
| 2 | −import { | |
| 3 | − OrganizationIdSchema, | |
| 4 | − WorkspaceIdSchema, | |
| 5 | − WorkspaceSchema, | |
| 6 | −} from "#schema"; | |
| 7 | −import * as Schema from "effect/Schema"; | |
| 8 | − | |
| 9 | −import { | |
| 10 | − ControlPlaneBadRequestError, | |
| 11 | − ControlPlaneForbiddenError, | |
| 12 | − ControlPlaneNotFoundError, | |
| 13 | − ControlPlaneStorageError, | |
| 14 | − ControlPlaneUnauthorizedError, | |
| 15 | −} from "../errors"; | |
| 16 | −import { | |
| 17 | − OptionalTrimmedNonEmptyStringSchema, | |
| 18 | − TrimmedNonEmptyStringSchema, | |
| 19 | −} from "../string-schemas"; | |
| 20 | − | |
| 21 | −export const CreateWorkspacePayloadSchema = Schema.Struct({ | |
| 22 | − name: TrimmedNonEmptyStringSchema, | |
| 23 | −}); | |
| 24 | − | |
| 25 | −export type CreateWorkspacePayload = typeof CreateWorkspacePayloadSchema.Type; | |
| 26 | − | |
| 27 | −export const UpdateWorkspacePayloadSchema = Schema.Struct({ | |
| 28 | − name: OptionalTrimmedNonEmptyStringSchema, | |
| 29 | −}); | |
| 30 | − | |
| 31 | −export type UpdateWorkspacePayload = typeof UpdateWorkspacePayloadSchema.Type; | |
| 32 | − | |
| 33 | −const organizationIdParam = HttpApiSchema.param("organizationId", OrganizationIdSchema); | |
| 34 | −const workspaceIdParam = HttpApiSchema.param("workspaceId", WorkspaceIdSchema); | |
| 35 | − | |
| 36 | −export class WorkspacesApi extends HttpApiGroup.make("workspaces") | |
| 37 | − .add( | |
| 38 | − HttpApiEndpoint.get("list")`/organizations/${organizationIdParam}/workspaces` | |
| 39 | − .addSuccess(Schema.Array(WorkspaceSchema)) | |
| 40 | − .addError(ControlPlaneBadRequestError) | |
| 41 | − .addError(ControlPlaneUnauthorizedError) | |
| 42 | − .addError(ControlPlaneForbiddenError) | |
| 43 | − .addError(ControlPlaneNotFoundError) | |
| 44 | − .addError(ControlPlaneStorageError), | |
| 45 | − ) | |
| 46 | − .add( | |
| 47 | − HttpApiEndpoint.post("create")`/organizations/${organizationIdParam}/workspaces` | |
| 48 | − .setPayload(CreateWorkspacePayloadSchema) | |
| 49 | − .addSuccess(WorkspaceSchema) | |
| 50 | − .addError(ControlPlaneBadRequestError) | |
| 51 | − .addError(ControlPlaneUnauthorizedError) | |
| 52 | − .addError(ControlPlaneForbiddenError) | |
| 53 | − .addError(ControlPlaneNotFoundError) | |
| 54 | − .addError(ControlPlaneStorageError), | |
| 55 | − ) | |
| 56 | − .add( | |
| 57 | − HttpApiEndpoint.get("get")`/workspaces/${workspaceIdParam}` | |
| 58 | − .addSuccess(WorkspaceSchema) | |
| 59 | − .addError(ControlPlaneBadRequestError) | |
| 60 | − .addError(ControlPlaneUnauthorizedError) | |
| 61 | − .addError(ControlPlaneForbiddenError) | |
| 62 | − .addError(ControlPlaneNotFoundError) | |
| 63 | − .addError(ControlPlaneStorageError), | |
| 64 | − ) | |
| 65 | − .add( | |
| 66 | − HttpApiEndpoint.patch("update")`/workspaces/${workspaceIdParam}` | |
| 67 | − .setPayload(UpdateWorkspacePayloadSchema) | |
| 68 | − .addSuccess(WorkspaceSchema) | |
| 69 | − .addError(ControlPlaneBadRequestError) | |
| 70 | − .addError(ControlPlaneUnauthorizedError) | |
| 71 | − .addError(ControlPlaneForbiddenError) | |
| 72 | − .addError(ControlPlaneNotFoundError) | |
| 73 | − .addError(ControlPlaneStorageError), | |
| 74 | − ) | |
| 75 | − .add( | |
| 76 | − HttpApiEndpoint.del("remove")`/workspaces/${workspaceIdParam}` | |
| 77 | − .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | |
| 78 | − .addError(ControlPlaneBadRequestError) | |
| 79 | − .addError(ControlPlaneUnauthorizedError) | |
| 80 | − .addError(ControlPlaneForbiddenError) | |
| 81 | − .addError(ControlPlaneStorageError), | |
| 82 | − ) | |
| 83 | − .prefix("/v1") {} | |
| @@ -1,89 +0,0 @@ | ||
| 1 | −import { HttpApiBuilder } from "@effect/platform"; | |
| 2 | −import type { OrganizationId, WorkspaceId } from "#schema"; | |
| 3 | − | |
| 4 | −import { requirePermission, withPolicy } from "#domain"; | |
| 5 | −import { | |
| 6 | − createWorkspace, | |
| 7 | − getWorkspace, | |
| 8 | − listWorkspaces, | |
| 9 | − removeWorkspace, | |
| 10 | − updateWorkspace, | |
| 11 | −} from "../../runtime/organizations-operations"; | |
| 12 | − | |
| 13 | −import { ControlPlaneApi } from "../api"; | |
| 14 | −import { | |
| 15 | − withRequestActor, | |
| 16 | − withWorkspaceRequestActor, | |
| 17 | −} from "../http-auth"; | |
| 18 | − | |
| 19 | −const requireReadWorkspace = (workspaceId: WorkspaceId) => | |
| 20 | − requirePermission({ | |
| 21 | − permission: "workspace:read", | |
| 22 | − workspaceId, | |
| 23 | − }); | |
| 24 | − | |
| 25 | −const requireManageWorkspace = (workspaceId: WorkspaceId) => | |
| 26 | − requirePermission({ | |
| 27 | − permission: "workspace:manage", | |
| 28 | − workspaceId, | |
| 29 | − }); | |
| 30 | − | |
| 31 | −const requireOrganizationWorkspaceRead = (organizationId: OrganizationId) => | |
| 32 | − requirePermission({ | |
| 33 | − permission: "workspace:read", | |
| 34 | − organizationId, | |
| 35 | − }); | |
| 36 | − | |
| 37 | −const requireOrganizationWorkspaceManage = (organizationId: OrganizationId) => | |
| 38 | − requirePermission({ | |
| 39 | − permission: "workspace:manage", | |
| 40 | − organizationId, | |
| 41 | − }); | |
| 42 | − | |
| 43 | −export const ControlPlaneWorkspacesLive = HttpApiBuilder.group( | |
| 44 | − ControlPlaneApi, | |
| 45 | − "workspaces", | |
| 46 | − (handlers) => | |
| 47 | − handlers | |
| 48 | − .handle("list", ({ path }) => | |
| 49 | − withRequestActor("workspaces.list", () => | |
| 50 | − withPolicy( | |
| 51 | − requireOrganizationWorkspaceRead(path.organizationId), | |
| 52 | − )(listWorkspaces(path.organizationId)) | |
| 53 | − ), | |
| 54 | − ) | |
| 55 | − .handle("create", ({ path, payload }) => | |
| 56 | − withRequestActor("workspaces.create", (actor) => | |
| 57 | − withPolicy( | |
| 58 | − requireOrganizationWorkspaceManage(path.organizationId), | |
| 59 | − )( | |
| 60 | − createWorkspace({ | |
| 61 | − organizationId: path.organizationId, | |
| 62 | − payload, | |
| 63 | − createdByAccountId: actor.principal.accountId, | |
| 64 | − }), | |
| 65 | − ), | |
| 66 | − ), | |
| 67 | − ) | |
| 68 | − .handle("get", ({ path }) => | |
| 69 | − withWorkspaceRequestActor("workspaces.get", path.workspaceId, () => | |
| 70 | − withPolicy(requireReadWorkspace(path.workspaceId))( | |
| 71 | − getWorkspace(path.workspaceId), | |
| 72 | − ), | |
| 73 | − ), | |
| 74 | − ) | |
| 75 | − .handle("update", ({ path, payload }) => | |
| 76 | − withWorkspaceRequestActor("workspaces.update", path.workspaceId, () => | |
| 77 | − withPolicy(requireManageWorkspace(path.workspaceId))( | |
| 78 | − updateWorkspace({ workspaceId: path.workspaceId, payload }), | |
| 79 | − ), | |
| 80 | − ), | |
| 81 | − ) | |
| 82 | − .handle("remove", ({ path }) => | |
| 83 | − withWorkspaceRequestActor("workspaces.remove", path.workspaceId, () => | |
| 84 | − withPolicy(requireManageWorkspace(path.workspaceId))( | |
| 85 | − removeWorkspace({ workspaceId: path.workspaceId }), | |
| 86 | − ), | |
| 87 | − ), | |
| 88 | − ), | |
| 89 | −); | |
| @@ -27,7 +27,6 @@ import { · 26 unmodified lines | ||
| 27 | 27 | type SqlControlPlanePersistence, |
| 28 | 28 | } from "./index"; |
| 29 | 29 | import { runCodeMigrations } from "./code-migrations"; |
| 30 | −import { loadSourceById } from "../runtime/source-store"; | |
| 31 | 30 | |
| 32 | 31 | const openApiBindingConfigJson = (specUrl: string): string => |
| 33 | 32 | JSON.stringify({ |
| @@ -693,31 +692,40 @@ describe("code-migrations", () => { · 659 unmodified lines | ||
| 693 | 692 | }), |
| 694 | 693 | ); |
| 695 | 694 | |
| 696 | − const openApiSource = await Effect.runPromise(loadSourceById( | |
| 697 | − upgradedPersistence.rows, | |
| 698 | − { | |
| 695 | + const openApiSource = await Effect.runPromise( | |
| 696 | + upgradedPersistence.rows.sources.getByWorkspaceAndId( | |
| 699 | 697 | workspaceId, |
| 700 | − sourceId: SourceIdSchema.make("src_legacy_openapi"), | |
| 701 | − }, | |
| 702 | − )); | |
| 703 | − expect(openApiSource.binding).toEqual({ | |
| 704 | − specUrl: "https://api.example.com/openapi.json", | |
| 705 | − defaultHeaders: { | |
| 706 | − accept: "application/json", | |
| 698 | + SourceIdSchema.make("src_legacy_openapi"), | |
| 699 | + ), | |
| 700 | + ); | |
| 701 | + expect( | |
| 702 | + JSON.parse(Option.getOrThrow(openApiSource).bindingConfigJson), | |
| 703 | + ).toEqual({ | |
| 704 | + adapterKey: "openapi", | |
| 705 | + version: 1, | |
| 706 | + payload: { | |
| 707 | + specUrl: "https://api.example.com/openapi.json", | |
| 708 | + defaultHeaders: { | |
| 709 | + accept: "application/json", | |
| 710 | + }, | |
| 707 | 711 | }, |
| 708 | 712 | }); |
| 709 | − expect(openApiSource.bindingVersion).toBe(1); | |
| 710 | 713 | |
| 711 | − const graphqlSource = await Effect.runPromise(loadSourceById( | |
| 712 | − upgradedPersistence.rows, | |
| 713 | − { | |
| 714 | + const graphqlSource = await Effect.runPromise( | |
| 715 | + upgradedPersistence.rows.sources.getByWorkspaceAndId( | |
| 714 | 716 | workspaceId, |
| 715 | − sourceId: SourceIdSchema.make("src_legacy_graphql"), | |
| 716 | − }, | |
| 717 | − )); | |
| 718 | − expect(graphqlSource.binding).toEqual({ | |
| 719 | − defaultHeaders: { | |
| 720 | − accept: "application/json", | |
| 717 | + SourceIdSchema.make("src_legacy_graphql"), | |
| 718 | + ), | |
| 719 | + ); | |
| 720 | + expect( | |
| 721 | + JSON.parse(Option.getOrThrow(graphqlSource).bindingConfigJson), | |
| 722 | + ).toEqual({ | |
| 723 | + adapterKey: "graphql", | |
| 724 | + version: 1, | |
| 725 | + payload: { | |
| 726 | + defaultHeaders: { | |
| 727 | + accept: "application/json", | |
| 728 | + }, | |
| 721 | 729 | }, |
| 722 | 730 | }); |
| 723 | 731 | |
| @@ -726,20 +734,25 @@ describe("code-migrations", () => { · 2 unmodified lines | ||
| 726 | 734 | ); |
| 727 | 735 | expect(Option.getOrNull(graphqlRecipe)?.adapterKey).toBe("graphql"); |
| 728 | 736 | |
| 729 | − const mcpSource = await Effect.runPromise(loadSourceById( | |
| 730 | − upgradedPersistence.rows, | |
| 731 | − { | |
| 737 | + const mcpSource = await Effect.runPromise( | |
| 738 | + upgradedPersistence.rows.sources.getByWorkspaceAndId( | |
| 732 | 739 | workspaceId, |
| 733 | − sourceId: SourceIdSchema.make("src_legacy_mcp"), | |
| 734 | − }, | |
| 735 | − )); | |
| 736 | − expect(mcpSource.binding).toEqual({ | |
| 737 | − transport: "streamable-http", | |
| 738 | − queryParams: { | |
| 739 | − tenant: "acme", | |
| 740 | − }, | |
| 741 | − headers: { | |
| 742 | − "x-tenant": "acme", | |
| 740 | + SourceIdSchema.make("src_legacy_mcp"), | |
| 741 | + ), | |
| 742 | + ); | |
| 743 | + expect( | |
| 744 | + JSON.parse(Option.getOrThrow(mcpSource).bindingConfigJson), | |
| 745 | + ).toEqual({ | |
| 746 | + adapterKey: "mcp", | |
| 747 | + version: 1, | |
| 748 | + payload: { | |
| 749 | + transport: "streamable-http", | |
| 750 | + queryParams: { | |
| 751 | + tenant: "acme", | |
| 752 | + }, | |
| 753 | + headers: { | |
| 754 | + "x-tenant": "acme", | |
| 755 | + }, | |
| 743 | 756 | }, |
| 744 | 757 | }); |
| 745 | 758 | |
| @@ -3,16 +3,11 @@ import { ControlPlaneAuthHeaders } from "../auth-headers"; · 2 unmodified lines | ||
| 3 | 3 | import { |
| 4 | 4 | ControlPlaneActorResolver, |
| 5 | 5 | type ControlPlaneActorResolverShape, |
| 6 | − deriveWorkspaceMembershipsForPrincipal, | |
| 7 | 6 | } from "#api"; |
| 8 | 7 | import { |
| 9 | 8 | ActorUnauthenticatedError, |
| 10 | 9 | createActor, |
| 11 | 10 | } from "#domain"; |
| 12 | −import { | |
| 13 | − SqlControlPlaneRowsService, | |
| 14 | − type SqlControlPlaneRows, | |
| 15 | −} from "#persistence"; | |
| 16 | 11 | import { |
| 17 | 12 | PrincipalProviderSchema, |
| 18 | 13 | PrincipalSchema, |
| @@ -23,6 +18,7 @@ import * as Layer from "effect/Layer"; · 4 unmodified lines | ||
| 23 | 18 | import * as Option from "effect/Option"; |
| 24 | 19 | import * as ParseResult from "effect/ParseResult"; |
| 25 | 20 | import * as Schema from "effect/Schema"; |
| 21 | +import { getRuntimeLocalWorkspaceOption } from "./local-runtime-context"; | |
| 26 | 22 | |
| 27 | 23 | export { ControlPlaneAuthHeaders }; |
| 28 | 24 | |
| @@ -95,57 +91,44 @@ const readPrincipalFromHeaders = ( · 66 unmodified lines | ||
| 95 | 91 | ); |
| 96 | 92 | }); |
| 97 | 93 | |
| 98 | −export const createHeaderActorResolver = ( | |
| 99 | − rows: SqlControlPlaneRows, | |
| 100 | −): ControlPlaneActorResolverShape => ({ | |
| 94 | +const createLocalActor = (principal: Principal) => | |
| 95 | + createActor({ | |
| 96 | + principal, | |
| 97 | + workspaceMemberships: [], | |
| 98 | + organizationMemberships: [], | |
| 99 | + }); | |
| 100 | + | |
| 101 | +export const createHeaderActorResolver = (): ControlPlaneActorResolverShape => ({ | |
| 101 | 102 | resolveActor: ({ headers }) => |
| 102 | 103 | Effect.gen(function* () { |
| 103 | 104 | const principal = yield* readPrincipalFromHeaders(headers); |
| 104 | − const organizationMemberships = yield* rows.organizationMemberships | |
| 105 | − .listByAccountId(principal.accountId) | |
| 106 | − .pipe( | |
| 107 | − Effect.mapError((cause) => | |
| 108 | − toUnauthenticatedError("Failed loading memberships", cause), | |
| 109 | − ), | |
| 110 | − ); | |
| 111 | − | |
| 112 | − return yield* createActor({ | |
| 113 | − principal, | |
| 114 | − workspaceMemberships: [], | |
| 115 | − organizationMemberships, | |
| 116 | − }); | |
| 105 | + return yield* createLocalActor(principal); | |
| 117 | 106 | }), |
| 118 | 107 | |
| 119 | 108 | resolveWorkspaceActor: ({ workspaceId, headers }) => |
| 120 | 109 | Effect.gen(function* () { |
| 121 | 110 | const principal = yield* readPrincipalFromHeaders(headers); |
| 122 | − const organizationMemberships = yield* rows.organizationMemberships | |
| 123 | − .listByAccountId(principal.accountId) | |
| 124 | − .pipe( | |
| 125 | − Effect.mapError((cause) => | |
| 126 | − toUnauthenticatedError("Failed loading memberships", cause), | |
| 127 | − ), | |
| 128 | − ); | |
| 129 | − | |
| 130 | − const workspaceOption = yield* rows.workspaces.getById(workspaceId).pipe( | |
| 131 | − Effect.mapError((cause) => | |
| 132 | − toUnauthenticatedError("Failed loading workspace context", cause), | |
| 133 | − ), | |
| 134 | − ); | |
| 135 | − const workspace = Option.getOrNull(workspaceOption); | |
| 136 | − | |
| 137 | − const workspaceMemberships = deriveWorkspaceMembershipsForPrincipal({ | |
| 138 | − principalAccountId: principal.accountId, | |
| 139 | − workspaceId, | |
| 140 | − workspace, | |
| 141 | − organizationMemberships, | |
| 142 | − }); | |
| 143 | − | |
| 144 | − return yield* createActor({ | |
| 145 | − principal, | |
| 146 | − workspaceMemberships, | |
| 147 | − organizationMemberships, | |
| 148 | − }); | |
| 111 | + const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 112 | + if ( | |
| 113 | + runtimeLocalWorkspace !== null | |
| 114 | + && runtimeLocalWorkspace.installation.workspaceId === workspaceId | |
| 115 | + && runtimeLocalWorkspace.installation.accountId === principal.accountId | |
| 116 | + ) { | |
| 117 | + return yield* createActor({ | |
| 118 | + principal, | |
| 119 | + workspaceMemberships: [{ | |
| 120 | + accountId: principal.accountId, | |
| 121 | + workspaceId, | |
| 122 | + role: "owner", | |
| 123 | + status: "active", | |
| 124 | + grantedAt: 0, | |
| 125 | + updatedAt: 0, | |
| 126 | + }], | |
| 127 | + organizationMemberships: [], | |
| 128 | + }); | |
| 129 | + } | |
| 130 | + | |
| 131 | + return yield* createLocalActor(principal); | |
| 149 | 132 | }), |
| 150 | 133 | }); |
| 151 | 134 | |
| @@ -154,7 +137,4 @@ export const RuntimeActorResolverLive = ( · 2 unmodified lines | ||
| 154 | 137 | ) => |
| 155 | 138 | actorResolver |
| 156 | 139 | ? Layer.succeed(ControlPlaneActorResolver, actorResolver) |
| 157 | − : Layer.effect( | |
| 158 | − ControlPlaneActorResolver, | |
| 159 | − Effect.map(SqlControlPlaneRowsService, (rows) => createHeaderActorResolver(rows)), | |
| 160 | − ); | |
| 140 | + : Layer.succeed(ControlPlaneActorResolver, createHeaderActorResolver()); | |
| @@ -19,7 +19,7 @@ import { · 18 unmodified lines | ||
| 19 | 19 | import type { ToolPath } from "@executor/codemode-core"; |
| 20 | 20 | |
| 21 | 21 | import { |
| 22 | − createSqlControlPlaneRuntime, | |
| 22 | + createControlPlaneRuntime, | |
| 23 | 23 | LiveExecutionManagerService, |
| 24 | 24 | } from "./index"; |
| 25 | 25 | import { createSourceFromPayload } from "./source-definitions"; |
| @@ -28,7 +28,7 @@ import { persistSource } from "./source-store"; · 2 unmodified lines | ||
| 28 | 28 | import { withControlPlaneClient } from "./test-http-client"; |
| 29 | 29 | |
| 30 | 30 | const makeRuntime = Effect.acquireRelease( |
| 31 | − createSqlControlPlaneRuntime({ | |
| 31 | + createControlPlaneRuntime({ | |
| 32 | 32 | localDataDir: ":memory:", |
| 33 | 33 | workspaceRoot: mkdtempSync(join(tmpdir(), "executor-control-plane-runtime-")), |
| 34 | 34 | }), |
| @@ -123,7 +123,7 @@ describe("control-plane-runtime", () => { · 88 unmodified lines | ||
| 123 | 123 | Effect.gen(function* () { |
| 124 | 124 | const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-local-config-runtime-")); |
| 125 | 125 | const runtime = yield* Effect.acquireRelease( |
| 126 | − createSqlControlPlaneRuntime({ | |
| 126 | + createControlPlaneRuntime({ | |
| 127 | 127 | localDataDir: ":memory:", |
| 128 | 128 | workspaceRoot, |
| 129 | 129 | }), |
| @@ -175,150 +175,9 @@ describe("control-plane-runtime", () => { · 45 unmodified lines | ||
| 175 | 175 | }; |
| 176 | 176 | expect(removedConfig.sources?.github).toBeUndefined(); |
| 177 | 177 | }), |
| 178 | + 60_000, | |
| 178 | 179 | ); |
| 179 | 180 | |
| 180 | − it.scoped("supports full CRUD flow over HTTP API", () => | |
| 181 | − Effect.gen(function* () { | |
| 182 | − const runtime = yield* makeRuntime; | |
| 183 | − const openApiServer = yield* makeOpenApiSpecServer; | |
| 184 | − | |
| 185 | − const createOrg = yield* withControlPlaneClient( | |
| 186 | − { runtime, accountId: "acc_1" }, | |
| 187 | − (client) => | |
| 188 | − client.organizations.create({ | |
| 189 | − payload: { | |
| 190 | − name: "Acme", | |
| 191 | − }, | |
| 192 | − }), | |
| 193 | − ); | |
| 194 | − const organizationId = createOrg.id; | |
| 195 | − | |
| 196 | − const createWorkspace = yield* withControlPlaneClient( | |
| 197 | − { runtime, accountId: "acc_1" }, | |
| 198 | − (client) => | |
| 199 | − client.workspaces.create({ | |
| 200 | − path: { organizationId }, | |
| 201 | − payload: { name: "Primary" }, | |
| 202 | − }), | |
| 203 | − ); | |
| 204 | − expect(createWorkspace.createdByAccountId).toBe("acc_1"); | |
| 205 | − const workspaceId = createWorkspace.id; | |
| 206 | − | |
| 207 | − yield* withControlPlaneClient( | |
| 208 | − { runtime, accountId: "acc_1" }, | |
| 209 | − (client) => | |
| 210 | − client.sources.create({ | |
| 211 | − path: { workspaceId }, | |
| 212 | − payload: { | |
| 213 | − name: "Github", | |
| 214 | − kind: "openapi", | |
| 215 | − endpoint: openApiServer.baseUrl, | |
| 216 | − binding: { | |
| 217 | − specUrl: openApiServer.specUrl, | |
| 218 | − defaultHeaders: null, | |
| 219 | − }, | |
| 220 | − auth: { | |
| 221 | − kind: "none", | |
| 222 | − }, | |
| 223 | − }, | |
| 224 | − }), | |
| 225 | − ); | |
| 226 | − | |
| 227 | − yield* withControlPlaneClient( | |
| 228 | − { runtime, accountId: "acc_1" }, | |
| 229 | − (client) => | |
| 230 | − client.policies.create({ | |
| 231 | − path: { workspaceId }, | |
| 232 | − payload: { | |
| 233 | − resourceType: "tool_path", | |
| 234 | − resourcePattern: "source.github.*", | |
| 235 | − matchType: "glob", | |
| 236 | − effect: "allow", | |
| 237 | − approvalMode: "auto", | |
| 238 | − priority: 50, | |
| 239 | − enabled: true, | |
| 240 | − }, | |
| 241 | − }), | |
| 242 | − ); | |
| 243 | − | |
| 244 | − const listSources = yield* withControlPlaneClient( | |
| 245 | − { runtime, accountId: "acc_1" }, | |
| 246 | − (client) => | |
| 247 | − client.sources.list({ | |
| 248 | − path: { workspaceId }, | |
| 249 | − }), | |
| 250 | − ); | |
| 251 | − expect(listSources.length).toBe(1); | |
| 252 | − | |
| 253 | − const listPolicies = yield* withControlPlaneClient( | |
| 254 | − { runtime, accountId: "acc_1" }, | |
| 255 | − (client) => | |
| 256 | − client.policies.list({ | |
| 257 | − path: { workspaceId }, | |
| 258 | − }), | |
| 259 | − ); | |
| 260 | − expect(listPolicies.length).toBe(1); | |
| 261 | − }), | |
| 262 | − ); | |
| 263 | − | |
| 264 | − it.scoped("discovers and connects an OpenAPI source through the HTTP API", () => | |
| 265 | − Effect.gen(function* () { | |
| 266 | − const runtime = yield* makeRuntime; | |
| 267 | − const openApiServer = yield* makeOpenApiSpecServer; | |
| 268 | − | |
| 269 | − const organization = yield* withControlPlaneClient( | |
| 270 | − { runtime, accountId: "acc_discover" }, | |
| 271 | − (client) => | |
| 272 | − client.organizations.create({ | |
| 273 | − payload: { name: "Discovery Org" }, | |
| 274 | − }), | |
| 275 | − ); | |
| 276 | − const workspace = yield* withControlPlaneClient( | |
| 277 | − { runtime, accountId: "acc_discover" }, | |
| 278 | − (client) => | |
| 279 | − client.workspaces.create({ | |
| 280 | − path: { organizationId: organization.id }, | |
| 281 | − payload: { name: "Discovery Workspace" }, | |
| 282 | − }), | |
| 283 | − ); | |
| 284 | − | |
| 285 | − const discovered = yield* withControlPlaneClient( | |
| 286 | − { runtime, accountId: "acc_discover" }, | |
| 287 | − (client) => | |
| 288 | − client.sources.discover({ | |
| 289 | − payload: { | |
| 290 | − url: openApiServer.specUrl, | |
| 291 | − }, | |
| 292 | − }), | |
| 293 | − ); | |
| 294 | − expect(discovered.detectedKind).toBe("openapi"); | |
| 295 | − expect(discovered.specUrl).toBe(openApiServer.specUrl); | |
| 296 | − expect(discovered.endpoint).toBe(openApiServer.baseUrl); | |
| 297 | − | |
| 298 | − const connected = yield* withControlPlaneClient( | |
| 299 | − { runtime, accountId: "acc_discover" }, | |
| 300 | − (client) => | |
| 301 | − client.sources.connect({ | |
| 302 | − path: { workspaceId: workspace.id }, | |
| 303 | − payload: { | |
| 304 | − kind: "openapi", | |
| 305 | − endpoint: discovered.endpoint, | |
| 306 | − specUrl: discovered.specUrl ?? openApiServer.specUrl, | |
| 307 | − name: discovered.name, | |
| 308 | − namespace: discovered.namespace, | |
| 309 | − auth: { kind: "none" }, | |
| 310 | − }, | |
| 311 | − }), | |
| 312 | − ); | |
| 313 | − expect(connected.kind).toBe("connected"); | |
| 314 | − expect(connected.source.kind).toBe("openapi"); | |
| 315 | − expect(connected.source.status).toBe("connected"); | |
| 316 | − expect(connected.source.binding).toEqual({ | |
| 317 | − specUrl: openApiServer.specUrl, | |
| 318 | − defaultHeaders: null, | |
| 319 | − }); | |
| 320 | − }), | |
| 321 | − ); | |
| 322 | 181 | |
| 323 | 182 | it.scoped("captures credential requests through the local HTML flow without persisting raw tokens", () => |
| 324 | 183 | Effect.gen(function* () { |
| @@ -608,264 +467,4 @@ describe("control-plane-runtime", () => { · 283 unmodified lines | ||
| 608 | 467 | }), |
| 609 | 468 | ); |
| 610 | 469 | |
| 611 | − it.scoped("scopes organization list/get to memberships", () => | |
| 612 | − Effect.gen(function* () { | |
| 613 | − const runtime = yield* makeRuntime; | |
| 614 | − | |
| 615 | − const orgOne = yield* withControlPlaneClient( | |
| 616 | − { runtime, accountId: "acc_1" }, | |
| 617 | − (client) => | |
| 618 | − client.organizations.create({ | |
| 619 | − payload: { name: "Org One" }, | |
| 620 | − }), | |
| 621 | − ); | |
| 622 | − | |
| 623 | − const orgTwo = yield* withControlPlaneClient( | |
| 624 | − { runtime, accountId: "acc_2" }, | |
| 625 | − (client) => | |
| 626 | − client.organizations.create({ | |
| 627 | − payload: { name: "Org Two" }, | |
| 628 | − }), | |
| 629 | − ); | |
| 630 | − | |
| 631 | − const listForAcc1 = yield* withControlPlaneClient( | |
| 632 | − { runtime, accountId: "acc_1" }, | |
| 633 | − (client) => client.organizations.list({}), | |
| 634 | − ); | |
| 635 | − | |
| 636 | − expect(listForAcc1.length).toBe(1); | |
| 637 | − | |
| 638 | − const getOtherOrgError = yield* expectLeft( | |
| 639 | − withControlPlaneClient( | |
| 640 | − { runtime, accountId: "acc_1" }, | |
| 641 | − (client) => | |
| 642 | − client.organizations.get({ | |
| 643 | − path: { organizationId: orgTwo.id }, | |
| 644 | − }), | |
| 645 | − ), | |
| 646 | − ); | |
| 647 | − | |
| 648 | − assertTrue(orgOne.id.length > 0); | |
| 649 | − expect(getOtherOrgError._tag).toBe("ControlPlaneNotFoundError"); | |
| 650 | − }), | |
| 651 | − ); | |
| 652 | − | |
| 653 | − it.scoped("prevents viewers from workspace manage actions", () => | |
| 654 | − Effect.gen(function* () { | |
| 655 | − const runtime = yield* makeRuntime; | |
| 656 | − | |
| 657 | − const organization = yield* withControlPlaneClient( | |
| 658 | − { runtime, accountId: "owner_acc" }, | |
| 659 | − (client) => | |
| 660 | − client.organizations.create({ | |
| 661 | − payload: { name: "Secured Org" }, | |
| 662 | − }), | |
| 663 | − ); | |
| 664 | − | |
| 665 | − const workspace = yield* withControlPlaneClient( | |
| 666 | − { runtime, accountId: "owner_acc" }, | |
| 667 | − (client) => | |
| 668 | − client.workspaces.create({ | |
| 669 | − path: { organizationId: organization.id }, | |
| 670 | − payload: { name: "Secured WS" }, | |
| 671 | − }), | |
| 672 | − ); | |
| 673 | − | |
| 674 | − yield* withControlPlaneClient( | |
| 675 | − { runtime, accountId: "owner_acc" }, | |
| 676 | − (client) => | |
| 677 | − client.memberships.create({ | |
| 678 | − path: { organizationId: organization.id }, | |
| 679 | − payload: { | |
| 680 | − accountId: "viewer_acc" as AccountId, | |
| 681 | − role: "viewer", | |
| 682 | − status: "active", | |
| 683 | − }, | |
| 684 | − }), | |
| 685 | − ); | |
| 686 | − | |
| 687 | − const viewerDeleteError = yield* expectLeft( | |
| 688 | − withControlPlaneClient( | |
| 689 | − { runtime, accountId: "viewer_acc" }, | |
| 690 | − (client) => | |
| 691 | − client.workspaces.remove({ | |
| 692 | − path: { workspaceId: workspace.id }, | |
| 693 | − }), | |
| 694 | − ), | |
| 695 | − ); | |
| 696 | − | |
| 697 | − expect(viewerDeleteError._tag).toBe("ControlPlaneForbiddenError"); | |
| 698 | − }), | |
| 699 | − ); | |
| 700 | − | |
| 701 | − it.scoped("blocks organization manage across tenants", () => | |
| 702 | − Effect.gen(function* () { | |
| 703 | − const runtime = yield* makeRuntime; | |
| 704 | − | |
| 705 | − const orgA = yield* withControlPlaneClient( | |
| 706 | − { runtime, accountId: "acc_a" }, | |
| 707 | − (client) => | |
| 708 | − client.organizations.create({ | |
| 709 | − payload: { name: "Org A" }, | |
| 710 | − }), | |
| 711 | − ); | |
| 712 | − | |
| 713 | − const orgB = yield* withControlPlaneClient( | |
| 714 | − { runtime, accountId: "acc_b" }, | |
| 715 | − (client) => | |
| 716 | − client.organizations.create({ | |
| 717 | − payload: { name: "Org B" }, | |
| 718 | − }), | |
| 719 | − ); | |
| 720 | − | |
| 721 | − const patchOtherOrgError = yield* expectLeft( | |
| 722 | − withControlPlaneClient( | |
| 723 | − { runtime, accountId: "acc_a" }, | |
| 724 | − (client) => | |
| 725 | − client.organizations.update({ | |
| 726 | − path: { organizationId: orgB.id }, | |
| 727 | − payload: { name: "Renamed" }, | |
| 728 | − }), | |
| 729 | − ), | |
| 730 | − ); | |
| 731 | − assertTrue(orgA.id.length > 0); | |
| 732 | − expect(patchOtherOrgError._tag).toBe("ControlPlaneForbiddenError"); | |
| 733 | − | |
| 734 | − const deleteOtherOrgError = yield* expectLeft( | |
| 735 | − withControlPlaneClient( | |
| 736 | − { runtime, accountId: "acc_a" }, | |
| 737 | − (client) => | |
| 738 | − client.organizations.remove({ | |
| 739 | − path: { organizationId: orgB.id }, | |
| 740 | − }), | |
| 741 | − ), | |
| 742 | − ); | |
| 743 | − expect(deleteOtherOrgError._tag).toBe("ControlPlaneForbiddenError"); | |
| 744 | − }), | |
| 745 | − ); | |
| 746 | − | |
| 747 | − it.scoped("suspended creators cannot manage previously created workspaces", () => | |
| 748 | − Effect.gen(function* () { | |
| 749 | − const runtime = yield* makeRuntime; | |
| 750 | − | |
| 751 | − const organization = yield* withControlPlaneClient( | |
| 752 | − { runtime, accountId: "creator_acc" }, | |
| 753 | − (client) => | |
| 754 | − client.organizations.create({ | |
| 755 | − payload: { name: "Creator Org" }, | |
| 756 | − }), | |
| 757 | − ); | |
| 758 | − | |
| 759 | − const workspace = yield* withControlPlaneClient( | |
| 760 | − { runtime, accountId: "creator_acc" }, | |
| 761 | − (client) => | |
| 762 | − client.workspaces.create({ | |
| 763 | − path: { organizationId: organization.id }, | |
| 764 | − payload: { name: "Creator WS" }, | |
| 765 | − }), | |
| 766 | − ); | |
| 767 | − | |
| 768 | − yield* withControlPlaneClient( | |
| 769 | − { runtime, accountId: "creator_acc" }, | |
| 770 | − (client) => | |
| 771 | − client.memberships.update({ | |
| 772 | − path: { | |
| 773 | − organizationId: organization.id, | |
| 774 | − accountId: "creator_acc" as AccountId, | |
| 775 | − }, | |
| 776 | − payload: { status: "suspended" }, | |
| 777 | − }), | |
| 778 | − ); | |
| 779 | − | |
| 780 | − const deleteWorkspaceError = yield* expectLeft( | |
| 781 | − withControlPlaneClient( | |
| 782 | − { runtime, accountId: "creator_acc" }, | |
| 783 | − (client) => | |
| 784 | − client.workspaces.remove({ | |
| 785 | − path: { workspaceId: workspace.id }, | |
| 786 | − }), | |
| 787 | − ), | |
| 788 | − ); | |
| 789 | − | |
| 790 | − expect(deleteWorkspaceError._tag).toBe("ControlPlaneForbiddenError"); | |
| 791 | − }), | |
| 792 | − ); | |
| 793 | − | |
| 794 | − it.scoped("deleting organization cascades and blocks stale org operations", () => | |
| 795 | − Effect.gen(function* () { | |
| 796 | − const runtime = yield* makeRuntime; | |
| 797 | − | |
| 798 | − const organization = yield* withControlPlaneClient( | |
| 799 | − { runtime, accountId: "acc_del" }, | |
| 800 | − (client) => | |
| 801 | − client.organizations.create({ | |
| 802 | − payload: { name: "Delete Me" }, | |
| 803 | − }), | |
| 804 | − ); | |
| 805 | − | |
| 806 | − const workspace = yield* withControlPlaneClient( | |
| 807 | − { runtime, accountId: "acc_del" }, | |
| 808 | − (client) => | |
| 809 | − client.workspaces.create({ | |
| 810 | − path: { organizationId: organization.id }, | |
| 811 | − payload: { name: "Delete WS" }, | |
| 812 | − }), | |
| 813 | − ); | |
| 814 | − | |
| 815 | − const deleteOrg = yield* withControlPlaneClient( | |
| 816 | − { runtime, accountId: "acc_del" }, | |
| 817 | − (client) => | |
| 818 | − client.organizations.remove({ | |
| 819 | − path: { organizationId: organization.id }, | |
| 820 | − }), | |
| 821 | − ); | |
| 822 | − assertTrue(deleteOrg.removed); | |
| 823 | − | |
| 824 | − const listStaleWorkspacesError = yield* expectLeft( | |
| 825 | − withControlPlaneClient( | |
| 826 | − { runtime, accountId: "acc_del" }, | |
| 827 | − (client) => | |
| 828 | − client.workspaces.list({ | |
| 829 | − path: { organizationId: organization.id }, | |
| 830 | − }), | |
| 831 | − ), | |
| 832 | − ); | |
| 833 | − expect(listStaleWorkspacesError._tag).toBe("ControlPlaneForbiddenError"); | |
| 834 | − | |
| 835 | − const createStaleWorkspaceError = yield* expectLeft( | |
| 836 | − withControlPlaneClient( | |
| 837 | − { runtime, accountId: "acc_del" }, | |
| 838 | − (client) => | |
| 839 | − client.workspaces.create({ | |
| 840 | − path: { organizationId: organization.id }, | |
| 841 | − payload: { name: "Should Fail" }, | |
| 842 | − }), | |
| 843 | − ), | |
| 844 | − ); | |
| 845 | − expect(createStaleWorkspaceError._tag).toBe("ControlPlaneForbiddenError"); | |
| 846 | − | |
| 847 | − const getDeletedWorkspaceError = yield* expectLeft( | |
| 848 | − withControlPlaneClient( | |
| 849 | − { runtime, accountId: "acc_del" }, | |
| 850 | − (client) => | |
| 851 | − client.workspaces.get({ | |
| 852 | − path: { workspaceId: workspace.id }, | |
| 853 | − }), | |
| 854 | − ), | |
| 855 | − ); | |
| 856 | − expect(getDeletedWorkspaceError._tag).toBe("ControlPlaneForbiddenError"); | |
| 857 | − }), | |
| 858 | − ); | |
| 859 | − | |
| 860 | − it.scoped("rejects unauthenticated calls", () => | |
| 861 | − Effect.gen(function* () { | |
| 862 | − const runtime = yield* makeRuntime; | |
| 863 | − | |
| 864 | − const error = yield* expectLeft( | |
| 865 | − withControlPlaneClient({ runtime }, (client) => client.organizations.list({})), | |
| 866 | − ); | |
| 867 | − | |
| 868 | − expect(error._tag).toBe("ControlPlaneUnauthorizedError"); | |
| 869 | − }), | |
| 870 | − ); | |
| 871 | 470 | }); |
| @@ -9,7 +9,7 @@ import { makeToolInvokerFromTools } from "@executor/codemode-core"; · 8 unmodified lines | ||
| 9 | 9 | import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess"; |
| 10 | 10 | |
| 11 | 11 | import { |
| 12 | − createSqlControlPlaneRuntime, | |
| 12 | + createControlPlaneRuntime, | |
| 13 | 13 | } from "./index"; |
| 14 | 14 | import { withControlPlaneClient } from "./test-http-client"; |
| 15 | 15 | |
| @@ -40,7 +40,7 @@ const makeExecutionResolver = () => { · 24 unmodified lines | ||
| 40 | 40 | }; |
| 41 | 41 | |
| 42 | 42 | const makeRuntime = Effect.acquireRelease( |
| 43 | − createSqlControlPlaneRuntime({ | |
| 43 | + createControlPlaneRuntime({ | |
| 44 | 44 | localDataDir: ":memory:", |
| 45 | 45 | workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-http-")), |
| 46 | 46 | executionResolver: makeExecutionResolver(), |
| @@ -18,7 +18,7 @@ import { discoverMcpToolsFromConnector } from "@executor/codemode-mcp"; · 17 unmodified lines | ||
| 18 | 18 | import { makeDenoSubprocessExecutor } from "@executor/runtime-deno-subprocess"; |
| 19 | 19 | |
| 20 | 20 | import { |
| 21 | − createSqlControlPlaneRuntime, | |
| 21 | + createControlPlaneRuntime, | |
| 22 | 22 | type ResolveExecutionEnvironment, |
| 23 | 23 | } from "./index"; |
| 24 | 24 | import { withControlPlaneClient } from "./test-http-client"; |
| @@ -284,7 +284,7 @@ describe("execution-mcp-resume", () => { · 259 unmodified lines | ||
| 284 | 284 | Effect.gen(function* () { |
| 285 | 285 | const mcpServer = yield* makeFormElicitationServer; |
| 286 | 286 | const runtime = yield* Effect.acquireRelease( |
| 287 | − createSqlControlPlaneRuntime({ | |
| 287 | + createControlPlaneRuntime({ | |
| 288 | 288 | localDataDir: ":memory:", |
| 289 | 289 | workspaceRoot: mkdtempSync(join(tmpdir(), "executor-execution-mcp-resume-")), |
| 290 | 290 | executionResolver: makeMcpExecutionResolver(mcpServer.endpoint), |
| @@ -88,12 +88,12 @@ export type RuntimeControlPlaneOptions = { · 87 unmodified lines | ||
| 88 | 88 | const detailsFromCause = (cause: unknown): string => |
| 89 | 89 | cause instanceof Error ? cause.message : String(cause); |
| 90 | 90 | |
| 91 | −const toLocalInstallationBootstrapError = ( | |
| 91 | +const toLocalRuntimeBootstrapError = ( | |
| 92 | 92 | cause: unknown, |
| 93 | 93 | ): SqlPersistenceBootstrapError => { |
| 94 | 94 | const details = detailsFromCause(cause); |
| 95 | 95 | return new SqlPersistenceBootstrapError({ |
| 96 | − message: `Failed provisioning local installation: ${details}`, | |
| 96 | + message: `Failed initializing local runtime: ${details}`, | |
| 97 | 97 | details, |
| 98 | 98 | }); |
| 99 | 99 | }; |
| @@ -165,19 +165,19 @@ export const createRuntimeControlPlaneLayer = ( · 65 unmodified lines | ||
| 165 | 165 | ); |
| 166 | 166 | }; |
| 167 | 167 | |
| 168 | −export type SqlControlPlaneRuntime = { | |
| 168 | +export type ControlPlaneRuntime = { | |
| 169 | 169 | persistence: SqlControlPlanePersistence; |
| 170 | 170 | localInstallation: LocalInstallation; |
| 171 | 171 | runtimeLayer: RuntimeControlPlaneLayer; |
| 172 | 172 | close: () => Promise<void>; |
| 173 | 173 | }; |
| 174 | 174 | |
| 175 | −export type CreateSqlControlPlaneRuntimeOptions = CreateSqlRuntimeOptions | |
| 175 | +export type CreateControlPlaneRuntimeOptions = CreateSqlRuntimeOptions | |
| 176 | 176 | & RuntimeControlPlaneOptions; |
| 177 | 177 | |
| 178 | −export const createSqlControlPlaneRuntime = ( | |
| 179 | − options: CreateSqlControlPlaneRuntimeOptions, | |
| 180 | −): Effect.Effect<SqlControlPlaneRuntime, SqlPersistenceBootstrapError> => | |
| 178 | +export const createControlPlaneRuntime = ( | |
| 179 | + options: CreateControlPlaneRuntimeOptions, | |
| 180 | +): Effect.Effect<ControlPlaneRuntime, SqlPersistenceBootstrapError> => | |
| 181 | 181 | Effect.gen(function* () { |
| 182 | 182 | const scope = yield* Scope.make(); |
| 183 | 183 | const persistenceAndRowsLayer = SqlControlPlaneRowsLive.pipe( |
| @@ -198,7 +198,7 @@ export const createSqlControlPlaneRuntime = ( · 14 unmodified lines | ||
| 198 | 198 | resolveLocalWorkspaceContext({ |
| 199 | 199 | workspaceRoot: options.workspaceRoot, |
| 200 | 200 | }), |
| 201 | − catch: toLocalInstallationBootstrapError, | |
| 201 | + catch: toLocalRuntimeBootstrapError, | |
| 202 | 202 | }).pipe( |
| 203 | 203 | Effect.catchAll((error) => |
| 204 | 204 | closeScope(scope).pipe( |
| @@ -210,7 +210,7 @@ export const createSqlControlPlaneRuntime = ( · 5 unmodified lines | ||
| 210 | 210 | rows, |
| 211 | 211 | context: localWorkspaceContext, |
| 212 | 212 | }).pipe( |
| 213 | − Effect.mapError(toLocalInstallationBootstrapError), | |
| 213 | + Effect.mapError(toLocalRuntimeBootstrapError), | |
| 214 | 214 | Effect.catchAll((error) => |
| 215 | 215 | closeScope(scope).pipe( |
| 216 | 216 | Effect.zipRight(Effect.fail(error)), |
| @@ -219,7 +219,7 @@ export const createSqlControlPlaneRuntime = ( · 2 unmodified lines | ||
| 219 | 219 | |
| 220 | 220 | const loadedLocalConfig = yield* Effect.tryPromise({ |
| 221 | 221 | try: () => loadLocalExecutorConfig(localWorkspaceContext), |
| 222 | − catch: toLocalInstallationBootstrapError, | |
| 222 | + catch: toLocalRuntimeBootstrapError, | |
| 223 | 223 | }).pipe( |
| 224 | 224 | Effect.catchAll((error) => |
| 225 | 225 | closeScope(scope).pipe( |
| @@ -228,14 +228,10 @@ export const createSqlControlPlaneRuntime = ( · 2 unmodified lines | ||
| 228 | 228 | ); |
| 229 | 229 | |
| 230 | 230 | const effectiveLocalConfig = yield* synchronizeLocalWorkspaceState({ |
| 231 | − rows, | |
| 232 | 231 | context: localWorkspaceContext, |
| 233 | 232 | loadedConfig: loadedLocalConfig, |
| 234 | − installation: { | |
| 235 | − workspaceId: localInstallation.workspaceId, | |
| 236 | − }, | |
| 237 | 233 | }).pipe( |
| 238 | − Effect.mapError(toLocalInstallationBootstrapError), | |
| 234 | + Effect.mapError(toLocalRuntimeBootstrapError), | |
| 239 | 235 | Effect.catchAll((error) => |
| 240 | 236 | closeScope(scope).pipe( |
| 241 | 237 | Effect.zipRight(Effect.fail(error)), |
| @@ -262,6 +258,7 @@ export const createSqlControlPlaneRuntime = ( · 20 unmodified lines | ||
| 262 | 258 | installation: { |
| 263 | 259 | workspaceId: localInstallation.workspaceId, |
| 264 | 260 | accountId: localInstallation.accountId, |
| 261 | + organizationId: localInstallation.organizationId, | |
| 265 | 262 | }, |
| 266 | 263 | loadedConfig: { |
| 267 | 264 | ...loadedLocalConfig, |
| @@ -277,7 +274,7 @@ export const createSqlControlPlaneRuntime = ( · 9 unmodified lines | ||
| 277 | 274 | resolveSecretMaterial, |
| 278 | 275 | }); |
| 279 | 276 | const actorResolver = |
| 280 | − options.actorResolver ?? createHeaderActorResolver(rows); | |
| 277 | + options.actorResolver ?? createHeaderActorResolver(); | |
| 281 | 278 | |
| 282 | 279 | const runtimeContext = Context.empty().pipe( |
| 283 | 280 | Context.add(ControlPlaneActorResolver, actorResolver), |
| @@ -288,6 +285,7 @@ export const createSqlControlPlaneRuntime = ( · 4 unmodified lines | ||
| 288 | 285 | installation: { |
| 289 | 286 | workspaceId: localInstallation.workspaceId, |
| 290 | 287 | accountId: localInstallation.accountId, |
| 288 | + organizationId: localInstallation.organizationId, | |
| 291 | 289 | }, |
| 292 | 290 | loadedConfig: { |
| 293 | 291 | ...loadedLocalConfig, |
| @@ -2,17 +2,16 @@ import { existsSync, mkdtempSync } from "node:fs"; · 1 unmodified line | ||
| 2 | 2 | import { tmpdir } from "node:os"; |
| 3 | 3 | import { join } from "node:path"; |
| 4 | 4 | import { describe, expect, it } from "@effect/vitest"; |
| 5 | −import { assertTrue } from "@effect/vitest/utils"; | |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| 7 | 6 | |
| 8 | −import { createSqlControlPlaneRuntime } from "./index"; | |
| 7 | +import { createControlPlaneRuntime } from "./index"; | |
| 9 | 8 | import { getOrProvisionLocalInstallation } from "./local-installation"; |
| 10 | 9 | import { resolveLocalWorkspaceContext } from "./local-config"; |
| 11 | 10 | |
| 12 | 11 | const TEST_WORKSPACE_ROOT = mkdtempSync(join(tmpdir(), "executor-local-installation-")); |
| 13 | 12 | |
| 14 | 13 | const makeRuntime = Effect.acquireRelease( |
| 15 | − createSqlControlPlaneRuntime({ | |
| 14 | + createControlPlaneRuntime({ | |
| 16 | 15 | localDataDir: ":memory:", |
| 17 | 16 | workspaceRoot: TEST_WORKSPACE_ROOT, |
| 18 | 17 | }), |
| @@ -20,24 +19,18 @@ const makeRuntime = Effect.acquireRelease( · 1 unmodified line | ||
| 20 | 19 | ); |
| 21 | 20 | |
| 22 | 21 | describe("local-installation", () => { |
| 23 | − it.scoped("provisions a local account, organization, and workspace on first boot", () => | |
| 22 | + it.scoped("derives a stable local identity on first boot", () => | |
| 24 | 23 | Effect.gen(function* () { |
| 25 | 24 | const runtime = yield* makeRuntime; |
| 26 | 25 | const installation = runtime.localInstallation; |
| 27 | 26 | |
| 28 | − const account = yield* runtime.persistence.rows.accounts.getById(installation.accountId); | |
| 29 | − const organization = yield* runtime.persistence.rows.organizations.getById( | |
| 30 | − installation.organizationId, | |
| 31 | − ); | |
| 32 | − const workspace = yield* runtime.persistence.rows.workspaces.getById( | |
| 33 | − installation.workspaceId, | |
| 34 | − ); | |
| 35 | − | |
| 36 | − assertTrue(account._tag === "Some"); | |
| 37 | − assertTrue(organization._tag === "Some"); | |
| 38 | − assertTrue(workspace._tag === "Some"); | |
| 27 | + expect(installation.id.startsWith("local_")).toBe(true); | |
| 28 | + expect(installation.accountId).toBe("acc_local_default"); | |
| 29 | + expect(installation.organizationId).toBe("org_local_personal"); | |
| 30 | + expect(installation.workspaceId.startsWith("ws_local_")).toBe(true); | |
| 39 | 31 | expect(existsSync(join(TEST_WORKSPACE_ROOT, ".executor", "executor.jsonc"))).toBe(false); |
| 40 | 32 | }), |
| 33 | + 60_000, | |
| 41 | 34 | ); |
| 42 | 35 | |
| 43 | 36 | it.scoped("is idempotent when loading the default local installation", () => |
| @@ -1,283 +1,57 @@ | ||
| 1 | −import { type SqlControlPlaneRows } from "#persistence"; | |
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | + | |
| 3 | +import type { SqlControlPlaneRows } from "#persistence"; | |
| 2 | 4 | import { |
| 3 | 5 | AccountIdSchema, |
| 4 | 6 | InstallationIdSchema, |
| 5 | 7 | OrganizationIdSchema, |
| 6 | − OrganizationMemberIdSchema, | |
| 7 | 8 | WorkspaceIdSchema, |
| 8 | − type Account, | |
| 9 | 9 | type LocalInstallation, |
| 10 | − type Organization, | |
| 11 | − type OrganizationMembership, | |
| 12 | − type Workspace, | |
| 13 | 10 | } from "#schema"; |
| 14 | 11 | import * as Effect from "effect/Effect"; |
| 15 | −import * as Option from "effect/Option"; | |
| 16 | − | |
| 17 | −import { | |
| 18 | − defaultWorkspaceDisplayName, | |
| 19 | − type ResolvedLocalWorkspaceContext, | |
| 20 | −} from "./local-config"; | |
| 21 | 12 | |
| 22 | −const LEGACY_LOCAL_INSTALLATION_ID = InstallationIdSchema.make("local_default"); | |
| 23 | −const LOCAL_ACCOUNT_SUBJECT = "local:default"; | |
| 24 | −const PERSONAL_ORGANIZATION_SLUG = "personal"; | |
| 13 | +import type { ResolvedLocalWorkspaceContext } from "./local-config"; | |
| 25 | 14 | |
| 26 | −const trimOrNull = (value: string | null | undefined): string | null => { | |
| 27 | − if (value == null) { | |
| 28 | − return null; | |
| 29 | − } | |
| 30 | − const trimmed = value.trim(); | |
| 31 | − return trimmed.length > 0 ? trimmed : null; | |
| 32 | −}; | |
| 15 | +const LOCAL_ACCOUNT_ID = AccountIdSchema.make("acc_local_default"); | |
| 16 | +const LOCAL_ORGANIZATION_ID = OrganizationIdSchema.make("org_local_personal"); | |
| 33 | 17 | |
| 34 | −const buildAccount = (now: number): Account => { | |
| 35 | − const id = AccountIdSchema.make(`acc_${crypto.randomUUID()}`); | |
| 18 | +const stableHash = (value: string): string => | |
| 19 | + createHash("sha256").update(value).digest("hex").slice(0, 16); | |
| 36 | 20 | |
| 37 | − return { | |
| 38 | − id, | |
| 39 | − provider: "local", | |
| 40 | − subject: LOCAL_ACCOUNT_SUBJECT, | |
| 41 | − email: null, | |
| 42 | − displayName: "Local User", | |
| 43 | − createdAt: now, | |
| 44 | − updatedAt: now, | |
| 45 | − }; | |
| 46 | −}; | |
| 47 | − | |
| 48 | −const buildOrganization = ( | |
| 49 | − accountId: Account["id"], | |
| 50 | − now: number, | |
| 51 | −): Organization => ({ | |
| 52 | − id: OrganizationIdSchema.make(`org_${crypto.randomUUID()}`), | |
| 53 | − slug: PERSONAL_ORGANIZATION_SLUG, | |
| 54 | − name: "Personal", | |
| 55 | − status: "active", | |
| 56 | − createdByAccountId: accountId, | |
| 57 | − createdAt: now, | |
| 58 | − updatedAt: now, | |
| 59 | −}); | |
| 60 | − | |
| 61 | −const buildOwnerMembership = ( | |
| 62 | − organizationId: Organization["id"], | |
| 63 | − accountId: Account["id"], | |
| 64 | − now: number, | |
| 65 | −): OrganizationMembership => ({ | |
| 66 | − id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`), | |
| 67 | − organizationId, | |
| 68 | − accountId, | |
| 69 | − role: "owner", | |
| 70 | − status: "active", | |
| 71 | − billable: true, | |
| 72 | − invitedByAccountId: null, | |
| 73 | − joinedAt: now, | |
| 74 | − createdAt: now, | |
| 75 | − updatedAt: now, | |
| 76 | −}); | |
| 77 | − | |
| 78 | −const buildWorkspace = ( | |
| 79 | − organizationId: Organization["id"], | |
| 80 | − accountId: Account["id"], | |
| 81 | − workspaceName: string, | |
| 82 | − now: number, | |
| 83 | −): Workspace => ({ | |
| 84 | − id: WorkspaceIdSchema.make(`ws_${crypto.randomUUID()}`), | |
| 85 | − organizationId, | |
| 86 | − name: workspaceName, | |
| 87 | − createdByAccountId: accountId, | |
| 88 | − createdAt: now, | |
| 89 | − updatedAt: now, | |
| 90 | −}); | |
| 21 | +const normalizeSlashPath = (value: string): string => | |
| 22 | + value.replaceAll("\\", "/"); | |
| 91 | 23 | |
| 92 | −const chooseWorkspaceName = ( | |
| 93 | − desiredName: string, | |
| 94 | − existing: ReadonlyArray<Workspace>, | |
| 95 | −): string => { | |
| 96 | − const trimmed = trimOrNull(desiredName) ?? "Workspace"; | |
| 97 | − const matching = existing.find((workspace) => workspace.name === trimmed); | |
| 98 | − if (!matching) { | |
| 99 | − return trimmed; | |
| 100 | − } | |
| 101 | − return `${trimmed}-${matching.id.slice(-6)}`; | |
| 102 | −}; | |
| 103 | − | |
| 104 | −const ensureLocalAccount = ( | |
| 105 | − rows: SqlControlPlaneRows, | |
| 106 | − now: number, | |
| 107 | −): Effect.Effect<Account, unknown> => | |
| 108 | − Effect.gen(function* () { | |
| 109 | − const existing = yield* rows.accounts.getByProviderAndSubject("local", LOCAL_ACCOUNT_SUBJECT); | |
| 110 | − if (Option.isSome(existing)) { | |
| 111 | − return existing.value; | |
| 112 | − } | |
| 113 | − | |
| 114 | − const account = buildAccount(now); | |
| 115 | − yield* rows.accounts.upsert(account); | |
| 116 | − return account; | |
| 117 | − }); | |
| 118 | − | |
| 119 | −const ensurePersonalOrganization = ( | |
| 120 | − rows: SqlControlPlaneRows, | |
| 121 | − accountId: Account["id"], | |
| 122 | − now: number, | |
| 123 | −): Effect.Effect<Organization, unknown> => | |
| 124 | − Effect.gen(function* () { | |
| 125 | − const existing = yield* rows.organizations.getBySlug(PERSONAL_ORGANIZATION_SLUG); | |
| 126 | − if (Option.isSome(existing)) { | |
| 127 | − return existing.value; | |
| 128 | − } | |
| 129 | − | |
| 130 | − const organization = buildOrganization(accountId, now); | |
| 131 | − const membership = buildOwnerMembership(organization.id, accountId, now); | |
| 132 | − yield* rows.organizations.insertWithOwnerMembership(organization, membership); | |
| 133 | − return organization; | |
| 134 | − }); | |
| 135 | − | |
| 136 | −const ensureOwnerMembership = ( | |
| 137 | − rows: SqlControlPlaneRows, | |
| 138 | − organizationId: Organization["id"], | |
| 139 | − accountId: Account["id"], | |
| 140 | − now: number, | |
| 141 | −): Effect.Effect<void, unknown> => | |
| 142 | − Effect.gen(function* () { | |
| 143 | − const existing = yield* rows.organizationMemberships.getByOrganizationAndAccount( | |
| 144 | − organizationId, | |
| 145 | − accountId, | |
| 146 | − ); | |
| 147 | − if (Option.isSome(existing)) { | |
| 148 | − return; | |
| 149 | − } | |
| 150 | − | |
| 151 | − yield* rows.organizationMemberships.upsert( | |
| 152 | − buildOwnerMembership(organizationId, accountId, now), | |
| 153 | − ); | |
| 154 | − }); | |
| 155 | − | |
| 156 | −const provisionWorkspaceForContext = (input: { | |
| 157 | − rows: SqlControlPlaneRows; | |
| 158 | − accountId: Account["id"]; | |
| 159 | − organizationId: Organization["id"]; | |
| 160 | − context: ResolvedLocalWorkspaceContext; | |
| 161 | − now: number; | |
| 162 | −}): Effect.Effect<Workspace, unknown> => | |
| 163 | − Effect.gen(function* () { | |
| 164 | − const workspaces = yield* input.rows.workspaces.listByOrganizationId(input.organizationId); | |
| 165 | − const workspaceName = chooseWorkspaceName( | |
| 166 | − defaultWorkspaceDisplayName(input.context), | |
| 167 | − workspaces, | |
| 168 | − ); | |
| 169 | − const workspace = buildWorkspace( | |
| 170 | − input.organizationId, | |
| 171 | − input.accountId, | |
| 172 | − workspaceName, | |
| 173 | − input.now, | |
| 174 | − ); | |
| 175 | − yield* input.rows.workspaces.insert(workspace); | |
| 176 | − return workspace; | |
| 177 | − }); | |
| 24 | +const deriveWorkspaceId = (context: ResolvedLocalWorkspaceContext) => | |
| 25 | + WorkspaceIdSchema.make( | |
| 26 | + `ws_local_${stableHash(normalizeSlashPath(context.workspaceRoot))}`, | |
| 27 | + ); | |
| 178 | 28 | |
| 179 | −const createInstallationForWorkspace = (input: { | |
| 180 | − installationId: LocalInstallation["id"]; | |
| 181 | − accountId: Account["id"]; | |
| 182 | − organizationId: Organization["id"]; | |
| 183 | − workspaceId: Workspace["id"]; | |
| 184 | − now: number; | |
| 185 | −}): LocalInstallation => ({ | |
| 186 | − id: input.installationId, | |
| 187 | − accountId: input.accountId, | |
| 188 | − organizationId: input.organizationId, | |
| 189 | − workspaceId: input.workspaceId, | |
| 190 | − createdAt: input.now, | |
| 191 | − updatedAt: input.now, | |
| 29 | +export const deriveLocalInstallation = ( | |
| 30 | + context: ResolvedLocalWorkspaceContext, | |
| 31 | +): LocalInstallation => ({ | |
| 32 | + id: InstallationIdSchema.make(context.installationId), | |
| 33 | + accountId: LOCAL_ACCOUNT_ID, | |
| 34 | + organizationId: LOCAL_ORGANIZATION_ID, | |
| 35 | + workspaceId: deriveWorkspaceId(context), | |
| 36 | + // Derived local identity is deterministic; timestamps are not authoritative state. | |
| 37 | + createdAt: 0, | |
| 38 | + updatedAt: 0, | |
| 192 | 39 | }); |
| 193 | 40 | |
| 194 | 41 | export const loadLocalInstallation = ( |
| 195 | − rows: SqlControlPlaneRows, | |
| 42 | + _rows: SqlControlPlaneRows, | |
| 196 | 43 | context: ResolvedLocalWorkspaceContext, |
| 197 | −): Effect.Effect<LocalInstallation | null, unknown> => | |
| 198 | − rows.localInstallations.getById(InstallationIdSchema.make(context.installationId)).pipe( | |
| 199 | − Effect.map((result) => (Option.isSome(result) ? result.value : null)), | |
| 200 | − ); | |
| 201 | − | |
| 202 | −const migrateLegacyLocalInstallation = (input: { | |
| 203 | − rows: SqlControlPlaneRows; | |
| 204 | − context: ResolvedLocalWorkspaceContext; | |
| 205 | − now: number; | |
| 206 | −}): Effect.Effect<LocalInstallation | null, unknown> => | |
| 207 | − Effect.gen(function* () { | |
| 208 | − const legacy = yield* input.rows.localInstallations.getById(LEGACY_LOCAL_INSTALLATION_ID); | |
| 209 | − if (Option.isNone(legacy)) { | |
| 210 | − return null; | |
| 211 | − } | |
| 212 | − | |
| 213 | − const migrated = createInstallationForWorkspace({ | |
| 214 | − installationId: InstallationIdSchema.make(input.context.installationId), | |
| 215 | − accountId: legacy.value.accountId, | |
| 216 | − organizationId: legacy.value.organizationId, | |
| 217 | − workspaceId: legacy.value.workspaceId, | |
| 218 | − now: input.now, | |
| 219 | − }); | |
| 220 | − yield* input.rows.localInstallations.upsert(migrated); | |
| 221 | − | |
| 222 | − yield* input.rows.workspaces.update(legacy.value.workspaceId, { | |
| 223 | − name: defaultWorkspaceDisplayName(input.context), | |
| 224 | − updatedAt: input.now, | |
| 225 | − }); | |
| 226 | − | |
| 227 | − return migrated; | |
| 228 | − }); | |
| 44 | +): Effect.Effect<LocalInstallation, never> => | |
| 45 | + Effect.succeed(deriveLocalInstallation(context)); | |
| 229 | 46 | |
| 230 | 47 | export const provisionLocalInstallation = (input: { |
| 231 | 48 | rows: SqlControlPlaneRows; |
| 232 | 49 | context: ResolvedLocalWorkspaceContext; |
| 233 | −}): Effect.Effect<LocalInstallation, unknown> => | |
| 234 | − Effect.gen(function* () { | |
| 235 | − const now = Date.now(); | |
| 236 | − const installationId = InstallationIdSchema.make(input.context.installationId); | |
| 237 | − | |
| 238 | − const existing = yield* input.rows.localInstallations.getById(installationId); | |
| 239 | − if (Option.isSome(existing)) { | |
| 240 | − return existing.value; | |
| 241 | − } | |
| 242 | − | |
| 243 | − const migrated = yield* migrateLegacyLocalInstallation({ | |
| 244 | − rows: input.rows, | |
| 245 | − context: input.context, | |
| 246 | − now, | |
| 247 | − }); | |
| 248 | − if (migrated !== null) { | |
| 249 | − return migrated; | |
| 250 | − } | |
| 251 | − | |
| 252 | − const account = yield* ensureLocalAccount(input.rows, now); | |
| 253 | − const organization = yield* ensurePersonalOrganization(input.rows, account.id, now); | |
| 254 | − yield* ensureOwnerMembership(input.rows, organization.id, account.id, now); | |
| 255 | − const workspace = yield* provisionWorkspaceForContext({ | |
| 256 | − rows: input.rows, | |
| 257 | − accountId: account.id, | |
| 258 | − organizationId: organization.id, | |
| 259 | − context: input.context, | |
| 260 | − now, | |
| 261 | − }); | |
| 262 | − const installation = createInstallationForWorkspace({ | |
| 263 | − installationId, | |
| 264 | − accountId: account.id, | |
| 265 | − organizationId: organization.id, | |
| 266 | − workspaceId: workspace.id, | |
| 267 | − now, | |
| 268 | − }); | |
| 269 | − | |
| 270 | − yield* input.rows.localInstallations.upsert(installation); | |
| 271 | − | |
| 272 | − return installation; | |
| 273 | − }); | |
| 50 | +}): Effect.Effect<LocalInstallation, never> => | |
| 51 | + loadLocalInstallation(input.rows, input.context); | |
| 274 | 52 | |
| 275 | 53 | export const getOrProvisionLocalInstallation = (input: { |
| 276 | 54 | rows: SqlControlPlaneRows; |
| 277 | 55 | context: ResolvedLocalWorkspaceContext; |
| 278 | −}): Effect.Effect<LocalInstallation, unknown> => | |
| 279 | − Effect.flatMap(loadLocalInstallation(input.rows, input.context), (existing) => | |
| 280 | − existing | |
| 281 | − ? Effect.succeed(existing) | |
| 282 | − : provisionLocalInstallation(input), | |
| 283 | − ); | |
| 56 | +}): Effect.Effect<LocalInstallation, never> => | |
| 57 | + loadLocalInstallation(input.rows, input.context); | |
| @@ -187,27 +187,7 @@ export const getLocalInstallation = () => · 186 unmodified lines | ||
| 187 | 187 | const context = runtimeLocalWorkspace?.context |
| 188 | 188 | ?? (yield* Effect.promise(() => resolveLocalWorkspaceContext())); |
| 189 | 189 | |
| 190 | − const installation = yield* loadLocalInstallation(store, context).pipe( | |
| 191 | − Effect.mapError((error) => | |
| 192 | − error instanceof ControlPlanePersistenceError | |
| 193 | − ? localOps.installation.storage(error) | |
| 194 | − : localOps.installation.unknownStorage( | |
| 195 | − error, | |
| 196 | − "Failed loading local installation", | |
| 197 | − ), | |
| 198 | − ), | |
| 199 | − ); | |
| 200 | − | |
| 201 | − if (installation === null) { | |
| 202 | − return yield* Effect.fail( | |
| 203 | − localOps.installation.notFound( | |
| 204 | − "Local installation not found", | |
| 205 | − "No local installation has been provisioned", | |
| 206 | − ), | |
| 207 | − ); | |
| 208 | − } | |
| 209 | − | |
| 210 | − return installation; | |
| 190 | + return yield* loadLocalInstallation(store, context); | |
| 211 | 191 | }); |
| 212 | 192 | |
| 213 | 193 | export const getSourceCredentialInteraction = (input: { |
| @@ -2,7 +2,7 @@ import * as Context from "effect/Context"; · 1 unmodified line | ||
| 2 | 2 | import * as Effect from "effect/Effect"; |
| 3 | 3 | import * as Option from "effect/Option"; |
| 4 | 4 | |
| 5 | −import type { AccountId, WorkspaceId } from "#schema"; | |
| 5 | +import type { AccountId, OrganizationId, WorkspaceId } from "#schema"; | |
| 6 | 6 | import type { |
| 7 | 7 | LoadedLocalExecutorConfig, |
| 8 | 8 | ResolvedLocalWorkspaceContext, |
| @@ -13,6 +13,7 @@ export type RuntimeLocalWorkspaceState = { · 4 unmodified lines | ||
| 13 | 13 | installation: { |
| 14 | 14 | workspaceId: WorkspaceId; |
| 15 | 15 | accountId: AccountId; |
| 16 | + organizationId: OrganizationId; | |
| 16 | 17 | }; |
| 17 | 18 | loadedConfig: LoadedLocalExecutorConfig; |
| 18 | 19 | }; |
| @@ -27,3 +28,23 @@ export const getRuntimeLocalWorkspaceOption = () => · 8 unmodified lines | ||
| 27 | 28 | ).pipe( |
| 28 | 29 | Effect.map((option) => (Option.isSome(option) ? option.value : null)), |
| 29 | 30 | ) as Effect.Effect<RuntimeLocalWorkspaceState | null, never, never>; |
| 31 | + | |
| 32 | +export const requireRuntimeLocalWorkspace = (workspaceId?: WorkspaceId) => | |
| 33 | + Effect.flatMap(getRuntimeLocalWorkspaceOption(), (runtimeLocalWorkspace) => { | |
| 34 | + if (runtimeLocalWorkspace === null) { | |
| 35 | + return Effect.fail(new Error("Runtime local workspace is unavailable")); | |
| 36 | + } | |
| 37 | + | |
| 38 | + if ( | |
| 39 | + workspaceId !== undefined | |
| 40 | + && runtimeLocalWorkspace.installation.workspaceId !== workspaceId | |
| 41 | + ) { | |
| 42 | + return Effect.fail( | |
| 43 | + new Error( | |
| 44 | + `Workspace ${workspaceId} is not the active local workspace ${runtimeLocalWorkspace.installation.workspaceId}`, | |
| 45 | + ), | |
| 46 | + ); | |
| 47 | + } | |
| 48 | + | |
| 49 | + return Effect.succeed(runtimeLocalWorkspace); | |
| 50 | + }); | |
| @@ -1,13 +1,7 @@ | ||
| 1 | −import type { SqlControlPlaneRows } from "#persistence"; | |
| 2 | −import { | |
| 3 | − type Policy, | |
| 4 | − type Workspace, | |
| 5 | −} from "#schema"; | |
| 1 | +import { type Policy } from "#schema"; | |
| 6 | 2 | import * as Effect from "effect/Effect"; |
| 7 | −import * as Option from "effect/Option"; | |
| 8 | 3 | |
| 9 | 4 | import { |
| 10 | − defaultWorkspaceDisplayName, | |
| 11 | 5 | type LoadedLocalExecutorConfig, |
| 12 | 6 | type ResolvedLocalWorkspaceContext, |
| 13 | 7 | } from "./local-config"; |
| @@ -45,33 +39,6 @@ export const derivePolicyConfigKey = ( · 31 unmodified lines | ||
| 45 | 39 | return candidate; |
| 46 | 40 | }; |
| 47 | 41 | |
| 48 | −const ensureWorkspaceMetadata = (input: { | |
| 49 | − rows: SqlControlPlaneRows; | |
| 50 | − installation: { | |
| 51 | − workspaceId: Workspace["id"]; | |
| 52 | − }; | |
| 53 | − context: ResolvedLocalWorkspaceContext; | |
| 54 | − config: LoadedLocalExecutorConfig["config"]; | |
| 55 | −}): Effect.Effect<void, Error, never> => | |
| 56 | − Effect.gen(function* () { | |
| 57 | − const workspace = yield* input.rows.workspaces.getById(input.installation.workspaceId); | |
| 58 | − if (Option.isNone(workspace)) { | |
| 59 | − return; | |
| 60 | − } | |
| 61 | − | |
| 62 | − const desiredName = | |
| 63 | − trimOrNull(input.config?.workspace?.name) | |
| 64 | − ?? defaultWorkspaceDisplayName(input.context); | |
| 65 | − if (workspace.value.name === desiredName) { | |
| 66 | − return; | |
| 67 | − } | |
| 68 | − | |
| 69 | − yield* input.rows.workspaces.update(workspace.value.id, { | |
| 70 | − name: desiredName, | |
| 71 | − updatedAt: Date.now(), | |
| 72 | − }); | |
| 73 | − }); | |
| 74 | − | |
| 75 | 42 | const pruneLocalWorkspaceState = (input: { |
| 76 | 43 | context: ResolvedLocalWorkspaceContext; |
| 77 | 44 | loadedConfig: LoadedLocalExecutorConfig; |
| @@ -122,21 +89,10 @@ const pruneLocalWorkspaceState = (input: { · 44 unmodified lines | ||
| 122 | 89 | }); |
| 123 | 90 | |
| 124 | 91 | export const synchronizeLocalWorkspaceState = (input: { |
| 125 | − rows: SqlControlPlaneRows; | |
| 126 | 92 | context: ResolvedLocalWorkspaceContext; |
| 127 | 93 | loadedConfig: LoadedLocalExecutorConfig; |
| 128 | − installation: { | |
| 129 | − workspaceId: Workspace["id"]; | |
| 130 | − }; | |
| 131 | 94 | }): Effect.Effect<LoadedLocalExecutorConfig["config"], Error, never> => |
| 132 | 95 | Effect.gen(function* () { |
| 133 | − yield* ensureWorkspaceMetadata({ | |
| 134 | − rows: input.rows, | |
| 135 | − installation: input.installation, | |
| 136 | − context: input.context, | |
| 137 | − config: input.loadedConfig.config, | |
| 138 | − }); | |
| 139 | − | |
| 140 | 96 | yield* pruneLocalWorkspaceState({ |
| 141 | 97 | context: input.context, |
| 142 | 98 | loadedConfig: input.loadedConfig, |
| @@ -1,21 +1,14 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | ControlPlaneBadRequestError, |
| 3 | − ControlPlaneNotFoundError, | |
| 4 | 3 | ControlPlaneStorageError, |
| 5 | 4 | } from "../api/errors"; |
| 6 | 5 | import { ControlPlanePersistenceError } from "#persistence"; |
| 7 | −import type { Organization } from "#schema"; | |
| 8 | 6 | import * as Effect from "effect/Effect"; |
| 9 | −import * as Option from "effect/Option"; | |
| 10 | 7 | |
| 11 | 8 | import { |
| 12 | 9 | asOperationErrors, |
| 13 | − operationErrors, | |
| 14 | 10 | type OperationErrorsLike, |
| 15 | 11 | } from "./operation-errors"; |
| 16 | −import type { ControlPlaneStoreShape } from "./store"; | |
| 17 | −import { slugify } from "./slug"; | |
| 18 | − | |
| 19 | 12 | export type Mutable<T> = { -readonly [K in keyof T]: T[K] }; |
| 20 | 13 | |
| 21 | 14 | const isUniqueViolation = (error: ControlPlanePersistenceError): boolean => |
| @@ -59,51 +52,3 @@ export const parseJsonString = ( · 37 unmodified lines | ||
| 59 | 52 | ); |
| 60 | 53 | }, |
| 61 | 54 | }); |
| 62 | − | |
| 63 | −export const ensureUniqueOrganizationSlug = ( | |
| 64 | − store: ControlPlaneStoreShape, | |
| 65 | − baseName: string, | |
| 66 | − operation = operationErrors("organizations.create"), | |
| 67 | −): Effect.Effect<string, ControlPlaneStorageError> => | |
| 68 | − Effect.gen(function* () { | |
| 69 | − const normalized = slugify(baseName); | |
| 70 | − const seed = normalized.length > 0 ? normalized : "item"; | |
| 71 | − | |
| 72 | − let counter = 0; | |
| 73 | − while (true) { | |
| 74 | − const candidate = counter === 0 ? seed : `${seed}-${counter + 1}`; | |
| 75 | − | |
| 76 | − const existing = yield* operation.child("slug_lookup").mapStorage( | |
| 77 | − store.organizations.getBySlug(candidate as Organization["slug"]), | |
| 78 | − ); | |
| 79 | − | |
| 80 | − if (Option.isNone(existing)) { | |
| 81 | − return candidate; | |
| 82 | − } | |
| 83 | − | |
| 84 | − counter += 1; | |
| 85 | − } | |
| 86 | − }); | |
| 87 | − | |
| 88 | −export const ensureOrganizationExists = ( | |
| 89 | − store: ControlPlaneStoreShape, | |
| 90 | − operation: OperationErrorsLike, | |
| 91 | − organizationId: Organization["id"], | |
| 92 | −): Effect.Effect<Organization, ControlPlaneNotFoundError | ControlPlaneStorageError> => | |
| 93 | − Effect.gen(function* () { | |
| 94 | − const errors = asOperationErrors(operation); | |
| 95 | − const organization = yield* errors.child("organization_lookup").mapStorage( | |
| 96 | − store.organizations.getById(organizationId), | |
| 97 | − ); | |
| 98 | − | |
| 99 | − if (Option.isNone(organization)) { | |
| 100 | − return yield* Effect.fail( | |
| 101 | − errors.notFound( | |
| 102 | − "Organization not found", | |
| 103 | − `organizationId=${organizationId}`, | |
| 104 | − ), | |
| 105 | − ); | |
| 106 | − } | |
| 107 | − | |
| 108 | − return organization.value; | |
| 109 | − }); | |
| @@ -1,434 +0,0 @@ | ||
| 1 | −import type { | |
| 2 | − CreateMembershipPayload, | |
| 3 | − UpdateMembershipPayload, | |
| 4 | −} from "../api/memberships/api"; | |
| 5 | −import type { | |
| 6 | − CreateOrganizationPayload, | |
| 7 | − UpdateOrganizationPayload, | |
| 8 | −} from "../api/organizations/api"; | |
| 9 | −import type { | |
| 10 | − CreateWorkspacePayload, | |
| 11 | − UpdateWorkspacePayload, | |
| 12 | −} from "../api/workspaces/api"; | |
| 13 | −import { | |
| 14 | − OrganizationIdSchema, | |
| 15 | − OrganizationMemberIdSchema, | |
| 16 | − WorkspaceIdSchema, | |
| 17 | − type AccountId, | |
| 18 | − type Organization, | |
| 19 | − type OrganizationId, | |
| 20 | − type OrganizationMembership, | |
| 21 | − type Workspace, | |
| 22 | − type WorkspaceId, | |
| 23 | −} from "#schema"; | |
| 24 | −import * as Effect from "effect/Effect"; | |
| 25 | −import * as Option from "effect/Option"; | |
| 26 | − | |
| 27 | −import { | |
| 28 | − ensureOrganizationExists, | |
| 29 | − ensureUniqueOrganizationSlug, | |
| 30 | − mapPersistenceError, | |
| 31 | − type Mutable, | |
| 32 | −} from "./operations-shared"; | |
| 33 | −import { | |
| 34 | − operationErrors, | |
| 35 | −} from "./operation-errors"; | |
| 36 | −import { | |
| 37 | − ControlPlaneStore, | |
| 38 | − type ControlPlaneStoreShape, | |
| 39 | −} from "./store"; | |
| 40 | − | |
| 41 | −const organizationOps = { | |
| 42 | − list: operationErrors("organizations.list"), | |
| 43 | − create: operationErrors("organizations.create"), | |
| 44 | − get: operationErrors("organizations.get"), | |
| 45 | − update: operationErrors("organizations.update"), | |
| 46 | − remove: operationErrors("organizations.remove"), | |
| 47 | −} as const; | |
| 48 | − | |
| 49 | −const membershipOps = { | |
| 50 | − list: operationErrors("memberships.list"), | |
| 51 | − create: operationErrors("memberships.create"), | |
| 52 | − update: operationErrors("memberships.update"), | |
| 53 | − remove: operationErrors("memberships.remove"), | |
| 54 | −} as const; | |
| 55 | − | |
| 56 | −const workspaceOps = { | |
| 57 | − list: operationErrors("workspaces.list"), | |
| 58 | − create: operationErrors("workspaces.create"), | |
| 59 | − get: operationErrors("workspaces.get"), | |
| 60 | − update: operationErrors("workspaces.update"), | |
| 61 | − remove: operationErrors("workspaces.remove"), | |
| 62 | −} as const; | |
| 63 | − | |
| 64 | −const listOrganizationsWithStore = ( | |
| 65 | − store: ControlPlaneStoreShape, | |
| 66 | − input: { accountId: AccountId }, | |
| 67 | −) => | |
| 68 | − Effect.gen(function* () { | |
| 69 | − const memberships = yield* organizationOps.list.child("memberships").mapStorage( | |
| 70 | − store.organizationMemberships.listByAccountId(input.accountId), | |
| 71 | − ); | |
| 72 | − | |
| 73 | − const activeOrganizationIds = Array.from( | |
| 74 | − new Set( | |
| 75 | − memberships | |
| 76 | − .filter((membership) => membership.status === "active") | |
| 77 | − .map((membership) => membership.organizationId), | |
| 78 | − ), | |
| 79 | − ); | |
| 80 | − | |
| 81 | − const organizations = yield* Effect.forEach(activeOrganizationIds, (organizationId) => | |
| 82 | − organizationOps.list.child("organization").mapStorage( | |
| 83 | − store.organizations.getById(organizationId), | |
| 84 | − ).pipe( | |
| 85 | − Effect.map((result) => (Option.isSome(result) ? result.value : null)), | |
| 86 | − )); | |
| 87 | − | |
| 88 | − return organizations.filter((organization): organization is Organization => organization !== null); | |
| 89 | − }); | |
| 90 | − | |
| 91 | −export const listOrganizations = (input: { accountId: AccountId }) => | |
| 92 | − Effect.flatMap(ControlPlaneStore, (store) => listOrganizationsWithStore(store, input)); | |
| 93 | − | |
| 94 | −export const createOrganization = (input: { | |
| 95 | − payload: CreateOrganizationPayload; | |
| 96 | − createdByAccountId?: Organization["createdByAccountId"]; | |
| 97 | −}) => | |
| 98 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 99 | − Effect.gen(function* () { | |
| 100 | − const name = input.payload.name; | |
| 101 | − const now = Date.now(); | |
| 102 | − | |
| 103 | − const slug = input.payload.slug | |
| 104 | − ? input.payload.slug | |
| 105 | − : yield* ensureUniqueOrganizationSlug(store, name, organizationOps.create); | |
| 106 | − | |
| 107 | − const organization: Organization = { | |
| 108 | − id: OrganizationIdSchema.make(`org_${crypto.randomUUID()}`), | |
| 109 | − slug, | |
| 110 | − name, | |
| 111 | − status: "active", | |
| 112 | − createdByAccountId: input.createdByAccountId ?? null, | |
| 113 | − createdAt: now, | |
| 114 | − updatedAt: now, | |
| 115 | − }; | |
| 116 | − | |
| 117 | − const ownerMembership: OrganizationMembership | null = input.createdByAccountId | |
| 118 | − ? { | |
| 119 | − id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`), | |
| 120 | − organizationId: organization.id, | |
| 121 | − accountId: input.createdByAccountId, | |
| 122 | − role: "owner", | |
| 123 | − status: "active", | |
| 124 | − billable: true, | |
| 125 | − invitedByAccountId: null, | |
| 126 | − joinedAt: now, | |
| 127 | − createdAt: now, | |
| 128 | − updatedAt: now, | |
| 129 | − } | |
| 130 | − : null; | |
| 131 | − | |
| 132 | − yield* mapPersistenceError( | |
| 133 | − organizationOps.create, | |
| 134 | − store.organizations.insertWithOwnerMembership(organization, ownerMembership), | |
| 135 | − ); | |
| 136 | − | |
| 137 | − return organization; | |
| 138 | − })); | |
| 139 | − | |
| 140 | −export const getOrganization = (input: { | |
| 141 | − organizationId: OrganizationId; | |
| 142 | − accountId: AccountId; | |
| 143 | −}) => | |
| 144 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 145 | − Effect.gen(function* () { | |
| 146 | − const membership = yield* organizationOps.get.child("membership").mapStorage( | |
| 147 | − store.organizationMemberships.getByOrganizationAndAccount( | |
| 148 | − input.organizationId, | |
| 149 | − input.accountId, | |
| 150 | − ), | |
| 151 | − ); | |
| 152 | − | |
| 153 | − if (Option.isNone(membership) || membership.value.status !== "active") { | |
| 154 | − return yield* Effect.fail( | |
| 155 | − organizationOps.get.notFound( | |
| 156 | − "Organization not found", | |
| 157 | − `organizationId=${input.organizationId}`, | |
| 158 | − ), | |
| 159 | − ); | |
| 160 | − } | |
| 161 | − | |
| 162 | − const existing = yield* organizationOps.get.mapStorage( | |
| 163 | − store.organizations.getById(input.organizationId), | |
| 164 | − ); | |
| 165 | − | |
| 166 | − if (Option.isNone(existing)) { | |
| 167 | − return yield* Effect.fail( | |
| 168 | − organizationOps.get.notFound( | |
| 169 | − "Organization not found", | |
| 170 | − `organizationId=${input.organizationId}`, | |
| 171 | − ), | |
| 172 | − ); | |
| 173 | − } | |
| 174 | − | |
| 175 | − return existing.value; | |
| 176 | − })); | |
| 177 | − | |
| 178 | −export const updateOrganization = (input: { | |
| 179 | − organizationId: OrganizationId; | |
| 180 | − payload: UpdateOrganizationPayload; | |
| 181 | −}) => | |
| 182 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 183 | − Effect.gen(function* () { | |
| 184 | − const patch: Partial<Omit<Mutable<Organization>, "id" | "createdAt">> = { | |
| 185 | − updatedAt: Date.now(), | |
| 186 | − }; | |
| 187 | − | |
| 188 | − if (input.payload.name !== undefined) { | |
| 189 | − patch.name = input.payload.name; | |
| 190 | − } | |
| 191 | − if (input.payload.status !== undefined) { | |
| 192 | − patch.status = input.payload.status; | |
| 193 | − } | |
| 194 | − | |
| 195 | − const updated = yield* mapPersistenceError( | |
| 196 | − organizationOps.update, | |
| 197 | − store.organizations.update(input.organizationId, patch), | |
| 198 | − ); | |
| 199 | − | |
| 200 | − if (Option.isNone(updated)) { | |
| 201 | − return yield* Effect.fail( | |
| 202 | − organizationOps.update.notFound( | |
| 203 | − "Organization not found", | |
| 204 | − `organizationId=${input.organizationId}`, | |
| 205 | − ), | |
| 206 | − ); | |
| 207 | − } | |
| 208 | − | |
| 209 | − return updated.value; | |
| 210 | − })); | |
| 211 | − | |
| 212 | −export const removeOrganization = (input: { organizationId: OrganizationId }) => | |
| 213 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 214 | − organizationOps.remove.mapStorage( | |
| 215 | − store.organizations.removeTreeById(input.organizationId), | |
| 216 | − ).pipe(Effect.map((removed) => ({ removed }))) | |
| 217 | − ); | |
| 218 | − | |
| 219 | −export const listMemberships = (organizationId: OrganizationId) => | |
| 220 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 221 | − Effect.gen(function* () { | |
| 222 | − yield* ensureOrganizationExists(store, membershipOps.list, organizationId); | |
| 223 | − | |
| 224 | − return yield* membershipOps.list.mapStorage( | |
| 225 | − store.organizationMemberships.listByOrganizationId(organizationId), | |
| 226 | − ); | |
| 227 | − })); | |
| 228 | − | |
| 229 | −export const createMembership = (input: { | |
| 230 | − organizationId: OrganizationId; | |
| 231 | − payload: CreateMembershipPayload; | |
| 232 | −}) => | |
| 233 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 234 | − Effect.gen(function* () { | |
| 235 | − yield* ensureOrganizationExists(store, membershipOps.create, input.organizationId); | |
| 236 | − | |
| 237 | − const now = Date.now(); | |
| 238 | − const membership: OrganizationMembership = { | |
| 239 | − id: OrganizationMemberIdSchema.make(`org_mem_${crypto.randomUUID()}`), | |
| 240 | − organizationId: input.organizationId, | |
| 241 | − accountId: input.payload.accountId, | |
| 242 | − role: input.payload.role, | |
| 243 | − status: input.payload.status ?? "active", | |
| 244 | − billable: input.payload.billable ?? true, | |
| 245 | − invitedByAccountId: input.payload.invitedByAccountId ?? null, | |
| 246 | − joinedAt: (input.payload.status ?? "active") === "active" ? now : null, | |
| 247 | − createdAt: now, | |
| 248 | − updatedAt: now, | |
| 249 | − }; | |
| 250 | − | |
| 251 | − yield* mapPersistenceError( | |
| 252 | − membershipOps.create, | |
| 253 | − store.organizationMemberships.upsert(membership), | |
| 254 | − ); | |
| 255 | − | |
| 256 | − const stored = yield* membershipOps.create.mapStorage( | |
| 257 | − store.organizationMemberships.getByOrganizationAndAccount( | |
| 258 | − input.organizationId, | |
| 259 | − input.payload.accountId, | |
| 260 | − ), | |
| 261 | − ); | |
| 262 | − if (Option.isNone(stored)) { | |
| 263 | − return yield* Effect.fail( | |
| 264 | − membershipOps.create.badRequest( | |
| 265 | − "Membership was not persisted", | |
| 266 | − `organizationId=${input.organizationId} accountId=${input.payload.accountId}`, | |
| 267 | − ), | |
| 268 | − ); | |
| 269 | − } | |
| 270 | − | |
| 271 | − return stored.value; | |
| 272 | − })); | |
| 273 | − | |
| 274 | −export const updateMembership = (input: { | |
| 275 | − organizationId: OrganizationId; | |
| 276 | − accountId: AccountId; | |
| 277 | − payload: UpdateMembershipPayload; | |
| 278 | −}) => | |
| 279 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 280 | − Effect.gen(function* () { | |
| 281 | − yield* ensureOrganizationExists(store, membershipOps.update, input.organizationId); | |
| 282 | − | |
| 283 | − const existing = yield* membershipOps.update.mapStorage( | |
| 284 | − store.organizationMemberships.getByOrganizationAndAccount( | |
| 285 | − input.organizationId, | |
| 286 | − input.accountId, | |
| 287 | − ), | |
| 288 | − ); | |
| 289 | − | |
| 290 | − if (Option.isNone(existing)) { | |
| 291 | − return yield* Effect.fail( | |
| 292 | − membershipOps.update.badRequest( | |
| 293 | − "Membership not found", | |
| 294 | − `organizationId=${input.organizationId} accountId=${input.accountId}`, | |
| 295 | − ), | |
| 296 | − ); | |
| 297 | − } | |
| 298 | − | |
| 299 | − const current = existing.value; | |
| 300 | − const now = Date.now(); | |
| 301 | − const next: OrganizationMembership = { | |
| 302 | − ...current, | |
| 303 | − role: input.payload.role ?? current.role, | |
| 304 | − status: input.payload.status ?? current.status, | |
| 305 | − billable: input.payload.billable ?? current.billable, | |
| 306 | − joinedAt: | |
| 307 | − (input.payload.status ?? current.status) === "active" | |
| 308 | − ? (current.joinedAt ?? now) | |
| 309 | − : current.joinedAt, | |
| 310 | − updatedAt: now, | |
| 311 | − }; | |
| 312 | − | |
| 313 | − yield* mapPersistenceError( | |
| 314 | − membershipOps.update, | |
| 315 | − store.organizationMemberships.upsert(next), | |
| 316 | − ); | |
| 317 | − | |
| 318 | − return next; | |
| 319 | − })); | |
| 320 | − | |
| 321 | −export const removeMembership = (input: { | |
| 322 | − organizationId: OrganizationId; | |
| 323 | − accountId: AccountId; | |
| 324 | −}) => | |
| 325 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 326 | − Effect.gen(function* () { | |
| 327 | − yield* ensureOrganizationExists(store, membershipOps.remove, input.organizationId); | |
| 328 | − | |
| 329 | − const removed = yield* membershipOps.remove.mapStorage( | |
| 330 | − store.organizationMemberships.removeByOrganizationAndAccount( | |
| 331 | − input.organizationId, | |
| 332 | − input.accountId, | |
| 333 | − ), | |
| 334 | − ); | |
| 335 | − | |
| 336 | − return { removed }; | |
| 337 | − })); | |
| 338 | − | |
| 339 | −export const listWorkspaces = (organizationId: OrganizationId) => | |
| 340 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 341 | − Effect.gen(function* () { | |
| 342 | − yield* ensureOrganizationExists(store, workspaceOps.list, organizationId); | |
| 343 | − | |
| 344 | − return yield* workspaceOps.list.mapStorage( | |
| 345 | − store.workspaces.listByOrganizationId(organizationId), | |
| 346 | − ); | |
| 347 | − })); | |
| 348 | − | |
| 349 | −export const createWorkspace = (input: { | |
| 350 | − organizationId: OrganizationId; | |
| 351 | − payload: CreateWorkspacePayload; | |
| 352 | − createdByAccountId?: Workspace["createdByAccountId"]; | |
| 353 | −}) => | |
| 354 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 355 | − Effect.gen(function* () { | |
| 356 | − yield* ensureOrganizationExists(store, workspaceOps.create, input.organizationId); | |
| 357 | − const name = input.payload.name; | |
| 358 | − const now = Date.now(); | |
| 359 | − | |
| 360 | − const workspace: Workspace = { | |
| 361 | − id: WorkspaceIdSchema.make(`ws_${crypto.randomUUID()}`), | |
| 362 | − organizationId: input.organizationId, | |
| 363 | − name, | |
| 364 | − createdByAccountId: input.createdByAccountId ?? null, | |
| 365 | − createdAt: now, | |
| 366 | − updatedAt: now, | |
| 367 | − }; | |
| 368 | − | |
| 369 | − yield* mapPersistenceError( | |
| 370 | − workspaceOps.create, | |
| 371 | − store.workspaces.insert(workspace), | |
| 372 | − ); | |
| 373 | − | |
| 374 | − return workspace; | |
| 375 | − })); | |
| 376 | − | |
| 377 | −export const getWorkspace = (workspaceId: WorkspaceId) => | |
| 378 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 379 | − Effect.gen(function* () { | |
| 380 | − const existing = yield* workspaceOps.get.mapStorage( | |
| 381 | − store.workspaces.getById(workspaceId), | |
| 382 | − ); | |
| 383 | − | |
| 384 | − if (Option.isNone(existing)) { | |
| 385 | − return yield* Effect.fail( | |
| 386 | − workspaceOps.get.notFound( | |
| 387 | − "Workspace not found", | |
| 388 | − `workspaceId=${workspaceId}`, | |
| 389 | − ), | |
| 390 | − ); | |
| 391 | − } | |
| 392 | − | |
| 393 | − return existing.value; | |
| 394 | − })); | |
| 395 | − | |
| 396 | −export const updateWorkspace = (input: { | |
| 397 | − workspaceId: WorkspaceId; | |
| 398 | − payload: UpdateWorkspacePayload; | |
| 399 | −}) => | |
| 400 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 401 | − Effect.gen(function* () { | |
| 402 | − const patch: Partial< | |
| 403 | − Omit<Mutable<Workspace>, "id" | "organizationId" | "createdAt"> | |
| 404 | − > = { | |
| 405 | − updatedAt: Date.now(), | |
| 406 | − }; | |
| 407 | − | |
| 408 | − if (input.payload.name !== undefined) { | |
| 409 | − patch.name = input.payload.name; | |
| 410 | − } | |
| 411 | − | |
| 412 | − const updated = yield* mapPersistenceError( | |
| 413 | − workspaceOps.update, | |
| 414 | − store.workspaces.update(input.workspaceId, patch), | |
| 415 | − ); | |
| 416 | − | |
| 417 | − if (Option.isNone(updated)) { | |
| 418 | − return yield* Effect.fail( | |
| 419 | − workspaceOps.update.notFound( | |
| 420 | − "Workspace not found", | |
| 421 | − `workspaceId=${input.workspaceId}`, | |
| 422 | − ), | |
| 423 | − ); | |
| 424 | − } | |
| 425 | − | |
| 426 | − return updated.value; | |
| 427 | − })); | |
| 428 | − | |
| 429 | −export const removeWorkspace = (input: { workspaceId: WorkspaceId }) => | |
| 430 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 431 | − workspaceOps.remove.mapStorage( | |
| 432 | − store.workspaces.removeById(input.workspaceId), | |
| 433 | − ).pipe(Effect.map((removed) => ({ removed }))) | |
| 434 | − ); | |
| @@ -7,20 +7,18 @@ import type { · 6 unmodified lines | ||
| 7 | 7 | import { |
| 8 | 8 | PolicyIdSchema, |
| 9 | 9 | type LocalExecutorConfig, |
| 10 | − type OrganizationId, | |
| 11 | 10 | type Policy, |
| 12 | 11 | type PolicyId, |
| 13 | 12 | type WorkspaceId, |
| 14 | 13 | } from "#schema"; |
| 15 | 14 | import * as Effect from "effect/Effect"; |
| 16 | −import * as Option from "effect/Option"; | |
| 17 | 15 | |
| 18 | 16 | import { |
| 19 | 17 | loadLocalExecutorConfig, |
| 20 | 18 | writeProjectLocalExecutorConfig, |
| 21 | 19 | } from "./local-config"; |
| 22 | 20 | import { |
| 23 | − getRuntimeLocalWorkspaceOption, | |
| 21 | + requireRuntimeLocalWorkspace, | |
| 24 | 22 | } from "./local-runtime-context"; |
| 25 | 23 | import { |
| 26 | 24 | loadLocalWorkspaceState, |
| @@ -29,17 +27,10 @@ import { · 2 unmodified lines | ||
| 29 | 27 | import { |
| 30 | 28 | derivePolicyConfigKey, |
| 31 | 29 | } from "./local-workspace-sync"; |
| 32 | −import { | |
| 33 | − mapPersistenceError, | |
| 34 | − parseJsonString, | |
| 35 | − type Mutable, | |
| 36 | −} from "./operations-shared"; | |
| 37 | 30 | import { |
| 38 | 31 | type OperationErrors, |
| 39 | 32 | operationErrors, |
| 40 | 33 | } from "./operation-errors"; |
| 41 | −import { ControlPlaneStore, type ControlPlaneStoreShape } from "./store"; | |
| 42 | − | |
| 43 | 34 | const policyOps = { |
| 44 | 35 | list: operationErrors("policies.list"), |
| 45 | 36 | create: operationErrors("policies.create"), |
| @@ -57,15 +48,6 @@ type PolicyScopeContext = { · 11 unmodified lines | ||
| 57 | 48 | const cloneJson = <T>(value: T): T => |
| 58 | 49 | JSON.parse(JSON.stringify(value)) as T; |
| 59 | 50 | |
| 60 | −const trimOrNull = (value: string | null | undefined): string | null => { | |
| 61 | − if (value == null) { | |
| 62 | − return null; | |
| 63 | − } | |
| 64 | − | |
| 65 | − const trimmed = value.trim(); | |
| 66 | − return trimmed.length > 0 ? trimmed : null; | |
| 67 | −}; | |
| 68 | − | |
| 69 | 51 | const localPolicyIdForConfigKey = (input: { |
| 70 | 52 | workspaceRoot: string; |
| 71 | 53 | configKey: string; |
| @@ -121,24 +103,9 @@ const ensureLocalWorkspacePolicyCompatible = ( · 49 unmodified lines | ||
| 121 | 103 | } |
| 122 | 104 | }); |
| 123 | 105 | |
| 124 | −export const loadRuntimeLocalWorkspacePolicies = (input: { | |
| 125 | − store: ControlPlaneStoreShape; | |
| 126 | − workspaceId: WorkspaceId; | |
| 127 | −}) => | |
| 106 | +export const loadRuntimeLocalWorkspacePolicies = (workspaceId: WorkspaceId) => | |
| 128 | 107 | Effect.gen(function* () { |
| 129 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 130 | − if ( | |
| 131 | − runtimeLocalWorkspace === null | |
| 132 | − || runtimeLocalWorkspace.installation.workspaceId !== input.workspaceId | |
| 133 | − ) { | |
| 134 | − return null; | |
| 135 | − } | |
| 136 | − | |
| 137 | − const workspace = yield* input.store.workspaces.getById(input.workspaceId); | |
| 138 | − if (Option.isNone(workspace)) { | |
| 139 | − return null; | |
| 140 | − } | |
| 141 | − | |
| 108 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); | |
| 142 | 109 | const loadedConfig = yield* Effect.tryPromise({ |
| 143 | 110 | try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context), |
| 144 | 111 | catch: (cause) => |
| @@ -160,8 +127,8 @@ export const loadRuntimeLocalWorkspacePolicies = (input: { · 15 unmodified lines | ||
| 160 | 127 | }), |
| 161 | 128 | configKey, |
| 162 | 129 | scopeType: "workspace" as const, |
| 163 | − organizationId: workspace.value.organizationId, | |
| 164 | − workspaceId: input.workspaceId, | |
| 130 | + organizationId: runtimeLocalWorkspace.installation.organizationId, | |
| 131 | + workspaceId, | |
| 165 | 132 | targetAccountId: null, |
| 166 | 133 | clientId: null, |
| 167 | 134 | resourceType: "tool_path" as const, |
| @@ -181,61 +148,31 @@ export const loadRuntimeLocalWorkspacePolicies = (input: { · 13 unmodified lines | ||
| 181 | 148 | runtimeLocalWorkspace, |
| 182 | 149 | loadedConfig, |
| 183 | 150 | workspaceState, |
| 184 | − workspace: workspace.value, | |
| 185 | 151 | policies, |
| 186 | 152 | }; |
| 187 | 153 | }); |
| 188 | 154 | |
| 189 | 155 | const loadWorkspacePolicyContext = ( |
| 190 | − store: ControlPlaneStoreShape, | |
| 191 | 156 | operation: OperationErrors, |
| 192 | 157 | workspaceId: WorkspaceId, |
| 193 | 158 | ) => |
| 194 | 159 | Effect.gen(function* () { |
| 195 | − const workspace = yield* operation.child("workspace").mapStorage( | |
| 196 | − store.workspaces.getById(workspaceId), | |
| 197 | − ); | |
| 198 | − if (Option.isNone(workspace)) { | |
| 199 | − return yield* Effect.fail( | |
| 160 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId).pipe( | |
| 161 | + Effect.mapError((cause) => | |
| 200 | 162 | operation.notFound( |
| 201 | 163 | "Workspace not found", |
| 202 | − `workspaceId=${workspaceId}`, | |
| 164 | + cause instanceof Error ? cause.message : String(cause), | |
| 203 | 165 | ), |
| 204 | − ); | |
| 205 | − } | |
| 166 | + ), | |
| 167 | + ); | |
| 206 | 168 | |
| 207 | 169 | return { |
| 208 | 170 | scopeType: "workspace", |
| 209 | − organizationId: workspace.value.organizationId, | |
| 171 | + organizationId: runtimeLocalWorkspace.installation.organizationId, | |
| 210 | 172 | workspaceId, |
| 211 | 173 | } satisfies PolicyScopeContext; |
| 212 | 174 | }); |
| 213 | 175 | |
| 214 | −const loadOrganizationPolicyContext = ( | |
| 215 | − store: ControlPlaneStoreShape, | |
| 216 | − operation: OperationErrors, | |
| 217 | − organizationId: OrganizationId, | |
| 218 | −) => | |
| 219 | − Effect.gen(function* () { | |
| 220 | − const organization = yield* operation.child("organization").mapStorage( | |
| 221 | − store.organizations.getById(organizationId), | |
| 222 | − ); | |
| 223 | − if (Option.isNone(organization)) { | |
| 224 | − return yield* Effect.fail( | |
| 225 | − operation.notFound( | |
| 226 | − "Organization not found", | |
| 227 | − `organizationId=${organizationId}`, | |
| 228 | − ), | |
| 229 | − ); | |
| 230 | − } | |
| 231 | − | |
| 232 | − return { | |
| 233 | − scopeType: "organization", | |
| 234 | − organizationId, | |
| 235 | − workspaceId: null, | |
| 236 | − } satisfies PolicyScopeContext; | |
| 237 | − }); | |
| 238 | − | |
| 239 | 176 | const policyMatchesScope = (policy: Policy, scope: PolicyScopeContext): boolean => |
| 240 | 177 | policy.scopeType === scope.scopeType |
| 241 | 178 | && policy.organizationId === scope.organizationId |
| @@ -270,527 +207,249 @@ const writeLocalPolicyFiles = (input: { · 28 unmodified lines | ||
| 270 | 207 | ), |
| 271 | 208 | ); |
| 272 | 209 | |
| 273 | −const createScopedPolicy = (input: { | |
| 274 | − scope: PolicyScopeContext; | |
| 275 | − payload: CreatePolicyPayload; | |
| 276 | −}) => | |
| 277 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 278 | − Effect.gen(function* () { | |
| 279 | − const now = Date.now(); | |
| 280 | − const operation = policyOps.create; | |
| 281 | − | |
| 282 | − if (input.scope.workspaceId !== null) { | |
| 283 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 284 | − store, | |
| 285 | − workspaceId: input.scope.workspaceId, | |
| 286 | − }).pipe( | |
| 287 | − Effect.mapError((cause) => | |
| 288 | − operation.unknownStorage( | |
| 289 | − cause, | |
| 290 | − "Failed loading local workspace policies", | |
| 291 | − ), | |
| 292 | − ), | |
| 293 | − ); | |
| 294 | − if (localWorkspace !== null) { | |
| 295 | − yield* ensureLocalWorkspacePolicyCompatible(operation, input.payload); | |
| 296 | − | |
| 297 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 298 | − const policies = { | |
| 299 | − ...(projectConfig.policies ?? {}), | |
| 300 | − }; | |
| 301 | − const configKey = derivePolicyConfigKey({ | |
| 302 | − configKey: null, | |
| 303 | − resourcePattern: input.payload.resourcePattern ?? "*", | |
| 304 | − effect: input.payload.effect ?? "allow", | |
| 305 | − approvalMode: input.payload.approvalMode ?? "auto", | |
| 306 | − }, new Set(Object.keys(policies))); | |
| 307 | − const id = localWorkspace.workspaceState.policies[configKey]?.id | |
| 308 | − ?? localPolicyIdForConfigKey({ | |
| 309 | − workspaceRoot: localWorkspace.runtimeLocalWorkspace.context.workspaceRoot, | |
| 310 | − configKey, | |
| 311 | − }); | |
| 312 | − policies[configKey] = { | |
| 313 | − match: input.payload.resourcePattern ?? "*", | |
| 314 | − action: input.payload.effect ?? "allow", | |
| 315 | − approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto", | |
| 316 | − ...(input.payload.enabled === false ? { enabled: false } : {}), | |
| 317 | − ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}), | |
| 318 | − }; | |
| 319 | − const existingState = localWorkspace.workspaceState.policies[configKey]; | |
| 320 | − const workspaceState = { | |
| 321 | − ...localWorkspace.workspaceState, | |
| 322 | − policies: { | |
| 323 | − ...localWorkspace.workspaceState.policies, | |
| 324 | − [configKey]: { | |
| 325 | − id, | |
| 326 | − createdAt: existingState?.createdAt ?? now, | |
| 327 | − updatedAt: now, | |
| 328 | − }, | |
| 329 | − }, | |
| 330 | − }; | |
| 331 | − yield* writeLocalPolicyFiles({ | |
| 332 | − operation, | |
| 333 | − context: localWorkspace.runtimeLocalWorkspace.context, | |
| 334 | − projectConfig: { | |
| 335 | − ...projectConfig, | |
| 336 | − policies, | |
| 337 | − }, | |
| 338 | − workspaceState, | |
| 339 | − }); | |
| 340 | − | |
| 341 | − return { | |
| 342 | − id, | |
| 343 | − configKey, | |
| 344 | − scopeType: "workspace", | |
| 345 | − organizationId: input.scope.organizationId, | |
| 346 | − workspaceId: input.scope.workspaceId, | |
| 347 | − targetAccountId: null, | |
| 348 | − clientId: null, | |
| 349 | − resourceType: "tool_path", | |
| 350 | − resourcePattern: policies[configKey]!.match, | |
| 351 | − matchType: "glob", | |
| 352 | − effect: policies[configKey]!.action, | |
| 353 | − approvalMode: policies[configKey]!.approval === "manual" ? "required" : "auto", | |
| 354 | − argumentConditionsJson: null, | |
| 355 | − priority: policies[configKey]!.priority ?? 0, | |
| 356 | − enabled: policies[configKey]!.enabled ?? true, | |
| 357 | − createdAt: workspaceState.policies[configKey]!.createdAt, | |
| 358 | − updatedAt: now, | |
| 359 | − } satisfies Policy; | |
| 360 | − } | |
| 361 | − } | |
| 362 | − | |
| 363 | − const policy: Policy = { | |
| 364 | − id: PolicyIdSchema.make(`pol_${crypto.randomUUID()}`), | |
| 365 | − configKey: null, | |
| 366 | − scopeType: input.scope.scopeType, | |
| 367 | − organizationId: input.scope.organizationId, | |
| 368 | − workspaceId: input.scope.workspaceId, | |
| 369 | − targetAccountId: input.payload.targetAccountId ?? null, | |
| 370 | − clientId: input.payload.clientId ?? null, | |
| 371 | − resourceType: input.payload.resourceType ?? "tool_path", | |
| 372 | − resourcePattern: input.payload.resourcePattern ?? "*", | |
| 373 | − matchType: input.payload.matchType ?? "glob", | |
| 374 | − effect: input.payload.effect ?? "allow", | |
| 375 | − approvalMode: input.payload.approvalMode ?? "auto", | |
| 376 | − argumentConditionsJson: input.payload.argumentConditionsJson ?? null, | |
| 377 | − priority: input.payload.priority ?? 0, | |
| 378 | − enabled: input.payload.enabled ?? true, | |
| 379 | − createdAt: now, | |
| 380 | − updatedAt: now, | |
| 381 | − }; | |
| 382 | − | |
| 383 | − if (policy.argumentConditionsJson !== null) { | |
| 384 | − yield* parseJsonString( | |
| 385 | − operation, | |
| 386 | − "argumentConditionsJson", | |
| 387 | − policy.argumentConditionsJson, | |
| 388 | − ); | |
| 389 | − } | |
| 390 | − | |
| 391 | − yield* mapPersistenceError( | |
| 392 | − operation, | |
| 393 | − store.policies.insert(policy), | |
| 394 | − ); | |
| 395 | − | |
| 396 | − if (policy.workspaceId === null) { | |
| 397 | − return policy; | |
| 398 | − } | |
| 399 | − | |
| 400 | − return policy; | |
| 401 | − })); | |
| 402 | − | |
| 403 | −const getScopedPolicy = (input: { | |
| 404 | − scope: PolicyScopeContext; | |
| 405 | − policyId: PolicyId; | |
| 406 | −}) => | |
| 407 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 408 | − Effect.gen(function* () { | |
| 409 | − if (input.scope.workspaceId !== null) { | |
| 410 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 411 | − store, | |
| 412 | − workspaceId: input.scope.workspaceId, | |
| 413 | − }).pipe( | |
| 414 | − Effect.mapError((cause) => | |
| 415 | − policyOps.get.unknownStorage( | |
| 416 | − cause, | |
| 417 | − "Failed loading local workspace policies", | |
| 418 | − ), | |
| 419 | − ), | |
| 420 | − ); | |
| 421 | − if (localWorkspace !== null) { | |
| 422 | − const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 423 | − if (policy === null) { | |
| 424 | − return yield* Effect.fail( | |
| 425 | − policyOps.get.notFound( | |
| 426 | − "Policy not found", | |
| 427 | − `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`, | |
| 428 | − ), | |
| 429 | − ); | |
| 430 | − } | |
| 431 | − | |
| 432 | − return policy; | |
| 433 | − } | |
| 434 | − } | |
| 435 | − | |
| 436 | − const existing = yield* policyOps.get.mapStorage( | |
| 437 | − store.policies.getById(input.policyId), | |
| 438 | − ); | |
| 439 | − | |
| 440 | − if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) { | |
| 441 | − return yield* Effect.fail( | |
| 442 | − policyOps.get.notFound( | |
| 443 | − "Policy not found", | |
| 444 | − `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`, | |
| 445 | − ), | |
| 446 | − ); | |
| 447 | − } | |
| 448 | − | |
| 449 | − return existing.value; | |
| 450 | − })); | |
| 451 | − | |
| 452 | −const updateScopedPolicy = (input: { | |
| 453 | − scope: PolicyScopeContext; | |
| 454 | − policyId: PolicyId; | |
| 455 | − payload: UpdatePolicyPayload; | |
| 456 | −}) => | |
| 457 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 458 | − Effect.gen(function* () { | |
| 459 | − if (input.scope.workspaceId !== null) { | |
| 460 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 461 | − store, | |
| 462 | − workspaceId: input.scope.workspaceId, | |
| 463 | − }).pipe( | |
| 464 | − Effect.mapError((cause) => | |
| 465 | − policyOps.update.unknownStorage( | |
| 466 | − cause, | |
| 467 | − "Failed loading local workspace policies", | |
| 468 | − ), | |
| 469 | − ), | |
| 470 | − ); | |
| 471 | − if (localWorkspace !== null) { | |
| 472 | − yield* ensureLocalWorkspacePolicyCompatible(policyOps.update, input.payload); | |
| 473 | − const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 474 | − if (existing === null || existing.configKey === null) { | |
| 475 | − return yield* Effect.fail( | |
| 476 | − policyOps.update.notFound( | |
| 477 | − "Policy not found", | |
| 478 | − `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`, | |
| 479 | − ), | |
| 480 | − ); | |
| 481 | − } | |
| 482 | − | |
| 483 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 484 | − const policies = { | |
| 485 | − ...(projectConfig.policies ?? {}), | |
| 486 | − }; | |
| 487 | − const existingConfig = policies[existing.configKey] ?? { | |
| 488 | − match: existing.resourcePattern, | |
| 489 | − action: existing.effect, | |
| 490 | − approval: existing.approvalMode === "required" ? "manual" : "auto", | |
| 491 | − }; | |
| 492 | − policies[existing.configKey] = { | |
| 493 | − ...existingConfig, | |
| 494 | − ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}), | |
| 495 | − ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}), | |
| 496 | − ...(input.payload.approvalMode !== undefined | |
| 497 | − ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" } | |
| 498 | − : {}), | |
| 499 | − ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}), | |
| 500 | − ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}), | |
| 501 | − }; | |
| 502 | − const existingState = localWorkspace.workspaceState.policies[existing.configKey]; | |
| 503 | − const updatedAt = Date.now(); | |
| 504 | − const workspaceState = { | |
| 505 | − ...localWorkspace.workspaceState, | |
| 506 | − policies: { | |
| 507 | − ...localWorkspace.workspaceState.policies, | |
| 508 | − [existing.configKey]: { | |
| 509 | − id: existing.id, | |
| 510 | − createdAt: existingState?.createdAt ?? existing.createdAt, | |
| 511 | − updatedAt, | |
| 512 | − }, | |
| 513 | − }, | |
| 514 | − }; | |
| 515 | − yield* writeLocalPolicyFiles({ | |
| 516 | − operation: policyOps.update, | |
| 517 | − context: localWorkspace.runtimeLocalWorkspace.context, | |
| 518 | − projectConfig: { | |
| 519 | − ...projectConfig, | |
| 520 | − policies, | |
| 521 | − }, | |
| 522 | − workspaceState, | |
| 523 | − }); | |
| 524 | − | |
| 525 | − return { | |
| 526 | − ...existing, | |
| 527 | − resourcePattern: policies[existing.configKey]!.match, | |
| 528 | − effect: policies[existing.configKey]!.action, | |
| 529 | − approvalMode: policies[existing.configKey]!.approval === "manual" ? "required" : "auto", | |
| 530 | − priority: policies[existing.configKey]!.priority ?? 0, | |
| 531 | − enabled: policies[existing.configKey]!.enabled ?? true, | |
| 532 | − updatedAt, | |
| 533 | − } satisfies Policy; | |
| 534 | − } | |
| 535 | − } | |
| 536 | − | |
| 537 | − const existing = yield* policyOps.update.mapStorage( | |
| 538 | − store.policies.getById(input.policyId), | |
| 539 | − ); | |
| 540 | − if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) { | |
| 541 | − return yield* Effect.fail( | |
| 542 | − policyOps.update.notFound( | |
| 543 | − "Policy not found", | |
| 544 | − `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`, | |
| 545 | − ), | |
| 546 | − ); | |
| 547 | − } | |
| 548 | − | |
| 549 | − const patch: Partial< | |
| 550 | − Omit<Mutable<Policy>, "id" | "scopeType" | "organizationId" | "workspaceId" | "createdAt"> | |
| 551 | − > = { | |
| 552 | − updatedAt: Date.now(), | |
| 553 | − }; | |
| 554 | − | |
| 555 | − if (input.payload.targetAccountId !== undefined) { | |
| 556 | − patch.targetAccountId = input.payload.targetAccountId; | |
| 557 | − } | |
| 558 | − if (input.payload.clientId !== undefined) { | |
| 559 | − patch.clientId = input.payload.clientId; | |
| 560 | − } | |
| 561 | − if (input.payload.resourceType !== undefined) { | |
| 562 | − patch.resourceType = input.payload.resourceType; | |
| 563 | − } | |
| 564 | − if (input.payload.resourcePattern !== undefined) { | |
| 565 | − patch.resourcePattern = input.payload.resourcePattern; | |
| 566 | − } | |
| 567 | − if (input.payload.matchType !== undefined) { | |
| 568 | − patch.matchType = input.payload.matchType; | |
| 569 | − } | |
| 570 | − if (input.payload.effect !== undefined) { | |
| 571 | − patch.effect = input.payload.effect; | |
| 572 | − } | |
| 573 | − if (input.payload.approvalMode !== undefined) { | |
| 574 | − patch.approvalMode = input.payload.approvalMode; | |
| 575 | − } | |
| 576 | − if (input.payload.argumentConditionsJson !== undefined) { | |
| 577 | − if (input.payload.argumentConditionsJson !== null) { | |
| 578 | − yield* parseJsonString( | |
| 579 | − policyOps.update, | |
| 580 | − "argumentConditionsJson", | |
| 581 | − input.payload.argumentConditionsJson, | |
| 582 | − ); | |
| 583 | − } | |
| 584 | − patch.argumentConditionsJson = input.payload.argumentConditionsJson; | |
| 585 | − } | |
| 586 | − if (input.payload.priority !== undefined) { | |
| 587 | − patch.priority = input.payload.priority; | |
| 588 | − } | |
| 589 | − if (input.payload.enabled !== undefined) { | |
| 590 | − patch.enabled = input.payload.enabled; | |
| 591 | − } | |
| 592 | − | |
| 593 | − const updated = yield* mapPersistenceError( | |
| 594 | − policyOps.update, | |
| 595 | − store.policies.update(input.policyId, patch), | |
| 596 | − ); | |
| 597 | − if (Option.isNone(updated)) { | |
| 598 | − return yield* Effect.fail( | |
| 599 | − policyOps.update.notFound( | |
| 600 | − "Policy not found", | |
| 601 | − `scopeType=${input.scope.scopeType} organizationId=${input.scope.organizationId} workspaceId=${input.scope.workspaceId} policyId=${input.policyId}`, | |
| 602 | − ), | |
| 603 | − ); | |
| 604 | − } | |
| 605 | − | |
| 606 | − if (updated.value.workspaceId === null) { | |
| 607 | − return updated.value; | |
| 608 | − } | |
| 609 | − | |
| 610 | − return updated.value; | |
| 611 | − })); | |
| 612 | − | |
| 613 | −const removeScopedPolicy = (input: { | |
| 614 | − scope: PolicyScopeContext; | |
| 615 | − policyId: PolicyId; | |
| 616 | −}) => | |
| 617 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 618 | − Effect.gen(function* () { | |
| 619 | − if (input.scope.workspaceId !== null) { | |
| 620 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 621 | − store, | |
| 622 | − workspaceId: input.scope.workspaceId, | |
| 623 | − }).pipe( | |
| 624 | − Effect.mapError((cause) => | |
| 625 | − policyOps.remove.unknownStorage( | |
| 626 | − cause, | |
| 627 | − "Failed loading local workspace policies", | |
| 628 | − ), | |
| 629 | − ), | |
| 630 | − ); | |
| 631 | − if (localWorkspace !== null) { | |
| 632 | − const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 633 | − if (existing === null || existing.configKey === null) { | |
| 634 | − return { removed: false }; | |
| 635 | − } | |
| 636 | − | |
| 637 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 638 | − const policies = { | |
| 639 | − ...(projectConfig.policies ?? {}), | |
| 640 | − }; | |
| 641 | − delete policies[existing.configKey]; | |
| 642 | − const { | |
| 643 | − [existing.configKey]: _removedPolicy, | |
| 644 | − ...remainingPolicies | |
| 645 | − } = localWorkspace.workspaceState.policies; | |
| 646 | − yield* writeLocalPolicyFiles({ | |
| 647 | − operation: policyOps.remove, | |
| 648 | − context: localWorkspace.runtimeLocalWorkspace.context, | |
| 649 | − projectConfig: { | |
| 650 | − ...projectConfig, | |
| 651 | − policies, | |
| 652 | − }, | |
| 653 | − workspaceState: { | |
| 654 | − ...localWorkspace.workspaceState, | |
| 655 | − policies: remainingPolicies, | |
| 656 | − }, | |
| 657 | − }); | |
| 658 | − return { removed: true }; | |
| 659 | − } | |
| 660 | − } | |
| 661 | − | |
| 662 | − const existing = yield* policyOps.remove.mapStorage( | |
| 663 | − store.policies.getById(input.policyId), | |
| 664 | − ); | |
| 665 | − if (Option.isNone(existing) || !policyMatchesScope(existing.value, input.scope)) { | |
| 666 | − return { removed: false }; | |
| 667 | − } | |
| 668 | − | |
| 669 | − const removed = yield* policyOps.remove.mapStorage( | |
| 670 | − store.policies.removeById(input.policyId), | |
| 671 | − ); | |
| 672 | − | |
| 673 | − return { removed }; | |
| 674 | − })); | |
| 675 | − | |
| 676 | −export const listOrganizationPolicies = (organizationId: OrganizationId) => | |
| 677 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 678 | − Effect.gen(function* () { | |
| 679 | − yield* loadOrganizationPolicyContext(store, policyOps.list, organizationId); | |
| 680 | − return yield* policyOps.list.mapStorage( | |
| 681 | − store.policies.listByOrganizationId(organizationId), | |
| 682 | − ); | |
| 683 | − })); | |
| 684 | − | |
| 685 | 210 | export const listPolicies = (workspaceId: WorkspaceId) => |
| 686 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 687 | − Effect.gen(function* () { | |
| 688 | − yield* loadWorkspacePolicyContext(store, policyOps.list, workspaceId); | |
| 689 | − const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 690 | − store, | |
| 691 | − workspaceId, | |
| 692 | − }).pipe( | |
| 693 | − Effect.mapError((cause) => | |
| 694 | − policyOps.list.unknownStorage( | |
| 695 | − cause, | |
| 696 | − "Failed loading local workspace policies", | |
| 697 | − ), | |
| 211 | + Effect.gen(function* () { | |
| 212 | + yield* loadWorkspacePolicyContext(policyOps.list, workspaceId); | |
| 213 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(workspaceId).pipe( | |
| 214 | + Effect.mapError((cause) => | |
| 215 | + policyOps.list.unknownStorage( | |
| 216 | + cause, | |
| 217 | + "Failed loading local workspace policies", | |
| 698 | 218 | ), |
| 699 | − ); | |
| 700 | − if (localWorkspace !== null) { | |
| 701 | − return localWorkspace.policies; | |
| 702 | − } | |
| 703 | − return yield* policyOps.list.mapStorage( | |
| 704 | − store.policies.listByWorkspaceId(workspaceId), | |
| 705 | − ); | |
| 706 | − })); | |
| 707 | − | |
| 708 | −export const createOrganizationPolicy = (input: { | |
| 709 | − organizationId: OrganizationId; | |
| 710 | − payload: CreatePolicyPayload; | |
| 711 | −}) => | |
| 712 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 713 | − Effect.flatMap( | |
| 714 | − loadOrganizationPolicyContext(store, policyOps.create, input.organizationId), | |
| 715 | − (scope) => createScopedPolicy({ scope, payload: input.payload }), | |
| 716 | − )); | |
| 219 | + ), | |
| 220 | + ); | |
| 221 | + return localWorkspace.policies; | |
| 222 | + }); | |
| 717 | 223 | |
| 718 | 224 | export const createPolicy = (input: { |
| 719 | 225 | workspaceId: WorkspaceId; |
| 720 | 226 | payload: CreatePolicyPayload; |
| 721 | 227 | }) => |
| 722 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 723 | − Effect.flatMap( | |
| 724 | − loadWorkspacePolicyContext(store, policyOps.create, input.workspaceId), | |
| 725 | − (scope) => createScopedPolicy({ scope, payload: input.payload }), | |
| 726 | − )); | |
| 228 | + Effect.gen(function* () { | |
| 229 | + const scope = yield* loadWorkspacePolicyContext(policyOps.create, input.workspaceId); | |
| 230 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 231 | + Effect.mapError((cause) => | |
| 232 | + policyOps.create.unknownStorage( | |
| 233 | + cause, | |
| 234 | + "Failed loading local workspace policies", | |
| 235 | + ), | |
| 236 | + ), | |
| 237 | + ); | |
| 238 | + yield* ensureLocalWorkspacePolicyCompatible(policyOps.create, input.payload); | |
| 727 | 239 | |
| 728 | −export const getOrganizationPolicy = (input: { | |
| 729 | − organizationId: OrganizationId; | |
| 730 | − policyId: PolicyId; | |
| 731 | −}) => | |
| 732 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 733 | − Effect.flatMap( | |
| 734 | − loadOrganizationPolicyContext(store, policyOps.get, input.organizationId), | |
| 735 | − (scope) => getScopedPolicy({ scope, policyId: input.policyId }), | |
| 736 | − )); | |
| 240 | + const now = Date.now(); | |
| 241 | + const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 242 | + const policies = { | |
| 243 | + ...(projectConfig.policies ?? {}), | |
| 244 | + }; | |
| 245 | + const configKey = derivePolicyConfigKey({ | |
| 246 | + configKey: null, | |
| 247 | + resourcePattern: input.payload.resourcePattern ?? "*", | |
| 248 | + effect: input.payload.effect ?? "allow", | |
| 249 | + approvalMode: input.payload.approvalMode ?? "auto", | |
| 250 | + }, new Set(Object.keys(policies))); | |
| 251 | + const id = localWorkspace.workspaceState.policies[configKey]?.id | |
| 252 | + ?? localPolicyIdForConfigKey({ | |
| 253 | + workspaceRoot: localWorkspace.runtimeLocalWorkspace.context.workspaceRoot, | |
| 254 | + configKey, | |
| 255 | + }); | |
| 256 | + | |
| 257 | + policies[configKey] = { | |
| 258 | + match: input.payload.resourcePattern ?? "*", | |
| 259 | + action: input.payload.effect ?? "allow", | |
| 260 | + approval: (input.payload.approvalMode ?? "auto") === "required" ? "manual" : "auto", | |
| 261 | + ...(input.payload.enabled === false ? { enabled: false } : {}), | |
| 262 | + ...((input.payload.priority ?? 0) !== 0 ? { priority: input.payload.priority ?? 0 } : {}), | |
| 263 | + }; | |
| 264 | + | |
| 265 | + const existingState = localWorkspace.workspaceState.policies[configKey]; | |
| 266 | + const workspaceState = { | |
| 267 | + ...localWorkspace.workspaceState, | |
| 268 | + policies: { | |
| 269 | + ...localWorkspace.workspaceState.policies, | |
| 270 | + [configKey]: { | |
| 271 | + id, | |
| 272 | + createdAt: existingState?.createdAt ?? now, | |
| 273 | + updatedAt: now, | |
| 274 | + }, | |
| 275 | + }, | |
| 276 | + }; | |
| 277 | + yield* writeLocalPolicyFiles({ | |
| 278 | + operation: policyOps.create, | |
| 279 | + context: localWorkspace.runtimeLocalWorkspace.context, | |
| 280 | + projectConfig: { | |
| 281 | + ...projectConfig, | |
| 282 | + policies, | |
| 283 | + }, | |
| 284 | + workspaceState, | |
| 285 | + }); | |
| 286 | + | |
| 287 | + return { | |
| 288 | + id, | |
| 289 | + configKey, | |
| 290 | + scopeType: "workspace", | |
| 291 | + organizationId: scope.organizationId, | |
| 292 | + workspaceId: scope.workspaceId, | |
| 293 | + targetAccountId: null, | |
| 294 | + clientId: null, | |
| 295 | + resourceType: "tool_path", | |
| 296 | + resourcePattern: policies[configKey]!.match, | |
| 297 | + matchType: "glob", | |
| 298 | + effect: policies[configKey]!.action, | |
| 299 | + approvalMode: policies[configKey]!.approval === "manual" ? "required" : "auto", | |
| 300 | + argumentConditionsJson: null, | |
| 301 | + priority: policies[configKey]!.priority ?? 0, | |
| 302 | + enabled: policies[configKey]!.enabled ?? true, | |
| 303 | + createdAt: workspaceState.policies[configKey]!.createdAt, | |
| 304 | + updatedAt: now, | |
| 305 | + } satisfies Policy; | |
| 306 | + }); | |
| 737 | 307 | |
| 738 | 308 | export const getPolicy = (input: { |
| 739 | 309 | workspaceId: WorkspaceId; |
| 740 | 310 | policyId: PolicyId; |
| 741 | 311 | }) => |
| 742 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 743 | − Effect.flatMap( | |
| 744 | − loadWorkspacePolicyContext(store, policyOps.get, input.workspaceId), | |
| 745 | − (scope) => getScopedPolicy({ scope, policyId: input.policyId }), | |
| 746 | − )); | |
| 747 | − | |
| 748 | −export const updateOrganizationPolicy = (input: { | |
| 749 | − organizationId: OrganizationId; | |
| 750 | − policyId: PolicyId; | |
| 751 | − payload: UpdatePolicyPayload; | |
| 752 | −}) => | |
| 753 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 754 | − Effect.flatMap( | |
| 755 | − loadOrganizationPolicyContext(store, policyOps.update, input.organizationId), | |
| 756 | − (scope) => updateScopedPolicy({ | |
| 757 | − scope, | |
| 758 | − policyId: input.policyId, | |
| 759 | − payload: input.payload, | |
| 760 | − }), | |
| 761 | − )); | |
| 312 | + Effect.gen(function* () { | |
| 313 | + const scope = yield* loadWorkspacePolicyContext(policyOps.get, input.workspaceId); | |
| 314 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 315 | + Effect.mapError((cause) => | |
| 316 | + policyOps.get.unknownStorage( | |
| 317 | + cause, | |
| 318 | + "Failed loading local workspace policies", | |
| 319 | + ), | |
| 320 | + ), | |
| 321 | + ); | |
| 322 | + const policy = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 323 | + if (policy === null || !policyMatchesScope(policy, scope)) { | |
| 324 | + return yield* Effect.fail( | |
| 325 | + policyOps.get.notFound( | |
| 326 | + "Policy not found", | |
| 327 | + `scopeType=${scope.scopeType} organizationId=${scope.organizationId} workspaceId=${scope.workspaceId} policyId=${input.policyId}`, | |
| 328 | + ), | |
| 329 | + ); | |
| 330 | + } | |
| 331 | + return policy; | |
| 332 | + }); | |
| 762 | 333 | |
| 763 | 334 | export const updatePolicy = (input: { |
| 764 | 335 | workspaceId: WorkspaceId; |
| 765 | 336 | policyId: PolicyId; |
| 766 | 337 | payload: UpdatePolicyPayload; |
| 767 | 338 | }) => |
| 768 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 769 | − Effect.flatMap( | |
| 770 | − loadWorkspacePolicyContext(store, policyOps.update, input.workspaceId), | |
| 771 | − (scope) => updateScopedPolicy({ | |
| 772 | − scope, | |
| 773 | − policyId: input.policyId, | |
| 774 | − payload: input.payload, | |
| 775 | − }), | |
| 776 | − )); | |
| 339 | + Effect.gen(function* () { | |
| 340 | + const scope = yield* loadWorkspacePolicyContext(policyOps.update, input.workspaceId); | |
| 341 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 342 | + Effect.mapError((cause) => | |
| 343 | + policyOps.update.unknownStorage( | |
| 344 | + cause, | |
| 345 | + "Failed loading local workspace policies", | |
| 346 | + ), | |
| 347 | + ), | |
| 348 | + ); | |
| 349 | + yield* ensureLocalWorkspacePolicyCompatible(policyOps.update, input.payload); | |
| 350 | + const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 351 | + if (existing === null || existing.configKey === null || !policyMatchesScope(existing, scope)) { | |
| 352 | + return yield* Effect.fail( | |
| 353 | + policyOps.update.notFound( | |
| 354 | + "Policy not found", | |
| 355 | + `scopeType=${scope.scopeType} organizationId=${scope.organizationId} workspaceId=${scope.workspaceId} policyId=${input.policyId}`, | |
| 356 | + ), | |
| 357 | + ); | |
| 358 | + } | |
| 777 | 359 | |
| 778 | −export const removeOrganizationPolicy = (input: { | |
| 779 | − organizationId: OrganizationId; | |
| 780 | − policyId: PolicyId; | |
| 781 | −}) => | |
| 782 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 783 | − Effect.flatMap( | |
| 784 | − loadOrganizationPolicyContext(store, policyOps.remove, input.organizationId), | |
| 785 | − (scope) => removeScopedPolicy({ scope, policyId: input.policyId }), | |
| 786 | − )); | |
| 360 | + const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 361 | + const policies = { | |
| 362 | + ...(projectConfig.policies ?? {}), | |
| 363 | + }; | |
| 364 | + const existingConfig = policies[existing.configKey] ?? { | |
| 365 | + match: existing.resourcePattern, | |
| 366 | + action: existing.effect, | |
| 367 | + approval: existing.approvalMode === "required" ? "manual" : "auto", | |
| 368 | + }; | |
| 369 | + policies[existing.configKey] = { | |
| 370 | + ...existingConfig, | |
| 371 | + ...(input.payload.resourcePattern !== undefined ? { match: input.payload.resourcePattern } : {}), | |
| 372 | + ...(input.payload.effect !== undefined ? { action: input.payload.effect } : {}), | |
| 373 | + ...(input.payload.approvalMode !== undefined | |
| 374 | + ? { approval: input.payload.approvalMode === "required" ? "manual" : "auto" } | |
| 375 | + : {}), | |
| 376 | + ...(input.payload.enabled !== undefined ? { enabled: input.payload.enabled } : {}), | |
| 377 | + ...(input.payload.priority !== undefined ? { priority: input.payload.priority } : {}), | |
| 378 | + }; | |
| 379 | + | |
| 380 | + const updatedAt = Date.now(); | |
| 381 | + const existingState = localWorkspace.workspaceState.policies[existing.configKey]; | |
| 382 | + const workspaceState = { | |
| 383 | + ...localWorkspace.workspaceState, | |
| 384 | + policies: { | |
| 385 | + ...localWorkspace.workspaceState.policies, | |
| 386 | + [existing.configKey]: { | |
| 387 | + id: existing.id, | |
| 388 | + createdAt: existingState?.createdAt ?? existing.createdAt, | |
| 389 | + updatedAt, | |
| 390 | + }, | |
| 391 | + }, | |
| 392 | + }; | |
| 393 | + yield* writeLocalPolicyFiles({ | |
| 394 | + operation: policyOps.update, | |
| 395 | + context: localWorkspace.runtimeLocalWorkspace.context, | |
| 396 | + projectConfig: { | |
| 397 | + ...projectConfig, | |
| 398 | + policies, | |
| 399 | + }, | |
| 400 | + workspaceState, | |
| 401 | + }); | |
| 402 | + | |
| 403 | + return { | |
| 404 | + ...existing, | |
| 405 | + resourcePattern: policies[existing.configKey]!.match, | |
| 406 | + effect: policies[existing.configKey]!.action, | |
| 407 | + approvalMode: policies[existing.configKey]!.approval === "manual" ? "required" : "auto", | |
| 408 | + priority: policies[existing.configKey]!.priority ?? 0, | |
| 409 | + enabled: policies[existing.configKey]!.enabled ?? true, | |
| 410 | + updatedAt, | |
| 411 | + } satisfies Policy; | |
| 412 | + }); | |
| 787 | 413 | |
| 788 | 414 | export const removePolicy = (input: { |
| 789 | 415 | workspaceId: WorkspaceId; |
| 790 | 416 | policyId: PolicyId; |
| 791 | 417 | }) => |
| 792 | − Effect.flatMap(ControlPlaneStore, (store) => | |
| 793 | − Effect.flatMap( | |
| 794 | − loadWorkspacePolicyContext(store, policyOps.remove, input.workspaceId), | |
| 795 | − (scope) => removeScopedPolicy({ scope, policyId: input.policyId }), | |
| 796 | − )); | |
| 418 | + Effect.gen(function* () { | |
| 419 | + const scope = yield* loadWorkspacePolicyContext(policyOps.remove, input.workspaceId); | |
| 420 | + const localWorkspace = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 421 | + Effect.mapError((cause) => | |
| 422 | + policyOps.remove.unknownStorage( | |
| 423 | + cause, | |
| 424 | + "Failed loading local workspace policies", | |
| 425 | + ), | |
| 426 | + ), | |
| 427 | + ); | |
| 428 | + const existing = localWorkspace.policies.find((candidate) => candidate.id === input.policyId) ?? null; | |
| 429 | + if (existing === null || existing.configKey === null || !policyMatchesScope(existing, scope)) { | |
| 430 | + return { removed: false }; | |
| 431 | + } | |
| 432 | + | |
| 433 | + const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 434 | + const policies = { | |
| 435 | + ...(projectConfig.policies ?? {}), | |
| 436 | + }; | |
| 437 | + delete policies[existing.configKey]; | |
| 438 | + const { | |
| 439 | + [existing.configKey]: _removedPolicy, | |
| 440 | + ...remainingPolicies | |
| 441 | + } = localWorkspace.workspaceState.policies; | |
| 442 | + yield* writeLocalPolicyFiles({ | |
| 443 | + operation: policyOps.remove, | |
| 444 | + context: localWorkspace.runtimeLocalWorkspace.context, | |
| 445 | + projectConfig: { | |
| 446 | + ...projectConfig, | |
| 447 | + policies, | |
| 448 | + }, | |
| 449 | + workspaceState: { | |
| 450 | + ...localWorkspace.workspaceState, | |
| 451 | + policies: remainingPolicies, | |
| 452 | + }, | |
| 453 | + }); | |
| 454 | + return { removed: true }; | |
| 455 | + }); | |
| @@ -1,4 +1,7 @@ | ||
| 1 | +import { mkdtempSync } from "node:fs"; | |
| 1 | 2 | import { createServer } from "node:http"; |
| 3 | +import { tmpdir } from "node:os"; | |
| 4 | +import { join } from "node:path"; | |
| 2 | 5 | |
| 3 | 6 | import { describe, expect, it } from "vitest"; |
| 4 | 7 | import * as Schema from "effect/Schema"; |
| @@ -26,6 +29,17 @@ import { · 21 unmodified lines | ||
| 26 | 29 | createTerminalSourceAuthSessionPatch, |
| 27 | 30 | } from "./source-auth-service"; |
| 28 | 31 | import { createLiveExecutionManager } from "./live-execution"; |
| 32 | +import { | |
| 33 | + loadLocalExecutorConfig, | |
| 34 | + resolveLocalWorkspaceContext, | |
| 35 | +} from "./local-config"; | |
| 36 | +import { | |
| 37 | + readLocalSourceArtifact, | |
| 38 | +} from "./local-source-artifacts"; | |
| 39 | +import { | |
| 40 | + RuntimeLocalWorkspaceService, | |
| 41 | + type RuntimeLocalWorkspaceState, | |
| 42 | +} from "./local-runtime-context"; | |
| 29 | 43 | import { createDefaultSecretMaterialResolver } from "./secret-material-providers"; |
| 30 | 44 | import { resolveSourceAuthMaterial } from "./source-auth-material"; |
| 31 | 45 | |
| @@ -163,6 +177,38 @@ const makePersistence = async (): Promise<SqlControlPlanePersistence> => · 131 unmodified lines | ||
| 163 | 177 | }), |
| 164 | 178 | ); |
| 165 | 179 | |
| 180 | +const makeRuntimeLocalWorkspaceState = async ( | |
| 181 | + workspaceId: ReturnType<typeof WorkspaceIdSchema.make>, | |
| 182 | + accountId: ReturnType<typeof AccountIdSchema.make>, | |
| 183 | +): Promise<RuntimeLocalWorkspaceState> => | |
| 184 | + Effect.runPromise( | |
| 185 | + Effect.gen(function* () { | |
| 186 | + const workspaceRoot = mkdtempSync( | |
| 187 | + join(tmpdir(), "executor-source-auth-service-"), | |
| 188 | + ); | |
| 189 | + const context = yield* Effect.tryPromise({ | |
| 190 | + try: () => resolveLocalWorkspaceContext({ workspaceRoot }), | |
| 191 | + catch: (cause) => | |
| 192 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 193 | + }); | |
| 194 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 195 | + try: () => loadLocalExecutorConfig(context), | |
| 196 | + catch: (cause) => | |
| 197 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 198 | + }); | |
| 199 | + | |
| 200 | + return { | |
| 201 | + context, | |
| 202 | + installation: { | |
| 203 | + workspaceId, | |
| 204 | + accountId, | |
| 205 | + organizationId: "org_local_personal" as never, | |
| 206 | + }, | |
| 207 | + loadedConfig, | |
| 208 | + } satisfies RuntimeLocalWorkspaceState; | |
| 209 | + }), | |
| 210 | + ); | |
| 211 | + | |
| 166 | 212 | describe("source-auth-service", () => { |
| 167 | 213 | const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema); |
| 168 | 214 | |
| @@ -225,36 +271,20 @@ describe("source-auth-service", () => { · 56 unmodified lines | ||
| 225 | 271 | |
| 226 | 272 | const persistence = await makePersistence(); |
| 227 | 273 | try { |
| 228 | − const now = Date.now(); | |
| 229 | 274 | const workspaceId = WorkspaceIdSchema.make("ws_google_oauth"); |
| 230 | 275 | const accountId = AccountIdSchema.make("acc_google_oauth"); |
| 231 | − | |
| 232 | − await Effect.runPromise( | |
| 233 | − persistence.rows.organizations.insert({ | |
| 234 | − id: "org_google_oauth" as any, | |
| 235 | − slug: "google-oauth", | |
| 236 | − name: "Google OAuth", | |
| 237 | − status: "active", | |
| 238 | − createdByAccountId: accountId, | |
| 239 | − createdAt: now, | |
| 240 | − updatedAt: now, | |
| 241 | − }), | |
| 242 | − ); | |
| 243 | − await Effect.runPromise( | |
| 244 | − persistence.rows.workspaces.insert({ | |
| 245 | − id: workspaceId, | |
| 246 | − organizationId: "org_google_oauth" as any, | |
| 247 | − name: "Workspace", | |
| 248 | − createdByAccountId: accountId, | |
| 249 | − createdAt: now, | |
| 250 | − updatedAt: now, | |
| 251 | − }), | |
| 276 | + const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState( | |
| 277 | + workspaceId, | |
| 278 | + accountId, | |
| 252 | 279 | ); |
| 253 | 280 | |
| 254 | 281 | const service = createRuntimeSourceAuthService({ |
| 255 | 282 | rows: persistence.rows, |
| 256 | 283 | liveExecutionManager: createLiveExecutionManager(), |
| 257 | 284 | getLocalServerBaseUrl: () => "http://localhost:8788", |
| 285 | + localConfig: runtimeLocalWorkspace.loadedConfig.config, | |
| 286 | + workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 287 | + localWorkspaceState: runtimeLocalWorkspace, | |
| 258 | 288 | }); |
| 259 | 289 | |
| 260 | 290 | await withGoogleAuthTestServer({ |
| @@ -290,7 +320,12 @@ describe("source-auth-service", () => { · 29 unmodified lines | ||
| 290 | 320 | clientId: "google-test-client", |
| 291 | 321 | clientSecret: "google-test-secret", |
| 292 | 322 | }, |
| 293 | − }), | |
| 323 | + }).pipe( | |
| 324 | + Effect.provideService( | |
| 325 | + RuntimeLocalWorkspaceService, | |
| 326 | + runtimeLocalWorkspace, | |
| 327 | + ), | |
| 328 | + ), | |
| 294 | 329 | ); |
| 295 | 330 | |
| 296 | 331 | expect(addResult.kind).toBe("oauth_required"); |
| @@ -461,22 +496,14 @@ describe("source-auth-service", () => { · 164 unmodified lines | ||
| 461 | 496 | expect(Option.isNone(oldSecret)).toBe(true); |
| 462 | 497 | } |
| 463 | 498 | |
| 464 | − const recipeOperations = await Effect.runPromise( | |
| 465 | − persistence.rows.sources.getByWorkspaceAndId( | |
| 466 | − workspaceId, | |
| 467 | − connectedSource.id, | |
| 468 | − ), | |
| 469 | − ); | |
| 470 | − expect(Option.isSome(recipeOperations)).toBe(true); | |
| 471 | − const storedSource = Option.getOrNull(recipeOperations)!; | |
| 472 | − const materializedOperations = await Effect.runPromise( | |
| 473 | − persistence.rows.sourceRecipeOperations.listByRevisionId( | |
| 474 | − storedSource.recipeRevisionId, | |
| 475 | − ), | |
| 476 | − ); | |
| 477 | − expect(materializedOperations.length).toBeGreaterThan(0); | |
| 499 | + const localArtifact = await readLocalSourceArtifact({ | |
| 500 | + context: runtimeLocalWorkspace.context, | |
| 501 | + sourceId: connectedSource.id, | |
| 502 | + }); | |
| 503 | + expect(localArtifact).not.toBeNull(); | |
| 504 | + expect(localArtifact?.operations.length).toBeGreaterThan(0); | |
| 478 | 505 | expect( |
| 479 | − materializedOperations.some( | |
| 506 | + localArtifact?.operations.some( | |
| 480 | 507 | (operation) => operation.toolId === "spreadsheets.values.get", |
| 481 | 508 | ), |
| 482 | 509 | ).toBe(true); |
| @@ -495,36 +522,20 @@ describe("source-auth-service", () => { · 12 unmodified lines | ||
| 495 | 522 | |
| 496 | 523 | const persistence = await makePersistence(); |
| 497 | 524 | try { |
| 498 | − const now = Date.now(); | |
| 499 | 525 | const workspaceId = WorkspaceIdSchema.make("ws_google_oauth_web"); |
| 500 | 526 | const accountId = AccountIdSchema.make("acc_google_oauth_web"); |
| 501 | − | |
| 502 | − await Effect.runPromise( | |
| 503 | − persistence.rows.organizations.insert({ | |
| 504 | − id: "org_google_oauth_web" as any, | |
| 505 | − slug: "google-oauth-web", | |
| 506 | − name: "Google OAuth Web", | |
| 507 | − status: "active", | |
| 508 | − createdByAccountId: accountId, | |
| 509 | − createdAt: now, | |
| 510 | − updatedAt: now, | |
| 511 | − }), | |
| 512 | − ); | |
| 513 | − await Effect.runPromise( | |
| 514 | − persistence.rows.workspaces.insert({ | |
| 515 | − id: workspaceId, | |
| 516 | − organizationId: "org_google_oauth_web" as any, | |
| 517 | − name: "Workspace", | |
| 518 | − createdByAccountId: accountId, | |
| 519 | − createdAt: now, | |
| 520 | − updatedAt: now, | |
| 521 | − }), | |
| 527 | + const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState( | |
| 528 | + workspaceId, | |
| 529 | + accountId, | |
| 522 | 530 | ); |
| 523 | 531 | |
| 524 | 532 | const service = createRuntimeSourceAuthService({ |
| 525 | 533 | rows: persistence.rows, |
| 526 | 534 | liveExecutionManager: createLiveExecutionManager(), |
| 527 | 535 | getLocalServerBaseUrl: () => "http://127.0.0.1:8788", |
| 536 | + localConfig: runtimeLocalWorkspace.loadedConfig.config, | |
| 537 | + workspaceRoot: runtimeLocalWorkspace.context.workspaceRoot, | |
| 538 | + localWorkspaceState: runtimeLocalWorkspace, | |
| 528 | 539 | }); |
| 529 | 540 | |
| 530 | 541 | await withGoogleAuthTestServer({ |
| @@ -565,6 +576,11 @@ describe("source-auth-service", () => { · 34 unmodified lines | ||
| 565 | 576 | { |
| 566 | 577 | baseUrl: "https://app.executor.dev", |
| 567 | 578 | }, |
| 579 | + ).pipe( | |
| 580 | + Effect.provideService( | |
| 581 | + RuntimeLocalWorkspaceService, | |
| 582 | + runtimeLocalWorkspace, | |
| 583 | + ), | |
| 568 | 584 | ), |
| 569 | 585 | ); |
| 570 | 586 | |
| @@ -1,11 +1,14 @@ | ||
| 1 | +import { mkdtempSync } from "node:fs"; | |
| 2 | +import { tmpdir } from "node:os"; | |
| 3 | +import { join } from "node:path"; | |
| 4 | + | |
| 1 | 5 | import { describe, expect, it } from "vitest"; |
| 2 | 6 | import * as Effect from "effect/Effect"; |
| 3 | 7 | |
| 4 | 8 | import { |
| 5 | 9 | AccountIdSchema, |
| 6 | − OrganizationIdSchema, | |
| 10 | + type LocalConfigSource, | |
| 7 | 11 | SourceIdSchema, |
| 8 | − SourceRecipeIdSchema, | |
| 9 | 12 | SourceRecipeRevisionIdSchema, |
| 10 | 13 | SourceRecipeSchemaBundleIdSchema, |
| 11 | 14 | WorkspaceIdSchema, |
| @@ -17,11 +20,27 @@ import { · 5 unmodified lines | ||
| 17 | 20 | type SqlControlPlanePersistence, |
| 18 | 21 | } from "#persistence"; |
| 19 | 22 | |
| 23 | +import { | |
| 24 | + loadLocalExecutorConfig, | |
| 25 | + resolveLocalWorkspaceContext, | |
| 26 | + writeProjectLocalExecutorConfig, | |
| 27 | +} from "./local-config"; | |
| 28 | +import { | |
| 29 | + buildLocalSourceArtifact, | |
| 30 | + writeLocalSourceArtifact, | |
| 31 | +} from "./local-source-artifacts"; | |
| 20 | 32 | import { |
| 21 | 33 | getSourceInspection, |
| 22 | 34 | getSourceInspectionSchemaBundle, |
| 23 | 35 | getSourceInspectionToolDetail, |
| 24 | 36 | } from "./source-inspection"; |
| 37 | +import { | |
| 38 | + RuntimeLocalWorkspaceService, | |
| 39 | + type RuntimeLocalWorkspaceState, | |
| 40 | +} from "./local-runtime-context"; | |
| 41 | +import { | |
| 42 | + writeLocalWorkspaceState, | |
| 43 | +} from "./local-workspace-state"; | |
| 25 | 44 | import { ControlPlaneStore } from "./store"; |
| 26 | 45 | |
| 27 | 46 | const makePersistence = () => |
| @@ -31,30 +50,37 @@ const makePersistence = () => · 3 unmodified lines | ||
| 31 | 50 | }), |
| 32 | 51 | ); |
| 33 | 52 | |
| 34 | −const seedWorkspace = async (input: { | |
| 35 | − persistence: SqlControlPlanePersistence; | |
| 36 | − workspaceId: ReturnType<typeof WorkspaceIdSchema.make>; | |
| 37 | − organizationId: ReturnType<typeof OrganizationIdSchema.make>; | |
| 38 | − accountId: ReturnType<typeof AccountIdSchema.make>; | |
| 39 | −}) => { | |
| 40 | − await Effect.runPromise(input.persistence.rows.organizations.insert({ | |
| 41 | − id: input.organizationId, | |
| 42 | − slug: `org-${input.organizationId}`, | |
| 43 | − name: `Org ${input.organizationId}`, | |
| 44 | − status: "active", | |
| 45 | − createdByAccountId: input.accountId, | |
| 46 | − createdAt: 1000, | |
| 47 | − updatedAt: 1000, | |
| 48 | − })); | |
| 49 | − await Effect.runPromise(input.persistence.rows.workspaces.insert({ | |
| 50 | − id: input.workspaceId, | |
| 51 | − organizationId: input.organizationId, | |
| 52 | − name: `Workspace ${input.workspaceId}`, | |
| 53 | − createdByAccountId: input.accountId, | |
| 54 | − createdAt: 1000, | |
| 55 | − updatedAt: 1000, | |
| 56 | − })); | |
| 57 | −}; | |
| 53 | +const makeRuntimeLocalWorkspaceState = ( | |
| 54 | + workspaceId: ReturnType<typeof WorkspaceIdSchema.make>, | |
| 55 | + accountId: ReturnType<typeof AccountIdSchema.make>, | |
| 56 | +) => | |
| 57 | + Effect.runPromise( | |
| 58 | + Effect.gen(function* () { | |
| 59 | + const workspaceRoot = mkdtempSync( | |
| 60 | + join(tmpdir(), "executor-source-inspection-"), | |
| 61 | + ); | |
| 62 | + const context = yield* Effect.tryPromise({ | |
| 63 | + try: () => resolveLocalWorkspaceContext({ workspaceRoot }), | |
| 64 | + catch: (cause) => | |
| 65 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 66 | + }); | |
| 67 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 68 | + try: () => loadLocalExecutorConfig(context), | |
| 69 | + catch: (cause) => | |
| 70 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 71 | + }); | |
| 72 | + | |
| 73 | + return { | |
| 74 | + context, | |
| 75 | + installation: { | |
| 76 | + workspaceId, | |
| 77 | + accountId, | |
| 78 | + organizationId: "org_local_personal" as never, | |
| 79 | + }, | |
| 80 | + loadedConfig, | |
| 81 | + } satisfies RuntimeLocalWorkspaceState; | |
| 82 | + }), | |
| 83 | + ); | |
| 58 | 84 | |
| 59 | 85 | const makeSource = (input: { |
| 60 | 86 | workspaceId: Source["workspaceId"]; |
| @@ -133,16 +159,39 @@ const makeOperation = ( · 72 unmodified lines | ||
| 133 | 159 | ...overrides, |
| 134 | 160 | }); |
| 135 | 161 | |
| 162 | +const configSourceFromSource = (source: Source): LocalConfigSource => ({ | |
| 163 | + kind: "openapi", | |
| 164 | + ...(source.name !== source.id ? { name: source.name } : {}), | |
| 165 | + ...(source.namespace && source.namespace !== source.id | |
| 166 | + ? { namespace: source.namespace } | |
| 167 | + : {}), | |
| 168 | + connection: { | |
| 169 | + endpoint: source.endpoint, | |
| 170 | + }, | |
| 171 | + binding: { | |
| 172 | + specUrl: | |
| 173 | + typeof source.binding.specUrl === "string" | |
| 174 | + ? source.binding.specUrl | |
| 175 | + : "https://example.com/openapi.json", | |
| 176 | + defaultHeaders: | |
| 177 | + (source.binding.defaultHeaders as Record<string, string> | null | undefined) | |
| 178 | + ?? null, | |
| 179 | + }, | |
| 180 | +}); | |
| 181 | + | |
| 136 | 182 | describe("source-inspection", () => { |
| 137 | 183 | it("returns a lightweight inspection bundle and loads rich detail on demand", async () => { |
| 138 | 184 | const persistence = await makePersistence(); |
| 139 | 185 | try { |
| 140 | 186 | const workspaceId = WorkspaceIdSchema.make("ws_source_inspection"); |
| 141 | − const organizationId = OrganizationIdSchema.make("org_source_inspection"); | |
| 142 | 187 | const accountId = AccountIdSchema.make("acc_source_inspection"); |
| 143 | 188 | const sourceId = SourceIdSchema.make("src_source_inspection"); |
| 144 | − const recipeId = SourceRecipeIdSchema.make("src_recipe_inspection"); | |
| 145 | − const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_inspection"); | |
| 189 | + const recipeRevisionId = | |
| 190 | + SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"); | |
| 191 | + const runtimeLocalWorkspace = await makeRuntimeLocalWorkspaceState( | |
| 192 | + workspaceId, | |
| 193 | + accountId, | |
| 194 | + ); | |
| 146 | 195 | const hugeDocument = JSON.stringify({ |
| 147 | 196 | openapi: "3.0.3", |
| 148 | 197 | info: { |
| @@ -162,103 +211,83 @@ describe("source-inspection", () => { · 13 unmodified lines | ||
| 162 | 211 | ), |
| 163 | 212 | }); |
| 164 | 213 | |
| 165 | − await seedWorkspace({ | |
| 166 | − persistence, | |
| 214 | + const source = makeSource({ | |
| 167 | 215 | workspaceId, |
| 168 | − organizationId, | |
| 169 | − accountId, | |
| 216 | + sourceId, | |
| 170 | 217 | }); |
| 171 | − await Effect.runPromise(persistence.rows.sourceRecipes.upsert({ | |
| 172 | − id: recipeId, | |
| 173 | − kind: "http_api", | |
| 174 | − adapterKey: "openapi", | |
| 175 | − providerKey: "generic_http", | |
| 176 | − name: "Cloudflare API", | |
| 177 | − summary: null, | |
| 178 | − visibility: "workspace", | |
| 179 | − latestRevisionId: recipeRevisionId, | |
| 180 | − createdAt: 1000, | |
| 181 | − updatedAt: 1000, | |
| 182 | − })); | |
| 183 | − await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({ | |
| 184 | − id: recipeRevisionId, | |
| 185 | − recipeId, | |
| 186 | − revisionNumber: 1, | |
| 187 | − sourceConfigJson: JSON.stringify({ | |
| 188 | − kind: "openapi", | |
| 189 | − endpoint: "https://api.cloudflare.com/client/v4", | |
| 190 | − specUrl: "https://example.com/openapi.json", | |
| 191 | − }), | |
| 192 | − manifestJson: JSON.stringify({ | |
| 193 | − sourceHash: "manifest_hash", | |
| 218 | + await writeProjectLocalExecutorConfig({ | |
| 219 | + context: runtimeLocalWorkspace.context, | |
| 220 | + config: { | |
| 221 | + sources: { | |
| 222 | + [sourceId]: configSourceFromSource(source), | |
| 223 | + }, | |
| 224 | + }, | |
| 225 | + }); | |
| 226 | + await writeLocalSourceArtifact({ | |
| 227 | + context: runtimeLocalWorkspace.context, | |
| 228 | + sourceId, | |
| 229 | + artifact: buildLocalSourceArtifact({ | |
| 230 | + source: { | |
| 231 | + ...source, | |
| 232 | + sourceHash: "manifest_hash", | |
| 233 | + }, | |
| 234 | + materialization: { | |
| 235 | + manifestJson: JSON.stringify({ | |
| 236 | + sourceHash: "manifest_hash", | |
| 237 | + }), | |
| 238 | + manifestHash: "manifest_hash", | |
| 239 | + sourceHash: "manifest_hash", | |
| 240 | + documents: [{ | |
| 241 | + id: "src_recipe_doc_inspection", | |
| 242 | + recipeRevisionId, | |
| 243 | + documentKind: "openapi", | |
| 244 | + documentKey: "https://example.com/openapi.json", | |
| 245 | + contentText: hugeDocument, | |
| 246 | + contentHash: "doc_hash", | |
| 247 | + fetchedAt: 1000, | |
| 248 | + createdAt: 1000, | |
| 249 | + updatedAt: 1000, | |
| 250 | + }], | |
| 251 | + schemaBundles: [{ | |
| 252 | + id: SourceRecipeSchemaBundleIdSchema.make( | |
| 253 | + "src_recipe_bundle_inspection", | |
| 254 | + ), | |
| 255 | + recipeRevisionId, | |
| 256 | + bundleKind: "json_schema_ref_map", | |
| 257 | + refsJson: JSON.stringify({ | |
| 258 | + "#/components/schemas/Pagination": { | |
| 259 | + type: "object", | |
| 260 | + properties: { | |
| 261 | + page: { type: "number" }, | |
| 262 | + }, | |
| 263 | + }, | |
| 264 | + }), | |
| 265 | + contentHash: "bundle_hash", | |
| 266 | + createdAt: 1000, | |
| 267 | + updatedAt: 1000, | |
| 268 | + }], | |
| 269 | + operations: [makeOperation({ | |
| 270 | + recipeRevisionId, | |
| 271 | + })], | |
| 272 | + }, | |
| 194 | 273 | }), |
| 195 | − manifestHash: "manifest_hash", | |
| 196 | − materializationHash: "materialization_hash", | |
| 197 | − createdAt: 1000, | |
| 198 | − updatedAt: 1000, | |
| 199 | − })); | |
| 200 | − await Effect.runPromise(persistence.rows.sourceRecipeDocuments.replaceForRevision({ | |
| 201 | − recipeRevisionId, | |
| 202 | − documents: [{ | |
| 203 | − id: "src_recipe_doc_inspection", | |
| 204 | − recipeRevisionId, | |
| 205 | − documentKind: "openapi", | |
| 206 | − documentKey: "https://example.com/openapi.json", | |
| 207 | − contentText: hugeDocument, | |
| 208 | − contentHash: "doc_hash", | |
| 209 | − fetchedAt: 1000, | |
| 210 | − createdAt: 1000, | |
| 211 | − updatedAt: 1000, | |
| 212 | − }], | |
| 213 | − })); | |
| 214 | − await Effect.runPromise(persistence.rows.sourceRecipeSchemaBundles.replaceForRevision({ | |
| 215 | − recipeRevisionId, | |
| 216 | − bundles: [{ | |
| 217 | − id: SourceRecipeSchemaBundleIdSchema.make("src_recipe_bundle_inspection"), | |
| 218 | − recipeRevisionId, | |
| 219 | − bundleKind: "json_schema_ref_map", | |
| 220 | − refsJson: JSON.stringify({ | |
| 221 | − "#/components/schemas/Pagination": { | |
| 222 | − type: "object", | |
| 223 | − properties: { | |
| 224 | − page: { type: "number" }, | |
| 225 | − }, | |
| 226 | − }, | |
| 227 | − }), | |
| 228 | − contentHash: "bundle_hash", | |
| 229 | − createdAt: 1000, | |
| 230 | − updatedAt: 1000, | |
| 231 | − }], | |
| 232 | − })); | |
| 233 | − await Effect.runPromise(persistence.rows.sourceRecipeOperations.replaceForRevision({ | |
| 234 | − recipeRevisionId, | |
| 235 | − operations: [makeOperation()], | |
| 236 | − })); | |
| 237 | − await Effect.runPromise(persistence.rows.sources.insert({ | |
| 238 | − id: sourceId, | |
| 239 | − workspaceId, | |
| 240 | − recipeId, | |
| 241 | − recipeRevisionId, | |
| 242 | − name: "Cloudflare API", | |
| 243 | − kind: "openapi", | |
| 244 | − endpoint: "https://api.cloudflare.com/client/v4", | |
| 245 | − status: "connected", | |
| 246 | − enabled: true, | |
| 247 | − namespace: "cloudflare.api", | |
| 248 | − importAuthPolicy: "reuse_runtime", | |
| 249 | − bindingConfigJson: JSON.stringify({ | |
| 250 | − adapterKey: "openapi", | |
| 274 | + }); | |
| 275 | + await writeLocalWorkspaceState({ | |
| 276 | + context: runtimeLocalWorkspace.context, | |
| 277 | + state: { | |
| 251 | 278 | version: 1, |
| 252 | − payload: { | |
| 253 | − specUrl: "https://example.com/openapi.json", | |
| 254 | − defaultHeaders: null, | |
| 279 | + sources: { | |
| 280 | + [sourceId]: { | |
| 281 | + status: "connected", | |
| 282 | + lastError: null, | |
| 283 | + sourceHash: "manifest_hash", | |
| 284 | + createdAt: 1000, | |
| 285 | + updatedAt: 1000, | |
| 286 | + }, | |
| 255 | 287 | }, |
| 256 | − }), | |
| 257 | − sourceHash: null, | |
| 258 | − lastError: null, | |
| 259 | − createdAt: 1000, | |
| 260 | − updatedAt: 1000, | |
| 261 | − })); | |
| 288 | + policies: {}, | |
| 289 | + }, | |
| 290 | + }); | |
| 262 | 291 | |
| 263 | 292 | const inspection = await Effect.runPromise( |
| 264 | 293 | getSourceInspection({ |
| @@ -266,6 +295,10 @@ describe("source-inspection", () => { · 1 unmodified line | ||
| 266 | 295 | sourceId, |
| 267 | 296 | }).pipe( |
| 268 | 297 | Effect.provideService(ControlPlaneStore, persistence.rows), |
| 298 | + Effect.provideService( | |
| 299 | + RuntimeLocalWorkspaceService, | |
| 300 | + runtimeLocalWorkspace, | |
| 301 | + ), | |
| 269 | 302 | ), |
| 270 | 303 | ); |
| 271 | 304 | |
| @@ -282,6 +315,10 @@ describe("source-inspection", () => { · 10 unmodified lines | ||
| 282 | 315 | toolPath: "cloudflare.api.zones.listZones", |
| 283 | 316 | }).pipe( |
| 284 | 317 | Effect.provideService(ControlPlaneStore, persistence.rows), |
| 318 | + Effect.provideService( | |
| 319 | + RuntimeLocalWorkspaceService, | |
| 320 | + runtimeLocalWorkspace, | |
| 321 | + ), | |
| 285 | 322 | ), |
| 286 | 323 | ); |
| 287 | 324 | |
| @@ -300,6 +337,10 @@ describe("source-inspection", () => { · 12 unmodified lines | ||
| 300 | 337 | schemaBundleId: "src_recipe_bundle_inspection", |
| 301 | 338 | }).pipe( |
| 302 | 339 | Effect.provideService(ControlPlaneStore, persistence.rows), |
| 340 | + Effect.provideService( | |
| 341 | + RuntimeLocalWorkspaceService, | |
| 342 | + runtimeLocalWorkspace, | |
| 343 | + ), | |
| 303 | 344 | ), |
| 304 | 345 | ); |
| 305 | 346 | |
| @@ -11,7 +11,7 @@ import { · 10 unmodified lines | ||
| 11 | 11 | writeLocalSourceArtifact, |
| 12 | 12 | } from "./local-source-artifacts"; |
| 13 | 13 | import { |
| 14 | − getRuntimeLocalWorkspaceOption, | |
| 14 | + requireRuntimeLocalWorkspace, | |
| 15 | 15 | } from "./local-runtime-context"; |
| 16 | 16 | import { |
| 17 | 17 | loadLocalWorkspaceState, |
| @@ -29,7 +29,6 @@ import { · 11 unmodified lines | ||
| 29 | 29 | import type { |
| 30 | 30 | ResolveSecretMaterial as ResolveSourceSecretMaterial, |
| 31 | 31 | } from "./secret-material-providers"; |
| 32 | −import { persistRecipeMaterialization } from "./source-recipe-support"; | |
| 33 | 32 | |
| 34 | 33 | const shouldIndexSource = (source: Source): boolean => |
| 35 | 34 | source.enabled |
| @@ -43,71 +42,9 @@ export const syncSourceMaterialization = (input: { · 7 unmodified lines | ||
| 43 | 42 | resolveSecretMaterial: ResolveSourceSecretMaterial; |
| 44 | 43 | }): Effect.Effect<void, Error, never> => |
| 45 | 44 | Effect.gen(function* () { |
| 46 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 47 | − if ( | |
| 48 | − runtimeLocalWorkspace !== null | |
| 49 | − && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId | |
| 50 | − ) { | |
| 51 | − if (!shouldIndexSource(input.source)) { | |
| 52 | − const state = yield* Effect.tryPromise({ | |
| 53 | − try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context), | |
| 54 | − catch: (cause) => | |
| 55 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 56 | − }); | |
| 57 | − const existingSourceState = state.sources[input.source.id]; | |
| 58 | − const nextState: LocalWorkspaceState = { | |
| 59 | − ...state, | |
| 60 | − sources: { | |
| 61 | − ...state.sources, | |
| 62 | − [input.source.id]: { | |
| 63 | − status: (input.source.enabled ? input.source.status : "draft") as SourceStatus, | |
| 64 | − lastError: null, | |
| 65 | − sourceHash: input.source.sourceHash, | |
| 66 | − createdAt: existingSourceState?.createdAt ?? input.source.createdAt, | |
| 67 | − updatedAt: Date.now(), | |
| 68 | − }, | |
| 69 | − }, | |
| 70 | − }; | |
| 71 | − yield* Effect.tryPromise({ | |
| 72 | − try: () => | |
| 73 | − writeLocalWorkspaceState({ | |
| 74 | − context: runtimeLocalWorkspace.context, | |
| 75 | − state: nextState, | |
| 76 | − }), | |
| 77 | − catch: (cause) => | |
| 78 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 79 | − }); | |
| 80 | − return; | |
| 81 | − } | |
| 82 | − | |
| 83 | − const adapter = getSourceAdapterForSource(input.source); | |
| 84 | − const materialization = yield* adapter.materializeSource({ | |
| 85 | − source: input.source, | |
| 86 | − resolveSecretMaterial: input.resolveSecretMaterial, | |
| 87 | − resolveAuthMaterialForSlot: (slot) => | |
| 88 | − resolveSourceAuthMaterial({ | |
| 89 | − rows: input.rows, | |
| 90 | − source: input.source, | |
| 91 | − slot, | |
| 92 | − actorAccountId: input.actorAccountId, | |
| 93 | − resolveSecretMaterial: input.resolveSecretMaterial, | |
| 94 | − }), | |
| 95 | − }); | |
| 96 | − | |
| 97 | − yield* Effect.tryPromise({ | |
| 98 | − try: () => | |
| 99 | − writeLocalSourceArtifact({ | |
| 100 | − context: runtimeLocalWorkspace.context, | |
| 101 | − sourceId: input.source.id, | |
| 102 | − artifact: buildLocalSourceArtifact({ | |
| 103 | − source: input.source, | |
| 104 | − materialization, | |
| 105 | − }), | |
| 106 | − }), | |
| 107 | − catch: (cause) => | |
| 108 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 109 | − }); | |
| 45 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId); | |
| 110 | 46 | |
| 47 | + if (!shouldIndexSource(input.source)) { | |
| 111 | 48 | const state = yield* Effect.tryPromise({ |
| 112 | 49 | try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context), |
| 113 | 50 | catch: (cause) => |
| @@ -119,9 +56,9 @@ export const syncSourceMaterialization = (input: { · 5 unmodified lines | ||
| 119 | 56 | sources: { |
| 120 | 57 | ...state.sources, |
| 121 | 58 | [input.source.id]: { |
| 122 | − status: "connected", | |
| 59 | + status: (input.source.enabled ? input.source.status : "draft") as SourceStatus, | |
| 123 | 60 | lastError: null, |
| 124 | − sourceHash: materialization.sourceHash, | |
| 61 | + sourceHash: input.source.sourceHash, | |
| 125 | 62 | createdAt: existingSourceState?.createdAt ?? input.source.createdAt, |
| 126 | 63 | updatedAt: Date.now(), |
| 127 | 64 | }, |
| @@ -130,19 +67,15 @@ export const syncSourceMaterialization = (input: { · 2 unmodified lines | ||
| 130 | 67 | yield* Effect.tryPromise({ |
| 131 | 68 | try: () => |
| 132 | 69 | writeLocalWorkspaceState({ |
| 133 | − context: runtimeLocalWorkspace.context, | |
| 134 | − state: nextState, | |
| 135 | − }), | |
| 70 | + context: runtimeLocalWorkspace.context, | |
| 71 | + state: nextState, | |
| 72 | + }), | |
| 136 | 73 | catch: (cause) => |
| 137 | 74 | cause instanceof Error ? cause : new Error(String(cause)), |
| 138 | 75 | }); |
| 139 | 76 | return; |
| 140 | 77 | } |
| 141 | 78 | |
| 142 | − if (!shouldIndexSource(input.source)) { | |
| 143 | − return; | |
| 144 | − } | |
| 145 | − | |
| 146 | 79 | const adapter = getSourceAdapterForSource(input.source); |
| 147 | 80 | const materialization = yield* adapter.materializeSource({ |
| 148 | 81 | source: input.source, |
| @@ -156,10 +89,47 @@ export const syncSourceMaterialization = (input: { · 7 unmodified lines | ||
| 156 | 89 | resolveSecretMaterial: input.resolveSecretMaterial, |
| 157 | 90 | }), |
| 158 | 91 | }); |
| 159 | − yield* persistRecipeMaterialization({ | |
| 160 | − rows: input.rows, | |
| 161 | − source: input.source, | |
| 162 | − materialization, | |
| 92 | + yield* Effect.tryPromise({ | |
| 93 | + try: () => | |
| 94 | + writeLocalSourceArtifact({ | |
| 95 | + context: runtimeLocalWorkspace.context, | |
| 96 | + sourceId: input.source.id, | |
| 97 | + artifact: buildLocalSourceArtifact({ | |
| 98 | + source: input.source, | |
| 99 | + materialization, | |
| 100 | + }), | |
| 101 | + }), | |
| 102 | + catch: (cause) => | |
| 103 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 104 | + }); | |
| 105 | + | |
| 106 | + const state = yield* Effect.tryPromise({ | |
| 107 | + try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context), | |
| 108 | + catch: (cause) => | |
| 109 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 110 | + }); | |
| 111 | + const existingSourceState = state.sources[input.source.id]; | |
| 112 | + const nextState: LocalWorkspaceState = { | |
| 113 | + ...state, | |
| 114 | + sources: { | |
| 115 | + ...state.sources, | |
| 116 | + [input.source.id]: { | |
| 117 | + status: "connected", | |
| 118 | + lastError: null, | |
| 119 | + sourceHash: materialization.sourceHash, | |
| 120 | + createdAt: existingSourceState?.createdAt ?? input.source.createdAt, | |
| 121 | + updatedAt: Date.now(), | |
| 122 | + }, | |
| 123 | + }, | |
| 124 | + }; | |
| 125 | + yield* Effect.tryPromise({ | |
| 126 | + try: () => | |
| 127 | + writeLocalWorkspaceState({ | |
| 128 | + context: runtimeLocalWorkspace.context, | |
| 129 | + state: nextState, | |
| 130 | + }), | |
| 131 | + catch: (cause) => | |
| 132 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 163 | 133 | }); |
| 164 | 134 | }); |
| 165 | 135 | |
| @@ -171,32 +141,24 @@ export const persistMcpRecipeMaterializationFromManifest = (input: { · 5 unmodified lines | ||
| 171 | 141 | >[0]["manifestEntries"]; |
| 172 | 142 | }): Effect.Effect<void, Error, never> => |
| 173 | 143 | Effect.gen(function* () { |
| 174 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 175 | − if ( | |
| 176 | − runtimeLocalWorkspace !== null | |
| 177 | − && runtimeLocalWorkspace.installation.workspaceId === input.source.workspaceId | |
| 178 | − ) { | |
| 179 | − const materialization = materializationFromMcpManifestEntries({ | |
| 180 | − recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 181 | − endpoint: input.source.endpoint, | |
| 182 | − manifestEntries: input.manifestEntries, | |
| 183 | − }); | |
| 144 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.source.workspaceId); | |
| 145 | + const materialization = materializationFromMcpManifestEntries({ | |
| 146 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 147 | + endpoint: input.source.endpoint, | |
| 148 | + manifestEntries: input.manifestEntries, | |
| 149 | + }); | |
| 184 | 150 | |
| 185 | − yield* Effect.tryPromise({ | |
| 186 | − try: () => | |
| 187 | − writeLocalSourceArtifact({ | |
| 188 | − context: runtimeLocalWorkspace.context, | |
| 189 | − sourceId: input.source.id, | |
| 190 | − artifact: buildLocalSourceArtifact({ | |
| 191 | − source: input.source, | |
| 192 | − materialization, | |
| 193 | − }), | |
| 151 | + yield* Effect.tryPromise({ | |
| 152 | + try: () => | |
| 153 | + writeLocalSourceArtifact({ | |
| 154 | + context: runtimeLocalWorkspace.context, | |
| 155 | + sourceId: input.source.id, | |
| 156 | + artifact: buildLocalSourceArtifact({ | |
| 157 | + source: input.source, | |
| 158 | + materialization, | |
| 194 | 159 | }), |
| 195 | − catch: (cause) => | |
| 196 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 197 | − }); | |
| 198 | − return; | |
| 199 | − } | |
| 200 | − | |
| 201 | − return yield* persistMcpRecipeRevisionFromManifestEntries(input); | |
| 160 | + }), | |
| 161 | + catch: (cause) => | |
| 162 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 163 | + }); | |
| 202 | 164 | }); |
| @@ -1,3 +1,7 @@ | ||
| 1 | +import { mkdtempSync } from "node:fs"; | |
| 2 | +import { tmpdir } from "node:os"; | |
| 3 | +import { join } from "node:path"; | |
| 4 | + | |
| 1 | 5 | import { describe, expect, it } from "vitest"; |
| 2 | 6 | import * as Effect from "effect/Effect"; |
| 3 | 7 | import { |
| @@ -8,8 +12,8 @@ import { · 4 unmodified lines | ||
| 8 | 12 | |
| 9 | 13 | import { |
| 10 | 14 | AccountIdSchema, |
| 11 | − OrganizationIdSchema, | |
| 12 | 15 | SourceIdSchema, |
| 16 | + type LocalConfigSource, | |
| 13 | 17 | SourceRecipeIdSchema, |
| 14 | 18 | SourceRecipeRevisionIdSchema, |
| 15 | 19 | WorkspaceIdSchema, |
| @@ -21,6 +25,22 @@ import { · 5 unmodified lines | ||
| 21 | 25 | type SqlControlPlanePersistence, |
| 22 | 26 | } from "#persistence"; |
| 23 | 27 | |
| 28 | +import { | |
| 29 | + loadLocalExecutorConfig, | |
| 30 | + resolveLocalWorkspaceContext, | |
| 31 | + writeProjectLocalExecutorConfig, | |
| 32 | +} from "./local-config"; | |
| 33 | +import { | |
| 34 | + buildLocalSourceArtifact, | |
| 35 | + writeLocalSourceArtifact, | |
| 36 | +} from "./local-source-artifacts"; | |
| 37 | +import { | |
| 38 | + RuntimeLocalWorkspaceService, | |
| 39 | + type RuntimeLocalWorkspaceState, | |
| 40 | +} from "./local-runtime-context"; | |
| 41 | +import { | |
| 42 | + writeLocalWorkspaceState, | |
| 43 | +} from "./local-workspace-state"; | |
| 24 | 44 | import { |
| 25 | 45 | expandRecipeTools, |
| 26 | 46 | loadSourceWithRecipe, |
| @@ -38,6 +58,45 @@ const makePersistence = () => · 11 unmodified lines | ||
| 38 | 58 | }), |
| 39 | 59 | ); |
| 40 | 60 | |
| 61 | +const makeRuntimeLocalWorkspaceState = ( | |
| 62 | + workspaceId: ReturnType<typeof WorkspaceIdSchema.make>, | |
| 63 | +) => | |
| 64 | + Effect.runPromise( | |
| 65 | + Effect.gen(function* () { | |
| 66 | + const workspaceRoot = mkdtempSync( | |
| 67 | + join(tmpdir(), "executor-source-recipes-runtime-"), | |
| 68 | + ); | |
| 69 | + const context = yield* Effect.tryPromise({ | |
| 70 | + try: () => resolveLocalWorkspaceContext({ workspaceRoot }), | |
| 71 | + catch: (cause) => | |
| 72 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 73 | + }); | |
| 74 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 75 | + try: () => loadLocalExecutorConfig(context), | |
| 76 | + catch: (cause) => | |
| 77 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 78 | + }); | |
| 79 | + | |
| 80 | + return { | |
| 81 | + context, | |
| 82 | + installation: { | |
| 83 | + workspaceId, | |
| 84 | + accountId: AccountIdSchema.make("acc_local_source_recipes"), | |
| 85 | + organizationId: "org_local_personal" as never, | |
| 86 | + }, | |
| 87 | + loadedConfig, | |
| 88 | + } satisfies RuntimeLocalWorkspaceState; | |
| 89 | + }), | |
| 90 | + ); | |
| 91 | + | |
| 92 | +const withRuntimeLocalWorkspace = <A, E>( | |
| 93 | + effect: Effect.Effect<A, E, never>, | |
| 94 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 95 | +) => | |
| 96 | + effect.pipe( | |
| 97 | + Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 98 | + ); | |
| 99 | + | |
| 41 | 100 | const openApiBindingConfigJson = (specUrl: string): string => |
| 42 | 101 | JSON.stringify({ |
| 43 | 102 | adapterKey: "openapi", |
| @@ -221,30 +280,128 @@ const makeMcpOperation = ( · 177 unmodified lines | ||
| 221 | 280 | ...overrides, |
| 222 | 281 | }); |
| 223 | 282 | |
| 224 | −const seedWorkspace = async (input: { | |
| 225 | − persistence: SqlControlPlanePersistence; | |
| 226 | − workspaceId: ReturnType<typeof WorkspaceIdSchema.make>; | |
| 227 | − organizationId: ReturnType<typeof OrganizationIdSchema.make>; | |
| 228 | − accountId: ReturnType<typeof AccountIdSchema.make>; | |
| 283 | +const configSourceFromSource = (source: Source): LocalConfigSource => { | |
| 284 | + switch (source.kind) { | |
| 285 | + case "openapi": | |
| 286 | + return { | |
| 287 | + kind: "openapi" as const, | |
| 288 | + ...(source.name !== source.id ? { name: source.name } : {}), | |
| 289 | + ...(source.namespace && source.namespace !== source.id | |
| 290 | + ? { namespace: source.namespace } | |
| 291 | + : {}), | |
| 292 | + ...(source.enabled ? {} : { enabled: false }), | |
| 293 | + connection: { | |
| 294 | + endpoint: source.endpoint, | |
| 295 | + }, | |
| 296 | + binding: { | |
| 297 | + specUrl: | |
| 298 | + typeof source.binding.specUrl === "string" | |
| 299 | + ? source.binding.specUrl | |
| 300 | + : "https://example.com/openapi.json", | |
| 301 | + defaultHeaders: | |
| 302 | + (source.binding.defaultHeaders as Record<string, string> | null | undefined) | |
| 303 | + ?? null, | |
| 304 | + }, | |
| 305 | + }; | |
| 306 | + case "graphql": | |
| 307 | + return { | |
| 308 | + kind: "graphql" as const, | |
| 309 | + ...(source.name !== source.id ? { name: source.name } : {}), | |
| 310 | + ...(source.namespace && source.namespace !== source.id | |
| 311 | + ? { namespace: source.namespace } | |
| 312 | + : {}), | |
| 313 | + ...(source.enabled ? {} : { enabled: false }), | |
| 314 | + connection: { | |
| 315 | + endpoint: source.endpoint, | |
| 316 | + }, | |
| 317 | + binding: { | |
| 318 | + defaultHeaders: | |
| 319 | + (source.binding.defaultHeaders as Record<string, string> | null | undefined) | |
| 320 | + ?? null, | |
| 321 | + }, | |
| 322 | + }; | |
| 323 | + case "mcp": | |
| 324 | + return { | |
| 325 | + kind: "mcp" as const, | |
| 326 | + ...(source.name !== source.id ? { name: source.name } : {}), | |
| 327 | + ...(source.namespace && source.namespace !== source.id | |
| 328 | + ? { namespace: source.namespace } | |
| 329 | + : {}), | |
| 330 | + ...(source.enabled ? {} : { enabled: false }), | |
| 331 | + connection: { | |
| 332 | + endpoint: source.endpoint, | |
| 333 | + }, | |
| 334 | + binding: { | |
| 335 | + transport: | |
| 336 | + (source.binding.transport as "auto" | "streamable-http" | "sse" | null | undefined) | |
| 337 | + ?? null, | |
| 338 | + queryParams: | |
| 339 | + (source.binding.queryParams as Record<string, string> | null | undefined) | |
| 340 | + ?? null, | |
| 341 | + headers: | |
| 342 | + (source.binding.headers as Record<string, string> | null | undefined) | |
| 343 | + ?? null, | |
| 344 | + }, | |
| 345 | + }; | |
| 346 | + default: | |
| 347 | + throw new Error(`Unsupported source kind in test fixture: ${source.kind}`); | |
| 348 | + } | |
| 349 | +}; | |
| 350 | + | |
| 351 | +const seedLocalWorkspaceSources = async (input: { | |
| 352 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | |
| 353 | + fixtures: ReadonlyArray<{ | |
| 354 | + source: Source; | |
| 355 | + materialization: { | |
| 356 | + manifestJson: string | null; | |
| 357 | + manifestHash: string | null; | |
| 358 | + sourceHash: string | null; | |
| 359 | + documents: ReadonlyArray<any>; | |
| 360 | + schemaBundles: ReadonlyArray<any>; | |
| 361 | + operations: ReadonlyArray<StoredSourceRecipeOperationRecord>; | |
| 362 | + }; | |
| 363 | + }>; | |
| 229 | 364 | }) => { |
| 230 | − const now = 1000; | |
| 231 | − await Effect.runPromise(input.persistence.rows.organizations.insert({ | |
| 232 | − id: input.organizationId, | |
| 233 | − slug: `org-${input.organizationId}`, | |
| 234 | − name: `Org ${input.organizationId}`, | |
| 235 | − status: "active", | |
| 236 | − createdByAccountId: input.accountId, | |
| 237 | − createdAt: now, | |
| 238 | − updatedAt: now, | |
| 239 | − })); | |
| 240 | − await Effect.runPromise(input.persistence.rows.workspaces.insert({ | |
| 241 | − id: input.workspaceId, | |
| 242 | − organizationId: input.organizationId, | |
| 243 | − name: `Workspace ${input.workspaceId}`, | |
| 244 | − createdByAccountId: input.accountId, | |
| 245 | − createdAt: now, | |
| 246 | − updatedAt: now, | |
| 247 | − })); | |
| 365 | + await writeProjectLocalExecutorConfig({ | |
| 366 | + context: input.runtimeLocalWorkspace.context, | |
| 367 | + config: { | |
| 368 | + sources: Object.fromEntries( | |
| 369 | + input.fixtures.map(({ source }) => [source.id, configSourceFromSource(source)]), | |
| 370 | + ), | |
| 371 | + }, | |
| 372 | + }); | |
| 373 | + | |
| 374 | + await Promise.all( | |
| 375 | + input.fixtures.map(({ source, materialization }) => | |
| 376 | + writeLocalSourceArtifact({ | |
| 377 | + context: input.runtimeLocalWorkspace.context, | |
| 378 | + sourceId: source.id, | |
| 379 | + artifact: buildLocalSourceArtifact({ | |
| 380 | + source, | |
| 381 | + materialization, | |
| 382 | + }), | |
| 383 | + })), | |
| 384 | + ); | |
| 385 | + | |
| 386 | + await writeLocalWorkspaceState({ | |
| 387 | + context: input.runtimeLocalWorkspace.context, | |
| 388 | + state: { | |
| 389 | + version: 1, | |
| 390 | + sources: Object.fromEntries( | |
| 391 | + input.fixtures.map(({ source }) => [ | |
| 392 | + source.id, | |
| 393 | + { | |
| 394 | + status: source.status, | |
| 395 | + lastError: source.lastError, | |
| 396 | + sourceHash: source.sourceHash, | |
| 397 | + createdAt: source.createdAt, | |
| 398 | + updatedAt: source.updatedAt, | |
| 399 | + }, | |
| 400 | + ]), | |
| 401 | + ), | |
| 402 | + policies: {}, | |
| 403 | + }, | |
| 404 | + }); | |
| 248 | 405 | }; |
| 249 | 406 | |
| 250 | 407 | describe("source-recipes-runtime", () => { |
| @@ -432,10 +589,8 @@ describe("source-recipes-runtime", () => { · 181 unmodified lines | ||
| 432 | 589 | const persistence = await makePersistence(); |
| 433 | 590 | try { |
| 434 | 591 | const workspaceId = WorkspaceIdSchema.make("ws_shared_revision"); |
| 435 | − const organizationId = OrganizationIdSchema.make("org_shared_revision"); | |
| 436 | − const accountId = AccountIdSchema.make("acc_shared_revision"); | |
| 437 | − const recipeId = SourceRecipeIdSchema.make("src_recipe_shared_revision"); | |
| 438 | − const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_shared_revision"); | |
| 592 | + const runtimeLocalWorkspace = | |
| 593 | + await makeRuntimeLocalWorkspaceState(workspaceId); | |
| 439 | 594 | const openApiDocument = JSON.stringify({ |
| 440 | 595 | openapi: "3.0.3", |
| 441 | 596 | info: { |
| @@ -475,101 +630,68 @@ describe("source-recipes-runtime", () => { · 33 unmodified lines | ||
| 475 | 630 | definition, |
| 476 | 631 | }); |
| 477 | 632 | |
| 478 | − await seedWorkspace({ | |
| 479 | − persistence, | |
| 480 | − workspaceId, | |
| 481 | − organizationId, | |
| 482 | − accountId, | |
| 633 | + await seedLocalWorkspaceSources({ | |
| 634 | + runtimeLocalWorkspace, | |
| 635 | + fixtures: ["GitHub One", "GitHub Two"].map((name, index) => ({ | |
| 636 | + source: makeSource({ | |
| 637 | + id: SourceIdSchema.make(`src_shared_revision_${index}`), | |
| 638 | + workspaceId, | |
| 639 | + name, | |
| 640 | + sourceHash: manifest.sourceHash, | |
| 641 | + createdAt: 1000 + index, | |
| 642 | + updatedAt: 1000 + index, | |
| 643 | + }), | |
| 644 | + materialization: { | |
| 645 | + manifestJson: JSON.stringify(manifest), | |
| 646 | + manifestHash: manifest.sourceHash, | |
| 647 | + sourceHash: manifest.sourceHash, | |
| 648 | + documents: [{ | |
| 649 | + id: `src_recipe_doc_shared_revision_${index}`, | |
| 650 | + recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"), | |
| 651 | + documentKind: "openapi", | |
| 652 | + documentKey: "https://api.github.com/openapi.json", | |
| 653 | + contentText: openApiDocument, | |
| 654 | + contentHash: manifest.sourceHash, | |
| 655 | + fetchedAt: 1000, | |
| 656 | + createdAt: 1000, | |
| 657 | + updatedAt: 1000, | |
| 658 | + }], | |
| 659 | + schemaBundles: [], | |
| 660 | + operations: [makeOperation({ | |
| 661 | + id: `src_recipe_op_shared_revision_${index}`, | |
| 662 | + recipeRevisionId: SourceRecipeRevisionIdSchema.make("src_recipe_rev_materialization"), | |
| 663 | + operationKey: definition.toolId, | |
| 664 | + toolId: definition.toolId, | |
| 665 | + title: definition.name, | |
| 666 | + description: definition.description, | |
| 667 | + searchText: `${definition.toolId} ${definition.name}`.toLowerCase(), | |
| 668 | + inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 669 | + outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 670 | + providerDataJson: presentation.providerDataJson, | |
| 671 | + })], | |
| 672 | + }, | |
| 673 | + })), | |
| 483 | 674 | }); |
| 484 | − await Effect.runPromise(persistence.rows.sourceRecipes.upsert({ | |
| 485 | − id: recipeId, | |
| 486 | − kind: "http_api", | |
| 487 | − adapterKey: "openapi", | |
| 488 | − providerKey: "generic_http", | |
| 489 | − name: "GitHub", | |
| 490 | − summary: null, | |
| 491 | − visibility: "workspace", | |
| 492 | − latestRevisionId: recipeRevisionId, | |
| 493 | − createdAt: 1000, | |
| 494 | − updatedAt: 1000, | |
| 495 | − })); | |
| 496 | − await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({ | |
| 497 | − id: recipeRevisionId, | |
| 498 | − recipeId, | |
| 499 | − revisionNumber: 1, | |
| 500 | − sourceConfigJson: JSON.stringify({ | |
| 501 | − kind: "openapi", | |
| 502 | − endpoint: "https://api.github.com", | |
| 503 | − specUrl: "https://api.github.com/openapi.json", | |
| 504 | − defaultHeaders: null, | |
| 505 | − }), | |
| 506 | − manifestJson: JSON.stringify(manifest), | |
| 507 | − manifestHash: manifest.sourceHash, | |
| 508 | − materializationHash: manifest.sourceHash, | |
| 509 | − createdAt: 1000, | |
| 510 | − updatedAt: 1000, | |
| 511 | − })); | |
| 512 | − await Effect.runPromise(persistence.rows.sourceRecipeDocuments.replaceForRevision({ | |
| 513 | − recipeRevisionId, | |
| 514 | − documents: [{ | |
| 515 | − id: "src_recipe_doc_shared_revision", | |
| 516 | − recipeRevisionId, | |
| 517 | − documentKind: "openapi", | |
| 518 | − documentKey: "https://api.github.com/openapi.json", | |
| 519 | − contentText: openApiDocument, | |
| 520 | − contentHash: manifest.sourceHash, | |
| 521 | − fetchedAt: 1000, | |
| 522 | − createdAt: 1000, | |
| 523 | − updatedAt: 1000, | |
| 524 | − }], | |
| 525 | − })); | |
| 526 | − await Effect.runPromise(persistence.rows.sourceRecipeOperations.replaceForRevision({ | |
| 527 | − recipeRevisionId, | |
| 528 | − operations: [makeOperation({ | |
| 529 | − id: "src_recipe_op_shared_revision", | |
| 530 | − recipeRevisionId, | |
| 531 | − operationKey: definition.toolId, | |
| 532 | − toolId: definition.toolId, | |
| 533 | − title: definition.name, | |
| 534 | − description: definition.description, | |
| 535 | − searchText: `${definition.toolId} ${definition.name}`.toLowerCase(), | |
| 536 | − inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 537 | − outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 538 | − providerDataJson: presentation.providerDataJson, | |
| 539 | − })], | |
| 540 | − })); | |
| 541 | − | |
| 542 | − for (const [index, name] of ["GitHub One", "GitHub Two"].entries()) { | |
| 543 | − await Effect.runPromise(persistence.rows.sources.insert({ | |
| 544 | − id: SourceIdSchema.make(`src_shared_revision_${index}`), | |
| 545 | − workspaceId, | |
| 546 | − recipeId, | |
| 547 | − recipeRevisionId, | |
| 548 | − name, | |
| 549 | − kind: "openapi", | |
| 550 | − endpoint: "https://api.github.com", | |
| 551 | − status: "connected", | |
| 552 | − enabled: true, | |
| 553 | − namespace: "github", | |
| 554 | − importAuthPolicy: "reuse_runtime", | |
| 555 | − bindingConfigJson: openApiBindingConfigJson("https://api.github.com/openapi.json"), | |
| 556 | − sourceHash: manifest.sourceHash, | |
| 557 | − lastError: null, | |
| 558 | − createdAt: 1000 + index, | |
| 559 | − updatedAt: 1000 + index, | |
| 560 | − })); | |
| 561 | − } | |
| 562 | 675 | |
| 563 | − const recipes = await Effect.runPromise(loadWorkspaceSourceRecipes({ | |
| 564 | − rows: persistence.rows, | |
| 565 | − workspaceId, | |
| 566 | − })); | |
| 676 | + const recipes = await Effect.runPromise( | |
| 677 | + withRuntimeLocalWorkspace( | |
| 678 | + loadWorkspaceSourceRecipes({ | |
| 679 | + rows: persistence.rows, | |
| 680 | + workspaceId, | |
| 681 | + }), | |
| 682 | + runtimeLocalWorkspace, | |
| 683 | + ), | |
| 684 | + ); | |
| 567 | 685 | |
| 568 | 686 | expect(recipes).toHaveLength(2); |
| 569 | 687 | expect(recipes[0]?.documents).toHaveLength(1); |
| 570 | 688 | expect(recipes[0]?.operations).toHaveLength(1); |
| 571 | − expect(recipes[0]?.documents).toBe(recipes[1]?.documents); | |
| 572 | − expect(recipes[0]?.operations).toBe(recipes[1]?.operations); | |
| 689 | + expect(recipes[0]?.documents[0]?.contentText).toBe( | |
| 690 | + recipes[1]?.documents[0]?.contentText, | |
| 691 | + ); | |
| 692 | + expect(recipes[0]?.operations[0]?.providerDataJson).toBe( | |
| 693 | + recipes[1]?.operations[0]?.providerDataJson, | |
| 694 | + ); | |
| 573 | 695 | } finally { |
| 574 | 696 | await persistence.close(); |
| 575 | 697 | } |
| @@ -579,68 +701,44 @@ describe("source-recipes-runtime", () => { · 3 unmodified lines | ||
| 579 | 701 | const persistence = await makePersistence(); |
| 580 | 702 | try { |
| 581 | 703 | const workspaceId = WorkspaceIdSchema.make("ws_empty_recipe_rows"); |
| 582 | − const organizationId = OrganizationIdSchema.make("org_empty_recipe_rows"); | |
| 583 | − const accountId = AccountIdSchema.make("acc_empty_recipe_rows"); | |
| 584 | 704 | const sourceId = SourceIdSchema.make("src_empty_recipe_rows"); |
| 585 | − const recipeId = SourceRecipeIdSchema.make("src_recipe_empty_recipe_rows"); | |
| 586 | − const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_empty_recipe_rows"); | |
| 587 | − | |
| 588 | − await seedWorkspace({ | |
| 589 | − persistence, | |
| 590 | − workspaceId, | |
| 591 | − organizationId, | |
| 592 | − accountId, | |
| 705 | + const runtimeLocalWorkspace = | |
| 706 | + await makeRuntimeLocalWorkspaceState(workspaceId); | |
| 707 | + | |
| 708 | + await seedLocalWorkspaceSources({ | |
| 709 | + runtimeLocalWorkspace, | |
| 710 | + fixtures: [{ | |
| 711 | + source: makeSource({ | |
| 712 | + id: sourceId, | |
| 713 | + workspaceId, | |
| 714 | + name: "GraphQL Demo", | |
| 715 | + kind: "graphql", | |
| 716 | + endpoint: "https://example.com/graphql", | |
| 717 | + namespace: "graphql", | |
| 718 | + binding: { | |
| 719 | + defaultHeaders: null, | |
| 720 | + }, | |
| 721 | + }), | |
| 722 | + materialization: { | |
| 723 | + manifestJson: null, | |
| 724 | + manifestHash: null, | |
| 725 | + sourceHash: null, | |
| 726 | + documents: [], | |
| 727 | + schemaBundles: [], | |
| 728 | + operations: [], | |
| 729 | + }, | |
| 730 | + }], | |
| 593 | 731 | }); |
| 594 | − await Effect.runPromise(persistence.rows.sourceRecipes.upsert({ | |
| 595 | − id: recipeId, | |
| 596 | − kind: "http_api", | |
| 597 | − adapterKey: "graphql", | |
| 598 | − providerKey: "generic_graphql", | |
| 599 | − name: "GraphQL Demo", | |
| 600 | − summary: null, | |
| 601 | − visibility: "workspace", | |
| 602 | − latestRevisionId: recipeRevisionId, | |
| 603 | − createdAt: 1000, | |
| 604 | − updatedAt: 1000, | |
| 605 | − })); | |
| 606 | − await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({ | |
| 607 | − id: recipeRevisionId, | |
| 608 | − recipeId, | |
| 609 | − revisionNumber: 1, | |
| 610 | − sourceConfigJson: JSON.stringify({ | |
| 611 | − kind: "graphql", | |
| 612 | − endpoint: "https://example.com/graphql", | |
| 613 | − defaultHeaders: null, | |
| 614 | − }), | |
| 615 | − manifestJson: null, | |
| 616 | − manifestHash: null, | |
| 617 | − materializationHash: null, | |
| 618 | − createdAt: 1000, | |
| 619 | − updatedAt: 1000, | |
| 620 | − })); | |
| 621 | − await Effect.runPromise(persistence.rows.sources.insert({ | |
| 622 | − id: sourceId, | |
| 623 | − workspaceId, | |
| 624 | − recipeId, | |
| 625 | − recipeRevisionId, | |
| 626 | − name: "GraphQL Demo", | |
| 627 | − kind: "graphql", | |
| 628 | − endpoint: "https://example.com/graphql", | |
| 629 | − status: "connected", | |
| 630 | − enabled: true, | |
| 631 | − namespace: "graphql", | |
| 632 | − importAuthPolicy: "reuse_runtime", | |
| 633 | − bindingConfigJson: graphqlBindingConfigJson(), | |
| 634 | − sourceHash: null, | |
| 635 | − lastError: null, | |
| 636 | − createdAt: 1000, | |
| 637 | − updatedAt: 1000, | |
| 638 | − })); | |
| 639 | 732 | |
| 640 | − const recipes = await Effect.runPromise(loadWorkspaceSourceRecipes({ | |
| 641 | − rows: persistence.rows, | |
| 642 | − workspaceId, | |
| 643 | − })); | |
| 733 | + const recipes = await Effect.runPromise( | |
| 734 | + withRuntimeLocalWorkspace( | |
| 735 | + loadWorkspaceSourceRecipes({ | |
| 736 | + rows: persistence.rows, | |
| 737 | + workspaceId, | |
| 738 | + }), | |
| 739 | + runtimeLocalWorkspace, | |
| 740 | + ), | |
| 741 | + ); | |
| 644 | 742 | |
| 645 | 743 | expect(recipes).toHaveLength(1); |
| 646 | 744 | expect(recipes[0]?.documents).toEqual([]); |
| @@ -654,84 +752,112 @@ describe("source-recipes-runtime", () => { · 7 unmodified lines | ||
| 654 | 752 | it("fails clearly when loading a missing source, missing revision, or invalid manifest", async () => { |
| 655 | 753 | const persistence = await makePersistence(); |
| 656 | 754 | try { |
| 657 | − await expect(Effect.runPromise(loadSourceWithRecipe({ | |
| 658 | − rows: persistence.rows, | |
| 659 | − workspaceId: WorkspaceIdSchema.make("ws_missing_source"), | |
| 660 | − sourceId: SourceIdSchema.make("src_missing_source"), | |
| 661 | − }))).rejects.toThrow("Source not found"); | |
| 755 | + const missingWorkspaceId = WorkspaceIdSchema.make("ws_missing_source"); | |
| 756 | + const missingRuntimeLocalWorkspace = | |
| 757 | + await makeRuntimeLocalWorkspaceState(missingWorkspaceId); | |
| 758 | + | |
| 759 | + await expect( | |
| 760 | + Effect.runPromise( | |
| 761 | + withRuntimeLocalWorkspace( | |
| 762 | + loadSourceWithRecipe({ | |
| 763 | + rows: persistence.rows, | |
| 764 | + workspaceId: missingWorkspaceId, | |
| 765 | + sourceId: SourceIdSchema.make("src_missing_source"), | |
| 766 | + }), | |
| 767 | + missingRuntimeLocalWorkspace, | |
| 768 | + ), | |
| 769 | + ), | |
| 770 | + ).rejects.toThrow("Source not found"); | |
| 662 | 771 | |
| 663 | 772 | const workspaceId = WorkspaceIdSchema.make("ws_bad_recipe_runtime"); |
| 664 | − const organizationId = OrganizationIdSchema.make("org_bad_recipe_runtime"); | |
| 665 | − const accountId = AccountIdSchema.make("acc_bad_recipe_runtime"); | |
| 666 | 773 | const sourceId = SourceIdSchema.make("src_bad_recipe_runtime"); |
| 667 | − const recipeId = SourceRecipeIdSchema.make("src_recipe_bad_recipe_runtime"); | |
| 668 | − const recipeRevisionId = SourceRecipeRevisionIdSchema.make("src_recipe_rev_bad_recipe_runtime"); | |
| 669 | − | |
| 670 | − await seedWorkspace({ | |
| 671 | − persistence, | |
| 672 | − workspaceId, | |
| 673 | − organizationId, | |
| 674 | − accountId, | |
| 774 | + const runtimeLocalWorkspace = | |
| 775 | + await makeRuntimeLocalWorkspaceState(workspaceId); | |
| 776 | + | |
| 777 | + await writeProjectLocalExecutorConfig({ | |
| 778 | + context: runtimeLocalWorkspace.context, | |
| 779 | + config: { | |
| 780 | + sources: { | |
| 781 | + [sourceId]: configSourceFromSource( | |
| 782 | + makeSource({ | |
| 783 | + id: sourceId, | |
| 784 | + workspaceId, | |
| 785 | + name: "Broken GitHub", | |
| 786 | + }), | |
| 787 | + ), | |
| 788 | + }, | |
| 789 | + }, | |
| 675 | 790 | }); |
| 676 | − await Effect.runPromise(persistence.rows.sourceRecipes.upsert({ | |
| 677 | − id: recipeId, | |
| 678 | − kind: "http_api", | |
| 679 | − adapterKey: "openapi", | |
| 680 | − providerKey: "generic_http", | |
| 681 | − name: "Broken GitHub", | |
| 682 | − summary: null, | |
| 683 | − visibility: "workspace", | |
| 684 | − latestRevisionId: recipeRevisionId, | |
| 685 | − createdAt: 1000, | |
| 686 | − updatedAt: 1000, | |
| 687 | − })); | |
| 688 | − await Effect.runPromise(persistence.rows.sources.insert({ | |
| 791 | + await writeLocalWorkspaceState({ | |
| 792 | + context: runtimeLocalWorkspace.context, | |
| 793 | + state: { | |
| 794 | + version: 1, | |
| 795 | + sources: { | |
| 796 | + [sourceId]: { | |
| 797 | + status: "connected", | |
| 798 | + lastError: null, | |
| 799 | + sourceHash: null, | |
| 800 | + createdAt: 1000, | |
| 801 | + updatedAt: 1000, | |
| 802 | + }, | |
| 803 | + }, | |
| 804 | + policies: {}, | |
| 805 | + }, | |
| 806 | + }); | |
| 807 | + | |
| 808 | + await expect( | |
| 809 | + Effect.runPromise( | |
| 810 | + withRuntimeLocalWorkspace( | |
| 811 | + loadSourceWithRecipe({ | |
| 812 | + rows: persistence.rows, | |
| 813 | + workspaceId, | |
| 814 | + sourceId, | |
| 815 | + }), | |
| 816 | + runtimeLocalWorkspace, | |
| 817 | + ), | |
| 818 | + ), | |
| 819 | + ).rejects.toThrow("Recipe artifact missing"); | |
| 820 | + | |
| 821 | + const invalidManifestSource = makeSource({ | |
| 689 | 822 | id: sourceId, |
| 690 | 823 | workspaceId, |
| 691 | − recipeId, | |
| 692 | − recipeRevisionId, | |
| 693 | 824 | name: "Broken GitHub", |
| 694 | − kind: "openapi", | |
| 695 | − endpoint: "https://api.github.com", | |
| 696 | − status: "connected", | |
| 697 | − enabled: true, | |
| 698 | − namespace: "github", | |
| 699 | − importAuthPolicy: "reuse_runtime", | |
| 700 | − bindingConfigJson: openApiBindingConfigJson("https://api.github.com/openapi.json"), | |
| 701 | − sourceHash: null, | |
| 702 | − lastError: null, | |
| 703 | − createdAt: 1000, | |
| 704 | − updatedAt: 1000, | |
| 705 | − })); | |
| 706 | − | |
| 707 | − await expect(Effect.runPromise(loadSourceWithRecipe({ | |
| 708 | − rows: persistence.rows, | |
| 709 | − workspaceId, | |
| 825 | + }); | |
| 826 | + const invalidArtifact = buildLocalSourceArtifact({ | |
| 827 | + source: invalidManifestSource, | |
| 828 | + materialization: { | |
| 829 | + manifestJson: "{}", | |
| 830 | + manifestHash: "manifest_hash_invalid", | |
| 831 | + sourceHash: "manifest_hash_invalid", | |
| 832 | + documents: [], | |
| 833 | + schemaBundles: [], | |
| 834 | + operations: [], | |
| 835 | + }, | |
| 836 | + }); | |
| 837 | + await writeLocalSourceArtifact({ | |
| 838 | + context: runtimeLocalWorkspace.context, | |
| 710 | 839 | sourceId, |
| 711 | − }))).rejects.toThrow("Recipe revision missing"); | |
| 712 | − | |
| 713 | − await Effect.runPromise(persistence.rows.sourceRecipeRevisions.upsert({ | |
| 714 | − id: recipeRevisionId, | |
| 715 | − recipeId, | |
| 716 | − revisionNumber: 1, | |
| 717 | − sourceConfigJson: JSON.stringify({ | |
| 718 | − kind: "openapi", | |
| 719 | − endpoint: "https://api.github.com", | |
| 720 | − specUrl: "https://api.github.com/openapi.json", | |
| 721 | − defaultHeaders: null, | |
| 722 | − }), | |
| 723 | − manifestJson: "{bad-json", | |
| 724 | − manifestHash: null, | |
| 725 | − materializationHash: null, | |
| 726 | − createdAt: 1000, | |
| 727 | − updatedAt: 1000, | |
| 728 | − })); | |
| 840 | + artifact: { | |
| 841 | + ...invalidArtifact, | |
| 842 | + revision: { | |
| 843 | + ...invalidArtifact.revision, | |
| 844 | + manifestJson: "{bad-json", | |
| 845 | + }, | |
| 846 | + }, | |
| 847 | + }); | |
| 729 | 848 | |
| 730 | − await expect(Effect.runPromise(loadSourceWithRecipe({ | |
| 731 | − rows: persistence.rows, | |
| 732 | − workspaceId, | |
| 733 | − sourceId, | |
| 734 | − }))).rejects.toThrow(`Invalid OpenAPI manifest for ${sourceId}`); | |
| 849 | + await expect( | |
| 850 | + Effect.runPromise( | |
| 851 | + withRuntimeLocalWorkspace( | |
| 852 | + loadSourceWithRecipe({ | |
| 853 | + rows: persistence.rows, | |
| 854 | + workspaceId, | |
| 855 | + sourceId, | |
| 856 | + }), | |
| 857 | + runtimeLocalWorkspace, | |
| 858 | + ), | |
| 859 | + ), | |
| 860 | + ).rejects.toThrow(`Invalid OpenAPI manifest for ${sourceId}`); | |
| 735 | 861 | } finally { |
| 736 | 862 | await persistence.close(); |
| 737 | 863 | } |
| @@ -13,13 +13,12 @@ import type { · 12 unmodified lines | ||
| 13 | 13 | WorkspaceId, |
| 14 | 14 | } from "#schema"; |
| 15 | 15 | import * as Effect from "effect/Effect"; |
| 16 | −import * as Option from "effect/Option"; | |
| 17 | 16 | |
| 18 | 17 | import { |
| 19 | 18 | readLocalSourceArtifact, |
| 20 | 19 | } from "./local-source-artifacts"; |
| 21 | 20 | import { |
| 22 | − getRuntimeLocalWorkspaceOption, | |
| 21 | + requireRuntimeLocalWorkspace, | |
| 23 | 22 | } from "./local-runtime-context"; |
| 24 | 23 | import { namespaceFromSourceName } from "./source-names"; |
| 25 | 24 | import { |
| @@ -162,133 +161,61 @@ export const loadWorkspaceSourceRecipes = (input: { · 136 unmodified lines | ||
| 162 | 161 | actorAccountId?: AccountId | null; |
| 163 | 162 | }): Effect.Effect<readonly LoadedSourceRecipe[], Error, never> => |
| 164 | 163 | Effect.gen(function* () { |
| 165 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 166 | − if ( | |
| 167 | − runtimeLocalWorkspace !== null | |
| 168 | − && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId | |
| 169 | − ) { | |
| 170 | − const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, { | |
| 171 | − actorAccountId: input.actorAccountId, | |
| 172 | − }); | |
| 173 | − | |
| 174 | − const localRecipes = yield* Effect.forEach(sources, (source) => | |
| 175 | − Effect.gen(function* () { | |
| 176 | − const artifact = yield* Effect.tryPromise({ | |
| 177 | − try: () => | |
| 178 | − readLocalSourceArtifact({ | |
| 179 | − context: runtimeLocalWorkspace.context, | |
| 180 | − sourceId: source.id, | |
| 181 | − }), | |
| 182 | − catch: (cause) => | |
| 183 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 184 | − }); | |
| 185 | − if (artifact === null) { | |
| 186 | − return null; | |
| 187 | − } | |
| 188 | − | |
| 189 | − const sourceRecord: StoredSourceRecord = { | |
| 190 | − id: source.id, | |
| 191 | − workspaceId: source.workspaceId, | |
| 192 | − recipeId: artifact.recipeId, | |
| 193 | − recipeRevisionId: artifact.revision.id, | |
| 194 | − name: source.name, | |
| 195 | − kind: source.kind, | |
| 196 | − endpoint: source.endpoint, | |
| 197 | − status: source.status, | |
| 198 | − enabled: source.enabled, | |
| 199 | − namespace: source.namespace, | |
| 200 | − importAuthPolicy: source.importAuthPolicy, | |
| 201 | − bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source), | |
| 202 | − sourceHash: source.sourceHash, | |
| 203 | − lastError: source.lastError, | |
| 204 | − createdAt: source.createdAt, | |
| 205 | − updatedAt: source.updatedAt, | |
| 206 | − }; | |
| 207 | − const manifest = yield* parseManifestForRecipe({ | |
| 208 | − source, | |
| 209 | − revision: artifact.revision, | |
| 210 | − }); | |
| 211 | − | |
| 212 | − return { | |
| 213 | − source, | |
| 214 | − sourceRecord, | |
| 215 | − revision: artifact.revision, | |
| 216 | − documents: artifact.documents, | |
| 217 | − schemaBundles: artifact.schemaBundles, | |
| 218 | − operations: artifact.operations, | |
| 219 | − manifest, | |
| 220 | − } satisfies LoadedSourceRecipe; | |
| 221 | − }), | |
| 222 | − ); | |
| 223 | − | |
| 224 | − return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null); | |
| 225 | − } | |
| 226 | − | |
| 227 | − const sourceRecords = yield* input.rows.sources.listByWorkspaceId(input.workspaceId); | |
| 164 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId); | |
| 228 | 165 | const sources = yield* loadSourcesInWorkspace(input.rows, input.workspaceId, { |
| 229 | 166 | actorAccountId: input.actorAccountId, |
| 230 | 167 | }); |
| 231 | − | |
| 232 | − const sourceById = new Map(sources.map((source) => [source.id, source])); | |
| 233 | − const relevantSourceRecords = sourceRecords.filter((sourceRecord) => sourceById.has(sourceRecord.id)); | |
| 234 | − const revisionIds = [...new Set(relevantSourceRecords.map((sourceRecord) => sourceRecord.recipeRevisionId))]; | |
| 235 | − | |
| 236 | − const revisions = yield* input.rows.sourceRecipeRevisions.listByIds(revisionIds); | |
| 237 | − const revisionById = new Map(revisions.map((revision) => [revision.id, revision])); | |
| 238 | − const documents = yield* input.rows.sourceRecipeDocuments.listByRevisionIds(revisionIds); | |
| 239 | − const schemaBundles = yield* input.rows.sourceRecipeSchemaBundles.listByRevisionIds(revisionIds); | |
| 240 | − const documentsByRevisionId = new Map<string, StoredSourceRecipeDocumentRecord[]>(); | |
| 241 | − for (const document of documents) { | |
| 242 | − const existing = documentsByRevisionId.get(document.recipeRevisionId) ?? []; | |
| 243 | − existing.push(document); | |
| 244 | − documentsByRevisionId.set(document.recipeRevisionId, existing); | |
| 245 | − } | |
| 246 | − const schemaBundlesByRevisionId = new Map<string, StoredSourceRecipeSchemaBundleRecord[]>(); | |
| 247 | − for (const schemaBundle of schemaBundles) { | |
| 248 | − const existing = schemaBundlesByRevisionId.get(schemaBundle.recipeRevisionId) ?? []; | |
| 249 | − existing.push(schemaBundle); | |
| 250 | − schemaBundlesByRevisionId.set(schemaBundle.recipeRevisionId, existing); | |
| 251 | − } | |
| 252 | − const operations = yield* input.rows.sourceRecipeOperations.listByRevisionIds(revisionIds); | |
| 253 | − const operationsByRevisionId = new Map<string, StoredSourceRecipeOperationRecord[]>(); | |
| 254 | − for (const operation of operations) { | |
| 255 | − const existing = operationsByRevisionId.get(operation.recipeRevisionId) ?? []; | |
| 256 | − existing.push(operation); | |
| 257 | − operationsByRevisionId.set(operation.recipeRevisionId, existing); | |
| 258 | − } | |
| 259 | − | |
| 260 | − return yield* Effect.forEach(relevantSourceRecords, (sourceRecord) => | |
| 168 | + const localRecipes = yield* Effect.forEach(sources, (source) => | |
| 261 | 169 | Effect.gen(function* () { |
| 262 | − const source = sourceById.get(sourceRecord.id); | |
| 263 | − if (!source) { | |
| 264 | − return yield* Effect.fail( | |
| 265 | − new Error(`Projected source missing for ${sourceRecord.id}`), | |
| 266 | − ); | |
| 170 | + const artifact = yield* Effect.tryPromise({ | |
| 171 | + try: () => | |
| 172 | + readLocalSourceArtifact({ | |
| 173 | + context: runtimeLocalWorkspace.context, | |
| 174 | + sourceId: source.id, | |
| 175 | + }), | |
| 176 | + catch: (cause) => | |
| 177 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 178 | + }); | |
| 179 | + if (artifact === null) { | |
| 180 | + return null; | |
| 267 | 181 | } |
| 268 | 182 | |
| 269 | − const revision = revisionById.get(sourceRecord.recipeRevisionId); | |
| 270 | − if (!revision) { | |
| 271 | − return yield* Effect.fail( | |
| 272 | − new Error(`Recipe revision missing for source ${sourceRecord.id}`), | |
| 273 | − ); | |
| 274 | − } | |
| 183 | + const sourceRecord: StoredSourceRecord = { | |
| 184 | + id: source.id, | |
| 185 | + workspaceId: source.workspaceId, | |
| 186 | + recipeId: artifact.recipeId, | |
| 187 | + recipeRevisionId: artifact.revision.id, | |
| 188 | + name: source.name, | |
| 189 | + kind: source.kind, | |
| 190 | + endpoint: source.endpoint, | |
| 191 | + status: source.status, | |
| 192 | + enabled: source.enabled, | |
| 193 | + namespace: source.namespace, | |
| 194 | + importAuthPolicy: source.importAuthPolicy, | |
| 195 | + bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source), | |
| 196 | + sourceHash: source.sourceHash, | |
| 197 | + lastError: source.lastError, | |
| 198 | + createdAt: source.createdAt, | |
| 199 | + updatedAt: source.updatedAt, | |
| 200 | + }; | |
| 275 | 201 | |
| 276 | 202 | const manifest = yield* parseManifestForRecipe({ |
| 277 | 203 | source, |
| 278 | − revision, | |
| 204 | + revision: artifact.revision, | |
| 279 | 205 | }); |
| 280 | 206 | |
| 281 | 207 | return { |
| 282 | 208 | source, |
| 283 | 209 | sourceRecord, |
| 284 | − revision, | |
| 285 | − documents: documentsByRevisionId.get(sourceRecord.recipeRevisionId) ?? [], | |
| 286 | − schemaBundles: schemaBundlesByRevisionId.get(sourceRecord.recipeRevisionId) ?? [], | |
| 287 | − operations: operationsByRevisionId.get(sourceRecord.recipeRevisionId) ?? [], | |
| 210 | + revision: artifact.revision, | |
| 211 | + documents: artifact.documents, | |
| 212 | + schemaBundles: artifact.schemaBundles, | |
| 213 | + operations: artifact.operations, | |
| 288 | 214 | manifest, |
| 289 | 215 | } satisfies LoadedSourceRecipe; |
| 290 | 216 | }), |
| 291 | 217 | ); |
| 218 | + return localRecipes.filter((recipe): recipe is LoadedSourceRecipe => recipe !== null); | |
| 292 | 219 | }); |
| 293 | 220 | |
| 294 | 221 | export const loadSourceWithRecipe = (input: { |
| @@ -298,104 +225,57 @@ export const loadSourceWithRecipe = (input: { · 3 unmodified lines | ||
| 298 | 225 | actorAccountId?: AccountId | null; |
| 299 | 226 | }): Effect.Effect<LoadedSourceRecipe, Error, never> => |
| 300 | 227 | Effect.gen(function* () { |
| 301 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 302 | − if ( | |
| 303 | − runtimeLocalWorkspace !== null | |
| 304 | − && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId | |
| 305 | − ) { | |
| 306 | − const source = yield* loadSourceById(input.rows, { | |
| 307 | − workspaceId: input.workspaceId, | |
| 308 | − sourceId: input.sourceId, | |
| 309 | − actorAccountId: input.actorAccountId, | |
| 310 | − }); | |
| 311 | − | |
| 312 | − const artifact = yield* Effect.tryPromise({ | |
| 313 | − try: () => | |
| 314 | − readLocalSourceArtifact({ | |
| 315 | − context: runtimeLocalWorkspace.context, | |
| 316 | − sourceId: source.id, | |
| 317 | − }), | |
| 318 | − catch: (cause) => | |
| 319 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 320 | − }); | |
| 321 | − if (artifact === null) { | |
| 322 | − return yield* Effect.fail( | |
| 323 | − new Error(`Recipe artifact missing for source ${input.sourceId}`), | |
| 324 | − ); | |
| 325 | − } | |
| 326 | − | |
| 327 | − const sourceRecord: StoredSourceRecord = { | |
| 328 | − id: source.id, | |
| 329 | − workspaceId: source.workspaceId, | |
| 330 | − recipeId: artifact.recipeId, | |
| 331 | − recipeRevisionId: artifact.revision.id, | |
| 332 | − name: source.name, | |
| 333 | − kind: source.kind, | |
| 334 | − endpoint: source.endpoint, | |
| 335 | − status: source.status, | |
| 336 | − enabled: source.enabled, | |
| 337 | − namespace: source.namespace, | |
| 338 | − importAuthPolicy: source.importAuthPolicy, | |
| 339 | − bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source), | |
| 340 | − sourceHash: source.sourceHash, | |
| 341 | − lastError: source.lastError, | |
| 342 | − createdAt: source.createdAt, | |
| 343 | − updatedAt: source.updatedAt, | |
| 344 | − }; | |
| 345 | − const manifest = yield* parseManifestForRecipe({ | |
| 346 | − source, | |
| 347 | − revision: artifact.revision, | |
| 348 | − }); | |
| 349 | − | |
| 350 | − return { | |
| 351 | − source, | |
| 352 | − sourceRecord, | |
| 353 | − revision: artifact.revision, | |
| 354 | − documents: artifact.documents, | |
| 355 | − schemaBundles: artifact.schemaBundles, | |
| 356 | − operations: artifact.operations, | |
| 357 | − manifest, | |
| 358 | − } satisfies LoadedSourceRecipe; | |
| 359 | − } | |
| 360 | − | |
| 361 | − const sourceRecord = yield* input.rows.sources.getByWorkspaceAndId( | |
| 362 | − input.workspaceId, | |
| 363 | − input.sourceId, | |
| 364 | − ); | |
| 365 | − if (Option.isNone(sourceRecord)) { | |
| 366 | − return yield* Effect.fail( | |
| 367 | − new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`), | |
| 368 | − ); | |
| 369 | − } | |
| 370 | − | |
| 228 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(input.workspaceId); | |
| 371 | 229 | const source = yield* loadSourceById(input.rows, { |
| 372 | 230 | workspaceId: input.workspaceId, |
| 373 | 231 | sourceId: input.sourceId, |
| 374 | 232 | actorAccountId: input.actorAccountId, |
| 375 | 233 | }); |
| 376 | − const revision = yield* input.rows.sourceRecipeRevisions.getById(sourceRecord.value.recipeRevisionId); | |
| 377 | − if (Option.isNone(revision)) { | |
| 234 | + const artifact = yield* Effect.tryPromise({ | |
| 235 | + try: () => | |
| 236 | + readLocalSourceArtifact({ | |
| 237 | + context: runtimeLocalWorkspace.context, | |
| 238 | + sourceId: source.id, | |
| 239 | + }), | |
| 240 | + catch: (cause) => | |
| 241 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 242 | + }); | |
| 243 | + if (artifact === null) { | |
| 378 | 244 | return yield* Effect.fail( |
| 379 | − new Error(`Recipe revision missing for source ${input.sourceId}`), | |
| 245 | + new Error(`Recipe artifact missing for source ${input.sourceId}`), | |
| 380 | 246 | ); |
| 381 | 247 | } |
| 382 | − const [documents, schemaBundles, operations, manifest] = yield* Effect.all([ | |
| 383 | − input.rows.sourceRecipeDocuments.listByRevisionId(sourceRecord.value.recipeRevisionId), | |
| 384 | − input.rows.sourceRecipeSchemaBundles.listByRevisionId(sourceRecord.value.recipeRevisionId), | |
| 385 | − input.rows.sourceRecipeOperations.listByRevisionId(sourceRecord.value.recipeRevisionId), | |
| 386 | − parseManifestForRecipe({ | |
| 387 | − source, | |
| 388 | − revision: revision.value, | |
| 389 | − }), | |
| 390 | − ]); | |
| 248 | + | |
| 249 | + const sourceRecord: StoredSourceRecord = { | |
| 250 | + id: source.id, | |
| 251 | + workspaceId: source.workspaceId, | |
| 252 | + recipeId: artifact.recipeId, | |
| 253 | + recipeRevisionId: artifact.revision.id, | |
| 254 | + name: source.name, | |
| 255 | + kind: source.kind, | |
| 256 | + endpoint: source.endpoint, | |
| 257 | + status: source.status, | |
| 258 | + enabled: source.enabled, | |
| 259 | + namespace: source.namespace, | |
| 260 | + importAuthPolicy: source.importAuthPolicy, | |
| 261 | + bindingConfigJson: getSourceAdapterForSource(source).serializeBindingConfig(source), | |
| 262 | + sourceHash: source.sourceHash, | |
| 263 | + lastError: source.lastError, | |
| 264 | + createdAt: source.createdAt, | |
| 265 | + updatedAt: source.updatedAt, | |
| 266 | + }; | |
| 267 | + const manifest = yield* parseManifestForRecipe({ | |
| 268 | + source, | |
| 269 | + revision: artifact.revision, | |
| 270 | + }); | |
| 391 | 271 | |
| 392 | 272 | return { |
| 393 | 273 | source, |
| 394 | − sourceRecord: sourceRecord.value, | |
| 395 | − revision: revision.value, | |
| 396 | − documents, | |
| 397 | − schemaBundles, | |
| 398 | − operations, | |
| 274 | + sourceRecord, | |
| 275 | + revision: artifact.revision, | |
| 276 | + documents: artifact.documents, | |
| 277 | + schemaBundles: artifact.schemaBundles, | |
| 278 | + operations: artifact.operations, | |
| 399 | 279 | manifest, |
| 400 | 280 | } satisfies LoadedSourceRecipe; |
| 401 | 281 | }); |
| @@ -1,10 +1,13 @@ | ||
| 1 | +import { mkdtempSync } from "node:fs"; | |
| 2 | +import { tmpdir } from "node:os"; | |
| 3 | +import { join } from "node:path"; | |
| 4 | + | |
| 1 | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 6 | |
| 3 | 7 | import { |
| 4 | 8 | AccountIdSchema, |
| 5 | 9 | decodeBuiltInAuthArtifactConfig, |
| 6 | 10 | McpSourceAuthSessionDataJsonSchema, |
| 7 | − OrganizationIdSchema, | |
| 8 | 11 | SecretMaterialIdSchema, |
| 9 | 12 | SourceAuthSessionIdSchema, |
| 10 | 13 | SourceIdSchema, |
| @@ -21,9 +24,16 @@ import { · 10 unmodified lines | ||
| 21 | 24 | type SqlControlPlanePersistence, |
| 22 | 25 | } from "../persistence"; |
| 23 | 26 | import { |
| 24 | − stableSourceRecipeId, | |
| 25 | − stableSourceRecipeRevisionId, | |
| 26 | −} from "./source-definitions"; | |
| 27 | + loadLocalExecutorConfig, | |
| 28 | + resolveLocalWorkspaceContext, | |
| 29 | +} from "./local-config"; | |
| 30 | +import { | |
| 31 | + RuntimeLocalWorkspaceService, | |
| 32 | + type RuntimeLocalWorkspaceState, | |
| 33 | +} from "./local-runtime-context"; | |
| 34 | +import { | |
| 35 | + loadLocalWorkspaceState, | |
| 36 | +} from "./local-workspace-state"; | |
| 27 | 37 | import { persistSource, removeSourceById } from "./source-store"; |
| 28 | 38 | |
| 29 | 39 | const makePersistence: Effect.Effect<SqlControlPlanePersistence, unknown, Scope.Scope> = |
| @@ -40,6 +50,42 @@ const makePersistence: Effect.Effect<SqlControlPlanePersistence, unknown, Scope. · 10 unmodified lines | ||
| 40 | 50 | |
| 41 | 51 | const encodeSessionDataJson = Schema.encodeSync(McpSourceAuthSessionDataJsonSchema); |
| 42 | 52 | |
| 53 | +const makeRuntimeLocalWorkspace = (input: { | |
| 54 | + workspaceId: Source["workspaceId"]; | |
| 55 | + accountId: ReturnType<typeof AccountIdSchema.make>; | |
| 56 | +}) => | |
| 57 | + Effect.gen(function* () { | |
| 58 | + const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-source-store-")); | |
| 59 | + const context = yield* Effect.tryPromise({ | |
| 60 | + try: () => resolveLocalWorkspaceContext({ workspaceRoot }), | |
| 61 | + catch: (cause) => | |
| 62 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 63 | + }); | |
| 64 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 65 | + try: () => loadLocalExecutorConfig(context), | |
| 66 | + catch: (cause) => | |
| 67 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 68 | + }); | |
| 69 | + | |
| 70 | + return { | |
| 71 | + context, | |
| 72 | + installation: { | |
| 73 | + workspaceId: input.workspaceId, | |
| 74 | + accountId: input.accountId, | |
| 75 | + organizationId: "org_local_personal" as never, | |
| 76 | + }, | |
| 77 | + loadedConfig, | |
| 78 | + } satisfies RuntimeLocalWorkspaceState; | |
| 79 | + }); | |
| 80 | + | |
| 81 | +const withRuntimeLocalWorkspace = <A, E>( | |
| 82 | + effect: Effect.Effect<A, E, never>, | |
| 83 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 84 | +) => | |
| 85 | + effect.pipe( | |
| 86 | + Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 87 | + ); | |
| 88 | + | |
| 43 | 89 | const makeOpenApiSource = (input: { |
| 44 | 90 | workspaceId: Source["workspaceId"]; |
| 45 | 91 | sourceId: Source["id"]; |
| @@ -78,28 +124,13 @@ describe("source-store", () => { · 32 unmodified lines | ||
| 78 | 124 | const persistence = yield* makePersistence; |
| 79 | 125 | const now = Date.now(); |
| 80 | 126 | const accountId = AccountIdSchema.make("acc_source_store"); |
| 81 | − const organizationId = OrganizationIdSchema.make("org_source_store"); | |
| 82 | 127 | const workspaceId = WorkspaceIdSchema.make("ws_source_store"); |
| 83 | − const sourceId = SourceIdSchema.make("src_source_store"); | |
| 128 | + const sourceId = SourceIdSchema.make("github"); | |
| 84 | 129 | const firstTokenId = SecretMaterialIdSchema.make("sec_source_store_first"); |
| 85 | 130 | const secondTokenId = SecretMaterialIdSchema.make("sec_source_store_second"); |
| 86 | − | |
| 87 | − yield* persistence.rows.organizations.insert({ | |
| 88 | − id: organizationId, | |
| 89 | − slug: "source-store", | |
| 90 | − name: "Source Store", | |
| 91 | − status: "active", | |
| 92 | − createdByAccountId: accountId, | |
| 93 | − createdAt: now, | |
| 94 | − updatedAt: now, | |
| 95 | − }); | |
| 96 | − yield* persistence.rows.workspaces.insert({ | |
| 97 | − id: workspaceId, | |
| 98 | − organizationId, | |
| 99 | − name: "Primary", | |
| 100 | − createdByAccountId: accountId, | |
| 101 | − createdAt: now, | |
| 102 | − updatedAt: now, | |
| 131 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({ | |
| 132 | + workspaceId, | |
| 133 | + accountId, | |
| 103 | 134 | }); |
| 104 | 135 | |
| 105 | 136 | yield* persistence.rows.secretMaterials.upsert({ |
| @@ -119,22 +150,25 @@ describe("source-store", () => { · 13 unmodified lines | ||
| 119 | 150 | updatedAt: now, |
| 120 | 151 | }); |
| 121 | 152 | |
| 122 | − yield* persistSource( | |
| 123 | − persistence.rows, | |
| 124 | − makeOpenApiSource({ | |
| 125 | − workspaceId, | |
| 126 | − sourceId, | |
| 127 | − now, | |
| 128 | − auth: { | |
| 129 | − kind: "bearer", | |
| 130 | − headerName: "Authorization", | |
| 131 | − prefix: "Bearer ", | |
| 132 | − token: { | |
| 133 | − providerId: "postgres", | |
| 134 | − handle: firstTokenId, | |
| 153 | + yield* withRuntimeLocalWorkspace( | |
| 154 | + persistSource( | |
| 155 | + persistence.rows, | |
| 156 | + makeOpenApiSource({ | |
| 157 | + workspaceId, | |
| 158 | + sourceId, | |
| 159 | + now, | |
| 160 | + auth: { | |
| 161 | + kind: "bearer", | |
| 162 | + headerName: "Authorization", | |
| 163 | + prefix: "Bearer ", | |
| 164 | + token: { | |
| 165 | + providerId: "postgres", | |
| 166 | + handle: firstTokenId, | |
| 167 | + }, | |
| 135 | 168 | }, |
| 136 | − }, | |
| 137 | − }), | |
| 169 | + }), | |
| 170 | + ), | |
| 171 | + runtimeLocalWorkspace, | |
| 138 | 172 | ); |
| 139 | 173 | |
| 140 | 174 | yield* persistence.rows.sourceAuthSessions.upsert({ |
| @@ -167,23 +201,26 @@ describe("source-store", () => { · 26 unmodified lines | ||
| 167 | 201 | updatedAt: now, |
| 168 | 202 | }); |
| 169 | 203 | |
| 170 | − yield* persistSource( | |
| 171 | − persistence.rows, | |
| 172 | − makeOpenApiSource({ | |
| 173 | − workspaceId, | |
| 174 | − sourceId, | |
| 175 | − now, | |
| 176 | − updatedAt: now + 1, | |
| 177 | − auth: { | |
| 178 | − kind: "bearer", | |
| 179 | − headerName: "Authorization", | |
| 180 | − prefix: "Bearer ", | |
| 181 | − token: { | |
| 182 | − providerId: "postgres", | |
| 183 | − handle: secondTokenId, | |
| 204 | + yield* withRuntimeLocalWorkspace( | |
| 205 | + persistSource( | |
| 206 | + persistence.rows, | |
| 207 | + makeOpenApiSource({ | |
| 208 | + workspaceId, | |
| 209 | + sourceId, | |
| 210 | + now, | |
| 211 | + updatedAt: now + 1, | |
| 212 | + auth: { | |
| 213 | + kind: "bearer", | |
| 214 | + headerName: "Authorization", | |
| 215 | + prefix: "Bearer ", | |
| 216 | + token: { | |
| 217 | + providerId: "postgres", | |
| 218 | + handle: secondTokenId, | |
| 219 | + }, | |
| 184 | 220 | }, |
| 185 | − }, | |
| 186 | − }), | |
| 221 | + }), | |
| 222 | + ), | |
| 223 | + runtimeLocalWorkspace, | |
| 187 | 224 | ); |
| 188 | 225 | |
| 189 | 226 | expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(firstTokenId))).toBe(true); |
| @@ -196,16 +233,30 @@ describe("source-store", () => { · 6 unmodified lines | ||
| 196 | 233 | : null, |
| 197 | 234 | ).toBe(secondTokenId); |
| 198 | 235 | |
| 199 | − const removed = yield* removeSourceById(persistence.rows, { | |
| 200 | − workspaceId, | |
| 201 | − sourceId, | |
| 202 | − }); | |
| 236 | + const removed = yield* withRuntimeLocalWorkspace( | |
| 237 | + removeSourceById(persistence.rows, { | |
| 238 | + workspaceId, | |
| 239 | + sourceId, | |
| 240 | + }), | |
| 241 | + runtimeLocalWorkspace, | |
| 242 | + ); | |
| 203 | 243 | expect(removed).toBe(true); |
| 204 | 244 | |
| 205 | 245 | expect(Option.isNone(yield* persistence.rows.secretMaterials.getById(secondTokenId))).toBe(true); |
| 206 | − expect(yield* persistence.rows.sources.listByWorkspaceId(workspaceId)).toHaveLength(0); | |
| 207 | 246 | expect(yield* persistence.rows.authArtifacts.listByWorkspaceId(workspaceId)).toHaveLength(0); |
| 208 | 247 | expect(yield* persistence.rows.sourceAuthSessions.listByWorkspaceId(workspaceId)).toHaveLength(0); |
| 248 | + const config = yield* Effect.tryPromise({ | |
| 249 | + try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context), | |
| 250 | + catch: (cause) => | |
| 251 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 252 | + }); | |
| 253 | + expect(config.config?.sources?.[sourceId]).toBeUndefined(); | |
| 254 | + const workspaceState = yield* Effect.tryPromise({ | |
| 255 | + try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context), | |
| 256 | + catch: (cause) => | |
| 257 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 258 | + }); | |
| 259 | + expect(workspaceState.sources[sourceId]).toBeUndefined(); | |
| 209 | 260 | }), |
| 210 | 261 | ); |
| 211 | 262 | |
| @@ -214,27 +265,12 @@ describe("source-store", () => { · 2 unmodified lines | ||
| 214 | 265 | const persistence = yield* makePersistence; |
| 215 | 266 | const now = Date.now(); |
| 216 | 267 | const accountId = AccountIdSchema.make("acc_actor_scoped"); |
| 217 | − const organizationId = OrganizationIdSchema.make("org_actor_scoped"); | |
| 218 | 268 | const workspaceId = WorkspaceIdSchema.make("ws_actor_scoped"); |
| 219 | − const sourceId = SourceIdSchema.make("src_actor_scoped"); | |
| 269 | + const sourceId = SourceIdSchema.make("github"); | |
| 220 | 270 | const sharedTokenId = SecretMaterialIdSchema.make("sec_actor_scoped_shared"); |
| 221 | − | |
| 222 | − yield* persistence.rows.organizations.insert({ | |
| 223 | − id: organizationId, | |
| 224 | − slug: "actor-scoped", | |
| 225 | − name: "Actor Scoped", | |
| 226 | − status: "active", | |
| 227 | − createdByAccountId: accountId, | |
| 228 | − createdAt: now, | |
| 229 | − updatedAt: now, | |
| 230 | − }); | |
| 231 | − yield* persistence.rows.workspaces.insert({ | |
| 232 | − id: workspaceId, | |
| 233 | − organizationId, | |
| 234 | − name: "Primary", | |
| 235 | − createdByAccountId: accountId, | |
| 236 | − createdAt: now, | |
| 237 | − updatedAt: now, | |
| 271 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspace({ | |
| 272 | + workspaceId, | |
| 273 | + accountId, | |
| 238 | 274 | }); |
| 239 | 275 | yield* persistence.rows.secretMaterials.upsert({ |
| 240 | 276 | id: sharedTokenId, |
| @@ -260,16 +296,22 @@ describe("source-store", () => { · 19 unmodified lines | ||
| 260 | 296 | }, |
| 261 | 297 | }); |
| 262 | 298 | |
| 263 | − yield* persistSource(persistence.rows, source); | |
| 264 | − yield* persistSource( | |
| 265 | − persistence.rows, | |
| 266 | − { | |
| 267 | − ...source, | |
| 268 | − updatedAt: now + 1, | |
| 269 | − }, | |
| 270 | − { | |
| 271 | − actorAccountId: accountId, | |
| 272 | − }, | |
| 299 | + yield* withRuntimeLocalWorkspace( | |
| 300 | + persistSource(persistence.rows, source), | |
| 301 | + runtimeLocalWorkspace, | |
| 302 | + ); | |
| 303 | + yield* withRuntimeLocalWorkspace( | |
| 304 | + persistSource( | |
| 305 | + persistence.rows, | |
| 306 | + { | |
| 307 | + ...source, | |
| 308 | + updatedAt: now + 1, | |
| 309 | + }, | |
| 310 | + { | |
| 311 | + actorAccountId: accountId, | |
| 312 | + }, | |
| 313 | + ), | |
| 314 | + runtimeLocalWorkspace, | |
| 273 | 315 | ); |
| 274 | 316 | |
| 275 | 317 | const authArtifacts = yield* persistence.rows.authArtifacts.listByWorkspaceAndSourceId({ |
| @@ -282,264 +324,4 @@ describe("source-store", () => { · 6 unmodified lines | ||
| 282 | 324 | expect(new Set(authArtifacts.map((artifact) => artifact.id)).size).toBe(2); |
| 283 | 325 | }), |
| 284 | 326 | ); |
| 285 | − | |
| 286 | − it.scoped("cleans up orphaned recipe data when the source config changes", () => | |
| 287 | − Effect.gen(function* () { | |
| 288 | − const persistence = yield* makePersistence; | |
| 289 | − const now = Date.now(); | |
| 290 | − const accountId = AccountIdSchema.make("acc_recipe_rewrite"); | |
| 291 | − const organizationId = OrganizationIdSchema.make("org_recipe_rewrite"); | |
| 292 | − const workspaceId = WorkspaceIdSchema.make("ws_recipe_rewrite"); | |
| 293 | − const sourceId = SourceIdSchema.make("src_recipe_rewrite"); | |
| 294 | − | |
| 295 | − yield* persistence.rows.organizations.insert({ | |
| 296 | − id: organizationId, | |
| 297 | − slug: "recipe-rewrite", | |
| 298 | − name: "Recipe Rewrite", | |
| 299 | − status: "active", | |
| 300 | − createdByAccountId: accountId, | |
| 301 | − createdAt: now, | |
| 302 | − updatedAt: now, | |
| 303 | − }); | |
| 304 | − yield* persistence.rows.workspaces.insert({ | |
| 305 | − id: workspaceId, | |
| 306 | − organizationId, | |
| 307 | − name: "Primary", | |
| 308 | − createdByAccountId: accountId, | |
| 309 | − createdAt: now, | |
| 310 | − updatedAt: now, | |
| 311 | − }); | |
| 312 | − | |
| 313 | − const initialSource = makeOpenApiSource({ | |
| 314 | − workspaceId, | |
| 315 | − sourceId, | |
| 316 | − now, | |
| 317 | − auth: { kind: "none" }, | |
| 318 | − }); | |
| 319 | − const initialRecipeId = stableSourceRecipeId(initialSource); | |
| 320 | − const initialRecipeRevisionId = stableSourceRecipeRevisionId(initialSource); | |
| 321 | − | |
| 322 | − yield* persistSource(persistence.rows, initialSource); | |
| 323 | − yield* persistence.rows.sourceRecipeDocuments.replaceForRevision({ | |
| 324 | − recipeRevisionId: initialRecipeRevisionId, | |
| 325 | − documents: [{ | |
| 326 | − id: "src_recipe_doc_recipe_rewrite", | |
| 327 | − recipeRevisionId: initialRecipeRevisionId, | |
| 328 | − documentKind: "openapi", | |
| 329 | − documentKey: | |
| 330 | − typeof initialSource.binding.specUrl === "string" | |
| 331 | − ? initialSource.binding.specUrl | |
| 332 | − : initialSource.endpoint, | |
| 333 | − contentText: "{}", | |
| 334 | − contentHash: "hash_recipe_rewrite", | |
| 335 | − fetchedAt: now, | |
| 336 | − createdAt: now, | |
| 337 | − updatedAt: now, | |
| 338 | − }], | |
| 339 | − }); | |
| 340 | − yield* persistence.rows.sourceRecipeOperations.replaceForRevision({ | |
| 341 | − recipeRevisionId: initialRecipeRevisionId, | |
| 342 | − operations: [{ | |
| 343 | − id: "src_recipe_op_recipe_rewrite", | |
| 344 | − recipeRevisionId: initialRecipeRevisionId, | |
| 345 | − operationKey: "getRepo", | |
| 346 | − transportKind: "http", | |
| 347 | − toolId: "getRepo", | |
| 348 | − title: "Get Repo", | |
| 349 | − description: "Read a repository", | |
| 350 | − operationKind: "read", | |
| 351 | − searchText: "github get repo", | |
| 352 | − inputSchemaJson: null, | |
| 353 | − outputSchemaJson: null, | |
| 354 | − providerKind: "openapi", | |
| 355 | − providerDataJson: JSON.stringify({ | |
| 356 | − kind: "openapi", | |
| 357 | − toolId: "getRepo", | |
| 358 | − rawToolId: "repos_getRepo", | |
| 359 | − operationId: "repos.getRepo", | |
| 360 | − group: "repos", | |
| 361 | − leaf: "getRepo", | |
| 362 | − tags: ["repos"], | |
| 363 | − method: "get", | |
| 364 | − path: "/repos/{owner}/{repo}", | |
| 365 | − operationHash: "hash_recipe_rewrite", | |
| 366 | − invocation: { | |
| 367 | − method: "get", | |
| 368 | − pathTemplate: "/repos/{owner}/{repo}", | |
| 369 | − parameters: [], | |
| 370 | − requestBody: null, | |
| 371 | − }, | |
| 372 | − }), | |
| 373 | − createdAt: now, | |
| 374 | − updatedAt: now, | |
| 375 | − }], | |
| 376 | − }); | |
| 377 | − | |
| 378 | − const updatedSource = makeOpenApiSource({ | |
| 379 | − workspaceId, | |
| 380 | − sourceId, | |
| 381 | − now, | |
| 382 | − updatedAt: now + 1, | |
| 383 | − endpoint: "https://api.example.com", | |
| 384 | − specUrl: "https://api.example.com/openapi.json", | |
| 385 | − auth: { kind: "none" }, | |
| 386 | − }); | |
| 387 | − const nextRecipeId = stableSourceRecipeId(updatedSource); | |
| 388 | − const nextRecipeRevisionId = stableSourceRecipeRevisionId(updatedSource); | |
| 389 | − | |
| 390 | − yield* persistSource(persistence.rows, updatedSource); | |
| 391 | − | |
| 392 | − expect(Option.isNone(yield* persistence.rows.sourceRecipes.getById(initialRecipeId))).toBe(true); | |
| 393 | − expect( | |
| 394 | − Option.isNone(yield* persistence.rows.sourceRecipeRevisions.getById(initialRecipeRevisionId)), | |
| 395 | − ).toBe(true); | |
| 396 | − expect( | |
| 397 | − yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(initialRecipeRevisionId), | |
| 398 | − ).toEqual([]); | |
| 399 | − expect( | |
| 400 | − yield* persistence.rows.sourceRecipeOperations.listByRevisionId(initialRecipeRevisionId), | |
| 401 | − ).toEqual([]); | |
| 402 | − expect(Option.isSome(yield* persistence.rows.sourceRecipes.getById(nextRecipeId))).toBe(true); | |
| 403 | − expect( | |
| 404 | − Option.isSome(yield* persistence.rows.sourceRecipeRevisions.getById(nextRecipeRevisionId)), | |
| 405 | − ).toBe(true); | |
| 406 | − }), | |
| 407 | − ); | |
| 408 | − | |
| 409 | − it.scoped("retains shared recipe data until the last source reference is removed", () => | |
| 410 | − Effect.gen(function* () { | |
| 411 | − const persistence = yield* makePersistence; | |
| 412 | − const now = Date.now(); | |
| 413 | − const accountId = AccountIdSchema.make("acc_shared_recipe"); | |
| 414 | − const organizationId = OrganizationIdSchema.make("org_shared_recipe"); | |
| 415 | − const workspaceId = WorkspaceIdSchema.make("ws_shared_recipe"); | |
| 416 | − const firstSourceId = SourceIdSchema.make("src_shared_recipe_one"); | |
| 417 | − const secondSourceId = SourceIdSchema.make("src_shared_recipe_two"); | |
| 418 | − | |
| 419 | − yield* persistence.rows.organizations.insert({ | |
| 420 | − id: organizationId, | |
| 421 | − slug: "shared-recipe", | |
| 422 | − name: "Shared Recipe", | |
| 423 | − status: "active", | |
| 424 | − createdByAccountId: accountId, | |
| 425 | − createdAt: now, | |
| 426 | − updatedAt: now, | |
| 427 | − }); | |
| 428 | − yield* persistence.rows.workspaces.insert({ | |
| 429 | − id: workspaceId, | |
| 430 | − organizationId, | |
| 431 | − name: "Primary", | |
| 432 | − createdByAccountId: accountId, | |
| 433 | − createdAt: now, | |
| 434 | − updatedAt: now, | |
| 435 | − }); | |
| 436 | − | |
| 437 | − const firstSource = makeOpenApiSource({ | |
| 438 | − workspaceId, | |
| 439 | − sourceId: firstSourceId, | |
| 440 | − now, | |
| 441 | − name: "GitHub One", | |
| 442 | − auth: { kind: "none" }, | |
| 443 | − }); | |
| 444 | − const secondSource = makeOpenApiSource({ | |
| 445 | − workspaceId, | |
| 446 | − sourceId: secondSourceId, | |
| 447 | − now, | |
| 448 | − updatedAt: now + 1, | |
| 449 | − name: "GitHub Two", | |
| 450 | − auth: { kind: "none" }, | |
| 451 | − }); | |
| 452 | − const sharedRecipeId = stableSourceRecipeId(firstSource); | |
| 453 | − const sharedRecipeRevisionId = stableSourceRecipeRevisionId(firstSource); | |
| 454 | − | |
| 455 | − yield* persistSource(persistence.rows, firstSource); | |
| 456 | − yield* persistSource(persistence.rows, secondSource); | |
| 457 | − yield* persistence.rows.sourceRecipeDocuments.replaceForRevision({ | |
| 458 | − recipeRevisionId: sharedRecipeRevisionId, | |
| 459 | − documents: [{ | |
| 460 | − id: "src_recipe_doc_shared_recipe", | |
| 461 | − recipeRevisionId: sharedRecipeRevisionId, | |
| 462 | − documentKind: "openapi", | |
| 463 | − documentKey: | |
| 464 | − typeof firstSource.binding.specUrl === "string" | |
| 465 | − ? firstSource.binding.specUrl | |
| 466 | − : firstSource.endpoint, | |
| 467 | − contentText: "{}", | |
| 468 | − contentHash: "hash_shared_recipe", | |
| 469 | − fetchedAt: now, | |
| 470 | − createdAt: now, | |
| 471 | − updatedAt: now, | |
| 472 | − }], | |
| 473 | − }); | |
| 474 | − yield* persistence.rows.sourceRecipeOperations.replaceForRevision({ | |
| 475 | − recipeRevisionId: sharedRecipeRevisionId, | |
| 476 | − operations: [{ | |
| 477 | − id: "src_recipe_op_shared_recipe", | |
| 478 | − recipeRevisionId: sharedRecipeRevisionId, | |
| 479 | − operationKey: "getRepo", | |
| 480 | − transportKind: "http", | |
| 481 | − toolId: "getRepo", | |
| 482 | − title: "Get Repo", | |
| 483 | − description: "Read a repository", | |
| 484 | − operationKind: "read", | |
| 485 | − searchText: "github get repo", | |
| 486 | − inputSchemaJson: null, | |
| 487 | − outputSchemaJson: null, | |
| 488 | − providerKind: "openapi", | |
| 489 | − providerDataJson: JSON.stringify({ | |
| 490 | − kind: "openapi", | |
| 491 | − toolId: "getRepo", | |
| 492 | − rawToolId: "repos_getRepo", | |
| 493 | − operationId: "repos.getRepo", | |
| 494 | − group: "repos", | |
| 495 | − leaf: "getRepo", | |
| 496 | − tags: ["repos"], | |
| 497 | − method: "get", | |
| 498 | − path: "/repos/{owner}/{repo}", | |
| 499 | − operationHash: "hash_shared_recipe", | |
| 500 | − invocation: { | |
| 501 | − method: "get", | |
| 502 | − pathTemplate: "/repos/{owner}/{repo}", | |
| 503 | − parameters: [], | |
| 504 | − requestBody: null, | |
| 505 | − }, | |
| 506 | − }), | |
| 507 | − createdAt: now, | |
| 508 | − updatedAt: now, | |
| 509 | − }], | |
| 510 | − }); | |
| 511 | − | |
| 512 | − yield* removeSourceById(persistence.rows, { | |
| 513 | − workspaceId, | |
| 514 | − sourceId: firstSourceId, | |
| 515 | − }); | |
| 516 | − | |
| 517 | − expect(Option.isSome(yield* persistence.rows.sourceRecipes.getById(sharedRecipeId))).toBe(true); | |
| 518 | − expect( | |
| 519 | − Option.isSome(yield* persistence.rows.sourceRecipeRevisions.getById(sharedRecipeRevisionId)), | |
| 520 | − ).toBe(true); | |
| 521 | − expect( | |
| 522 | − yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(sharedRecipeRevisionId), | |
| 523 | − ).toHaveLength(1); | |
| 524 | − expect( | |
| 525 | − yield* persistence.rows.sourceRecipeOperations.listByRevisionId(sharedRecipeRevisionId), | |
| 526 | − ).toHaveLength(1); | |
| 527 | − | |
| 528 | − yield* removeSourceById(persistence.rows, { | |
| 529 | − workspaceId, | |
| 530 | − sourceId: secondSourceId, | |
| 531 | − }); | |
| 532 | − | |
| 533 | − expect(Option.isNone(yield* persistence.rows.sourceRecipes.getById(sharedRecipeId))).toBe(true); | |
| 534 | − expect( | |
| 535 | − Option.isNone(yield* persistence.rows.sourceRecipeRevisions.getById(sharedRecipeRevisionId)), | |
| 536 | − ).toBe(true); | |
| 537 | − expect( | |
| 538 | − yield* persistence.rows.sourceRecipeDocuments.listByRevisionId(sharedRecipeRevisionId), | |
| 539 | − ).toEqual([]); | |
| 540 | − expect( | |
| 541 | − yield* persistence.rows.sourceRecipeOperations.listByRevisionId(sharedRecipeRevisionId), | |
| 542 | − ).toEqual([]); | |
| 543 | − }), | |
| 544 | − ); | |
| 545 | 327 | }); |
| @@ -6,26 +6,23 @@ import type { · 5 unmodified lines | ||
| 6 | 6 | LocalConfigSource, |
| 7 | 7 | Source, |
| 8 | 8 | SourceId, |
| 9 | − SourceRecipeId, | |
| 10 | − SourceRecipeRevisionId, | |
| 11 | 9 | WorkspaceId, |
| 12 | 10 | } from "#schema"; |
| 13 | 11 | import { SourceIdSchema } from "#schema"; |
| 14 | 12 | import { type SqlControlPlaneRows } from "#persistence"; |
| 15 | 13 | import * as Effect from "effect/Effect"; |
| 16 | −import * as Option from "effect/Option"; | |
| 17 | 14 | |
| 18 | 15 | import { |
| 19 | − createSourceRecipeRecord, | |
| 20 | − createSourceRecipeRevisionRecord, | |
| 21 | − projectSourceFromStorage, | |
| 22 | − projectSourcesFromStorage, | |
| 23 | 16 | stableSourceRecipeId, |
| 24 | 17 | stableSourceRecipeRevisionId, |
| 25 | 18 | splitSourceForStorage, |
| 26 | 19 | } from "./source-definitions"; |
| 20 | +import { | |
| 21 | + sourceAuthFromAuthArtifact, | |
| 22 | +} from "./auth-artifacts"; | |
| 27 | 23 | import { |
| 28 | 24 | loadLocalExecutorConfig, |
| 25 | + type LoadedLocalExecutorConfig, | |
| 29 | 26 | type ResolvedLocalWorkspaceContext, |
| 30 | 27 | writeProjectLocalExecutorConfig, |
| 31 | 28 | } from "./local-config"; |
| @@ -34,7 +31,7 @@ import { · 2 unmodified lines | ||
| 34 | 31 | removeLocalSourceArtifact, |
| 35 | 32 | } from "./local-source-artifacts"; |
| 36 | 33 | import { |
| 37 | − getRuntimeLocalWorkspaceOption, | |
| 34 | + requireRuntimeLocalWorkspace, | |
| 38 | 35 | } from "./local-runtime-context"; |
| 39 | 36 | import { |
| 40 | 37 | loadLocalWorkspaceState, |
| @@ -136,7 +133,7 @@ const deriveLocalSourceId = ( · 95 unmodified lines | ||
| 136 | 133 | return SourceIdSchema.make(candidate); |
| 137 | 134 | }; |
| 138 | 135 | |
| 139 | −const resolveLocalConfigSecretProviderAlias = (config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]): string | null => { | |
| 136 | +const resolveLocalConfigSecretProviderAlias = (config: LoadedLocalExecutorConfig["config"]): string | null => { | |
| 140 | 137 | const defaultAlias = trimOrNull(config?.secrets?.defaults?.env); |
| 141 | 138 | if (defaultAlias !== null && config?.secrets?.providers?.[defaultAlias]) { |
| 142 | 139 | return defaultAlias; |
| @@ -147,7 +144,7 @@ const resolveLocalConfigSecretProviderAlias = (config: Awaited<ReturnType<typeof · 4 unmodified lines | ||
| 147 | 144 | |
| 148 | 145 | const sourceAuthFromConfigInput = (input: { |
| 149 | 146 | auth: unknown; |
| 150 | − config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]; | |
| 147 | + config: LoadedLocalExecutorConfig["config"]; | |
| 151 | 148 | existing: Source["auth"] | null; |
| 152 | 149 | }): Source["auth"] => { |
| 153 | 150 | if (input.auth === undefined) { |
| @@ -175,9 +172,11 @@ const sourceAuthFromConfigInput = (input: { · 21 unmodified lines | ||
| 175 | 172 | }; |
| 176 | 173 | const providerAlias = trimOrNull(explicit.provider); |
| 177 | 174 | const providerId = providerAlias |
| 178 | − ? toConfigSecretProviderId(providerAlias) | |
| 175 | + ? (providerAlias === "params" ? "params" : toConfigSecretProviderId(providerAlias)) | |
| 179 | 176 | : explicit.source === "env" |
| 180 | 177 | ? "env" |
| 178 | + : explicit.source === "params" | |
| 179 | + ? "params" | |
| 181 | 180 | : null; |
| 182 | 181 | const handle = trimOrNull(explicit.id); |
| 183 | 182 | if (providerId && handle) { |
| @@ -199,7 +198,7 @@ const sourceAuthFromConfigInput = (input: { · 15 unmodified lines | ||
| 199 | 198 | const configAuthFromSource = (input: { |
| 200 | 199 | source: Source; |
| 201 | 200 | existingConfigAuth: LocalConfigSecretInput | undefined; |
| 202 | − config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]; | |
| 201 | + config: LoadedLocalExecutorConfig["config"]; | |
| 203 | 202 | }): LocalConfigSecretInput | undefined => { |
| 204 | 203 | if (input.source.auth.kind !== "bearer") { |
| 205 | 204 | return input.existingConfigAuth; |
| @@ -209,6 +208,14 @@ const configAuthFromSource = (input: { · 3 unmodified lines | ||
| 209 | 208 | return input.source.auth.token.handle; |
| 210 | 209 | } |
| 211 | 210 | |
| 211 | + if (input.source.auth.token.providerId === "params") { | |
| 212 | + return { | |
| 213 | + source: "params", | |
| 214 | + provider: "params", | |
| 215 | + id: input.source.auth.token.handle, | |
| 216 | + }; | |
| 217 | + } | |
| 218 | + | |
| 212 | 219 | const provider = fromConfigSecretProviderId(input.source.auth.token.providerId); |
| 213 | 220 | if (provider !== null) { |
| 214 | 221 | const configuredProvider = input.config?.secrets?.providers?.[provider]; |
| @@ -226,11 +233,7 @@ const configAuthFromSource = (input: { · 11 unmodified lines | ||
| 226 | 233 | |
| 227 | 234 | const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) => |
| 228 | 235 | Effect.gen(function* () { |
| 229 | − const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 230 | − if (runtimeLocalWorkspace === null || runtimeLocalWorkspace.installation.workspaceId !== workspaceId) { | |
| 231 | − return null; | |
| 232 | − } | |
| 233 | − | |
| 236 | + const runtimeLocalWorkspace = yield* requireRuntimeLocalWorkspace(workspaceId); | |
| 234 | 237 | const loadedConfig = yield* Effect.tryPromise({ |
| 235 | 238 | try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context), |
| 236 | 239 | catch: (cause) => |
| @@ -253,7 +256,7 @@ const resolveRuntimeLocalWorkspace = (workspaceId: WorkspaceId) => · 16 unmodified lines | ||
| 253 | 256 | const buildLocalSourceRecord = (input: { |
| 254 | 257 | context: ResolvedLocalWorkspaceContext; |
| 255 | 258 | workspaceId: WorkspaceId; |
| 256 | − loadedConfig: Awaited<ReturnType<typeof loadLocalExecutorConfig>>; | |
| 259 | + loadedConfig: LoadedLocalExecutorConfig; | |
| 257 | 260 | workspaceState: LocalWorkspaceState; |
| 258 | 261 | sourceId: SourceId; |
| 259 | 262 | actorAccountId?: AccountId | null; |
| @@ -335,11 +338,23 @@ const buildLocalSourceRecord = (input: { · 75 unmodified lines | ||
| 335 | 338 | updatedAt: baseSource.updatedAt, |
| 336 | 339 | }; |
| 337 | 340 | |
| 338 | − const source = yield* projectSourceFromStorage({ | |
| 339 | − sourceRecord, | |
| 340 | − runtimeAuthArtifact, | |
| 341 | − importAuthArtifact, | |
| 342 | − }); | |
| 341 | + const source: Source = { | |
| 342 | + ...baseSource, | |
| 343 | + auth: | |
| 344 | + runtimeAuthArtifact === null | |
| 345 | + ? baseSource.auth | |
| 346 | + : sourceAuthFromAuthArtifact(runtimeAuthArtifact), | |
| 347 | + importAuth: | |
| 348 | + baseSource.importAuthPolicy === "separate" | |
| 349 | + ? importAuthArtifact === null | |
| 350 | + ? baseSource.importAuth | |
| 351 | + : sourceAuthFromAuthArtifact(importAuthArtifact) | |
| 352 | + : { kind: "none" }, | |
| 353 | + sourceHash: sourceRecord.sourceHash, | |
| 354 | + lastError: sourceRecord.lastError, | |
| 355 | + createdAt: sourceRecord.createdAt, | |
| 356 | + updatedAt: sourceRecord.updatedAt, | |
| 357 | + }; | |
| 343 | 358 | |
| 344 | 359 | return { |
| 345 | 360 | source, |
| @@ -356,49 +371,23 @@ export const loadSourcesInWorkspace = ( · 10 unmodified lines | ||
| 356 | 371 | ) => |
| 357 | 372 | Effect.gen(function* () { |
| 358 | 373 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(workspaceId); |
| 359 | − if (localWorkspace !== null) { | |
| 360 | − const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId); | |
| 361 | − return yield* Effect.forEach( | |
| 362 | − Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}), | |
| 363 | − (sourceId) => | |
| 364 | − Effect.map( | |
| 365 | − buildLocalSourceRecord({ | |
| 366 | − context: localWorkspace.context, | |
| 367 | − workspaceId, | |
| 368 | − loadedConfig: localWorkspace.loadedConfig, | |
| 369 | − workspaceState: localWorkspace.workspaceState, | |
| 370 | − sourceId: SourceIdSchema.make(sourceId), | |
| 371 | − actorAccountId: options.actorAccountId, | |
| 372 | − authArtifacts, | |
| 373 | − }), | |
| 374 | − ({ source }) => source, | |
| 375 | − ), | |
| 376 | − ); | |
| 377 | − } | |
| 378 | − | |
| 379 | − const sourceRecords = yield* rows.sources.listByWorkspaceId(workspaceId); | |
| 380 | 374 | const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(workspaceId); |
| 381 | − const filteredAuthArtifacts = sourceRecords.flatMap((sourceRecord) => { | |
| 382 | − const matches = authArtifacts.filter((artifact) => artifact.sourceId === sourceRecord.id); | |
| 383 | − const preferred = selectPreferredAuthArtifact({ | |
| 384 | − authArtifacts: matches, | |
| 385 | − actorAccountId: options.actorAccountId, | |
| 386 | − slot: "runtime", | |
| 387 | − }); | |
| 388 | − const preferredImport = selectPreferredAuthArtifact({ | |
| 389 | − authArtifacts: matches, | |
| 390 | − actorAccountId: options.actorAccountId, | |
| 391 | − slot: "import", | |
| 392 | − }); | |
| 393 | − return [preferred, preferredImport].filter( | |
| 394 | − (artifact): artifact is AuthArtifact => artifact !== null, | |
| 395 | − ); | |
| 396 | − }); | |
| 397 | − | |
| 398 | − return yield* projectSourcesFromStorage({ | |
| 399 | − sourceRecords, | |
| 400 | − authArtifacts: filteredAuthArtifacts, | |
| 401 | − }); | |
| 375 | + return yield* Effect.forEach( | |
| 376 | + Object.keys(localWorkspace.loadedConfig.config?.sources ?? {}), | |
| 377 | + (sourceId) => | |
| 378 | + Effect.map( | |
| 379 | + buildLocalSourceRecord({ | |
| 380 | + context: localWorkspace.context, | |
| 381 | + workspaceId, | |
| 382 | + loadedConfig: localWorkspace.loadedConfig, | |
| 383 | + workspaceState: localWorkspace.workspaceState, | |
| 384 | + sourceId: SourceIdSchema.make(sourceId), | |
| 385 | + actorAccountId: options.actorAccountId, | |
| 386 | + authArtifacts, | |
| 387 | + }), | |
| 388 | + ({ source }) => source, | |
| 389 | + ), | |
| 390 | + ); | |
| 402 | 391 | }); |
| 403 | 392 | |
| 404 | 393 | export const loadSourceById = (rows: SqlControlPlaneRows, input: { |
| @@ -408,64 +397,30 @@ export const loadSourceById = (rows: SqlControlPlaneRows, input: { · 3 unmodified lines | ||
| 408 | 397 | }) => |
| 409 | 398 | Effect.gen(function* () { |
| 410 | 399 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId); |
| 411 | − if (localWorkspace !== null) { | |
| 412 | − const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId); | |
| 413 | − if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { | |
| 414 | − return yield* Effect.fail( | |
| 415 | − new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`), | |
| 416 | − ); | |
| 417 | − } | |
| 418 | − | |
| 419 | − const localSource = yield* buildLocalSourceRecord({ | |
| 420 | − context: localWorkspace.context, | |
| 421 | − workspaceId: input.workspaceId, | |
| 422 | − loadedConfig: localWorkspace.loadedConfig, | |
| 423 | − workspaceState: localWorkspace.workspaceState, | |
| 424 | − sourceId: input.sourceId, | |
| 425 | − actorAccountId: input.actorAccountId, | |
| 426 | − authArtifacts, | |
| 427 | − }); | |
| 428 | − | |
| 429 | − return localSource.source; | |
| 430 | − } | |
| 431 | − | |
| 432 | − const sourceRecord = yield* rows.sources.getByWorkspaceAndId( | |
| 433 | − input.workspaceId, | |
| 434 | − input.sourceId, | |
| 435 | − ); | |
| 436 | − | |
| 437 | − if (Option.isNone(sourceRecord)) { | |
| 400 | + const authArtifacts = yield* rows.authArtifacts.listByWorkspaceId(input.workspaceId); | |
| 401 | + if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { | |
| 438 | 402 | return yield* Effect.fail( |
| 439 | 403 | new Error(`Source not found: workspaceId=${input.workspaceId} sourceId=${input.sourceId}`), |
| 440 | 404 | ); |
| 441 | 405 | } |
| 442 | 406 | |
| 443 | − const authArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({ | |
| 407 | + const localSource = yield* buildLocalSourceRecord({ | |
| 408 | + context: localWorkspace.context, | |
| 444 | 409 | workspaceId: input.workspaceId, |
| 410 | + loadedConfig: localWorkspace.loadedConfig, | |
| 411 | + workspaceState: localWorkspace.workspaceState, | |
| 445 | 412 | sourceId: input.sourceId, |
| 446 | − }); | |
| 447 | − const authArtifact = selectPreferredAuthArtifact({ | |
| 448 | − authArtifacts, | |
| 449 | 413 | actorAccountId: input.actorAccountId, |
| 450 | − slot: "runtime", | |
| 451 | − }); | |
| 452 | − const importAuthArtifact = selectPreferredAuthArtifact({ | |
| 453 | 414 | authArtifacts, |
| 454 | − actorAccountId: input.actorAccountId, | |
| 455 | − slot: "import", | |
| 456 | 415 | }); |
| 457 | 416 | |
| 458 | − return yield* projectSourceFromStorage({ | |
| 459 | − sourceRecord: sourceRecord.value, | |
| 460 | − runtimeAuthArtifact: authArtifact, | |
| 461 | − importAuthArtifact, | |
| 462 | − }); | |
| 417 | + return localSource.source; | |
| 463 | 418 | }); |
| 464 | 419 | |
| 465 | 420 | const configSourceFromLocalSource = (input: { |
| 466 | 421 | source: Source; |
| 467 | 422 | existingConfigAuth: LocalConfigSecretInput | undefined; |
| 468 | − config: Awaited<ReturnType<typeof loadLocalExecutorConfig>>["config"]; | |
| 423 | + config: LoadedLocalExecutorConfig["config"]; | |
| 469 | 424 | }): LocalConfigSource => { |
| 470 | 425 | const auth = configAuthFromSource({ |
| 471 | 426 | source: input.source, |
| @@ -554,113 +509,61 @@ const removeAuthArtifactsForSource = (rows: SqlControlPlaneRows, input: { · 82 unmodified lines | ||
| 554 | 509 | return existingAuthArtifacts.length; |
| 555 | 510 | }); |
| 556 | 511 | |
| 557 | −const cleanupOrphanedRecipeData = (rows: SqlControlPlaneRows, input: { | |
| 558 | − recipeId: SourceRecipeId; | |
| 559 | − recipeRevisionId: SourceRecipeRevisionId; | |
| 560 | −}) => | |
| 561 | − Effect.gen(function* () { | |
| 562 | − const revisionReferenceCount = yield* rows.sources.countByRecipeRevisionId( | |
| 563 | − input.recipeRevisionId, | |
| 564 | − ); | |
| 565 | − if (revisionReferenceCount === 0) { | |
| 566 | − yield* rows.sourceRecipeDocuments.removeByRevisionId(input.recipeRevisionId); | |
| 567 | − yield* rows.sourceRecipeSchemaBundles.removeByRevisionId(input.recipeRevisionId); | |
| 568 | − yield* rows.sourceRecipeOperations.removeByRevisionId(input.recipeRevisionId); | |
| 569 | − } | |
| 570 | − | |
| 571 | − const recipeReferenceCount = yield* rows.sources.countByRecipeId(input.recipeId); | |
| 572 | − if (recipeReferenceCount > 0) { | |
| 573 | − return; | |
| 574 | − } | |
| 575 | − | |
| 576 | − const recipeRevisions = yield* rows.sourceRecipeRevisions.listByRecipeId(input.recipeId); | |
| 577 | − yield* Effect.forEach( | |
| 578 | − recipeRevisions, | |
| 579 | − (recipeRevision) => | |
| 580 | − Effect.all([ | |
| 581 | − rows.sourceRecipeDocuments.removeByRevisionId(recipeRevision.id), | |
| 582 | − rows.sourceRecipeSchemaBundles.removeByRevisionId(recipeRevision.id), | |
| 583 | − rows.sourceRecipeOperations.removeByRevisionId(recipeRevision.id), | |
| 584 | − ]), | |
| 585 | − { discard: true }, | |
| 586 | − ); | |
| 587 | − yield* rows.sourceRecipeRevisions.removeByRecipeId(input.recipeId); | |
| 588 | − yield* rows.sourceRecipes.removeById(input.recipeId); | |
| 589 | − }); | |
| 590 | − | |
| 591 | 512 | export const removeSourceById = (rows: SqlControlPlaneRows, input: { |
| 592 | 513 | workspaceId: WorkspaceId; |
| 593 | 514 | sourceId: Source["id"]; |
| 594 | 515 | }) => |
| 595 | 516 | Effect.gen(function* () { |
| 596 | 517 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(input.workspaceId); |
| 597 | − if (localWorkspace !== null) { | |
| 598 | − if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { | |
| 599 | − return false; | |
| 600 | − } | |
| 601 | − | |
| 602 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 603 | − const sources = { | |
| 604 | − ...(projectConfig.sources ?? {}), | |
| 605 | − }; | |
| 606 | − delete sources[input.sourceId]; | |
| 607 | − yield* Effect.tryPromise({ | |
| 608 | − try: () => | |
| 609 | − writeProjectLocalExecutorConfig({ | |
| 610 | − context: localWorkspace.context, | |
| 611 | − config: { | |
| 612 | − ...projectConfig, | |
| 613 | − sources, | |
| 614 | − }, | |
| 615 | − }), | |
| 616 | − catch: (cause) => | |
| 617 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 618 | − }); | |
| 619 | − | |
| 620 | − const { | |
| 621 | − [input.sourceId]: _removedSource, | |
| 622 | − ...remainingSources | |
| 623 | − } = localWorkspace.workspaceState.sources; | |
| 624 | − const workspaceState: LocalWorkspaceState = { | |
| 625 | − ...localWorkspace.workspaceState, | |
| 626 | − sources: remainingSources, | |
| 627 | − }; | |
| 628 | − yield* Effect.tryPromise({ | |
| 629 | − try: () => | |
| 630 | − writeLocalWorkspaceState({ | |
| 631 | − context: localWorkspace.context, | |
| 632 | − state: workspaceState, | |
| 633 | − }), | |
| 634 | − catch: (cause) => | |
| 635 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 636 | − }); | |
| 637 | − yield* Effect.tryPromise({ | |
| 638 | − try: () => | |
| 639 | − removeLocalSourceArtifact({ | |
| 640 | − context: localWorkspace.context, | |
| 641 | − sourceId: input.sourceId, | |
| 642 | − }), | |
| 643 | − catch: (cause) => | |
| 644 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 645 | − }); | |
| 646 | − | |
| 647 | − yield* rows.sourceAuthSessions.removeByWorkspaceAndSourceId( | |
| 648 | − input.workspaceId, | |
| 649 | − input.sourceId, | |
| 650 | − ); | |
| 651 | − yield* rows.sourceOauthClients.removeByWorkspaceAndSourceId({ | |
| 652 | − workspaceId: input.workspaceId, | |
| 653 | − sourceId: input.sourceId, | |
| 654 | − }); | |
| 655 | − yield* removeAuthArtifactsForSource(rows, input); | |
| 656 | − return true; | |
| 657 | − } | |
| 658 | − | |
| 659 | − const sourceRecord = yield* rows.sources.getByWorkspaceAndId(input.workspaceId, input.sourceId); | |
| 660 | − if (Option.isNone(sourceRecord)) { | |
| 518 | + if (!localWorkspace.loadedConfig.config?.sources?.[input.sourceId]) { | |
| 661 | 519 | return false; |
| 662 | 520 | } |
| 663 | 521 | |
| 522 | + const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 523 | + const sources = { | |
| 524 | + ...(projectConfig.sources ?? {}), | |
| 525 | + }; | |
| 526 | + delete sources[input.sourceId]; | |
| 527 | + yield* Effect.tryPromise({ | |
| 528 | + try: () => | |
| 529 | + writeProjectLocalExecutorConfig({ | |
| 530 | + context: localWorkspace.context, | |
| 531 | + config: { | |
| 532 | + ...projectConfig, | |
| 533 | + sources, | |
| 534 | + }, | |
| 535 | + }), | |
| 536 | + catch: (cause) => | |
| 537 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 538 | + }); | |
| 539 | + | |
| 540 | + const { | |
| 541 | + [input.sourceId]: _removedSource, | |
| 542 | + ...remainingSources | |
| 543 | + } = localWorkspace.workspaceState.sources; | |
| 544 | + const workspaceState: LocalWorkspaceState = { | |
| 545 | + ...localWorkspace.workspaceState, | |
| 546 | + sources: remainingSources, | |
| 547 | + }; | |
| 548 | + yield* Effect.tryPromise({ | |
| 549 | + try: () => | |
| 550 | + writeLocalWorkspaceState({ | |
| 551 | + context: localWorkspace.context, | |
| 552 | + state: workspaceState, | |
| 553 | + }), | |
| 554 | + catch: (cause) => | |
| 555 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 556 | + }); | |
| 557 | + yield* Effect.tryPromise({ | |
| 558 | + try: () => | |
| 559 | + removeLocalSourceArtifact({ | |
| 560 | + context: localWorkspace.context, | |
| 561 | + sourceId: input.sourceId, | |
| 562 | + }), | |
| 563 | + catch: (cause) => | |
| 564 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 565 | + }); | |
| 566 | + | |
| 664 | 567 | yield* rows.sourceAuthSessions.removeByWorkspaceAndSourceId( |
| 665 | 568 | input.workspaceId, |
| 666 | 569 | input.sourceId, |
| @@ -670,15 +573,6 @@ export const removeSourceById = (rows: SqlControlPlaneRows, input: { · 3 unmodified lines | ||
| 670 | 573 | sourceId: input.sourceId, |
| 671 | 574 | }); |
| 672 | 575 | yield* removeAuthArtifactsForSource(rows, input); |
| 673 | − const removed = yield* rows.sources.removeByWorkspaceAndId(input.workspaceId, input.sourceId); | |
| 674 | − if (!removed) { | |
| 675 | − return false; | |
| 676 | − } | |
| 677 | − | |
| 678 | − yield* cleanupOrphanedRecipeData(rows, { | |
| 679 | − recipeId: sourceRecord.value.recipeId, | |
| 680 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 681 | − }); | |
| 682 | 576 | |
| 683 | 577 | return true; |
| 684 | 578 | }); |
| @@ -692,157 +586,20 @@ export const persistSource = ( · 7 unmodified lines | ||
| 692 | 586 | ) => |
| 693 | 587 | Effect.gen(function* () { |
| 694 | 588 | const localWorkspace = yield* resolveRuntimeLocalWorkspace(source.workspaceId); |
| 695 | − if (localWorkspace !== null) { | |
| 696 | − const nextSource = { | |
| 697 | − ...source, | |
| 698 | − id: | |
| 699 | − localWorkspace.loadedConfig.config?.sources?.[source.id] | |
| 700 | − || localWorkspace.workspaceState.sources[source.id] | |
| 701 | − ? source.id | |
| 702 | − : deriveLocalSourceId( | |
| 703 | − source, | |
| 704 | − new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})), | |
| 705 | − ), | |
| 706 | − } satisfies Source; | |
| 707 | − const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({ | |
| 708 | − workspaceId: nextSource.workspaceId, | |
| 709 | − sourceId: nextSource.id, | |
| 710 | − }); | |
| 711 | − const existingRuntimeAuthArtifact = selectExactAuthArtifact({ | |
| 712 | − authArtifacts: existingAuthArtifacts, | |
| 713 | − actorAccountId: options.actorAccountId, | |
| 714 | − slot: "runtime", | |
| 715 | − }); | |
| 716 | − const existingImportAuthArtifact = selectExactAuthArtifact({ | |
| 717 | − authArtifacts: existingAuthArtifacts, | |
| 718 | − actorAccountId: options.actorAccountId, | |
| 719 | − slot: "import", | |
| 720 | − }); | |
| 721 | − const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 722 | − const sources = { | |
| 723 | − ...(projectConfig.sources ?? {}), | |
| 724 | − }; | |
| 725 | − const existingConfigSource = sources[nextSource.id]; | |
| 726 | − sources[nextSource.id] = configSourceFromLocalSource({ | |
| 727 | − source: nextSource, | |
| 728 | − existingConfigAuth: existingConfigSource?.connection.auth, | |
| 729 | − config: localWorkspace.loadedConfig.config, | |
| 730 | − }); | |
| 731 | − yield* Effect.tryPromise({ | |
| 732 | − try: () => | |
| 733 | − writeProjectLocalExecutorConfig({ | |
| 734 | − context: localWorkspace.context, | |
| 735 | − config: { | |
| 736 | − ...projectConfig, | |
| 737 | − sources, | |
| 738 | − }, | |
| 739 | − }), | |
| 740 | − catch: (cause) => | |
| 741 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 742 | − }); | |
| 743 | − | |
| 744 | − const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ | |
| 745 | − source: nextSource, | |
| 746 | − recipeId: stableSourceRecipeId(nextSource), | |
| 747 | − recipeRevisionId: stableSourceRecipeRevisionId(nextSource), | |
| 748 | − actorAccountId: options.actorAccountId, | |
| 749 | − existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null, | |
| 750 | − existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null, | |
| 751 | − }); | |
| 752 | − | |
| 753 | − if (runtimeAuthArtifact === null) { | |
| 754 | − if (existingRuntimeAuthArtifact !== null) { | |
| 755 | − yield* removeAuthLeaseAndSecrets(rows, { | |
| 756 | − authArtifactId: existingRuntimeAuthArtifact.id, | |
| 757 | − }); | |
| 758 | − } | |
| 759 | − yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({ | |
| 760 | − workspaceId: nextSource.workspaceId, | |
| 761 | − sourceId: nextSource.id, | |
| 762 | − actorAccountId: options.actorAccountId ?? null, | |
| 763 | − slot: "runtime", | |
| 764 | − }); | |
| 765 | − } else { | |
| 766 | − yield* rows.authArtifacts.upsert(runtimeAuthArtifact); | |
| 767 | − if ( | |
| 768 | − existingRuntimeAuthArtifact !== null | |
| 769 | − && existingRuntimeAuthArtifact.id !== runtimeAuthArtifact.id | |
| 770 | − ) { | |
| 771 | − yield* removeAuthLeaseAndSecrets(rows, { | |
| 772 | − authArtifactId: existingRuntimeAuthArtifact.id, | |
| 773 | − }); | |
| 774 | − } | |
| 775 | − } | |
| 776 | − | |
| 777 | − yield* cleanupAuthArtifactSecretRefs(rows, { | |
| 778 | − previous: existingRuntimeAuthArtifact ?? null, | |
| 779 | − next: runtimeAuthArtifact, | |
| 780 | − }); | |
| 781 | − | |
| 782 | − if (importAuthArtifact === null) { | |
| 783 | − if (existingImportAuthArtifact !== null) { | |
| 784 | − yield* removeAuthLeaseAndSecrets(rows, { | |
| 785 | − authArtifactId: existingImportAuthArtifact.id, | |
| 786 | − }); | |
| 787 | − } | |
| 788 | − yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({ | |
| 789 | − workspaceId: nextSource.workspaceId, | |
| 790 | − sourceId: nextSource.id, | |
| 791 | − actorAccountId: options.actorAccountId ?? null, | |
| 792 | − slot: "import", | |
| 793 | − }); | |
| 794 | − } else { | |
| 795 | − yield* rows.authArtifacts.upsert(importAuthArtifact); | |
| 796 | − if ( | |
| 797 | − existingImportAuthArtifact !== null | |
| 798 | − && existingImportAuthArtifact.id !== importAuthArtifact.id | |
| 799 | − ) { | |
| 800 | − yield* removeAuthLeaseAndSecrets(rows, { | |
| 801 | − authArtifactId: existingImportAuthArtifact.id, | |
| 802 | − }); | |
| 803 | − } | |
| 804 | − } | |
| 805 | − | |
| 806 | − yield* cleanupAuthArtifactSecretRefs(rows, { | |
| 807 | − previous: existingImportAuthArtifact ?? null, | |
| 808 | − next: importAuthArtifact, | |
| 809 | − }); | |
| 810 | − | |
| 811 | − const existingSourceState = localWorkspace.workspaceState.sources[nextSource.id]; | |
| 812 | − const workspaceState: LocalWorkspaceState = { | |
| 813 | − ...localWorkspace.workspaceState, | |
| 814 | − sources: { | |
| 815 | − ...localWorkspace.workspaceState.sources, | |
| 816 | − [nextSource.id]: { | |
| 817 | − status: nextSource.status, | |
| 818 | − lastError: nextSource.lastError, | |
| 819 | − sourceHash: nextSource.sourceHash, | |
| 820 | − createdAt: existingSourceState?.createdAt ?? nextSource.createdAt, | |
| 821 | − updatedAt: nextSource.updatedAt, | |
| 822 | − }, | |
| 823 | − }, | |
| 824 | − }; | |
| 825 | − yield* Effect.tryPromise({ | |
| 826 | − try: () => | |
| 827 | − writeLocalWorkspaceState({ | |
| 828 | − context: localWorkspace.context, | |
| 829 | − state: workspaceState, | |
| 830 | − }), | |
| 831 | − catch: (cause) => | |
| 832 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 833 | − }); | |
| 834 | − | |
| 835 | − return yield* loadSourceById(rows, { | |
| 836 | − workspaceId: source.workspaceId, | |
| 837 | − sourceId: nextSource.id, | |
| 838 | − actorAccountId: options.actorAccountId, | |
| 839 | − }); | |
| 840 | − } | |
| 841 | − | |
| 842 | − const existing = yield* rows.sources.getByWorkspaceAndId(source.workspaceId, source.id); | |
| 589 | + const nextSource = { | |
| 590 | + ...source, | |
| 591 | + id: | |
| 592 | + localWorkspace.loadedConfig.config?.sources?.[source.id] | |
| 593 | + || localWorkspace.workspaceState.sources[source.id] | |
| 594 | + ? source.id | |
| 595 | + : deriveLocalSourceId( | |
| 596 | + source, | |
| 597 | + new Set(Object.keys(localWorkspace.loadedConfig.config?.sources ?? {})), | |
| 598 | + ), | |
| 599 | + } satisfies Source; | |
| 843 | 600 | const existingAuthArtifacts = yield* rows.authArtifacts.listByWorkspaceAndSourceId({ |
| 844 | − workspaceId: source.workspaceId, | |
| 845 | − sourceId: source.id, | |
| 601 | + workspaceId: nextSource.workspaceId, | |
| 602 | + sourceId: nextSource.id, | |
| 846 | 603 | }); |
| 847 | 604 | const existingRuntimeAuthArtifact = selectExactAuthArtifact({ |
| 848 | 605 | authArtifacts: existingAuthArtifacts, |
| @@ -854,73 +611,38 @@ export const persistSource = ( · 5 unmodified lines | ||
| 854 | 611 | actorAccountId: options.actorAccountId, |
| 855 | 612 | slot: "import", |
| 856 | 613 | }); |
| 857 | − | |
| 858 | − const nextRecipeId = stableSourceRecipeId(source); | |
| 859 | − const nextRecipeRevisionId = Option.isSome(existing) && existing.value.recipeId === nextRecipeId | |
| 860 | − ? existing.value.recipeRevisionId | |
| 861 | − : stableSourceRecipeRevisionId(source); | |
| 862 | − const existingTargetRevision = yield* rows.sourceRecipeRevisions.getById(nextRecipeRevisionId); | |
| 863 | − const nextRevision = createSourceRecipeRevisionRecord({ | |
| 864 | − source, | |
| 865 | − recipeId: nextRecipeId, | |
| 866 | − recipeRevisionId: nextRecipeRevisionId, | |
| 867 | − revisionNumber: Option.isSome(existingTargetRevision) | |
| 868 | − ? existingTargetRevision.value.revisionNumber | |
| 869 | − : 1, | |
| 870 | − manifestJson: Option.isSome(existingTargetRevision) | |
| 871 | − ? existingTargetRevision.value.manifestJson | |
| 872 | − : null, | |
| 873 | − manifestHash: Option.isSome(existingTargetRevision) | |
| 874 | − ? existingTargetRevision.value.manifestHash | |
| 875 | − : null, | |
| 876 | − materializationHash: Option.isSome(existingTargetRevision) | |
| 877 | − ? existingTargetRevision.value.materializationHash | |
| 878 | − : null, | |
| 614 | + const projectConfig = cloneJson(localWorkspace.loadedConfig.projectConfig ?? {}); | |
| 615 | + const sources = { | |
| 616 | + ...(projectConfig.sources ?? {}), | |
| 617 | + }; | |
| 618 | + const existingConfigSource = sources[nextSource.id]; | |
| 619 | + sources[nextSource.id] = configSourceFromLocalSource({ | |
| 620 | + source: nextSource, | |
| 621 | + existingConfigAuth: existingConfigSource?.connection.auth, | |
| 622 | + config: localWorkspace.loadedConfig.config, | |
| 879 | 623 | }); |
| 880 | − | |
| 881 | − const nextRecipe = createSourceRecipeRecord({ | |
| 882 | − source, | |
| 883 | − recipeId: nextRecipeId, | |
| 884 | − latestRevisionId: nextRevision.id, | |
| 624 | + yield* Effect.tryPromise({ | |
| 625 | + try: () => | |
| 626 | + writeProjectLocalExecutorConfig({ | |
| 627 | + context: localWorkspace.context, | |
| 628 | + config: { | |
| 629 | + ...projectConfig, | |
| 630 | + sources, | |
| 631 | + }, | |
| 632 | + }), | |
| 633 | + catch: (cause) => | |
| 634 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 885 | 635 | }); |
| 886 | 636 | |
| 887 | − const { sourceRecord, runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ | |
| 888 | − source, | |
| 889 | − recipeId: nextRecipe.id, | |
| 890 | − recipeRevisionId: nextRevision.id, | |
| 637 | + const { runtimeAuthArtifact, importAuthArtifact } = splitSourceForStorage({ | |
| 638 | + source: nextSource, | |
| 639 | + recipeId: stableSourceRecipeId(nextSource), | |
| 640 | + recipeRevisionId: stableSourceRecipeRevisionId(nextSource), | |
| 891 | 641 | actorAccountId: options.actorAccountId, |
| 892 | 642 | existingRuntimeAuthArtifactId: existingRuntimeAuthArtifact?.id ?? null, |
| 893 | 643 | existingImportAuthArtifactId: existingImportAuthArtifact?.id ?? null, |
| 894 | 644 | }); |
| 895 | 645 | |
| 896 | − if (Option.isNone(existing)) { | |
| 897 | − yield* rows.sources.insert(sourceRecord); | |
| 898 | − } else { | |
| 899 | − const { | |
| 900 | − id: _id, | |
| 901 | − workspaceId: _workspaceId, | |
| 902 | − createdAt: _createdAt, | |
| 903 | − ...patch | |
| 904 | − } = sourceRecord; | |
| 905 | − yield* rows.sources.update(source.workspaceId, source.id, patch); | |
| 906 | − } | |
| 907 | − | |
| 908 | − yield* rows.sourceRecipes.upsert(nextRecipe); | |
| 909 | − yield* rows.sourceRecipeRevisions.upsert(nextRevision); | |
| 910 | − | |
| 911 | − if ( | |
| 912 | − Option.isSome(existing) | |
| 913 | − && ( | |
| 914 | − existing.value.recipeId !== nextRecipeId | |
| 915 | − || existing.value.recipeRevisionId !== nextRecipeRevisionId | |
| 916 | − ) | |
| 917 | − ) { | |
| 918 | − yield* cleanupOrphanedRecipeData(rows, { | |
| 919 | − recipeId: existing.value.recipeId, | |
| 920 | − recipeRevisionId: existing.value.recipeRevisionId, | |
| 921 | − }); | |
| 922 | − } | |
| 923 | − | |
| 924 | 646 | if (runtimeAuthArtifact === null) { |
| 925 | 647 | if (existingRuntimeAuthArtifact !== null) { |
| 926 | 648 | yield* removeAuthLeaseAndSecrets(rows, { |
| @@ -928,8 +650,8 @@ export const persistSource = ( · 1 unmodified line | ||
| 928 | 650 | }); |
| 929 | 651 | } |
| 930 | 652 | yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({ |
| 931 | − workspaceId: source.workspaceId, | |
| 932 | − sourceId: source.id, | |
| 653 | + workspaceId: nextSource.workspaceId, | |
| 654 | + sourceId: nextSource.id, | |
| 933 | 655 | actorAccountId: options.actorAccountId ?? null, |
| 934 | 656 | slot: "runtime", |
| 935 | 657 | }); |
| @@ -957,8 +679,8 @@ export const persistSource = ( · 21 unmodified lines | ||
| 957 | 679 | }); |
| 958 | 680 | } |
| 959 | 681 | yield* rows.authArtifacts.removeByWorkspaceSourceAndActor({ |
| 960 | − workspaceId: source.workspaceId, | |
| 961 | − sourceId: source.id, | |
| 682 | + workspaceId: nextSource.workspaceId, | |
| 683 | + sourceId: nextSource.id, | |
| 962 | 684 | actorAccountId: options.actorAccountId ?? null, |
| 963 | 685 | slot: "import", |
| 964 | 686 | }); |
| @@ -979,5 +701,33 @@ export const persistSource = ( · 14 unmodified lines | ||
| 979 | 701 | next: importAuthArtifact, |
| 980 | 702 | }); |
| 981 | 703 | |
| 982 | − return source; | |
| 704 | + const existingSourceState = localWorkspace.workspaceState.sources[nextSource.id]; | |
| 705 | + const workspaceState: LocalWorkspaceState = { | |
| 706 | + ...localWorkspace.workspaceState, | |
| 707 | + sources: { | |
| 708 | + ...localWorkspace.workspaceState.sources, | |
| 709 | + [nextSource.id]: { | |
| 710 | + status: nextSource.status, | |
| 711 | + lastError: nextSource.lastError, | |
| 712 | + sourceHash: nextSource.sourceHash, | |
| 713 | + createdAt: existingSourceState?.createdAt ?? nextSource.createdAt, | |
| 714 | + updatedAt: nextSource.updatedAt, | |
| 715 | + }, | |
| 716 | + }, | |
| 717 | + }; | |
| 718 | + yield* Effect.tryPromise({ | |
| 719 | + try: () => | |
| 720 | + writeLocalWorkspaceState({ | |
| 721 | + context: localWorkspace.context, | |
| 722 | + state: workspaceState, | |
| 723 | + }), | |
| 724 | + catch: (cause) => | |
| 725 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 726 | + }); | |
| 727 | + | |
| 728 | + return yield* loadSourceById(rows, { | |
| 729 | + workspaceId: nextSource.workspaceId, | |
| 730 | + sourceId: nextSource.id, | |
| 731 | + actorAccountId: options.actorAccountId, | |
| 732 | + }); | |
| 983 | 733 | }); |
| @@ -15,10 +15,10 @@ import { · 14 unmodified lines | ||
| 15 | 15 | |
| 16 | 16 | import { |
| 17 | 17 | ControlPlaneAuthHeaders, |
| 18 | − type SqlControlPlaneRuntime, | |
| 18 | + type ControlPlaneRuntime, | |
| 19 | 19 | } from "./index"; |
| 20 | 20 | |
| 21 | −const createClientLayer = (runtime: SqlControlPlaneRuntime) => { | |
| 21 | +const createClientLayer = (runtime: ControlPlaneRuntime) => { | |
| 22 | 22 | const apiLayer = createControlPlaneApiLayer(runtime.runtimeLayer); |
| 23 | 23 | |
| 24 | 24 | return HttpApiBuilder.serve().pipe( |
| @@ -48,7 +48,7 @@ type ControlPlaneClient = Effect.Effect.Success< · 23 unmodified lines | ||
| 48 | 48 | |
| 49 | 49 | export const withControlPlaneClient = <A, E>( |
| 50 | 50 | input: { |
| 51 | − runtime: SqlControlPlaneRuntime; | |
| 51 | + runtime: ControlPlaneRuntime; | |
| 52 | 52 | accountId?: string; |
| 53 | 53 | }, |
| 54 | 54 | f: (client: ControlPlaneClient) => Effect.Effect<A, E, never>, |
| @@ -1,5 +1,8 @@ | ||
| 1 | 1 | import { randomUUID } from "node:crypto"; |
| 2 | +import { mkdtempSync } from "node:fs"; | |
| 2 | 3 | import { createServer } from "node:http"; |
| 4 | +import { tmpdir } from "node:os"; | |
| 5 | +import { join } from "node:path"; | |
| 3 | 6 | |
| 4 | 7 | import { |
| 5 | 8 | HttpApi, |
| @@ -45,7 +48,6 @@ import * as Effect from "effect/Effect"; · 39 unmodified lines | ||
| 45 | 48 | import * as Exit from "effect/Exit"; |
| 46 | 49 | import * as Layer from "effect/Layer"; |
| 47 | 50 | import * as Scope from "effect/Scope"; |
| 48 | −import * as Option from "effect/Option"; | |
| 49 | 51 | import { Schema } from "effect"; |
| 50 | 52 | import * as z from "zod/v4"; |
| 51 | 53 | |
| @@ -59,9 +61,28 @@ import { · 7 unmodified lines | ||
| 59 | 61 | createRuntimeSourceAuthService, |
| 60 | 62 | type RuntimeSourceAuthService, |
| 61 | 63 | } from "./source-auth-service"; |
| 64 | +import { | |
| 65 | + loadLocalExecutorConfig, | |
| 66 | + resolveLocalWorkspaceContext, | |
| 67 | + writeProjectLocalExecutorConfig, | |
| 68 | +} from "./local-config"; | |
| 62 | 69 | import { createLiveExecutionManager } from "./live-execution"; |
| 63 | −import { persistSource } from "./source-store"; | |
| 64 | −import { persistMcpRecipeMaterializationFromManifest } from "./source-materialization"; | |
| 70 | +import { | |
| 71 | + RuntimeLocalWorkspaceService, | |
| 72 | + type RuntimeLocalWorkspaceState, | |
| 73 | +} from "./local-runtime-context"; | |
| 74 | +import { | |
| 75 | + buildLocalSourceArtifact, | |
| 76 | + readLocalSourceArtifact, | |
| 77 | + writeLocalSourceArtifact, | |
| 78 | +} from "./local-source-artifacts"; | |
| 79 | +import { | |
| 80 | + loadLocalWorkspaceState, | |
| 81 | + writeLocalWorkspaceState, | |
| 82 | +} from "./local-workspace-state"; | |
| 83 | +import { | |
| 84 | + materializationFromMcpManifestEntries, | |
| 85 | +} from "./source-adapters/mcp"; | |
| 65 | 86 | import { createWorkspaceExecutionEnvironmentResolver } from "./workspace-execution-environment"; |
| 66 | 87 | |
| 67 | 88 | type CountedMcpServer = { |
| @@ -579,7 +600,7 @@ const makeGraphqlServer = Effect.acquireRelease( · 511 unmodified lines | ||
| 579 | 600 | ); |
| 580 | 601 | |
| 581 | 602 | const persistConnectedEchoTool = (input: { |
| 582 | − persistence: SqlControlPlanePersistence; | |
| 603 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | |
| 583 | 604 | endpoint: string; |
| 584 | 605 | workspaceId: WorkspaceId; |
| 585 | 606 | sourceId: SourceId; |
| @@ -610,32 +631,85 @@ const persistConnectedEchoTool = (input: { · 24 unmodified lines | ||
| 610 | 631 | updatedAt: now, |
| 611 | 632 | }; |
| 612 | 633 | |
| 613 | − yield* persistSource(input.persistence.rows, source); | |
| 614 | − yield* persistMcpRecipeMaterializationFromManifest({ | |
| 615 | − rows: input.persistence.rows, | |
| 616 | − source, | |
| 617 | − manifestEntries: [ | |
| 618 | − { | |
| 619 | − toolId: "echo", | |
| 620 | − toolName: "echo", | |
| 621 | − description: "Echo the provided string", | |
| 622 | − inputSchemaJson: JSON.stringify({ | |
| 623 | − type: "object", | |
| 624 | − properties: { | |
| 625 | − value: { | |
| 626 | − type: "string", | |
| 634 | + yield* Effect.tryPromise({ | |
| 635 | + try: () => | |
| 636 | + writeProjectLocalExecutorConfig({ | |
| 637 | + context: input.runtimeLocalWorkspace.context, | |
| 638 | + config: { | |
| 639 | + sources: { | |
| 640 | + [input.sourceId]: { | |
| 641 | + kind: "mcp", | |
| 642 | + name: source.name, | |
| 643 | + ...(source.namespace ? { namespace: source.namespace } : {}), | |
| 644 | + connection: { | |
| 645 | + endpoint: source.endpoint, | |
| 646 | + }, | |
| 647 | + binding: { | |
| 648 | + transport: "streamable-http", | |
| 649 | + }, | |
| 627 | 650 | }, |
| 628 | 651 | }, |
| 629 | − required: ["value"], | |
| 630 | − additionalProperties: false, | |
| 652 | + }, | |
| 653 | + }), | |
| 654 | + catch: (cause) => | |
| 655 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 656 | + }); | |
| 657 | + yield* Effect.tryPromise({ | |
| 658 | + try: () => | |
| 659 | + writeLocalSourceArtifact({ | |
| 660 | + context: input.runtimeLocalWorkspace.context, | |
| 661 | + sourceId: source.id, | |
| 662 | + artifact: buildLocalSourceArtifact({ | |
| 663 | + source, | |
| 664 | + materialization: materializationFromMcpManifestEntries({ | |
| 665 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 666 | + endpoint: source.endpoint, | |
| 667 | + manifestEntries: [ | |
| 668 | + { | |
| 669 | + toolId: "echo", | |
| 670 | + toolName: "echo", | |
| 671 | + description: "Echo the provided string", | |
| 672 | + inputSchemaJson: JSON.stringify({ | |
| 673 | + type: "object", | |
| 674 | + properties: { | |
| 675 | + value: { type: "string" }, | |
| 676 | + }, | |
| 677 | + required: ["value"], | |
| 678 | + additionalProperties: false, | |
| 679 | + }), | |
| 680 | + }, | |
| 681 | + ], | |
| 682 | + }), | |
| 631 | 683 | }), |
| 632 | − }, | |
| 633 | − ], | |
| 684 | + }), | |
| 685 | + catch: (cause) => | |
| 686 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 634 | 687 | }); |
| 635 | − }); | |
| 688 | + yield* Effect.tryPromise({ | |
| 689 | + try: () => | |
| 690 | + writeLocalWorkspaceState({ | |
| 691 | + context: input.runtimeLocalWorkspace.context, | |
| 692 | + state: { | |
| 693 | + version: 1, | |
| 694 | + sources: { | |
| 695 | + [input.sourceId]: { | |
| 696 | + status: "connected", | |
| 697 | + lastError: null, | |
| 698 | + sourceHash: null, | |
| 699 | + createdAt: now, | |
| 700 | + updatedAt: now, | |
| 701 | + }, | |
| 702 | + }, | |
| 703 | + policies: {}, | |
| 704 | + }, | |
| 705 | + }), | |
| 706 | + catch: (cause) => | |
| 707 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 708 | + }); | |
| 709 | + }) as Effect.Effect<void, Error, never>; | |
| 636 | 710 | |
| 637 | 711 | const persistConnectedGithubOpenApiSource = (input: { |
| 638 | − persistence: SqlControlPlanePersistence; | |
| 712 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState; | |
| 639 | 713 | workspaceId: WorkspaceId; |
| 640 | 714 | sourceId: SourceId; |
| 641 | 715 | endpoint?: string; |
| @@ -831,7 +905,22 @@ const persistConnectedGithubOpenApiSource = (input: { · 189 unmodified lines | ||
| 831 | 905 | updatedAt: now, |
| 832 | 906 | }; |
| 833 | 907 | |
| 834 | − yield* persistSource(input.persistence.rows, source); | |
| 908 | + const configAuth = | |
| 909 | + source.auth.kind === "bearer" | |
| 910 | + ? source.auth.token.providerId === "params" | |
| 911 | + ? { | |
| 912 | + source: "params" as const, | |
| 913 | + provider: "params", | |
| 914 | + id: source.auth.token.handle, | |
| 915 | + } | |
| 916 | + : source.auth.token.providerId === "env" | |
| 917 | + ? source.auth.token.handle | |
| 918 | + : { | |
| 919 | + source: "env" as const, | |
| 920 | + provider: "env", | |
| 921 | + id: source.auth.token.handle, | |
| 922 | + } | |
| 923 | + : undefined; | |
| 835 | 924 | |
| 836 | 925 | const manifest = yield* extractOpenApiManifest( |
| 837 | 926 | source.name, |
| @@ -842,131 +931,189 @@ const persistConnectedGithubOpenApiSource = (input: { · 4 unmodified lines | ||
| 842 | 931 | ), |
| 843 | 932 | ); |
| 844 | 933 | const definitions = compileOpenApiToolDefinitions(manifest); |
| 845 | − const sourceRecord = | |
| 846 | − yield* input.persistence.rows.sources.getByWorkspaceAndId( | |
| 847 | − input.workspaceId, | |
| 848 | − input.sourceId, | |
| 849 | − ); | |
| 850 | − if (Option.isNone(sourceRecord)) { | |
| 851 | − return yield* Effect.fail( | |
| 852 | − new Error(`Missing stored source ${input.sourceId}`), | |
| 853 | − ); | |
| 854 | − } | |
| 855 | − | |
| 856 | − yield* input.persistence.rows.sourceRecipeRevisions.update( | |
| 857 | − sourceRecord.value.recipeRevisionId, | |
| 858 | − { | |
| 859 | − manifestJson: JSON.stringify(manifest), | |
| 860 | − manifestHash: manifest.sourceHash, | |
| 861 | − updatedAt: now, | |
| 862 | − }, | |
| 863 | − ); | |
| 864 | − yield* input.persistence.rows.sourceRecipeDocuments.replaceForRevision({ | |
| 865 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 866 | − documents: [ | |
| 867 | − { | |
| 868 | − id: `src_recipe_doc_${randomUUID()}`, | |
| 869 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 870 | − documentKind: "openapi", | |
| 871 | − documentKey: | |
| 872 | − typeof source.binding.specUrl === "string" | |
| 873 | − ? source.binding.specUrl | |
| 874 | − : source.endpoint, | |
| 875 | − contentText: openApiDocumentText, | |
| 876 | − contentHash: manifest.sourceHash, | |
| 877 | − fetchedAt: now, | |
| 878 | − createdAt: now, | |
| 879 | − updatedAt: now, | |
| 880 | − }, | |
| 881 | − ], | |
| 934 | + yield* Effect.tryPromise({ | |
| 935 | + try: () => | |
| 936 | + writeProjectLocalExecutorConfig({ | |
| 937 | + context: input.runtimeLocalWorkspace.context, | |
| 938 | + config: { | |
| 939 | + sources: { | |
| 940 | + [input.sourceId]: { | |
| 941 | + kind: "openapi", | |
| 942 | + name: source.name, | |
| 943 | + ...(source.namespace ? { namespace: source.namespace } : {}), | |
| 944 | + connection: { | |
| 945 | + endpoint: source.endpoint, | |
| 946 | + ...(configAuth !== undefined ? { auth: configAuth } : {}), | |
| 947 | + }, | |
| 948 | + binding: { | |
| 949 | + specUrl: String(source.binding.specUrl), | |
| 950 | + defaultHeaders: null, | |
| 951 | + }, | |
| 952 | + }, | |
| 953 | + }, | |
| 954 | + }, | |
| 955 | + }), | |
| 956 | + catch: (cause) => | |
| 957 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 882 | 958 | }); |
| 883 | 959 | const refEntries = Object.entries(manifest.refHintTable ?? {}); |
| 884 | − yield* input.persistence.rows.sourceRecipeSchemaBundles.replaceForRevision({ | |
| 885 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 886 | − bundles: | |
| 887 | − refEntries.length > 0 | |
| 888 | − ? [ | |
| 889 | − { | |
| 890 | − id: SourceRecipeSchemaBundleIdSchema.make( | |
| 891 | − `src_recipe_bundle_${randomUUID()}`, | |
| 892 | − ), | |
| 893 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 894 | − bundleKind: "json_schema_ref_map", | |
| 895 | − refsJson: JSON.stringify( | |
| 896 | − Object.fromEntries( | |
| 897 | − refEntries.map(([ref, rawValue]) => { | |
| 898 | − try { | |
| 899 | − return [ref, JSON.parse(rawValue) as unknown]; | |
| 900 | − } catch { | |
| 901 | − return [ref, rawValue]; | |
| 902 | − } | |
| 903 | − }), | |
| 960 | + yield* Effect.tryPromise({ | |
| 961 | + try: () => | |
| 962 | + writeLocalSourceArtifact({ | |
| 963 | + context: input.runtimeLocalWorkspace.context, | |
| 964 | + sourceId: source.id, | |
| 965 | + artifact: buildLocalSourceArtifact({ | |
| 966 | + source: { | |
| 967 | + ...source, | |
| 968 | + sourceHash: manifest.sourceHash, | |
| 969 | + }, | |
| 970 | + materialization: { | |
| 971 | + manifestJson: JSON.stringify(manifest), | |
| 972 | + manifestHash: manifest.sourceHash, | |
| 973 | + sourceHash: manifest.sourceHash, | |
| 974 | + documents: [ | |
| 975 | + { | |
| 976 | + id: `src_recipe_doc_${randomUUID()}`, | |
| 977 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 978 | + documentKind: "openapi", | |
| 979 | + documentKey: | |
| 980 | + typeof source.binding.specUrl === "string" | |
| 981 | + ? source.binding.specUrl | |
| 982 | + : source.endpoint, | |
| 983 | + contentText: openApiDocumentText, | |
| 984 | + contentHash: manifest.sourceHash, | |
| 985 | + fetchedAt: now, | |
| 986 | + createdAt: now, | |
| 987 | + updatedAt: now, | |
| 988 | + }, | |
| 989 | + ], | |
| 990 | + schemaBundles: | |
| 991 | + refEntries.length > 0 | |
| 992 | + ? [ | |
| 993 | + { | |
| 994 | + id: SourceRecipeSchemaBundleIdSchema.make( | |
| 995 | + `src_recipe_bundle_${randomUUID()}`, | |
| 996 | + ), | |
| 997 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 998 | + bundleKind: "json_schema_ref_map", | |
| 999 | + refsJson: JSON.stringify( | |
| 1000 | + Object.fromEntries( | |
| 1001 | + refEntries.map(([ref, rawValue]) => { | |
| 1002 | + try { | |
| 1003 | + return [ref, JSON.parse(rawValue) as unknown]; | |
| 1004 | + } catch { | |
| 1005 | + return [ref, rawValue]; | |
| 1006 | + } | |
| 1007 | + }), | |
| 1008 | + ), | |
| 1009 | + ), | |
| 1010 | + contentHash: manifest.sourceHash, | |
| 1011 | + createdAt: now, | |
| 1012 | + updatedAt: now, | |
| 1013 | + }, | |
| 1014 | + ] | |
| 1015 | + : [], | |
| 1016 | + operations: definitions.map((definition) => { | |
| 1017 | + const presentation = buildOpenApiToolPresentation({ | |
| 1018 | + definition, | |
| 1019 | + }); | |
| 1020 | + const method = definition.method.toUpperCase(); | |
| 1021 | + | |
| 1022 | + return { | |
| 1023 | + id: `src_recipe_op_${randomUUID()}`, | |
| 1024 | + recipeRevisionId: "src_recipe_rev_materialization" as never, | |
| 1025 | + operationKey: definition.toolId, | |
| 1026 | + transportKind: "http", | |
| 1027 | + toolId: definition.toolId, | |
| 1028 | + title: definition.name, | |
| 1029 | + description: definition.description, | |
| 1030 | + operationKind: | |
| 1031 | + method === "GET" || method === "HEAD" | |
| 1032 | + ? "read" | |
| 1033 | + : method === "DELETE" | |
| 1034 | + ? "delete" | |
| 1035 | + : "write", | |
| 1036 | + searchText: normalizeSearchText( | |
| 1037 | + definition.toolId, | |
| 1038 | + definition.name, | |
| 1039 | + definition.description, | |
| 1040 | + definition.rawToolId, | |
| 1041 | + definition.operationId ?? undefined, | |
| 1042 | + definition.method, | |
| 1043 | + definition.path, | |
| 1044 | + definition.group, | |
| 1045 | + definition.leaf, | |
| 1046 | + definition.tags.join(" "), | |
| 904 | 1047 | ), |
| 905 | − ), | |
| 906 | − contentHash: manifest.sourceHash, | |
| 1048 | + inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 1049 | + outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 1050 | + providerKind: "openapi", | |
| 1051 | + providerDataJson: presentation.providerDataJson, | |
| 1052 | + createdAt: now, | |
| 1053 | + updatedAt: now, | |
| 1054 | + }; | |
| 1055 | + }), | |
| 1056 | + }, | |
| 1057 | + }), | |
| 1058 | + }), | |
| 1059 | + catch: (cause) => | |
| 1060 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1061 | + }); | |
| 1062 | + yield* Effect.tryPromise({ | |
| 1063 | + try: () => | |
| 1064 | + writeLocalWorkspaceState({ | |
| 1065 | + context: input.runtimeLocalWorkspace.context, | |
| 1066 | + state: { | |
| 1067 | + version: 1, | |
| 1068 | + sources: { | |
| 1069 | + [input.sourceId]: { | |
| 1070 | + status: "connected", | |
| 1071 | + lastError: null, | |
| 1072 | + sourceHash: manifest.sourceHash, | |
| 907 | 1073 | createdAt: now, |
| 908 | 1074 | updatedAt: now, |
| 909 | 1075 | }, |
| 910 | − ] | |
| 911 | − : [], | |
| 912 | − }); | |
| 913 | − yield* input.persistence.rows.sourceRecipeOperations.replaceForRevision({ | |
| 914 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 915 | − operations: definitions.map((definition) => { | |
| 916 | − const presentation = buildOpenApiToolPresentation({ | |
| 917 | − definition, | |
| 918 | − }); | |
| 919 | − const method = definition.method.toUpperCase(); | |
| 920 | − | |
| 921 | − return { | |
| 922 | − id: `src_recipe_op_${randomUUID()}`, | |
| 923 | − recipeRevisionId: sourceRecord.value.recipeRevisionId, | |
| 924 | − operationKey: definition.toolId, | |
| 925 | − transportKind: "http", | |
| 926 | − toolId: definition.toolId, | |
| 927 | − title: definition.name, | |
| 928 | − description: definition.description, | |
| 929 | − operationKind: | |
| 930 | − method === "GET" || method === "HEAD" | |
| 931 | − ? "read" | |
| 932 | − : method === "DELETE" | |
| 933 | − ? "delete" | |
| 934 | − : "write", | |
| 935 | − searchText: normalizeSearchText( | |
| 936 | − definition.toolId, | |
| 937 | − definition.name, | |
| 938 | − definition.description, | |
| 939 | − definition.rawToolId, | |
| 940 | − definition.operationId ?? undefined, | |
| 941 | − definition.method, | |
| 942 | − definition.path, | |
| 943 | − definition.group, | |
| 944 | − definition.leaf, | |
| 945 | − definition.tags.join(" "), | |
| 946 | − ), | |
| 947 | − inputSchemaJson: presentation.inputSchemaJson ?? null, | |
| 948 | − outputSchemaJson: presentation.outputSchemaJson ?? null, | |
| 949 | − providerKind: "openapi", | |
| 950 | − providerDataJson: presentation.providerDataJson, | |
| 951 | − createdAt: now, | |
| 952 | − updatedAt: now, | |
| 953 | − }; | |
| 954 | − }), | |
| 1076 | + }, | |
| 1077 | + policies: {}, | |
| 1078 | + }, | |
| 1079 | + }), | |
| 1080 | + catch: (cause) => | |
| 1081 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 955 | 1082 | }); |
| 956 | − yield* input.persistence.rows.sources.update( | |
| 957 | − input.workspaceId, | |
| 958 | − input.sourceId, | |
| 959 | − { | |
| 960 | − sourceHash: manifest.sourceHash, | |
| 961 | − updatedAt: now, | |
| 962 | − }, | |
| 963 | − ); | |
| 964 | 1083 | |
| 965 | 1084 | return; |
| 1085 | + }) as Effect.Effect<void, Error, never>; | |
| 1086 | + | |
| 1087 | +const makeRuntimeLocalWorkspaceState = (workspaceId: WorkspaceId): Effect.Effect<RuntimeLocalWorkspaceState, Error, never> => | |
| 1088 | + Effect.gen(function* () { | |
| 1089 | + const workspaceRoot = mkdtempSync(join(tmpdir(), "executor-workspace-env-")); | |
| 1090 | + const context = yield* Effect.tryPromise({ | |
| 1091 | + try: () => resolveLocalWorkspaceContext({ workspaceRoot }), | |
| 1092 | + catch: (cause) => | |
| 1093 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1094 | + }); | |
| 1095 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 1096 | + try: () => loadLocalExecutorConfig(context), | |
| 1097 | + catch: (cause) => | |
| 1098 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1099 | + }); | |
| 1100 | + return { | |
| 1101 | + context, | |
| 1102 | + installation: { | |
| 1103 | + workspaceId, | |
| 1104 | + accountId: AccountIdSchema.make("acc_local_test"), | |
| 1105 | + organizationId: "org_local_personal" as never, | |
| 1106 | + }, | |
| 1107 | + loadedConfig, | |
| 1108 | + } satisfies RuntimeLocalWorkspaceState; | |
| 966 | 1109 | }); |
| 967 | 1110 | |
| 968 | −const makeResolver = (persistence: SqlControlPlanePersistence) => | |
| 969 | − createWorkspaceExecutionEnvironmentResolver({ | |
| 1111 | +const makeResolver = ( | |
| 1112 | + persistence: SqlControlPlanePersistence, | |
| 1113 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 1114 | +) => | |
| 1115 | + ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | |
| 1116 | + createWorkspaceExecutionEnvironmentResolver({ | |
| 970 | 1117 | rows: persistence.rows, |
| 971 | 1118 | sourceAuthService: { |
| 972 | 1119 | getLocalServerBaseUrl: () => null, |
| @@ -983,19 +1130,25 @@ const makeResolver = (persistence: SqlControlPlanePersistence) => · 10 unmodified lines | ||
| 983 | 1130 | completeSourceCredentialSetup: () => |
| 984 | 1131 | Effect.fail(new Error("not implemented in test")), |
| 985 | 1132 | } as RuntimeSourceAuthService, |
| 986 | − }); | |
| 1133 | + })(input).pipe( | |
| 1134 | + Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 1135 | + )); | |
| 987 | 1136 | |
| 988 | 1137 | const makeResolverWithLiveSourceAuth = ( |
| 989 | 1138 | persistence: SqlControlPlanePersistence, |
| 1139 | + runtimeLocalWorkspace: RuntimeLocalWorkspaceState, | |
| 990 | 1140 | ) => |
| 991 | − createWorkspaceExecutionEnvironmentResolver({ | |
| 1141 | + ((input: Parameters<ReturnType<typeof createWorkspaceExecutionEnvironmentResolver>>[0]) => | |
| 1142 | + createWorkspaceExecutionEnvironmentResolver({ | |
| 992 | 1143 | rows: persistence.rows, |
| 993 | 1144 | sourceAuthService: createRuntimeSourceAuthService({ |
| 994 | 1145 | rows: persistence.rows, |
| 995 | 1146 | liveExecutionManager: createLiveExecutionManager(), |
| 996 | 1147 | getLocalServerBaseUrl: () => "http://127.0.0.1:8788", |
| 997 | 1148 | }), |
| 998 | − }); | |
| 1149 | + })(input).pipe( | |
| 1150 | + Effect.provideService(RuntimeLocalWorkspaceService, runtimeLocalWorkspace), | |
| 1151 | + )); | |
| 999 | 1152 | |
| 1000 | 1153 | describe("workspace-execution-environment", () => { |
| 1001 | 1154 | it.scoped("discovers persisted tools without live MCP listing", () => |
| @@ -1004,8 +1157,9 @@ describe("workspace-execution-environment", () => { · 2 unmodified lines | ||
| 1004 | 1157 | const persistence = yield* makePersistence; |
| 1005 | 1158 | const workspaceId = WorkspaceIdSchema.make("ws_catalog"); |
| 1006 | 1159 | const sourceId = SourceIdSchema.make("src_catalog"); |
| 1160 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1007 | 1161 | yield* persistConnectedEchoTool({ |
| 1008 | − persistence, | |
| 1162 | + runtimeLocalWorkspace, | |
| 1009 | 1163 | endpoint: server.endpoint, |
| 1010 | 1164 | workspaceId, |
| 1011 | 1165 | sourceId, |
| @@ -1017,7 +1171,7 @@ describe("workspace-execution-environment", () => { · 5 unmodified lines | ||
| 1017 | 1171 | cause instanceof Error ? cause : new Error(String(cause)), |
| 1018 | 1172 | }).pipe(Effect.orDie); |
| 1019 | 1173 | |
| 1020 | − const resolveEnvironment = makeResolver(persistence); | |
| 1174 | + const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1021 | 1175 | const environment = yield* resolveEnvironment({ |
| 1022 | 1176 | workspaceId, |
| 1023 | 1177 | accountId: AccountIdSchema.make("acc_catalog"), |
| @@ -1050,13 +1204,14 @@ describe("workspace-execution-environment", () => { · 26 unmodified lines | ||
| 1050 | 1204 | const persistence = yield* makePersistence; |
| 1051 | 1205 | const workspaceId = WorkspaceIdSchema.make("ws_github_discovery"); |
| 1052 | 1206 | const sourceId = SourceIdSchema.make("src_github_discovery"); |
| 1207 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1053 | 1208 | yield* persistConnectedGithubOpenApiSource({ |
| 1054 | − persistence, | |
| 1209 | + runtimeLocalWorkspace, | |
| 1055 | 1210 | workspaceId, |
| 1056 | 1211 | sourceId, |
| 1057 | 1212 | }); |
| 1058 | 1213 | |
| 1059 | − const resolveEnvironment = makeResolver(persistence); | |
| 1214 | + const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1060 | 1215 | const environment = yield* resolveEnvironment({ |
| 1061 | 1216 | workspaceId, |
| 1062 | 1217 | accountId: AccountIdSchema.make("acc_github_discovery"), |
| @@ -1103,14 +1258,15 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | ||
| 1103 | 1258 | const persistence = yield* makePersistence; |
| 1104 | 1259 | const workspaceId = WorkspaceIdSchema.make("ws_invoke"); |
| 1105 | 1260 | const sourceId = SourceIdSchema.make("src_invoke"); |
| 1261 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1106 | 1262 | yield* persistConnectedEchoTool({ |
| 1107 | − persistence, | |
| 1263 | + runtimeLocalWorkspace, | |
| 1108 | 1264 | endpoint: server.endpoint, |
| 1109 | 1265 | workspaceId, |
| 1110 | 1266 | sourceId, |
| 1111 | 1267 | }); |
| 1112 | 1268 | |
| 1113 | − const resolveEnvironment = makeResolver(persistence); | |
| 1269 | + const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1114 | 1270 | const environment = yield* resolveEnvironment({ |
| 1115 | 1271 | workspaceId, |
| 1116 | 1272 | accountId: AccountIdSchema.make("acc_invoke"), |
| @@ -1139,7 +1295,8 @@ describe("workspace-execution-environment", () => { · 22 unmodified lines | ||
| 1139 | 1295 | const specServer = yield* makeOpenApiSpecServer; |
| 1140 | 1296 | const persistence = yield* makePersistence; |
| 1141 | 1297 | const workspaceId = WorkspaceIdSchema.make("ws_add_openapi"); |
| 1142 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence); | |
| 1298 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1299 | + const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1143 | 1300 | const now = Date.now(); |
| 1144 | 1301 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1145 | 1302 | "sec_test_openapi_bearer", |
| @@ -1220,21 +1377,35 @@ describe("workspace-execution-environment", () => { · 74 unmodified lines | ||
| 1220 | 1377 | encodeURIComponent("exec_add_openapi:executor.sources.add:"), |
| 1221 | 1378 | ); |
| 1222 | 1379 | |
| 1223 | − const storedSource = | |
| 1224 | − yield* persistence.rows.sources.getByWorkspaceAndId( | |
| 1225 | − workspaceId, | |
| 1226 | − added.id, | |
| 1227 | − ); | |
| 1228 | − expect(Option.isSome(storedSource)).toBe(true); | |
| 1229 | − if (Option.isSome(storedSource)) { | |
| 1230 | − const documents = | |
| 1231 | − yield* persistence.rows.sourceRecipeDocuments.listByRevisionId( | |
| 1232 | − storedSource.value.recipeRevisionId, | |
| 1233 | − ); | |
| 1234 | − expect(documents[0]?.contentText).toContain( | |
| 1235 | − '"operationId":"repos.getRepo"', | |
| 1236 | − ); | |
| 1237 | − } | |
| 1380 | + const loadedConfig = yield* Effect.tryPromise({ | |
| 1381 | + try: () => loadLocalExecutorConfig(runtimeLocalWorkspace.context), | |
| 1382 | + catch: (cause) => | |
| 1383 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1384 | + }); | |
| 1385 | + expect(loadedConfig.config?.sources?.[added.id]).toMatchObject({ | |
| 1386 | + kind: "openapi", | |
| 1387 | + connection: { | |
| 1388 | + endpoint: `${specServer.baseUrl}/`, | |
| 1389 | + }, | |
| 1390 | + }); | |
| 1391 | + const artifact = yield* Effect.tryPromise({ | |
| 1392 | + try: () => | |
| 1393 | + readLocalSourceArtifact({ | |
| 1394 | + context: runtimeLocalWorkspace.context, | |
| 1395 | + sourceId: added.id, | |
| 1396 | + }), | |
| 1397 | + catch: (cause) => | |
| 1398 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1399 | + }); | |
| 1400 | + expect(artifact?.documents[0]?.contentText).toContain( | |
| 1401 | + '"operationId":"repos.getRepo"', | |
| 1402 | + ); | |
| 1403 | + const workspaceState = yield* Effect.tryPromise({ | |
| 1404 | + try: () => loadLocalWorkspaceState(runtimeLocalWorkspace.context), | |
| 1405 | + catch: (cause) => | |
| 1406 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1407 | + }); | |
| 1408 | + expect(workspaceState.sources[added.id]?.status).toBe("connected"); | |
| 1238 | 1409 | |
| 1239 | 1410 | const freshEnvironment = yield* resolveEnvironment({ |
| 1240 | 1411 | workspaceId, |
| @@ -1280,7 +1451,8 @@ describe("workspace-execution-environment", () => { · 39 unmodified lines | ||
| 1280 | 1451 | const graphqlServer = yield* makeGraphqlServer; |
| 1281 | 1452 | const persistence = yield* makePersistence; |
| 1282 | 1453 | const workspaceId = WorkspaceIdSchema.make("ws_add_graphql"); |
| 1283 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence); | |
| 1454 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1455 | + const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1284 | 1456 | const now = Date.now(); |
| 1285 | 1457 | const tokenSecretMaterialId = SecretMaterialIdSchema.make( |
| 1286 | 1458 | "sec_test_graphql_bearer", |
| @@ -1339,33 +1511,38 @@ describe("workspace-execution-environment", () => { · 52 unmodified lines | ||
| 1339 | 1511 | expect(added.auth.kind).toBe("bearer"); |
| 1340 | 1512 | expect(added.auth.token?.providerId).toBe("postgres"); |
| 1341 | 1513 | |
| 1342 | − const storedSource = | |
| 1343 | − yield* persistence.rows.sources.getByWorkspaceAndId( | |
| 1344 | − workspaceId, | |
| 1345 | − added.id, | |
| 1346 | − ); | |
| 1347 | − expect(Option.isSome(storedSource)).toBe(true); | |
| 1348 | − if (Option.isSome(storedSource)) { | |
| 1349 | − const documents = | |
| 1350 | − yield* persistence.rows.sourceRecipeDocuments.listByRevisionId( | |
| 1351 | − storedSource.value.recipeRevisionId, | |
| 1352 | − ); | |
| 1353 | − expect(documents[0]?.contentText).toContain('"__schema"'); | |
| 1354 | − expect( | |
| 1355 | − (yield* persistence.rows.sourceRecipeSchemaBundles.listByRevisionId( | |
| 1356 | − storedSource.value.recipeRevisionId, | |
| 1357 | − )).length, | |
| 1358 | − ).toBe(1); | |
| 1514 | + const artifact = yield* Effect.tryPromise({ | |
| 1515 | + try: () => | |
| 1516 | + readLocalSourceArtifact({ | |
| 1517 | + context: runtimeLocalWorkspace.context, | |
| 1518 | + sourceId: added.id, | |
| 1519 | + }), | |
| 1520 | + catch: (cause) => | |
| 1521 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1522 | + }); | |
| 1523 | + expect(artifact?.documents[0]?.contentText).toContain('"__schema"'); | |
| 1524 | + expect(artifact?.schemaBundles.length).toBe(1); | |
| 1525 | + if (artifact === null) { | |
| 1526 | + throw new Error(`Missing local source artifact for ${added.id}`); | |
| 1359 | 1527 | } |
| 1360 | 1528 | |
| 1361 | − const connectedSourceRecord = Option.getOrThrow(storedSource); | |
| 1362 | − yield* persistence.rows.sourceRecipeRevisions.update( | |
| 1363 | − connectedSourceRecord.recipeRevisionId, | |
| 1364 | − { | |
| 1365 | − manifestJson: null, | |
| 1366 | − updatedAt: Date.now(), | |
| 1367 | − }, | |
| 1368 | − ); | |
| 1529 | + yield* Effect.tryPromise({ | |
| 1530 | + try: () => | |
| 1531 | + writeLocalSourceArtifact({ | |
| 1532 | + context: runtimeLocalWorkspace.context, | |
| 1533 | + sourceId: added.id, | |
| 1534 | + artifact: { | |
| 1535 | + ...artifact, | |
| 1536 | + revision: { | |
| 1537 | + ...artifact.revision, | |
| 1538 | + manifestJson: null, | |
| 1539 | + updatedAt: Date.now(), | |
| 1540 | + }, | |
| 1541 | + }, | |
| 1542 | + }), | |
| 1543 | + catch: (cause) => | |
| 1544 | + cause instanceof Error ? cause : new Error(String(cause)), | |
| 1545 | + }); | |
| 1369 | 1546 | |
| 1370 | 1547 | const freshEnvironment = yield* resolveEnvironment({ |
| 1371 | 1548 | workspaceId, |
| @@ -1515,9 +1692,10 @@ describe("workspace-execution-environment", () => { · 143 unmodified lines | ||
| 1515 | 1692 | const persistence = yield* makePersistence; |
| 1516 | 1693 | const workspaceId = WorkspaceIdSchema.make("ws_params_auth"); |
| 1517 | 1694 | const sourceId = SourceIdSchema.make("src_params_auth"); |
| 1695 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1518 | 1696 | |
| 1519 | 1697 | yield* persistConnectedGithubOpenApiSource({ |
| 1520 | − persistence, | |
| 1698 | + runtimeLocalWorkspace, | |
| 1521 | 1699 | workspaceId, |
| 1522 | 1700 | sourceId, |
| 1523 | 1701 | endpoint: specServer.baseUrl, |
| @@ -1527,7 +1705,7 @@ describe("workspace-execution-environment", () => { · 3 unmodified lines | ||
| 1527 | 1705 | }, |
| 1528 | 1706 | }); |
| 1529 | 1707 | |
| 1530 | − const resolveEnvironment = makeResolver(persistence); | |
| 1708 | + const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1531 | 1709 | const environment = yield* resolveEnvironment({ |
| 1532 | 1710 | workspaceId, |
| 1533 | 1711 | accountId: AccountIdSchema.make("acc_params_auth"), |
| @@ -1567,14 +1745,15 @@ describe("workspace-execution-environment", () => { · 33 unmodified lines | ||
| 1567 | 1745 | "ws_openapi_describe_bundle", |
| 1568 | 1746 | ); |
| 1569 | 1747 | const sourceId = SourceIdSchema.make("src_openapi_describe_bundle"); |
| 1748 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1570 | 1749 | |
| 1571 | 1750 | yield* persistConnectedGithubOpenApiSource({ |
| 1572 | − persistence, | |
| 1751 | + runtimeLocalWorkspace, | |
| 1573 | 1752 | workspaceId, |
| 1574 | 1753 | sourceId, |
| 1575 | 1754 | }); |
| 1576 | 1755 | |
| 1577 | − const resolveEnvironment = makeResolver(persistence); | |
| 1756 | + const resolveEnvironment = makeResolver(persistence, runtimeLocalWorkspace); | |
| 1578 | 1757 | const environment = yield* resolveEnvironment({ |
| 1579 | 1758 | workspaceId, |
| 1580 | 1759 | accountId: AccountIdSchema.make("acc_openapi_describe_bundle"), |
| @@ -1621,10 +1800,12 @@ describe("workspace-execution-environment", () => { · 40 unmodified lines | ||
| 1621 | 1800 | () => |
| 1622 | 1801 | Effect.gen(function* () { |
| 1623 | 1802 | const persistence = yield* makePersistence; |
| 1624 | − const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence); | |
| 1803 | + const workspaceId = WorkspaceIdSchema.make("ws_describe_add_source"); | |
| 1804 | + const runtimeLocalWorkspace = yield* makeRuntimeLocalWorkspaceState(workspaceId); | |
| 1805 | + const resolveEnvironment = makeResolverWithLiveSourceAuth(persistence, runtimeLocalWorkspace); | |
| 1625 | 1806 | |
| 1626 | 1807 | const environment = yield* resolveEnvironment({ |
| 1627 | − workspaceId: WorkspaceIdSchema.make("ws_describe_add_source"), | |
| 1808 | + workspaceId, | |
| 1628 | 1809 | accountId: AccountIdSchema.make("acc_describe_add_source"), |
| 1629 | 1810 | executionId: ExecutionIdSchema.make("exec_describe_add_source"), |
| 1630 | 1811 | }); |
| @@ -395,49 +395,24 @@ const authorizePersistedToolInvocation = (input: { · 394 unmodified lines | ||
| 395 | 395 | >[0]["onElicitation"]; |
| 396 | 396 | }): Effect.Effect<void, Error, never> => |
| 397 | 397 | Effect.gen(function* () { |
| 398 | − const workspace = yield* input.rows.workspaces | |
| 399 | − .getById(input.workspaceId) | |
| 400 | − .pipe( | |
| 401 | − Effect.mapError((cause) => | |
| 402 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 403 | − ), | |
| 404 | − ); | |
| 405 | − const storedPolicies = Option.isSome(workspace) | |
| 406 | − ? yield* input.rows.policies | |
| 407 | − .listForWorkspaceContext({ | |
| 408 | − organizationId: workspace.value.organizationId, | |
| 409 | − workspaceId: input.workspaceId, | |
| 410 | − }) | |
| 411 | − .pipe( | |
| 412 | − Effect.mapError((cause) => | |
| 413 | − cause instanceof Error ? cause : new Error(String(cause)), | |
| 414 | − ), | |
| 415 | − ) | |
| 416 | − : []; | |
| 417 | − const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies({ | |
| 418 | − store: input.rows, | |
| 419 | − workspaceId: input.workspaceId, | |
| 420 | − }).pipe( | |
| 398 | + const runtimeLocalWorkspace = yield* getRuntimeLocalWorkspaceOption(); | |
| 399 | + const localWorkspacePolicies = yield* loadRuntimeLocalWorkspacePolicies(input.workspaceId).pipe( | |
| 421 | 400 | Effect.mapError((cause) => |
| 422 | 401 | cause instanceof Error ? cause : new Error(String(cause)), |
| 423 | 402 | ), |
| 424 | 403 | ); |
| 425 | − const policies = localWorkspacePolicies === null | |
| 426 | − ? storedPolicies | |
| 427 | − : [ | |
| 428 | − ...storedPolicies.filter((policy) => policy.scopeType !== "workspace"), | |
| 429 | − ...localWorkspacePolicies.policies, | |
| 430 | − ]; | |
| 431 | 404 | |
| 432 | 405 | const decision = evaluateInvocationPolicy({ |
| 433 | 406 | descriptor: input.descriptor, |
| 434 | 407 | args: input.args, |
| 435 | − policies, | |
| 408 | + policies: localWorkspacePolicies.policies, | |
| 436 | 409 | context: { |
| 437 | 410 | workspaceId: input.workspaceId, |
| 438 | − organizationId: Option.isSome(workspace) | |
| 439 | − ? workspace.value.organizationId | |
| 440 | − : ("org_unknown" as never), | |
| 411 | + organizationId: | |
| 412 | + runtimeLocalWorkspace !== null | |
| 413 | + && runtimeLocalWorkspace.installation.workspaceId === input.workspaceId | |
| 414 | + ? runtimeLocalWorkspace.installation.organizationId | |
| 415 | + : ("org_unknown" as never), | |
| 441 | 416 | accountId: input.accountId, |
| 442 | 417 | clientId: |
| 443 | 418 | typeof input.context?.clientId === "string" && |
| @@ -6,6 +6,7 @@ export const LocalConfigSecretProviderSourceSchema = Schema.Literal( · 5 unmodified lines | ||
| 6 | 6 | "env", |
| 7 | 7 | "file", |
| 8 | 8 | "exec", |
| 9 | + "params", | |
| 9 | 10 | ); |
| 10 | 11 | |
| 11 | 12 | export const LocalConfigEnvSecretProviderSchema = Schema.Struct({ |
| @@ -1,10 +1,5 @@ | ||
| 1 | −import { | |
| 2 | − createInsertSchema, | |
| 3 | − createSelectSchema, | |
| 4 | − createUpdateSchema, | |
| 5 | −} from "drizzle-orm/effect-schema"; | |
| 1 | +import * as Schema from "effect/Schema"; | |
| 6 | 2 | |
| 7 | −import { localInstallationsTable } from "../../persistence/schema"; | |
| 8 | 3 | import { TimestampMsSchema } from "../common"; |
| 9 | 4 | import { |
| 10 | 5 | AccountIdSchema, |
| @@ -13,28 +8,16 @@ import { · 2 unmodified lines | ||
| 13 | 8 | WorkspaceIdSchema, |
| 14 | 9 | } from "../ids"; |
| 15 | 10 | |
| 16 | −const localInstallationSchemaOverrides = { | |
| 11 | +export const LocalInstallationSchema = Schema.Struct({ | |
| 17 | 12 | id: InstallationIdSchema, |
| 18 | 13 | accountId: AccountIdSchema, |
| 19 | 14 | organizationId: OrganizationIdSchema, |
| 20 | 15 | workspaceId: WorkspaceIdSchema, |
| 21 | 16 | createdAt: TimestampMsSchema, |
| 22 | 17 | updatedAt: TimestampMsSchema, |
| 23 | −} as const; | |
| 24 | − | |
| 25 | −export const LocalInstallationSchema = createSelectSchema( | |
| 26 | − localInstallationsTable, | |
| 27 | − localInstallationSchemaOverrides, | |
| 28 | −); | |
| 29 | − | |
| 30 | −export const LocalInstallationInsertSchema = createInsertSchema( | |
| 31 | − localInstallationsTable, | |
| 32 | − localInstallationSchemaOverrides, | |
| 33 | −); | |
| 18 | +}); | |
| 34 | 19 | |
| 35 | −export const LocalInstallationUpdateSchema = createUpdateSchema( | |
| 36 | − localInstallationsTable, | |
| 37 | − localInstallationSchemaOverrides, | |
| 38 | −); | |
| 20 | +export const LocalInstallationInsertSchema = LocalInstallationSchema; | |
| 21 | +export const LocalInstallationUpdateSchema = Schema.partial(LocalInstallationSchema); | |
| 39 | 22 | |
| 40 | 23 | export type LocalInstallation = typeof LocalInstallationSchema.Type; |
| @@ -5,10 +5,10 @@ import { NodeHttpServer } from "@effect/platform-node"; · 4 unmodified lines | ||
| 5 | 5 | import { describe, expect, it } from "@effect/vitest"; |
| 6 | 6 | import { |
| 7 | 7 | createControlPlaneApiLayer, |
| 8 | − createSqlControlPlaneRuntime, | |
| 8 | + createControlPlaneRuntime, | |
| 9 | 9 | type LocalInstallation, |
| 10 | 10 | type Source, |
| 11 | − type SqlControlPlaneRuntime, | |
| 11 | + type ControlPlaneRuntime, | |
| 12 | 12 | } from "@executor/control-plane"; |
| 13 | 13 | import * as Context from "effect/Context"; |
| 14 | 14 | import * as Effect from "effect/Effect"; |
| @@ -69,7 +69,7 @@ type RunningServer = { · 54 unmodified lines | ||
| 69 | 69 | }; |
| 70 | 70 | |
| 71 | 71 | type ApiServer = RunningServer & { |
| 72 | − runtime: SqlControlPlaneRuntime; | |
| 72 | + runtime: ControlPlaneRuntime; | |
| 73 | 73 | }; |
| 74 | 74 | |
| 75 | 75 | type HookHarness<T> = { |
| @@ -95,7 +95,7 @@ const closeScope = (scope: Scope.CloseableScope) => · 19 unmodified lines | ||
| 95 | 95 | |
| 96 | 96 | const startControlPlaneServer = async (): Promise<ApiServer> => { |
| 97 | 97 | const runtime = await Effect.runPromise( |
| 98 | − createSqlControlPlaneRuntime({ | |
| 98 | + createControlPlaneRuntime({ | |
| 99 | 99 | localDataDir: ":memory:", |
| 100 | 100 | workspaceRoot: mkdtempSync(join(tmpdir(), "executor-react-test-")), |
| 101 | 101 | }), |
| @@ -11,10 +11,10 @@ import { Readable } from "node:stream"; · 10 unmodified lines | ||
| 11 | 11 | import { HttpApiBuilder, HttpServer } from "@effect/platform"; |
| 12 | 12 | import { |
| 13 | 13 | createControlPlaneApiLayer, |
| 14 | − createSqlControlPlaneRuntime, | |
| 14 | + createControlPlaneRuntime, | |
| 15 | 15 | type ResolveExecutionEnvironment, |
| 16 | 16 | type ResolveSecretMaterial, |
| 17 | − type SqlControlPlaneRuntime, | |
| 17 | + type ControlPlaneRuntime, | |
| 18 | 18 | } from "@executor/control-plane"; |
| 19 | 19 | import { createExecutorMcpRequestHandler } from "@executor/executor-mcp"; |
| 20 | 20 | import * as Cause from "effect/Cause"; |
| @@ -58,7 +58,7 @@ type StaticUiOptions = { · 37 unmodified lines | ||
| 58 | 58 | }; |
| 59 | 59 | |
| 60 | 60 | export type LocalExecutorServer = { |
| 61 | − readonly runtime: SqlControlPlaneRuntime; | |
| 61 | + readonly runtime: ControlPlaneRuntime; | |
| 62 | 62 | readonly port: number; |
| 63 | 63 | readonly host: string; |
| 64 | 64 | readonly baseUrl: string; |
| @@ -77,7 +77,7 @@ export type StartLocalExecutorServerOptions = { · 12 unmodified lines | ||
| 77 | 77 | }; |
| 78 | 78 | |
| 79 | 79 | export type LocalExecutorRequestHandler = { |
| 80 | − readonly runtime: SqlControlPlaneRuntime; | |
| 80 | + readonly runtime: ControlPlaneRuntime; | |
| 81 | 81 | readonly handleApiRequest: (request: Request) => Promise<Response>; |
| 82 | 82 | readonly getBaseUrl: () => string | undefined; |
| 83 | 83 | readonly setBaseUrl: (baseUrl: string) => void; |
| @@ -86,7 +86,7 @@ export type LocalExecutorRequestHandler = { · 2 unmodified lines | ||
| 86 | 86 | type ControlPlaneWebHandler = ReturnType<typeof HttpApiBuilder.toWebHandler>; |
| 87 | 87 | type ExecutorMcpHandler = ReturnType<typeof createExecutorMcpRequestHandler>; |
| 88 | 88 | |
| 89 | −const disposeRuntime = (runtime: SqlControlPlaneRuntime) => | |
| 89 | +const disposeRuntime = (runtime: ControlPlaneRuntime) => | |
| 90 | 90 | Effect.tryPromise({ |
| 91 | 91 | try: () => runtime.close(), |
| 92 | 92 | catch: (cause) => |
| @@ -98,7 +98,7 @@ const createRuntime = ( · 5 unmodified lines | ||
| 98 | 98 | getLocalServerBaseUrl: () => string | undefined, |
| 99 | 99 | options: StartLocalExecutorServerOptions, |
| 100 | 100 | ) => |
| 101 | − createSqlControlPlaneRuntime({ | |
| 101 | + createControlPlaneRuntime({ | |
| 102 | 102 | localDataDir, |
| 103 | 103 | migrationsFolder: options.migrationsFolder, |
| 104 | 104 | workspaceRoot: options.workspaceRoot, |
| @@ -209,7 +209,7 @@ const createRuntimeWithLegacyMigration = ( · 104 unmodified lines | ||
| 209 | 209 | ); |
| 210 | 210 | }); |
| 211 | 211 | |
| 212 | −const createControlPlaneWebHandler = (runtime: SqlControlPlaneRuntime) => | |
| 212 | +const createControlPlaneWebHandler = (runtime: ControlPlaneRuntime) => | |
| 213 | 213 | Effect.acquireRelease( |
| 214 | 214 | Effect.sync(() => |
| 215 | 215 | HttpApiBuilder.toWebHandler( |