-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest-typed.ts
More file actions
122 lines (104 loc) · 4.95 KB
/
Copy pathtest-typed.ts
File metadata and controls
122 lines (104 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// TypeScript smoke test — demonstrates dynamodb-toolkit is usable from typed
// consumers and that the published `.d.ts` sidecars flow typing through the
// public API.
//
// Manual — not wired into `npm test`. Invocations:
// npm run ts-check # type-checks this file (tsconfig.json includes tests/**/*)
// node tests/test-typed.ts # executes it (needs Node >= 22.6; unflagged on >= 23.6)
//
// Once wired into CI, move the file to `tests/test-typed-smoke.ts` (or similar)
// and extend the tape6 glob — tape-six runs `.ts` files natively on Node >= 22.
import test from 'tape-six';
import {Adapter, Raw, raw, TransactionLimitExceededError} from 'dynamodb-toolkit';
import {buildUpdate, buildCondition, cleanParams, type ConditionClause} from 'dynamodb-toolkit/expressions';
import {applyTransaction, TRANSACTION_LIMIT, backoff, type TransactWriteDescriptor} from 'dynamodb-toolkit/batch';
import type {DynamoDBDocumentClient, UpdateCommandInput} from '@aws-sdk/lib-dynamodb';
interface Planet extends Record<string, unknown> {
name: string;
climate?: string;
diameter?: number;
version?: number;
}
type PlanetKey = Pick<Planet, 'name'>;
// A minimal client stub that satisfies the `DynamoDBDocumentClient` shape for
// the slice of the interface the toolkit uses.
const makeClient = <T>(handler: (cmd: unknown) => Promise<T>): DynamoDBDocumentClient => ({send: handler}) as unknown as DynamoDBDocumentClient;
test('typed: Adapter constructs with typed item + key', t => {
const adapter = new Adapter<Planet, PlanetKey>({
client: makeClient(async () => ({})),
table: 'Planets',
keyFields: ['name'] // (keyof Planet & string)[] — compile-time checked
});
t.equal(adapter.table, 'Planets');
// keyFields normalize to KeyFieldSpec objects on construction. The string
// shorthand `'name'` is equivalent to `{name: 'name', type: 'string'}`.
t.deepEqual(adapter.keyFields, [{name: 'name', type: 'string'}]);
});
test('typed: raw() preserves the inner type through the wrapper', t => {
const wrapped: Raw<Planet> = raw<Planet>({name: 'Tatooine', climate: 'arid'});
t.ok(wrapped instanceof Raw);
t.equal(wrapped.item.climate, 'arid');
});
test('typed: expression builders compose additively over SDK params', t => {
// Real-world shape: a caller starts from the SDK's input type and each
// builder augments the optional expression fields in place.
const params: UpdateCommandInput = {TableName: 'Planets', Key: {name: 'Hoth'}};
const afterUpdate = buildUpdate({climate: 'frozen', diameter: 7200}, {delete: ['deprecated'], arrayOps: [{op: 'add', path: 'version', value: 1}]}, params);
t.matchString(afterUpdate.UpdateExpression, /^SET /);
const clauses: ConditionClause[] = [
{path: 'version', op: '=', value: 1},
{
op: 'or',
clauses: [
{path: 'climate', op: 'exists'},
{path: 'diameter', op: '>', value: 0}
]
}
];
const afterCondition = buildCondition(clauses, afterUpdate);
t.matchString(afterCondition.ConditionExpression ?? '', /#cd/);
cleanParams(afterCondition);
t.ok(afterCondition.ExpressionAttributeNames, 'names retained');
});
test('typed: make* builders return discriminated BatchDescriptors', async t => {
const adapter = new Adapter<Planet, PlanetKey>({
client: makeClient(async () => ({})),
table: 'Planets',
keyFields: ['name']
});
const postD = await adapter.makePost({name: 'Mustafar', climate: 'volcanic'});
t.equal(postD.action, 'put');
t.matchString(postD.params.ConditionExpression ?? '', /attribute_not_exists/);
const patchD = await adapter.makePatch({name: 'Mustafar'}, {climate: 'ashen'}, {returnFailedItem: true});
t.equal(patchD.action, 'patch');
t.equal(patchD.params.ReturnValuesOnConditionCheckFailure, 'ALL_OLD');
// Descriptor arrays are typed — applyTransaction accepts the union.
const descriptors: TransactWriteDescriptor[] = [postD, patchD];
t.equal(descriptors.length, 2);
});
test('typed: single-op CRUD surfaces returnFailedItem', async t => {
let sent: Record<string, unknown> | undefined;
const adapter = new Adapter<Planet, PlanetKey>({
client: makeClient(async (cmd: unknown) => {
sent = (cmd as {input: Record<string, unknown>}).input;
return {};
}),
table: 'Planets',
keyFields: ['name']
});
await adapter.put({name: 'Hoth', climate: 'frozen'}, {returnFailedItem: true});
t.equal(sent?.ReturnValuesOnConditionCheckFailure, 'ALL_OLD');
});
test('typed: batch helpers + TransactionLimitExceededError are exported', t => {
t.equal(typeof applyTransaction, 'function');
t.equal(TRANSACTION_LIMIT, 100);
// backoff is a generator — exercising the iterator proves the type.
const gen = backoff(10, 100, true);
const {value, done} = gen.next();
t.equal(typeof value, 'number');
t.equal(done, false);
// Error class is constructible + carries the typed `actionCount` field.
const err = new TransactionLimitExceededError(101);
t.equal(err.actionCount, 101);
t.ok(err instanceof Error);
});