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
2 changes: 2 additions & 0 deletions __tests__/containers/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { TouchedIndexesFeature } from "../../src/features/TouchedIndexes/index.t
import { OsRecordDecompressorFeature } from "../../src/features/OsRecordDecompressor/index.ts";
import { OsScannerFeature } from "../../src/features/OsScanner/index.ts";
import { OsProcessorFeature } from "../../src/features/OsProcessor/index.ts";
import { IndexConfigurationProviderFeature } from "../../src/features/IndexConfigurationProvider/index.ts";
import { AccessCheckerFeature } from "../../src/features/AccessChecker/index.ts";
import { MockDynamoDbClient } from "../services/DynamoDbClient/MockDynamoDbClient.ts";
import { MockOpenSearchClient } from "../services/OpenSearchClient/MockOpenSearchClient.ts";
Expand Down Expand Up @@ -118,6 +119,7 @@ export function createOsContainer(options: OsContainerOptions = {}): Container {
container.registerInstance(DroppedRecordLog, new MockDroppedRecordLog());
container.registerInstance(TransferredRecordLog, new MockTransferredRecordLog());
PipelineRunnerFeature.register(container);
IndexConfigurationProviderFeature.register(container);
TouchedIndexesFeature.register(container);
DdbExecutorFeature.register(container);
OsRecordDecompressorFeature.register(container);
Expand Down
28 changes: 28 additions & 0 deletions documentation/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Documentation agent guidelines

This directory documents user-facing configuration points — abstractions users can override via `config.register` to customize transfer behavior.

## Adding a new configuration

1. Create `configurations/<AbstractionName>/README.md`.
2. Add a breadcrumb line as the very first line, before the title:
```
[Documentation](../../README.md) > [Configurations](../../README.md#configurations) > AbstractionName
```
Adjust the relative path depth to match the file's location. Every doc page must link back to its parent.
3. Follow the structure used in `configurations/IndexConfigurationProvider/README.md`:
- **Title** — abstraction name.
- **When it runs** — where in the transfer lifecycle the abstraction is called and by whom.
- **Default behavior** — what the built-in implementation does.
- **Override example** — complete, copy-pasteable code showing a custom implementation class, `createImplementation`, and the `register` hook in config.
- **Per-X configuration** — if the method receives a discriminator (index name, table name, etc.), show a branching example.
- **API** — interface and type signatures.
- **Source** — path to the feature directory.
4. Add a row to the table in `documentation/README.md`.

## Rules

- Examples must compile against the current public API (`src/index.ts`). If an export is missing, add it to the public API first.
- Show `createImplementation` with the correct abstraction — users register Implementation classes, not raw classes.
- Keep examples minimal — enough to demonstrate the override, not a production-ready solution.
- Do not duplicate AGENTS.md content from the project root. This file covers documentation conventions only.
25 changes: 25 additions & 0 deletions documentation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Curated list of available documentation

### Configurations

A user can configure the behavior of the application by providing their own implementations for an abstraction via the `register` hook in `createConfig()`.

```typescript
export default createConfig({
// ... source, target, pipeline ...
register: async container => {
container.register(MyCustomImplementation);
}
});
```

Each configuration has its own folder under `./configurations/` with a README describing what it does, the default behavior, and how to override it.

| Abstraction | Description | Docs |
| ---------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| `IndexConfigurationProvider` | Controls OpenSearch index mappings and settings applied on index creation and update | [README](./configurations/IndexConfigurationProvider/README.md) |
| `ModelProvider` | Loads CMS model definitions used by transformers for field metadata | [README](./configurations/ModelProvider/README.md) |
| `BeforeTransferHook` | Runs before the transfer starts (orchestrator only, after access checks) | [README](./configurations/BeforeTransferHook/README.md) |
| `AfterTransferHook` | Runs after the transfer completes (orchestrator only, skipped on failure) | [README](./configurations/AfterTransferHook/README.md) |
| `BeforeLoadPresetHook` | Runs before the preset is loaded (each worker, receives config) | [README](./configurations/BeforeLoadPresetHook/README.md) |
| `AfterLoadPresetHook` | Runs after the preset is loaded (each worker, receives config + preset) | [README](./configurations/AfterLoadPresetHook/README.md) |
62 changes: 62 additions & 0 deletions documentation/configurations/AfterLoadPresetHook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[Documentation](../../README.md) > [Configurations](../../README.md#configurations) > AfterLoadPresetHook

# AfterLoadPresetHook

Runs custom logic **after** the preset is loaded and configured — in each worker process.

## When it runs

Each worker (`processSegment/handler.ts`) calls `afterLoadPresetHook.execute(config, preset)` after `preset.configure({...})` completes, before the pipeline runner starts processing records. Receives both the resolved config and the loaded preset.

## Default behavior

One built-in hook is registered: **`ModelPreloaderHook`** — preloads tenant/locale pairs from the source table and then calls `modelProvider.preloadModels(tenantLocales)`. This ensures CMS model definitions are available to transformers before any records flow.

## Composite behavior

Hooks use `{ multiple: true }` — registering a hook **adds** to the list rather than replacing existing ones. Your hook runs after the built-in `ModelPreloaderHook`. Multiple hooks execute sequentially in registration order.

## Override example

Log which preset was loaded and how many pipelines it registered:

```typescript
// features/presetLogger.ts
import {
AfterLoadPresetHook,
type MigrationConfiguration,
type MigrationPreset
} from "@webiny/data-transfer";

class PresetLogger implements AfterLoadPresetHook.Interface {
public async execute(_config: MigrationConfiguration, preset: MigrationPreset): Promise<void> {
console.log(`Loaded preset: ${preset.name} — ${preset.description}`);
}
}

export const PresetLoggerHook = AfterLoadPresetHook.createImplementation({
implementation: PresetLogger,
dependencies: []
});
```

Register it in the config:

```typescript
export default createConfig({
// ...
register: async container => {
container.register(PresetLoggerHook);
}
});
```

## API

```typescript
interface AfterLoadPresetHook.Interface {
execute(config: MigrationConfiguration, preset: MigrationPreset): Promise<void>;
}
```

**Source:** `src/features/PresetLifecycle/`
61 changes: 61 additions & 0 deletions documentation/configurations/AfterTransferHook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[Documentation](../../README.md) > [Configurations](../../README.md#configurations) > AfterTransferHook

# AfterTransferHook

Runs custom logic **after** the transfer completes — after all workers finish, in the orchestrator process only.

## When it runs

The orchestrator (`run/handler.ts`) calls `afterTransferHook.execute()` once, after all worker shards have completed. **Skipped on shard failure** — if any worker fails, after-hooks do not run.

## Default behavior

No built-in hooks are registered. The composite executes an empty list.

## Composite behavior

Hooks use `{ multiple: true }` — registering a hook **adds** to the list rather than replacing existing ones. Multiple hooks execute sequentially in registration order.

## Override example

Log transfer completion to an external system:

```typescript
// features/completionHook.ts
import { AfterTransferHook } from "@webiny/data-transfer";

class LogCompletion implements AfterTransferHook.Interface {
public async execute(): Promise<void> {
await fetch("https://hooks.slack.com/...", {
method: "POST",
body: JSON.stringify({ text: "Transfer completed successfully." })
});
}
}

export const LogCompletionHook = AfterTransferHook.createImplementation({
implementation: LogCompletion,
dependencies: []
});
```

Register it in the config:

```typescript
export default createConfig({
// ...
register: async container => {
container.register(LogCompletionHook);
}
});
```

## API

```typescript
interface AfterTransferHook.Interface {
execute(): Promise<void>;
}
```

**Source:** `src/features/TransferLifecycle/`
60 changes: 60 additions & 0 deletions documentation/configurations/BeforeLoadPresetHook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[Documentation](../../README.md) > [Configurations](../../README.md#configurations) > BeforeLoadPresetHook

# BeforeLoadPresetHook

Runs custom logic **before** the preset is loaded — in each worker process.

## When it runs

Each worker (`processSegment/handler.ts`) calls `beforeLoadPresetHook.execute(config)` after bootstrap and `config.register`, but before `presetLoader.load(presetName)`. Receives the resolved `MigrationConfig`.

## Default behavior

No built-in hooks are registered. The composite executes an empty list.

## Composite behavior

Hooks use `{ multiple: true }` — registering a hook **adds** to the list rather than replacing existing ones. Multiple hooks execute sequentially in registration order.

## Override example

Validate config preconditions before the preset wires up pipelines:

```typescript
// features/configValidator.ts
import { BeforeLoadPresetHook, type MigrationConfiguration } from "@webiny/data-transfer";

class ConfigValidator implements BeforeLoadPresetHook.Interface {
public async execute(config: MigrationConfiguration): Promise<void> {
if (!config.source.opensearch) {
throw new Error("This project requires OpenSearch configuration.");
}
}
}

export const ConfigValidatorHook = BeforeLoadPresetHook.createImplementation({
implementation: ConfigValidator,
dependencies: []
});
```

Register it in the config:

```typescript
export default createConfig({
// ...
register: async container => {
container.register(ConfigValidatorHook);
}
});
```

## API

```typescript
interface BeforeLoadPresetHook.Interface {
execute(config: MigrationConfiguration): Promise<void>;
}
```

**Source:** `src/features/PresetLifecycle/`
61 changes: 61 additions & 0 deletions documentation/configurations/BeforeTransferHook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[Documentation](../../README.md) > [Configurations](../../README.md#configurations) > BeforeTransferHook

# BeforeTransferHook

Runs custom logic **before** the transfer starts — after access checks pass, before workers are spawned.

## When it runs

The orchestrator (`run/handler.ts`) calls `beforeTransferHook.execute()` once, in the main process only (not in worker processes). Runs after `config.register`, preset configuration, and access checks all succeed.

## Default behavior

No built-in hooks are registered. The composite executes an empty list.

## Composite behavior

Hooks use `{ multiple: true }` — registering a hook **adds** to the list rather than replacing existing ones. Multiple hooks execute sequentially in registration order.

## Override example

Send a Slack notification before the transfer begins:

```typescript
// features/slackNotifyHook.ts
import { BeforeTransferHook } from "@webiny/data-transfer";

class SlackNotifyBeforeTransfer implements BeforeTransferHook.Interface {
public async execute(): Promise<void> {
await fetch("https://hooks.slack.com/...", {
method: "POST",
body: JSON.stringify({ text: "Transfer starting..." })
});
}
}

export const SlackNotifyBeforeTransferHook = BeforeTransferHook.createImplementation({
implementation: SlackNotifyBeforeTransfer,
dependencies: []
});
```

Register it in the config:

```typescript
export default createConfig({
// ...
register: async container => {
container.register(SlackNotifyBeforeTransferHook);
}
});
```

## API

```typescript
interface BeforeTransferHook.Interface {
execute(): Promise<void>;
}
```

**Source:** `src/features/TransferLifecycle/`
Loading
Loading