Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/@aws-cdk/mixins-preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ Mixins.of(bucket)
.apply(new AutoDeleteObjects());
```

### Fluent Syntax with `.with()`

For convenience, you can use the `.with()` method for a more fluent syntax:

```typescript
import '@aws-cdk/mixins-preview/with';

const bucket = new s3.CfnBucket(scope, "MyBucket")
.with(new EnableVersioning())
.with(new AutoDeleteObjects());
```

The `.with()` method is available after importing `@aws-cdk/mixins-preview/with`, which augments all constructs with this method. It provides the same functionality as `Mixins.of().apply()` but with a more chainable API.

> **Note**: The `.with()` fluent syntax is only available in JavaScript and TypeScript. Other jsii languages (Python, Java, C#, and Go) should use the `Mixins.of(...).mustApply()` syntax instead. The import requirement is temporary during the preview phase. Once the API is stable, the `.with()` method will be available by default on all constructs and in all languages.

## Creating Custom Mixins

Mixins are simple classes that implement the `IMixin` interface:
Expand Down
59 changes: 59 additions & 0 deletions packages/@aws-cdk/mixins-preview/lib/core/applicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { IConstruct } from 'constructs';
import { ValidationError } from 'aws-cdk-lib/core';
import type { IMixin } from './mixins';
import { ConstructSelector } from './selectors';

/**
* Applies mixins to constructs.
*/
export class MixinApplicator {
private readonly scope: IConstruct;
private readonly selector: ConstructSelector;

constructor(
scope: IConstruct,
selector: ConstructSelector = ConstructSelector.all(),
) {
this.scope = scope;
this.selector = selector;
}

/**
* Applies a mixin to selected constructs.
*/
apply(mixin: IMixin): this {
const constructs = this.selector.select(this.scope);
for (const construct of constructs) {
if (mixin.supports(construct)) {
const errors = mixin.validate?.(construct) ?? [];
if (errors.length > 0) {
throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, this.scope);
}
mixin.applyTo(construct);
}
}
return this;
}

/**
* Applies a mixin and requires that it be applied to at least one construct.
*/
mustApply(mixin: IMixin): this {
const constructs = this.selector.select(this.scope);
let applied = false;
for (const construct of constructs) {
if (mixin.supports(construct)) {
const errors = mixin.validate?.(construct) ?? [];
if (errors.length > 0) {
throw new ValidationError(`Mixin validation failed: ${errors.join(', ')}`, construct);
}
mixin.applyTo(construct);
applied = true;
}
}
if (!applied) {
throw new ValidationError(`Mixin ${mixin.constructor.name} could not be applied to any constructs`, this.scope);
}
return this;
}
}
4 changes: 4 additions & 0 deletions packages/@aws-cdk/mixins-preview/lib/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Re-export all core functionality from separate modules
export * from './mixins';
export * from './selectors';
export * from './applicator';
68 changes: 68 additions & 0 deletions packages/@aws-cdk/mixins-preview/lib/core/mixins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { IConstruct } from 'constructs';
import type { ConstructSelector } from './selectors';
import { MixinApplicator } from './applicator';

// this will change when we update the interface to deliberately break compatibility checks
const MIXIN_SYMBOL = Symbol.for('@aws-cdk/mixins-preview.Mixin.pre1');

/**
* Main entry point for applying mixins.
*/
export class Mixins {
/**
* Creates a MixinApplicator for the given scope.
*/
static of(scope: IConstruct, selector?: ConstructSelector): MixinApplicator {
return new MixinApplicator(scope, selector);
}
}

/**
* A mixin is a reusable piece of functionality that can be applied to constructs
* to add behavior, properties, or modify existing functionality without inheritance.
*/
export interface IMixin {
/**
* Determines whether this mixin can be applied to the given construct.
*/
supports(construct: IConstruct): boolean;

/**
* Validates the construct before applying the mixin.
*/
validate?(construct: IConstruct): string[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this not failing the jsii build?

I also still think validate() doesn't need to be here.

Copy link
Contributor Author

@mrgrain mrgrain Nov 14, 2025

Choose a reason for hiding this comment

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

why should it be failing the build (it doesn't)?

Copy link
Contributor

Choose a reason for hiding this comment

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

That might be an oversight. There is approximately 0% chance this gets translated appropriately to Java.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like jsii is silently ignoring it 🙈

        {
          "abstract": true,
          "docs": {
            "stability": "experimental",
            "summary": "Validates the construct before applying the mixin."
          },
          "locationInModule": {
            "filename": "lib/core/mixins.ts",
            "line": 33
          },
          "name": "validate",
          "parameters": [
            {
              "name": "construct",
              "type": {
                "fqn": "constructs.IConstruct"
              }
            }
          ],
          "returns": {
            "type": {
              "collection": {
                "elementtype": {
                  "primitive": "string"
                },
                "kind": "array"
              }
            }
          }
        }


/**
* Applies the mixin functionality to the target construct.
*/
applyTo(construct: IConstruct): IConstruct;
}

/**
* Abstract base class for mixins that provides default implementations.
*/
export abstract class Mixin implements IMixin {
/**
* Checks if `x` is a Mixin.
*
* @param x Any object
* @returns true if `x` is an object created from a class which extends `Mixin`.
*/
static isMixin(x: any): x is Mixin {
return x != null && typeof x === 'object' && MIXIN_SYMBOL in x;
}

constructor() {
Object.defineProperty(this, MIXIN_SYMBOL, { value: true });
}

public supports(_construct: IConstruct): boolean {
return true;
}

public validate(_construct: IConstruct): string[] {
return [];
}

abstract applyTo(construct: IConstruct): IConstruct;
}
109 changes: 109 additions & 0 deletions packages/@aws-cdk/mixins-preview/lib/core/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { IConstruct } from 'constructs';
import { CfnResource } from 'aws-cdk-lib/core';

/**
* Selects constructs from a construct tree based on various criteria.
*/
export abstract class ConstructSelector {
/**
* Selects all constructs in the tree.
*/
static all(): ConstructSelector {
return new AllConstructsSelector();
}

/**
* Selects CfnResource constructs or the default CfnResource child.
*/
static cfnResource(): ConstructSelector {
return new CfnResourceSelector();
}

/**
* Selects constructs of a specific type.
*/
static resourcesOfType(type: string | any): ConstructSelector {
return new ResourceTypeSelector(type);
}

/**
* Selects constructs whose IDs match a pattern.
*/
static byId(pattern: any): ConstructSelector {
return new IdPatternSelector(pattern);
}

/**
* Selects constructs from the given scope based on the selector's criteria.
*/
abstract select(scope: IConstruct): IConstruct[];
}

class AllConstructsSelector extends ConstructSelector {
select(scope: IConstruct): IConstruct[] {
return scope.node.findAll();
}
}

class CfnResourceSelector extends ConstructSelector {
select(scope: IConstruct): IConstruct[] {
if (CfnResource.isCfnResource(scope)) {
return [scope];
}
const defaultChild = scope.node.defaultChild;
if (CfnResource.isCfnResource(defaultChild)) {
return [defaultChild];
}
return [];
}
}

class ResourceTypeSelector extends ConstructSelector {
constructor(private readonly type: string | any) {
super();
}

select(scope: IConstruct): IConstruct[] {
const result: IConstruct[] = [];
const visit = (node: IConstruct) => {
if (typeof this.type === 'string') {
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type) {
result.push(node);
}
} else if ('isCfnResource' in this.type && 'CFN_RESOURCE_TYPE_NAME' in this.type) {
if (CfnResource.isCfnResource(node) && node.cfnResourceType === this.type.CFN_RESOURCE_TYPE_NAME) {
result.push(node);
}
} else {
if (node instanceof this.type) {
result.push(node);
}
}
for (const child of node.node.children) {
visit(child);
}
};
visit(scope);
return result;
}
}

class IdPatternSelector extends ConstructSelector {
constructor(private readonly pattern: any) {
super();
}

select(scope: IConstruct): IConstruct[] {
const result: IConstruct[] = [];
const visit = (node: IConstruct) => {
if (this.pattern && typeof this.pattern.test === 'function' && this.pattern.test(node.node.id)) {
result.push(node);
}
for (const child of node.node.children) {
visit(child);
}
};
visit(scope);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as path from 'path';
import type { Construct } from 'constructs';
import type { CustomResourceProviderOptions } from 'aws-cdk-lib/core';
import { Stack, CustomResourceProviderBase, determineLatestNodeRuntimeName } from 'aws-cdk-lib/core';

export class AutoDeleteObjectsProvider extends CustomResourceProviderBase {
public static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string {
return this.getOrCreateProvider(scope, uniqueid, props).serviceToken;
}

public static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteObjectsProvider {
const id = `${uniqueid}CustomResourceProvider`;
const stack = Stack.of(scope);
const existing = stack.node.tryFindChild(id) as AutoDeleteObjectsProvider;
return existing ?? new AutoDeleteObjectsProvider(stack, id, props);
}

private constructor(scope: Construct, id: string, props?: CustomResourceProviderOptions) {
super(scope, id, {
...props,
codeDirectory: path.join(__dirname, '..', 'dist', 'aws-s3', 'auto-delete-objects-handler'),
runtimeName: determineLatestNodeRuntimeName(scope),
});
this.node.addMetadata('aws:cdk:is-custom-resource-handler-customResourceProvider', true);
}
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/mixins-preview/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './mixins';
export * as core from './core';
export * as mixins from './mixins';
74 changes: 1 addition & 73 deletions packages/@aws-cdk/mixins-preview/lib/mixins/index.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1 @@
import type { IConstruct } from 'constructs';

/**
* A mixin is a reusable piece of functionality that can be applied to constructs
* to add behavior, properties, or modify existing functionality without inheritance.
*
* Mixins follow a three-phase pattern:
* 1. Check if the mixin supports the target construct (supports)
* 2. Optionally validate the construct before applying (validate)
* 3. Apply the mixin functionality to the construct (applyTo)
*/
export interface IMixin {
/**
* Determines whether this mixin can be applied to the given construct.
*
* This method should perform type checking and compatibility validation
* to ensure the mixin can safely operate on the construct.
*
* @param construct - The construct to check for compatibility
* @returns true if the mixin supports this construct type, false otherwise
*/
supports(construct: IConstruct): boolean;

/**
* Validates the construct before applying the mixin.
*
* This optional method allows the mixin to perform additional validation
* beyond basic type compatibility. It can check for required properties,
* configuration constraints, or other preconditions.
*
* @param construct - The construct to validate
* @returns An array of validation error messages, or empty array if valid
*/
validate?(construct: IConstruct): string[];

/**
* Applies the mixin functionality to the target construct.
*
* This method performs the actual work of the mixin, such as:
* - Adding new properties or methods
* - Modifying existing behavior
* - Setting up additional resources or configurations
* - Establishing relationships with other constructs
*
* @param construct - The construct to apply the mixin to
* @returns The modified construct (may be the same instance or a wrapper)
*/
applyTo(construct: IConstruct): IConstruct;
}

/**
* Abstract base class for mixins that provides default implementations
* and simplifies mixin creation.
*/
export abstract class Mixin implements IMixin {
/**
* Default implementation that supports any construct.
* Override this method to add type-specific support logic.
*/
public supports(_construct: IConstruct): boolean {
return true;
}

/**
* Default validation implementation that returns no errors.
* Override this method to add custom validation logic.
*/
public validate(_construct: IConstruct): string[] {
return [];
}

abstract applyTo(construct: IConstruct): IConstruct;
}
export * from './property-mixins';
25 changes: 25 additions & 0 deletions packages/@aws-cdk/mixins-preview/lib/mixins/property-mixins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Strategy for handling nested properties in L1 property mixins
*/
export enum PropertyMergeStrategy {
/**
* Override all properties
*/
OVERRIDE = 'override',
/**
* Deep merge nested objects, override primitives and arrays
*/
MERGE = 'merge',
}

/**
* Options for applying CfnProperty mixins
*/
export interface CfnPropertyMixinOptions {
/**
* Strategy for merging nested properties
*
* @default - PropertyMergeStrategy.MERGE
*/
readonly strategy?: PropertyMergeStrategy;
}
Loading
Loading