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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ npm install @scylladb/alternator-client @aws-sdk/client-dynamodb

For document commands, also install `@aws-sdk/lib-dynamodb`.

Node.js 20 or newer is required for the Node runtime.

The package uses conditional exports. Node resolves the Node build; browser,
worker, and default ESM conditions resolve the Edge build, which contains no
Node built-in imports. To force the Edge entrypoint in a non-edge test harness:

```ts
import { AlternatorDynamoDBClient } from "@scylladb/alternator-client/edge";
```

For document commands in an Edge bundle, use:

```ts
import { AlternatorDynamoDBDocumentClient } from "@scylladb/alternator-client/document/edge";
```

## Low-Level Client

```ts
Expand Down Expand Up @@ -145,7 +161,6 @@ new AlternatorDynamoDBClient({
scheme: "http",
port: 8080,
routing: routing.cluster(),
runtime: "node",
logger: console,

discovery: {
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";

const tsFiles = ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts"];
const tsFiles = ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "vitest.integration.config.ts", "tsup.config.ts"];

export default tseslint.config(
{
Expand Down
7 changes: 6 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 40 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,51 @@
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"browser": "./dist/edge.js",
"types": "./dist/index.d.ts",
"engines": {
"node": ">=20"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"browser": {
"import": "./dist/edge.js"
},
"worker": {
"import": "./dist/edge.js"
},
"node": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"default": {
"import": "./dist/edge.js"
}
},
"./edge": {
"types": "./dist/edge.d.ts",
"import": "./dist/edge.js"
},
"./document": {
"types": "./dist/document.d.ts",
"import": "./dist/document.js",
"require": "./dist/document.cjs"
"browser": {
"import": "./dist/document-edge.js"
},
"worker": {
"import": "./dist/document-edge.js"
},
"node": {
"import": "./dist/document.js",
"require": "./dist/document.cjs"
},
"default": {
"import": "./dist/document-edge.js"
}
},
"./document/edge": {
"types": "./dist/document-edge.d.ts",
"import": "./dist/document-edge.js"
},
"./package.json": "./package.json"
},
Expand All @@ -27,7 +61,7 @@
],
"sideEffects": false,
"scripts": {
"build": "tsup src/index.ts src/document.ts --format esm,cjs --dts --sourcemap --clean",
"build": "tsup",
"lint": "eslint . --max-warnings=0",
"typecheck": "tsc --noEmit",
"test": "vitest run",
Expand All @@ -36,13 +70,13 @@
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.1075.0",
"@aws-sdk/lib-dynamodb": "^3.1075.0",
"@smithy/fetch-http-handler": "^5.5.2",
"@smithy/node-http-handler": "^4.8.2",
"@smithy/protocol-http": "^5.5.2",
"@smithy/types": "^4.15.0"
},
"devDependencies": {
"@aws-sdk/lib-dynamodb": "^3.1075.0",
"@eslint/js": "^10.0.1",
"@types/node": "^24.0.0",
"eslint": "^10.5.0",
Expand Down
149 changes: 149 additions & 0 deletions src/client-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
DescribeTableCommand,
DynamoDBClient,
type DynamoDBClientConfig,
type ServiceInputTypes,
type ServiceOutputTypes,
} from "@aws-sdk/client-dynamodb";
import type { HttpHandler, HttpHandlerUserInput } from "@smithy/protocol-http";
import type { HttpHandlerOptions } from "@smithy/types";
import { DEFAULT_REGION, firstEndpointUrl, NO_AUTH_CREDENTIALS, normalizeConfig } from "./config.js";
import { AlternatorDiscovery } from "./discovery.js";
import { KeyRouteAffinityPlanner } from "./affinity.js";
import { createAlternatorPostSigningMiddleware, createAlternatorRequestMiddleware } from "./middleware.js";
import type { AlternatorBodyCompressor } from "./compression-types.js";
import type { AlternatorDynamoDBClientConfig, AlternatorNode, NormalizedAlternatorConfig } from "./types.js";

export interface AlternatorDynamoDBClientApi {
nodes(): AlternatorNode[];
refreshNodes(): Promise<AlternatorNode[]>;
supportsScopedDiscovery(): Promise<boolean>;
validateRouting(): Promise<void>;
partitionKey(tableName: string): string | undefined;
}

export interface AlternatorRuntimePlatform {
assertRuntimeSupport(config: NormalizedAlternatorConfig): void;
createRequestHandler(
input: AlternatorDynamoDBClientConfig,
config: NormalizedAlternatorConfig,
): HttpHandlerUserInput;
compressBody: AlternatorBodyCompressor;
}

export abstract class AlternatorDynamoDBClientBase extends DynamoDBClient {
readonly alternator: AlternatorDynamoDBClientApi;
private readonly alternatorConfig: NormalizedAlternatorConfig;
private readonly discovery: AlternatorDiscovery;
private readonly keyAffinity: KeyRouteAffinityPlanner;

protected constructor(config: AlternatorDynamoDBClientConfig, platform: AlternatorRuntimePlatform) {
const alternatorConfig = normalizeConfig(config);
platform.assertRuntimeSupport(alternatorConfig);

const requestHandler = platform.createRequestHandler(config, alternatorConfig);
const dynamoConfig = buildDynamoConfig(config, alternatorConfig, requestHandler);

super(dynamoConfig);

this.alternatorConfig = alternatorConfig;
this.discovery = new AlternatorDiscovery(
alternatorConfig,
this.config.requestHandler as HttpHandler,
);
this.keyAffinity = new KeyRouteAffinityPlanner(
alternatorConfig.keyRouteAffinity,
(tableName) => this.discoverPartitionKey(tableName),
alternatorConfig.logger,
);
this.alternator = {
nodes: () => this.discovery.getLiveNodes(),
refreshNodes: () => this.discovery.refreshLiveNodes(),
supportsScopedDiscovery: () => this.discovery.checkRackDatacenterSupport(),
validateRouting: () => this.discovery.checkIfRackAndDatacenterSetCorrectly(),
partitionKey: (tableName) => this.keyAffinity.getPartitionKeyName(tableName),
};

this.middlewareStack.addRelativeTo(
createAlternatorRequestMiddleware<ServiceInputTypes, ServiceOutputTypes>({
discovery: this.discovery,
config: alternatorConfig,
keyAffinity: this.keyAffinity,
compressBody: platform.compressBody,
}),
{
relation: "before",
toMiddleware: "httpSigningMiddleware",
name: "alternatorRequestMiddleware",
override: true,
},
);

this.middlewareStack.addRelativeTo(
createAlternatorPostSigningMiddleware<ServiceInputTypes, ServiceOutputTypes>(alternatorConfig),
{
relation: "after",
toMiddleware: "httpSigningMiddleware",
name: "alternatorPostSigningMiddleware",
override: true,
},
);
}

override destroy(): void {
this.discovery.destroy();
super.destroy();
}

private async discoverPartitionKey(tableName: string): Promise<void> {
const response = await this.send(new DescribeTableCommand({ TableName: tableName }));
const keyName = response.Table?.KeySchema?.find((key) => key.KeyType === "HASH")?.AttributeName;
if (!keyName) {
this.alternatorConfig.logger.warn?.("alternator key affinity: DescribeTable returned no partition key", {
tableName,
});
return;
}
this.keyAffinity.setPartitionKeyName(tableName, keyName);
}
}

function buildDynamoConfig(
input: AlternatorDynamoDBClientConfig,
alternatorConfig: NormalizedAlternatorConfig,
requestHandler: HttpHandlerUserInput,
): DynamoDBClientConfig {
const {
seeds: _seeds,
scheme: _scheme,
port: _port,
routing: _routing,
runtime: _runtime,
compression: _compression,
headerOptimization: _headerOptimization,
userAgent: _userAgent,
keyRouteAffinity: _keyRouteAffinity,
tls: _tls,
discovery: _discovery,
connection: _connection,
logger: _logger,
credentials,
region,
...awsConfigWithEndpoint
} = input as AlternatorDynamoDBClientConfig & { endpoint?: unknown };
const { endpoint: _endpoint, ...awsConfig } = awsConfigWithEndpoint;

const dynamoConfig: DynamoDBClientConfig & { applyChecksum?: boolean } = {
...awsConfig,
endpoint: firstEndpointUrl(alternatorConfig),
region: region ?? DEFAULT_REGION,
credentials: credentials ?? NO_AUTH_CREDENTIALS,
requestHandler,
};
if (alternatorConfig.headerOptimization.enabled) {
dynamoConfig.applyChecksum = false;
}
return dynamoConfig;
}

export type AlternatorRequestHandler = HttpHandler<HttpHandlerOptions>;
14 changes: 14 additions & 0 deletions src/client-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { AlternatorDynamoDBClientBase } from "./client-base.js";
import { edgeRuntimePlatform } from "./runtime-edge.js";
import type { AlternatorDynamoDBClientConfig } from "./types.js";

export class AlternatorDynamoDBClient extends AlternatorDynamoDBClientBase {
constructor(config: AlternatorDynamoDBClientConfig) {
super(config, edgeRuntimePlatform);
}
}

export type {
AlternatorDynamoDBClientApi,
AlternatorRequestHandler,
} from "./client-base.js";
Loading