Skip to content

feat(core): add combine() multi-ref combinator (#270)#282

Merged
laazyj merged 1 commit into
mainfrom
feat/core-combine-multi-ref
Jul 8, 2026
Merged

feat(core): add combine() multi-ref combinator (#270)#282
laazyj merged 1 commit into
mainfrom
feat/core-combine-multi-ref

Conversation

@laazyj

@laazyj laazyj commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #270. Chosen over the option B builder in #276.

What

Adds combine(refs, transform?) to @composurecdk/core — a Ref combinator. It resolves a record of Resolvable values against the build context and returns a single Ref to the merged record, optionally transformed:

addMethod("GET", combine(
  { table: ref<TableV2BuilderResult>("table"), role: ref<RoleBuilderResult>("apiRole") },
  ({ table, role }) => new AwsIntegration({
    service: "dynamodb", action: "Scan",
    options: {
      credentialsRole: role.role,
      requestTemplates: { "application/json": scanTemplate(table.table.tableName) },
    },
  }),
));

Why

ref() reaches exactly one component, so a consumer that assembles a construct from more than one sibling — e.g. a direct API Gateway → AWS-service AwsIntegration needing both a target's identifier (for its VTL template) and a credentials role — had no single reference to hand to a Resolvable<T> seam. The workaround was a fake Lifecycle whose only job was to merge two sibling refs — a compose graph node modelling no resource, wired with untyped context casts.

combine closes that gap without new machinery:

  • Returns a Ref — drops into every existing Resolvable<T> seam (addMethod, …) with no change to any consuming builder, and composes further with .get()/.map().
  • Owns nothing — creates no construct, holds no state; every sibling it references stays a first-class node in the compose graph.
  • Infers its typesResolved<R> mirrors resolve() at runtime (a Ref<T> yields T, a concrete value passes through), so the merged record's keys/types follow from the input and entries may mix refs and concrete values.

Why not option B (#276)

The #276 builder's real payload was auto-creating an IAM credentials role per integration — net-new machinery aws-cdk-lib does not provide, obscuring a part of the design CDK keeps explicit. combine addresses the actual, general gap instead; the credentials role stays an explicit sibling granted against with consumer-side grants (ADR-0013). AwsIntegration is just the motivating example — combine applies wherever one construct is assembled from several siblings.

Docs & ADR

  • docs/architecture.md — new "Combining refs — one consumer, many dependencies" subsection under Ref, with the example and explicit when-not-to-use guidance.
  • ADR-0015 — generalised decision: intent, when to use, when not to (single sibling → plain ref; permission wiring → consumer-side grants; not an ownership escape hatch). Indexed and cross-referenced from ADR-0013's out-of-scope note.

Scope

Core + docs only, one cohesive change. No apigateway surface change, no auto-created roles. Option B's branch/PR #276 is left untouched to close separately.

Testing

7 new unit tests (merge, independent resolution, transform, mixed concrete/ref entries, error propagation, .map()/.get() composition, Resolvable-seam interchangeability). 100% function/line coverage on ref.ts; npm run verify green across all 21 projects.

@laazyj laazyj self-assigned this Jul 8, 2026

@laazyj laazyj left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorten the ADR to make it tighter

Comment thread docs/adr/0015-combine-multi-ref-combinator.md Outdated
@laazyj laazyj force-pushed the feat/core-combine-multi-ref branch from 30f200e to 5f45966 Compare July 8, 2026 17:28
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Coverage

Overall line coverage: 99.28% across 21 package(s).

Package Statements Branches Functions Lines
acm 🟢 95.34% 🟢 90.47% 🟢 100.00% 🟢 97.43%
apigateway 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
budgets 🟢 99.20% 🟢 95.55% 🟢 100.00% 🟢 100.00%
cloudformation 🟢 97.69% 🟢 95.40% 🟢 100.00% 🟢 99.16%
cloudfront 🟢 99.40% 🟢 95.14% 🟢 100.00% 🟢 100.00%
cloudwatch 🟢 95.56% 🟡 89.55% 🟢 100.00% 🟢 98.23%
core 🟢 100.00% 🟢 97.36% 🟢 100.00% 🟢 100.00%
dynamodb 🟢 98.50% 🟡 88.88% 🟢 100.00% 🟢 100.00%
ec2 🟢 98.88% 🟢 97.82% 🟢 100.00% 🟢 99.59%
eslint-plugin 🟡 89.88% 🟡 83.33% 🟢 100.00% 🟢 97.90%
events 🟢 97.50% 🟢 92.30% 🟢 100.00% 🟢 100.00%
examples 🟢 94.78% 🟡 76.66% 🟢 100.00% 🟢 99.06%
iam 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
lambda 🟢 97.88% 🟢 96.26% 🟢 100.00% 🟢 98.30%
logs 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
module-compat 🟢 100.00% 🟢 100.00% 🟢 100.00% 🟢 100.00%
neptune 🟢 96.00% 🟡 84.78% 🟢 100.00% 🟢 98.57%
route53 🟢 98.24% 🟢 96.04% 🟢 100.00% 🟢 99.04%
s3 🟢 98.94% 🟢 98.24% 🟢 100.00% 🟢 100.00%
sns 🟢 98.73% 🟢 96.66% 🟢 100.00% 🟢 100.00%
sqs 🟢 99.24% 🟢 97.18% 🟢 100.00% 🟢 100.00%
Total 🟢 97.67% 🟢 93.77% 🟢 100.00% 🟢 99.28%

Add `combine(refs, transform?)` to `@composurecdk/core`: a Ref
combinator that resolves a record of Resolvable values against the
build context and returns a single Ref to the merged record, optionally
transformed.

`ref()` reaches exactly one component, so a consumer that assembles a
construct from more than one sibling (e.g. a direct API Gateway ->
AWS-service AwsIntegration needing both a target's identifier for its
VTL template and a credentials role) had no single reference to hand to
a Resolvable<T> seam. The workaround was a fake Lifecycle whose only job
was to merge two sibling refs -- a graph node modelling no resource.

combine closes that gap without new machinery: it returns an ordinary
Ref, so it drops into every existing Resolvable seam with no builder
change, owns nothing (every sibling stays a first-class compose node),
and infers the merged record's types from the input.

This is the resolution to #270 chosen over the option B builder in #276,
which would have auto-created an IAM role per integration -- net-new
behaviour aws-cdk-lib does not provide, obscuring design CDK keeps
explicit. The credentials role stays an explicit sibling granted against
with consumer-side grants (ADR-0013).

Documents the concept in architecture.md, adds ADR-0015 describing the
intent and when (not) to use it, and cross-references it from ADR-0013's
out-of-scope note.

Closes #270
@laazyj laazyj force-pushed the feat/core-combine-multi-ref branch from 5f45966 to 569b59d Compare July 8, 2026 17:32
@laazyj laazyj merged commit 9a067c9 into main Jul 8, 2026
21 checks passed
@laazyj laazyj deleted the feat/core-combine-multi-ref branch July 8, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(apigateway): AWS-service integration should own its credentials role (enables consumer-side grants)

1 participant