diff --git a/packages/aws/package.json b/packages/aws/package.json index f133182..2079aae 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "@notation/aws.iac": "workspace:*", - "@notation/core": "workspace:*", "@notation/resource": "workspace:*", "@notation/std.iac": "workspace:*", "@types/aws-lambda": "^8.10.147", diff --git a/packages/aws/src/api-gateway/api.ts b/packages/aws/src/api-gateway/api.ts index 60dcdc7..4d4fec0 100644 --- a/packages/aws/src/api-gateway/api.ts +++ b/packages/aws/src/api-gateway/api.ts @@ -1,11 +1,7 @@ import * as aws from "@notation/aws.iac"; -import type { ResourceCollector } from "@notation/core"; -export const api = (collector: ResourceCollector, rgConfig: { name: string }) => { - const apiGroup = new aws.AwsResourceGroup("API Gateway", { - ...rgConfig, - collector, - }); +export const api = (rgConfig: { name: string }) => { + const apiGroup = new aws.AwsResourceGroup("API Gateway", rgConfig); const apiResource = apiGroup.add( new aws.apiGateway.Api({ diff --git a/packages/aws/src/api-gateway/route.ts b/packages/aws/src/api-gateway/route.ts index 7a1bee6..cb4cc54 100644 --- a/packages/aws/src/api-gateway/route.ts +++ b/packages/aws/src/api-gateway/route.ts @@ -3,13 +3,11 @@ import type { JWTAuthorizedApiGatewayHandler, } from "src/shared"; import * as aws from "@notation/aws.iac"; -import type { ResourceCollector } from "@notation/core"; import { api } from "./api"; import { AuthorizerConfig } from "./auth"; import { mapAuthConfig, mapAuthType } from "./utils"; export const route = ( - collector: ResourceCollector, apiGroup: ReturnType, method: string, // todo: http methods only path: `/${string}`, @@ -23,11 +21,8 @@ export const route = ( const apiResource = apiGroup.findResource(aws.apiGateway.Api)!; const routeId = `${apiResource.id}-${method}-${path}`; - const lambdaGroup = - handler instanceof aws.AwsResourceGroup - ? handler - : // at compile time, runtime module becomes infra resource group - (handler as any as aws.AwsResourceGroup); + // at compile time, a runtime handler import becomes an infra resource group + const lambdaGroup = handler as aws.AwsResourceGroup; const lambdaResource = lambdaGroup.findResource(aws.lambda.LambdaFunction)!; @@ -63,7 +58,6 @@ export const route = ( const routeGroup = new aws.AwsResourceGroup("API Gateway/Route", { dependencies: { router: apiGroup.id, fn: lambdaGroup.id }, - collector, }); if (auth.type != "NONE") { diff --git a/packages/aws/src/api-gateway/router.ts b/packages/aws/src/api-gateway/router.ts index a59efff..104ff29 100644 --- a/packages/aws/src/api-gateway/router.ts +++ b/packages/aws/src/api-gateway/router.ts @@ -6,16 +6,12 @@ import * as aws from "@notation/aws.iac"; import { route } from "./route"; import { api } from "./api"; import { AuthorizerConfig, JWTAuthorizerConfig, NO_AUTH } from "./auth"; -import type { ResourceCollector } from "@notation/core"; -export const router = ( - collector: ResourceCollector, - apiGroup: ReturnType, -) => { +export const router = (apiGroup: ReturnType) => { const createRouteCallback = (method: string) => (path: `/${string}`, handler: ApiGatewayHandler | aws.AwsResourceGroup) => { - return route(collector, apiGroup, method, path, NO_AUTH, handler); + return route(apiGroup, method, path, NO_AUTH, handler); }; return { @@ -25,7 +21,7 @@ export const router = ( patch: createRouteCallback("PATCH"), delete: createRouteCallback("DELETE"), withJWTAuthorizer: (auth: JWTAuthorizerConfig) => { - const authorizer = new AuthorizedRouteBuilder(collector, apiGroup); + const authorizer = new AuthorizedRouteBuilder(apiGroup); return authorizer.withJWTAuthorizer(auth); }, }; @@ -34,10 +30,8 @@ export const router = ( class AuthorizedRouteBuilder { auth: AuthorizerConfig = NO_AUTH; apiGroup: ReturnType; - collector: ResourceCollector; - constructor(collector: ResourceCollector, apiGroup: ReturnType) { - this.collector = collector; + constructor(apiGroup: ReturnType) { this.apiGroup = apiGroup; } @@ -47,7 +41,7 @@ class AuthorizedRouteBuilder { path: `/${string}`, handler: JWTAuthorizedApiGatewayHandler, ) => { - return route(this.collector, this.apiGroup, method, path, authorizer, handler); + return route(this.apiGroup, method, path, authorizer, handler); }; withJWTAuthorizer(authorizer: JWTAuthorizerConfig) { diff --git a/packages/aws/src/event-bridge/event-bridge-schedule.ts b/packages/aws/src/event-bridge/event-bridge-schedule.ts index 65d8f99..e4f79d8 100644 --- a/packages/aws/src/event-bridge/event-bridge-schedule.ts +++ b/packages/aws/src/event-bridge/event-bridge-schedule.ts @@ -1,33 +1,23 @@ -import type { ResourceCollector } from "@notation/core"; import { EventBridgeHandler } from "src/shared/lambda.handler"; import { Schedule } from "./schedule"; import * as aws from "@notation/aws.iac"; import { toAwsScheduleExpression } from "./aws-conversions"; -export const schedule = ( - collector: ResourceCollector, - config: { - name: string; - schedule: Schedule; - handler: - | EventBridgeHandler<"Scheduled Event", any> - // todo: narrow to lambda group - | aws.AwsResourceGroup; - }, -): aws.AwsResourceGroup => { +export const schedule = (config: { + name: string; + schedule: Schedule; + handler: + | EventBridgeHandler<"Scheduled Event", any> + // todo: narrow to lambda group + | aws.AwsResourceGroup; +}): aws.AwsResourceGroup => { const eventBridgeScheduleGroup = new aws.AwsResourceGroup( "aws/eventBridge/schedule", - { - ...config, - collector, - }, + config, ); - const lambdaGroup = - config.handler instanceof aws.AwsResourceGroup - ? config.handler - : // at compile time, runtime module becomes infra resource group - (config.handler as any as aws.AwsResourceGroup); + // at compile time, a runtime handler import becomes an infra resource group + const lambdaGroup = config.handler as aws.AwsResourceGroup; const lambdaResource = lambdaGroup.findResource(aws.lambda.LambdaFunction)!; diff --git a/packages/aws/src/lambda/lambda.ts b/packages/aws/src/lambda/lambda.ts index 5eecdb2..0b6cc3a 100644 --- a/packages/aws/src/lambda/lambda.ts +++ b/packages/aws/src/lambda/lambda.ts @@ -2,7 +2,6 @@ import * as aws from "@notation/aws.iac"; import * as std from "@notation/std.iac"; import crypto from "crypto"; import path from "path"; -import type { ResourceCollector } from "@notation/core"; type LambdaConfig = { id?: string; @@ -14,14 +13,8 @@ type LambdaConfig = { runtime?: aws.lambda.LambdaFunctionConfig["Runtime"]; }; -export const lambda = ( - collector: ResourceCollector, - config: LambdaConfig, -): aws.AwsResourceGroup => { - const functionGroup = new aws.AwsResourceGroup("Lambda", { - config, - collector, - }); +export const lambda = (config: LambdaConfig): aws.AwsResourceGroup => { + const functionGroup = new aws.AwsResourceGroup("Lambda", { config }); const filePath = config.code.path; let lambdaId = config.id; diff --git a/packages/aws/test/api-gateway.test.ts b/packages/aws/test/api-gateway.test.ts index 9cc3e23..d47ff53 100644 --- a/packages/aws/test/api-gateway.test.ts +++ b/packages/aws/test/api-gateway.test.ts @@ -1,19 +1,12 @@ -import { test, expect, beforeEach } from "vitest"; -import { ResourceCollector } from "@notation/core"; +import { test, expect } from "vitest"; import { apiGateway } from "@notation/aws.iac"; import { NO_AUTH, api, router } from "src/api-gateway"; import { route } from "src/api-gateway/route"; import { lambda } from "src/lambda"; -let collector: ResourceCollector; - -beforeEach(() => { - collector = new ResourceCollector(); -}); - test("route resource group idempotency snapshot", () => { - const apiResourceGroup = api(collector, { name: "api" }); - const fnResourceGroup = lambda(collector, { + const apiResourceGroup = api({ name: "api" }); + const fnResourceGroup = lambda({ code: { type: "file", path: "src/fns/handler.fn.js", @@ -21,32 +14,18 @@ test("route resource group idempotency snapshot", () => { handler: "handler.fn.js", }); - route( - collector, - apiResourceGroup, - "GET", - "/hello", - NO_AUTH, - fnResourceGroup as any, - ); + route(apiResourceGroup, "GET", "/hello", NO_AUTH, fnResourceGroup as any); const fnResourceGroupSnapshot = JSON.stringify(fnResourceGroup); - route( - collector, - apiResourceGroup, - "POST", - "/hello", - NO_AUTH, - fnResourceGroup as any, - ); + route(apiResourceGroup, "POST", "/hello", NO_AUTH, fnResourceGroup as any); const fnResourceGroupSnapshot2 = JSON.stringify(fnResourceGroup); expect(fnResourceGroupSnapshot).toEqual(fnResourceGroupSnapshot2); }); test("router provides methods for each HTTP verb", () => { - const apiResourceGroup = api(collector, { name: "api" }); - const apiRouter = router(collector, apiResourceGroup); - const handler = lambda(collector, { + const apiResourceGroup = api({ name: "api" }); + const apiRouter = router(apiResourceGroup); + const handler = lambda({ code: { type: "file", path: "src/fns/handler.fn.js", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d0b4bb8..17756af 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,4 +4,3 @@ export * from "./utils/paths"; export * from "./visualiser/chart"; export * from "./orchestrator/resource"; export * from "./orchestrator/resource-group"; -export * from "./orchestrator/resource-collector"; diff --git a/packages/core/src/orchestrator/graph.ts b/packages/core/src/orchestrator/graph.ts index 0ddc29c..1f78513 100644 --- a/packages/core/src/orchestrator/graph.ts +++ b/packages/core/src/orchestrator/graph.ts @@ -1,28 +1,23 @@ import path from "path"; +import { + collectResourceGraph, + type BaseResource, + type ResourceGroup, +} from "@notation/resource"; import { filePaths } from "src/utils/paths"; -import { ResourceCollector } from "./resource-collector"; -export async function getResourceGraph(entryPoint: string) { - const collector = new ResourceCollector(); +type ResourceGraph = { + resourceGroups: ResourceGroup[]; + resources: BaseResource[]; +}; + +export async function getResourceGraph( + entryPoint: string, +): Promise { const outFilePath = filePaths.dist.infra(entryPoint); // todo: move into worker thread. this will cause memory leaks - const mod = await import(path.join(process.cwd(), `${outFilePath}?${Date.now()}`)); - - const register = (mod as any).register ?? (mod as any).default; - - if (typeof register !== "function") { - throw new Error( - `Infra entrypoint must export register(collector) (or a default function). Received exports: ${Object.keys(mod).join( - ", ", - )}`, - ); - } - - await register(collector); - - return { - resourceGroups: collector.getResourceGroups(), - resources: collector.getResources(), - }; + return collectResourceGraph(async () => { + await import(path.join(process.cwd(), `${outFilePath}?${Date.now()}`)); + }); } diff --git a/packages/core/src/orchestrator/resource-collector.ts b/packages/core/src/orchestrator/resource-collector.ts deleted file mode 100644 index eec79b5..0000000 --- a/packages/core/src/orchestrator/resource-collector.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { BaseResource } from "./resource"; -import type { - ResourceCollector as ResourceCollectorContract, - ResourceGroup, -} from "@notation/resource"; - -export class ResourceCollector implements ResourceCollectorContract { - private resourceGroups: ResourceGroup[] = []; - private resources: BaseResource[] = []; - private nextResourceGroupId = 0; - - allocateResourceGroupId(): number { - return this.nextResourceGroupId++; - } - - registerResourceGroup(group: ResourceGroup) { - this.resourceGroups.push(group); - } - - registerResource(resource: BaseResource) { - if (this.resources.includes(resource)) { - throw new Error(`Resource ${resource.type} has already been registered.`); - } - - this.resources.push(resource); - } - - getResourceGroups() { - return this.resourceGroups; - } - - getResources() { - return this.resources; - } -} diff --git a/packages/core/test/orchestrator/resource-group.test.ts b/packages/core/test/orchestrator/resource-group.test.ts deleted file mode 100644 index 9890d16..0000000 --- a/packages/core/test/orchestrator/resource-group.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { beforeEach, expect, it } from "vitest"; -import { ResourceCollector, ResourceGroup } from "src"; -import { - TestResource, - TestResource2, - testResourceConfig, -} from "./resource.doubles"; - -let collector: ResourceCollector; - -beforeEach(() => { - collector = new ResourceCollector(); -}); - -class TestResourceGroup extends ResourceGroup { - platform = "test-platform"; -} - -const testResource = new TestResource({ - id: "test-resource-1", - config: testResourceConfig, -}); - -const testResource2 = new TestResource2({ - id: "test-resource-2", - config: testResourceConfig, -}); - -it("creates a resource group", () => { - const resourceGroup = new TestResourceGroup("test-group", { - a: 1, - collector, - }); - expect(resourceGroup.id).toBe(0); - expect(resourceGroup.type).toBe("test-group"); - expect(resourceGroup.platform).toBe("test-platform"); - expect(resourceGroup.config).toEqual({ a: 1 }); - expect(resourceGroup.resources).toEqual([]); -}); - -it("registers resource groups in the collector", () => { - new TestResourceGroup("test-group", { type: "test1", collector }); - new TestResourceGroup("test-group-2", { type: "test2", collector }); - - const groups = collector.getResourceGroups(); - expect(groups).toHaveLength(2); - expect(groups[0].type).toBe("test-group"); - expect(groups[1].type).toBe("test-group-2"); -}); - -it("creates a resource within a group", () => { - const resourceGroup = new TestResourceGroup("test-group", { collector }); - const resource = resourceGroup.add(testResource); - expect(resourceGroup.resources).toContain(resource); -}); - -it("registers resources in the collector", () => { - const resourceGroup = new TestResourceGroup("test-group", { collector }); - resourceGroup.add(testResource); - resourceGroup.add(testResource2); - - const resources = collector.getResources(); - expect(resources).toHaveLength(2); - expect(resources[0]).toBe(testResource); - expect(resources[1]).toBe(testResource2); -}); - -it("increments resource group IDs", () => { - const rg1 = new TestResourceGroup("test-group", { - type: "group1", - collector, - }); - const rg2 = new TestResourceGroup("test-group", { - type: "group2", - collector, - }); - - expect(rg1.id).toBe(0); - expect(rg2.id).toBe(1); -}); - -it("finds a resource within a group", () => { - const resourceGroup = new TestResourceGroup("test-group", { - type: "group1", - collector, - }); - const resource = resourceGroup.add(testResource); - - expect(resourceGroup.findResource(TestResource)).toBe(resource); - expect(resourceGroup.findResource(TestResource2)).toBe(undefined); -}); - -it("references resources within groups", () => { - const rg1 = new TestResourceGroup("test-group", { collector }); - const r1 = rg1.add(testResource); - const r2 = rg1.add(testResource2); - - expect(collector.getResources()).toContain(r1); - expect(collector.getResources()).toContain(r2); -}); - -it("throws an error when adding an existing resource", () => { - const rg1 = new TestResourceGroup("test-group", { collector }); - rg1.add(testResource); - expect(() => rg1.add(testResource)).toThrow(); -}); - -it("increments resource IDs globally", () => { - const rg1 = new TestResourceGroup("test-group", { collector }); - const r1 = rg1.add(testResource); - const rg2 = new TestResourceGroup("test-group", { collector }); - const r2 = rg2.add(testResource2); - expect(r1.id).toBe("test-resource-1"); - expect(r2.id).toBe("test-resource-2"); -}); diff --git a/packages/core/test/provisioner/resource-registry.test.ts b/packages/core/test/provisioner/resource-registry.test.ts index 941912d..c5a7c6d 100644 --- a/packages/core/test/provisioner/resource-registry.test.ts +++ b/packages/core/test/provisioner/resource-registry.test.ts @@ -9,7 +9,7 @@ import { const TestResource = resource({ type: "test/service/resource" }) .defineSchema({}) .defineOperations({ - create: async () => ({}), + create: async () => undefined, delete: async () => undefined, }); diff --git a/packages/create-notation/templates/starter/infra/api.ts b/packages/create-notation/templates/starter/infra/api.ts index c8c8338..7c16c5c 100644 --- a/packages/create-notation/templates/starter/infra/api.ts +++ b/packages/create-notation/templates/starter/infra/api.ts @@ -1,10 +1,7 @@ -import type { ResourceCollector } from "@notation/core"; import { api, router } from "@notation/aws/api-gateway"; import { getTodos } from "runtime/todos.fn"; -export function register(collector: ResourceCollector) { - const todoApi = api(collector, { name: "todo-api" }); - const todoRouter = router(collector, todoApi); +const todoApi = api({ name: "todo-api" }); +const todoRouter = router(todoApi); - todoRouter.get("/todos", getTodos); -} +todoRouter.get("/todos", getTodos); diff --git a/packages/resource/package.json b/packages/resource/package.json index 50e2bad..2a1cf14 100644 --- a/packages/resource/package.json +++ b/packages/resource/package.json @@ -4,6 +4,12 @@ "version": "0.12.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, "files": [ "dist" ], diff --git a/packages/resource/src/index.ts b/packages/resource/src/index.ts index b3c94c7..46a899b 100644 --- a/packages/resource/src/index.ts +++ b/packages/resource/src/index.ts @@ -2,3 +2,7 @@ export * from "./types"; export * from "./resource.schema"; export * from "./resource"; export * from "./resource-group"; +export { + collectResourceGraph, + type ResourceGraph, +} from "./resource-collection"; diff --git a/packages/resource/src/resource-collection.ts b/packages/resource/src/resource-collection.ts new file mode 100644 index 0000000..bc51b18 --- /dev/null +++ b/packages/resource/src/resource-collection.ts @@ -0,0 +1,39 @@ +import type { BaseResource } from "./resource"; +import type { ResourceGroup } from "./resource-group"; + +export type ResourceGraph = { + resourceGroups: ResourceGroup[]; + resources: BaseResource[]; +}; + +type ResourceCollection = { + graph: ResourceGraph; + nextResourceGroupId: number; +}; + +let activeCollection: ResourceCollection | undefined; + +export async function collectResourceGraph( + evaluate: () => unknown | Promise, +): Promise { + if (activeCollection) { + throw new Error("A resource graph is already being collected."); + } + + const collection: ResourceCollection = { + graph: { resourceGroups: [], resources: [] }, + nextResourceGroupId: 0, + }; + + activeCollection = collection; + try { + await evaluate(); + return collection.graph; + } finally { + activeCollection = undefined; + } +} + +export function getActiveResourceCollection(): ResourceCollection | undefined { + return activeCollection; +} diff --git a/packages/resource/src/resource-group.ts b/packages/resource/src/resource-group.ts index bcb1d04..a62e336 100644 --- a/packages/resource/src/resource-group.ts +++ b/packages/resource/src/resource-group.ts @@ -1,22 +1,12 @@ import type { BaseResource } from "./resource"; - -export interface ResourceCollector { - allocateResourceGroupId(): number; - registerResourceGroup(group: ResourceGroup): void; - registerResource(resource: BaseResource): void; -} +import { + getActiveResourceCollection, + type ResourceGraph, +} from "./resource-collection"; export type ResourceGroupOptions = { /** - * Optional collector used by orchestration graph builders. - * - * When provided, the group ID is allocated by the collector and the group/resources - * are registered into it. - */ - collector?: ResourceCollector; - - /** - * Optional pre-assigned ID. Only used when no collector is provided. + * Optional pre-assigned ID. Used outside graph collection. */ id?: number; @@ -31,24 +21,30 @@ export abstract class ResourceGroup { config: Record; resources: BaseResource[]; - #collector?: ResourceCollector; + #graph?: ResourceGraph; constructor(type: string, opts: ResourceGroupOptions) { - const { dependencies, collector, id, ...config } = opts; + const { dependencies, id, ...config } = opts; + const collection = getActiveResourceCollection(); this.type = type; - this.#collector = collector; - this.id = collector ? collector.allocateResourceGroupId() : (id ?? -1); + this.#graph = collection?.graph; + this.id = collection ? collection.nextResourceGroupId++ : (id ?? -1); this.dependencies = dependencies || {}; this.config = config || {}; this.resources = []; - if (collector) collector.registerResourceGroup(this); + collection?.graph.resourceGroups.push(this); return this; } add(resource: T) { - if (this.#collector) { - this.#collector.registerResource(resource); + if (this.#graph) { + if (this.#graph.resources.includes(resource)) { + throw new Error( + `Resource ${resource.type} has already been registered.`, + ); + } + this.#graph.resources.push(resource); } else if (this.resources.includes(resource)) { throw new Error(`Resource ${resource.type} has already been registered.`); } @@ -58,9 +54,10 @@ export abstract class ResourceGroup { return resource; } - findResource BaseResource>(ResourceClass: T) { + findResource BaseResource>( + ResourceClass: T, + ) { return this.resources.find((r) => r instanceof ResourceClass) as - | InstanceType - | undefined; + InstanceType | undefined; } } diff --git a/packages/resource/test/resource-group.test.ts b/packages/resource/test/resource-group.test.ts new file mode 100644 index 0000000..247da20 --- /dev/null +++ b/packages/resource/test/resource-group.test.ts @@ -0,0 +1,117 @@ +import { expect, it } from "vitest"; +import { collectResourceGraph, ResourceGroup } from "src"; +import { + TestResource, + TestResource2, + testResourceConfig, +} from "./resource.doubles"; + +class TestResourceGroup extends ResourceGroup { + platform = "test-platform"; +} + +const testResource = new TestResource({ + id: "test-resource-1", + config: testResourceConfig, +}); + +const testResource2 = new TestResource2({ + id: "test-resource-2", + config: testResourceConfig, +}); + +it("creates a resource group", async () => { + const graph = await collectResourceGraph(() => { + new TestResourceGroup("test-group", { a: 1 }); + }); + const resourceGroup = graph.resourceGroups[0] as TestResourceGroup; + + expect(resourceGroup.id).toBe(0); + expect(resourceGroup.type).toBe("test-group"); + expect(resourceGroup.platform).toBe("test-platform"); + expect(resourceGroup.config).toEqual({ a: 1 }); + expect(resourceGroup.resources).toEqual([]); +}); + +it("collects resource groups", async () => { + const graph = await collectResourceGraph(() => { + new TestResourceGroup("test-group", { type: "test1" }); + new TestResourceGroup("test-group-2", { type: "test2" }); + }); + + expect(graph.resourceGroups).toHaveLength(2); + expect(graph.resourceGroups[0].type).toBe("test-group"); + expect(graph.resourceGroups[1].type).toBe("test-group-2"); +}); + +it("creates a resource within a group", () => { + const resourceGroup = new TestResourceGroup("test-group", {}); + const resource = resourceGroup.add(testResource); + expect(resourceGroup.resources).toContain(resource); +}); + +it("collects resources", async () => { + const graph = await collectResourceGraph(() => { + const resourceGroup = new TestResourceGroup("test-group", {}); + resourceGroup.add(testResource); + resourceGroup.add(testResource2); + }); + + expect(graph.resources).toEqual([testResource, testResource2]); +}); + +it("increments resource group IDs", async () => { + const graph = await collectResourceGraph(() => { + new TestResourceGroup("test-group", { type: "group1" }); + new TestResourceGroup("test-group", { type: "group2" }); + }); + + expect(graph.resourceGroups[0].id).toBe(0); + expect(graph.resourceGroups[1].id).toBe(1); +}); + +it("finds a resource within a group", () => { + const resourceGroup = new TestResourceGroup("test-group", { + type: "group1", + }); + const resource = resourceGroup.add(testResource); + + expect(resourceGroup.findResource(TestResource)).toBe(resource); + expect(resourceGroup.findResource(TestResource2)).toBe(undefined); +}); + +it("throws an error when adding an existing resource", () => { + const resourceGroup = new TestResourceGroup("test-group", {}); + resourceGroup.add(testResource); + expect(() => resourceGroup.add(testResource)).toThrow(); +}); + +it("isolates consecutive collections", async () => { + const first = await collectResourceGraph(() => { + new TestResourceGroup("first", {}); + }); + const second = await collectResourceGraph(() => { + new TestResourceGroup("second", {}); + }); + + expect(first.resourceGroups[0].id).toBe(0); + expect(second.resourceGroups[0].id).toBe(0); + expect(first.resourceGroups[0].type).toBe("first"); + expect(second.resourceGroups[0].type).toBe("second"); +}); + +it("releases collection state after an error", async () => { + await expect( + collectResourceGraph(() => { + new TestResourceGroup("failed", {}); + throw new Error("failed"); + }), + ).rejects.toThrow("failed"); + + const graph = await collectResourceGraph(() => { + new TestResourceGroup("recovered", {}); + }); + + expect(graph.resourceGroups[0].id).toBe(0); + expect(graph.resourceGroups[0].type).toBe("recovered"); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 873d38c..a76bfc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,9 +120,6 @@ importers: '@notation/aws.iac': specifier: workspace:* version: link:../aws.iac - '@notation/core': - specifier: workspace:* - version: link:../core '@notation/resource': specifier: workspace:* version: link:../resource @@ -1034,36 +1031,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.1.4': resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.1.4': resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.4': resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.4': resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.1.4': resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.1.4': resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} @@ -1125,66 +1128,79 @@ packages: resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.2': resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.2': resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.2': resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.2': resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.2': resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.2': resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.2': resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.2': resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.2': resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.2': resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.2': resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.2': resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.62.2': resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} @@ -1288,24 +1304,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.2': resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.2': resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.2': resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.2': resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} @@ -1950,24 +1970,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}