History

Replace branded ID casts with schema constructors

Rhys Sullivan committed 8 months ago

Commit 93ee28f1 parent 8c9d74211 files · +114 −86Browse files

Unified · Split

11 files
  1. packages/control-plane/src/api/auth/workspace-membership.test.ts +20 −10
  2. packages/control-plane/src/api/executions/http.ts +5 −7
  3. packages/control-plane/src/api/memberships/http.ts +5 −4
  4. packages/control-plane/src/api/organizations/http.ts +3 −2
  5. packages/control-plane/src/api/policies/http.ts +7 −9
  6. packages/control-plane/src/api/sources/http.ts +7 −9
  7. packages/control-plane/src/api/workspaces/http.ts +11 −13
  8. packages/control-plane/src/domain/auth/actor.test.ts +15 −8
  9. packages/control-plane/src/domain/auth/policy.test.ts +6 −2
  10. packages/control-plane/src/persistence/control-plane-rows.test.ts +33 −20
  11. packages/control-plane/src/runtime/control-plane-runtime.test.ts +2 −2
packages/control-plane/src/api/auth/workspace-membership.test.ts+20−10
@@ -1,15 +1,25 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import {
3+ AccountIdSchema,
4+ OrganizationIdSchema,
5+ OrganizationMemberIdSchema,
6+ WorkspaceIdSchema,
7+} from "#schema";
28
39 import { deriveWorkspaceMembershipsForPrincipal } from "./workspace-membership";
410
511 describe("workspace membership derivation", () => {
612 it("prefers highest active role in matching organization", () => {
13+ const principalAccountId = AccountIdSchema.make("acc_1");
14+ const workspaceId = WorkspaceIdSchema.make("ws_1");
15+ const organizationId = OrganizationIdSchema.make("org_1");
16+
717 const memberships = deriveWorkspaceMembershipsForPrincipal({
8− principalAccountId: "acc_1" as never,
9− workspaceId: "ws_1" as never,
18+ principalAccountId,
19+ workspaceId,
1020 workspace: {
11− id: "ws_1" as never,
12− organizationId: "org_1" as never,
21+ id: workspaceId,
22+ organizationId,
1323 name: "Main",
1424 createdByAccountId: null,
1525 createdAt: 1,
@@ -17,9 +27,9 @@ describe("workspace membership derivation", () => { · 1 unmodified line
1727 },
1828 organizationMemberships: [
1929 {
20− id: "mem_1" as never,
21− organizationId: "org_1" as never,
22− accountId: "acc_1" as never,
30+ id: OrganizationMemberIdSchema.make("mem_1"),
31+ organizationId,
32+ accountId: principalAccountId,
2333 role: "viewer",
2434 status: "active",
2535 billable: true,
@@ -29,9 +39,9 @@ describe("workspace membership derivation", () => { · 3 unmodified lines
2939 updatedAt: 1,
3040 },
3141 {
32− id: "mem_2" as never,
33− organizationId: "org_1" as never,
34− accountId: "acc_1" as never,
42+ id: OrganizationMemberIdSchema.make("mem_2"),
43+ organizationId,
44+ accountId: principalAccountId,
3545 role: "admin",
3646 status: "active",
3747 billable: true,
packages/control-plane/src/api/executions/http.ts+5−7
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { WorkspaceId } from "#schema";
34
45 import {
56 Actor,
@@ -37,21 +38,18 @@ const toUnauthorizedError = ( · 31 unmodified lines
3738 details: "Authentication required",
3839 });
3940
40−const resolveWorkspaceActor = (workspaceId: string) =>
41+const resolveWorkspaceActor = (workspaceId: WorkspaceId) =>
4142 Effect.gen(function* () {
4243 const actorResolver = yield* ControlPlaneActorResolver;
4344 const request = yield* HttpServerRequest.HttpServerRequest;
4445
45− return yield* actorResolver.resolveWorkspaceActor({
46− workspaceId: workspaceId as never,
47− headers: request.headers,
48− });
46+ return yield* actorResolver.resolveWorkspaceActor({ workspaceId, headers: request.headers });
4947 });
5048
51−const requireExecuteWorkspace = (workspaceId: string) =>
49+const requireExecuteWorkspace = (workspaceId: WorkspaceId) =>
5250 requirePermission({
5351 permission: "workspace:read",
54− workspaceId: workspaceId as never,
52+ workspaceId,
5553 });
5654
5755 export const ControlPlaneExecutionsLive = HttpApiBuilder.group(
packages/control-plane/src/api/memberships/http.ts+5−4
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { OrganizationId } from "#schema";
34
45 import {
56 Actor,
@@ -43,16 +44,16 @@ const resolveActor = Effect.gen(function* () { · 37 unmodified lines
4344 return yield* actorResolver.resolveActor({ headers: request.headers });
4445 });
4546
46−const requireReadMemberships = (organizationId: string) =>
47+const requireReadMemberships = (organizationId: OrganizationId) =>
4748 requirePermission({
4849 permission: "memberships:read",
49− organizationId: organizationId as never,
50+ organizationId,
5051 });
5152
52−const requireWriteMemberships = (organizationId: string) =>
53+const requireWriteMemberships = (organizationId: OrganizationId) =>
5354 requirePermission({
5455 permission: "memberships:write",
55− organizationId: organizationId as never,
56+ organizationId,
5657 });
5758
5859 export const ControlPlaneMembershipsLive = HttpApiBuilder.group(
packages/control-plane/src/api/organizations/http.ts+3−2
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { OrganizationId } from "#schema";
34
45 import {
56 Actor,
@@ -47,10 +48,10 @@ const requireReadOrganizations = requirePermission({ · 41 unmodified lines
4748 permission: "organizations:read",
4849 });
4950
50−const requireManageOrganizations = (organizationId: string) =>
51+const requireManageOrganizations = (organizationId: OrganizationId) =>
5152 requirePermission({
5253 permission: "organizations:manage",
53− organizationId: organizationId as never,
54+ organizationId,
5455 });
5556
5657 export const ControlPlaneOrganizationsLive = HttpApiBuilder.group(
packages/control-plane/src/api/policies/http.ts+7−9
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { WorkspaceId } from "#schema";
34
45 import {
56 Actor,
@@ -37,27 +38,24 @@ const toUnauthorizedError = ( · 31 unmodified lines
3738 details: "Authentication required",
3839 });
3940
40−const resolveWorkspaceActor = (workspaceId: string) =>
41+const resolveWorkspaceActor = (workspaceId: WorkspaceId) =>
4142 Effect.gen(function* () {
4243 const actorResolver = yield* ControlPlaneActorResolver;
4344 const request = yield* HttpServerRequest.HttpServerRequest;
4445
45− return yield* actorResolver.resolveWorkspaceActor({
46− workspaceId: workspaceId as never,
47− headers: request.headers,
48− });
46+ return yield* actorResolver.resolveWorkspaceActor({ workspaceId, headers: request.headers });
4947 });
5048
51−const requireReadPolicies = (workspaceId: string) =>
49+const requireReadPolicies = (workspaceId: WorkspaceId) =>
5250 requirePermission({
5351 permission: "policies:read",
54− workspaceId: workspaceId as never,
52+ workspaceId,
5553 });
5654
57−const requireWritePolicies = (workspaceId: string) =>
55+const requireWritePolicies = (workspaceId: WorkspaceId) =>
5856 requirePermission({
5957 permission: "policies:write",
60− workspaceId: workspaceId as never,
58+ workspaceId,
6159 });
6260
6361 export const ControlPlanePoliciesLive = HttpApiBuilder.group(
packages/control-plane/src/api/sources/http.ts+7−9
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { WorkspaceId } from "#schema";
34
45 import {
56 Actor,
@@ -37,27 +38,24 @@ const toUnauthorizedError = ( · 31 unmodified lines
3738 details: "Authentication required",
3839 });
3940
40−const resolveWorkspaceActor = (workspaceId: string) =>
41+const resolveWorkspaceActor = (workspaceId: WorkspaceId) =>
4142 Effect.gen(function* () {
4243 const actorResolver = yield* ControlPlaneActorResolver;
4344 const request = yield* HttpServerRequest.HttpServerRequest;
4445
45− return yield* actorResolver.resolveWorkspaceActor({
46− workspaceId: workspaceId as never,
47− headers: request.headers,
48− });
46+ return yield* actorResolver.resolveWorkspaceActor({ workspaceId, headers: request.headers });
4947 });
5048
51−const requireReadSources = (workspaceId: string) =>
49+const requireReadSources = (workspaceId: WorkspaceId) =>
5250 requirePermission({
5351 permission: "sources:read",
54− workspaceId: workspaceId as never,
52+ workspaceId,
5553 });
5654
57−const requireWriteSources = (workspaceId: string) =>
55+const requireWriteSources = (workspaceId: WorkspaceId) =>
5856 requirePermission({
5957 permission: "sources:write",
60− workspaceId: workspaceId as never,
58+ workspaceId,
6159 });
6260
6361 export const ControlPlaneSourcesLive = HttpApiBuilder.group(
packages/control-plane/src/api/workspaces/http.ts+11−13
@@ -1,5 +1,6 @@
11 import { HttpApiBuilder, HttpServerRequest } from "@effect/platform";
22 import * as Effect from "effect/Effect";
3+import type { OrganizationId, WorkspaceId } from "#schema";
34
45 import {
56 Actor,
@@ -43,39 +44,36 @@ const resolveActor = Effect.gen(function* () { · 37 unmodified lines
4344 return yield* actorResolver.resolveActor({ headers: request.headers });
4445 });
4546
46−const resolveWorkspaceActor = (workspaceId: string) =>
47+const resolveWorkspaceActor = (workspaceId: WorkspaceId) =>
4748 Effect.gen(function* () {
4849 const actorResolver = yield* ControlPlaneActorResolver;
4950 const request = yield* HttpServerRequest.HttpServerRequest;
5051
51− return yield* actorResolver.resolveWorkspaceActor({
52− workspaceId: workspaceId as never,
53− headers: request.headers,
54− });
52+ return yield* actorResolver.resolveWorkspaceActor({ workspaceId, headers: request.headers });
5553 });
5654
57−const requireReadWorkspace = (workspaceId: string) =>
55+const requireReadWorkspace = (workspaceId: WorkspaceId) =>
5856 requirePermission({
5957 permission: "workspace:read",
60− workspaceId: workspaceId as never,
58+ workspaceId,
6159 });
6260
63−const requireManageWorkspace = (workspaceId: string) =>
61+const requireManageWorkspace = (workspaceId: WorkspaceId) =>
6462 requirePermission({
6563 permission: "workspace:manage",
66− workspaceId: workspaceId as never,
64+ workspaceId,
6765 });
6866
69−const requireOrganizationWorkspaceRead = (organizationId: string) =>
67+const requireOrganizationWorkspaceRead = (organizationId: OrganizationId) =>
7068 requirePermission({
7169 permission: "workspace:read",
72− organizationId: organizationId as never,
70+ organizationId,
7371 });
7472
75−const requireOrganizationWorkspaceManage = (organizationId: string) =>
73+const requireOrganizationWorkspaceManage = (organizationId: OrganizationId) =>
7674 requirePermission({
7775 permission: "workspace:manage",
78− organizationId: organizationId as never,
76+ organizationId,
7977 });
8078
8179 export const ControlPlaneWorkspacesLive = HttpApiBuilder.group(
packages/control-plane/src/domain/auth/actor.test.ts+15−8
@@ -1,4 +1,5 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import { AccountIdSchema, WorkspaceIdSchema } from "#schema";
23 import * as Effect from "effect/Effect";
34
45 import {
@@ -10,9 +11,12 @@ import { · 5 unmodified lines
1011 describe("control-plane-domain actor", () => {
1112 it.effect("allows workspace permissions from active membership", () =>
1213 Effect.gen(function* () {
14+ const accountId = AccountIdSchema.make("acc_1");
15+ const workspaceId = WorkspaceIdSchema.make("ws_1");
16+
1317 const actor = yield* makeActor({
1418 principal: {
15− accountId: "acc_1" as never,
19+ accountId,
1620 provider: "local",
1721 subject: "local:acc_1",
1822 email: null,
@@ -20,8 +24,8 @@ describe("control-plane-domain actor", () => { · 1 unmodified line
2024 },
2125 workspaceMemberships: [
2226 {
23− accountId: "acc_1" as never,
24− workspaceId: "ws_1" as never,
27+ accountId,
28+ workspaceId,
2529 role: "editor",
2630 status: "active",
2731 grantedAt: 1,
@@ -33,16 +37,19 @@ describe("control-plane-domain actor", () => { · 5 unmodified lines
3337
3438 yield* actor.requirePermission({
3539 permission: "sources:write",
36− workspaceId: "ws_1" as never,
40+ workspaceId,
3741 });
3842 }),
3943 );
4044
4145 it.effect("denies permission when role is insufficient", () =>
4246 Effect.gen(function* () {
47+ const accountId = AccountIdSchema.make("acc_1");
48+ const workspaceId = WorkspaceIdSchema.make("ws_1");
49+
4350 const actor = yield* makeActor({
4451 principal: {
45− accountId: "acc_1" as never,
52+ accountId,
4653 provider: "local",
4754 subject: "local:acc_1",
4855 email: null,
@@ -50,8 +57,8 @@ describe("control-plane-domain actor", () => { · 1 unmodified line
5057 },
5158 workspaceMemberships: [
5259 {
53− accountId: "acc_1" as never,
54− workspaceId: "ws_1" as never,
60+ accountId,
61+ workspaceId,
5562 role: "viewer",
5663 status: "active",
5764 grantedAt: 1,
@@ -64,7 +71,7 @@ describe("control-plane-domain actor", () => { · 6 unmodified lines
6471 const denied = yield* Effect.either(
6572 actor.requirePermission({
6673 permission: "sources:write",
67− workspaceId: "ws_1" as never,
74+ workspaceId,
6875 }),
6976 );
7077
packages/control-plane/src/domain/auth/policy.test.ts+6−2
@@ -1,4 +1,5 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import { AccountIdSchema, WorkspaceIdSchema } from "#schema";
23 import * as Effect from "effect/Effect";
34
45 import { Actor, makeAllowAllActor } from "./actor";
@@ -7,8 +8,11 @@ import { requirePermission, withPolicy } from "./policy"; · 2 unmodified lines
78 describe("control-plane-domain policy", () => {
89 it.effect("runs protected effect when permission is granted", () =>
910 Effect.gen(function* () {
11+ const accountId = AccountIdSchema.make("acc_1");
12+ const workspaceId = WorkspaceIdSchema.make("ws_1");
13+
1014 const principal = {
11− accountId: "acc_1" as never,
15+ accountId,
1216 provider: "local" as const,
1317 subject: "local:acc_1",
1418 email: null,
@@ -18,7 +22,7 @@ describe("control-plane-domain policy", () => { · 3 unmodified lines
1822 const result = yield* withPolicy(
1923 requirePermission({
2024 permission: "workspace:read",
21− workspaceId: "ws_1" as never,
25+ workspaceId,
2226 }),
2327 )(Effect.succeed("ok")).pipe(
2428 Effect.provideService(Actor, makeAllowAllActor(principal)),
packages/control-plane/src/persistence/control-plane-rows.test.ts+33−20
@@ -1,4 +1,12 @@
11 import { describe, expect, it } from "@effect/vitest";
2+import {
3+ AccountIdSchema,
4+ OrganizationIdSchema,
5+ OrganizationMemberIdSchema,
6+ PolicyIdSchema,
7+ SourceIdSchema,
8+ WorkspaceIdSchema,
9+} from "#schema";
210 import * as Effect from "effect/Effect";
311 import * as Option from "effect/Option";
412
@@ -20,29 +28,32 @@ describe("control-plane-persistence-drizzle", () => { · 15 unmodified lines
2028 Effect.gen(function* () {
2129 const persistence = yield* makePersistence;
2230 const now = Date.now();
31+ const organizationId = OrganizationIdSchema.make("org_1");
32+ const accountId = AccountIdSchema.make("acc_1");
33+ const workspaceId = WorkspaceIdSchema.make("ws_1");
2334
2435 yield* persistence.rows.organizations.insert({
25− id: "org_1" as never,
36+ id: organizationId,
2637 slug: "acme",
2738 name: "Acme",
2839 status: "active",
29− createdByAccountId: "acc_1" as never,
40+ createdByAccountId: accountId,
3041 createdAt: now,
3142 updatedAt: now,
3243 });
3344
3445 yield* persistence.rows.workspaces.insert({
35− id: "ws_1" as never,
36− organizationId: "org_1" as never,
46+ id: workspaceId,
47+ organizationId,
3748 name: "Main",
38− createdByAccountId: "acc_1" as never,
49+ createdByAccountId: accountId,
3950 createdAt: now,
4051 updatedAt: now,
4152 });
4253
4354 yield* persistence.rows.sources.insert({
44− id: "src_1" as never,
45− workspaceId: "ws_1" as never,
55+ id: SourceIdSchema.make("src_1"),
56+ workspaceId,
4657 name: "Github",
4758 kind: "openapi",
4859 endpoint: "https://api.github.com/openapi.json",
@@ -56,8 +67,8 @@ describe("control-plane-persistence-drizzle", () => { · 7 unmodified lines
5667 });
5768
5869 yield* persistence.rows.policies.insert({
59− id: "pol_1" as never,
60− workspaceId: "ws_1" as never,
70+ id: PolicyIdSchema.make("pol_1"),
71+ workspaceId,
6172 targetAccountId: null,
6273 clientId: null,
6374 resourceType: "tool_path",
@@ -72,14 +83,14 @@ describe("control-plane-persistence-drizzle", () => { · 8 unmodified lines
7283 updatedAt: now,
7384 });
7485
75− const workspace = yield* persistence.rows.workspaces.getById("ws_1" as never);
86+ const workspace = yield* persistence.rows.workspaces.getById(workspaceId);
7687 expect(Option.isSome(workspace)).toBe(true);
7788
78− const sources = yield* persistence.rows.sources.listByWorkspaceId("ws_1" as never);
89+ const sources = yield* persistence.rows.sources.listByWorkspaceId(workspaceId);
7990 expect(sources).toHaveLength(1);
8091 expect(sources[0]?.name).toBe("Github");
8192
82− const policies = yield* persistence.rows.policies.listByWorkspaceId("ws_1" as never);
93+ const policies = yield* persistence.rows.policies.listByWorkspaceId(workspaceId);
8394 expect(policies).toHaveLength(1);
8495 expect(policies[0]?.resourcePattern).toBe("source.github.*");
8596 }),
@@ -89,11 +100,13 @@ describe("control-plane-persistence-drizzle", () => { · 3 unmodified lines
89100 Effect.gen(function* () {
90101 const persistence = yield* makePersistence;
91102 const now = Date.now();
103+ const organizationId = OrganizationIdSchema.make("org_1");
104+ const accountId = AccountIdSchema.make("acc_1");
92105
93106 yield* persistence.rows.organizationMemberships.upsert({
94− id: "mem_1" as never,
95− organizationId: "org_1" as never,
96− accountId: "acc_1" as never,
107+ id: OrganizationMemberIdSchema.make("mem_1"),
108+ organizationId,
109+ accountId,
97110 role: "viewer",
98111 status: "active",
99112 billable: true,
@@ -104,9 +117,9 @@ describe("control-plane-persistence-drizzle", () => { · 4 unmodified lines
104117 });
105118
106119 yield* persistence.rows.organizationMemberships.upsert({
107− id: "mem_2" as never,
108− organizationId: "org_1" as never,
109− accountId: "acc_1" as never,
120+ id: OrganizationMemberIdSchema.make("mem_2"),
121+ organizationId,
122+ accountId,
110123 role: "admin",
111124 status: "active",
112125 billable: true,
@@ -117,8 +130,8 @@ describe("control-plane-persistence-drizzle", () => { · 4 unmodified lines
117130 });
118131
119132 const membership = yield* persistence.rows.organizationMemberships.getByOrganizationAndAccount(
120− "org_1" as never,
121− "acc_1" as never,
133+ organizationId,
134+ accountId,
122135 );
123136
124137 expect(Option.isSome(membership)).toBe(true);
packages/control-plane/src/runtime/control-plane-runtime.test.ts+2−2
@@ -1,7 +1,7 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import * as Effect from "effect/Effect";
33
4−import type { AccountId } from "#schema";
4+import { WorkspaceIdSchema, type AccountId } from "#schema";
55
66 import { makeSqlControlPlaneRuntime } from "./index";
77 import { withControlPlaneClient } from "./test-http-client";
@@ -312,7 +312,7 @@ describe("control-plane-runtime", () => { · 304 unmodified lines
312312 expect(deleteOrg.removed).toBe(true);
313313
314314 const deletedWorkspaceLookup = yield* Effect.either(
315− runtime.service.getWorkspace(workspace.id as never),
315+ runtime.service.getWorkspace(WorkspaceIdSchema.make(workspace.id)),
316316 );
317317 expect(deletedWorkspaceLookup._tag).toBe("Left");
318318