@kensio/yulin 1.21.16 → 1.21.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sdk/router/sim-sdk-command-router.type.d.ts +9 -0
- package/dist/sdk/sim-sdk-command-dispatcher.js +1 -1
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall-options.d.ts +42 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall-options.js +36 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall.d.ts +6 -5
- package/dist/service/dynamodb/document/sim-dynamodb-document-marshall.js +37 -43
- package/dist/service/dynamodb/document/sim-dynamodb-document-number.d.ts +4 -2
- package/dist/service/dynamodb/document/sim-dynamodb-document-number.js +5 -3
- package/dist/service/dynamodb/document/sim-dynamodb-document-path.d.ts +0 -4
- package/dist/service/dynamodb/document/sim-dynamodb-document-path.js +17 -10
- package/dist/service/dynamodb/document/sim-dynamodb-document-route.js +5 -1
- package/dist/service/dynamodb/document/sim-dynamodb-document-scalar.d.ts +13 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-scalar.js +46 -0
- package/dist/service/dynamodb/document/sim-dynamodb-document-set.d.ts +8 -3
- package/dist/service/dynamodb/document/sim-dynamodb-document-set.js +21 -14
- package/docs/sdk/README.md +5 -4
- package/docs/services/dynamodb/README.md +81 -2
- package/package.json +1 -1
|
@@ -22,6 +22,15 @@ export interface SimSdkCommandContext {
|
|
|
22
22
|
* session needs that split, because its session ARN owns no policies.
|
|
23
23
|
*/
|
|
24
24
|
readonly caller?: SimAwsCaller | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The SDK client the Command was sent through, when the send came from one.
|
|
27
|
+
*
|
|
28
|
+
* A route needs it only for configuration the Command itself does not
|
|
29
|
+
* carry, such as the marshalling options a `DynamoDBDocumentClient` was
|
|
30
|
+
* built with. A request bridged from the wire has no client object behind
|
|
31
|
+
* it, so this is left out there.
|
|
32
|
+
*/
|
|
33
|
+
readonly client?: unknown;
|
|
25
34
|
}
|
|
26
35
|
/**
|
|
27
36
|
* Route one intercepted SDK Command to a simulated service operation.
|
|
@@ -41,7 +41,7 @@ export class SimSdkCommandDispatcher {
|
|
|
41
41
|
.supportedCommandNames()
|
|
42
42
|
.join(", ")}`);
|
|
43
43
|
}
|
|
44
|
-
return await route(command, { caller });
|
|
44
|
+
return await route(command, { caller, client });
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Get the ambient SimAws.runAs caller for this dispatcher's SimAws
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The marshalling options a document client was built with.
|
|
3
|
+
*
|
|
4
|
+
* `DynamoDBDocumentClient.from(client, { marshallOptions })` changes what the
|
|
5
|
+
* real conversion does with values it would otherwise refuse, so the simulated
|
|
6
|
+
* conversion reads the same options rather than always applying the defaults.
|
|
7
|
+
* Every one of them is off unless the client asked for it, which is how the
|
|
8
|
+
* real document client leaves them.
|
|
9
|
+
*/
|
|
10
|
+
export interface SimDynamoDbDocumentMarshallOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Drop an undefined value out of a map, a list or a set instead of refusing
|
|
13
|
+
* it.
|
|
14
|
+
*/
|
|
15
|
+
readonly removeUndefinedValues: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Write an empty string, an empty binary value and an empty set as NULL.
|
|
18
|
+
*/
|
|
19
|
+
readonly convertEmptyValues: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Read a class instance as a map of its own properties.
|
|
22
|
+
*/
|
|
23
|
+
readonly convertClassInstanceToMap: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Write a number outside the safe integer range, digits already lost, rather
|
|
26
|
+
* than refusing it.
|
|
27
|
+
*/
|
|
28
|
+
readonly allowImpreciseNumbers: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What a document client built with no options of its own converts by.
|
|
32
|
+
*/
|
|
33
|
+
export declare const simDynamoDbDocumentMarshallDefaults: SimDynamoDbDocumentMarshallOptions;
|
|
34
|
+
/**
|
|
35
|
+
* Read the marshalling options off the client a Command was sent through.
|
|
36
|
+
*
|
|
37
|
+
* `DynamoDBDocumentClient` keeps the translation config it was built with on
|
|
38
|
+
* its resolved config, which is where the real marshalling middleware reads it
|
|
39
|
+
* from. A client that named none, and anything that is not a document client
|
|
40
|
+
* at all, converts by the defaults.
|
|
41
|
+
*/
|
|
42
|
+
export declare function simDynamoDbDocumentMarshallOptions(client: unknown): SimDynamoDbDocumentMarshallOptions;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isRecord } from "../../../util/type-guard/record.js";
|
|
2
|
+
/**
|
|
3
|
+
* What a document client built with no options of its own converts by.
|
|
4
|
+
*/
|
|
5
|
+
export const simDynamoDbDocumentMarshallDefaults = {
|
|
6
|
+
removeUndefinedValues: false,
|
|
7
|
+
convertEmptyValues: false,
|
|
8
|
+
convertClassInstanceToMap: false,
|
|
9
|
+
allowImpreciseNumbers: false,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Read the marshalling options off the client a Command was sent through.
|
|
13
|
+
*
|
|
14
|
+
* `DynamoDBDocumentClient` keeps the translation config it was built with on
|
|
15
|
+
* its resolved config, which is where the real marshalling middleware reads it
|
|
16
|
+
* from. A client that named none, and anything that is not a document client
|
|
17
|
+
* at all, converts by the defaults.
|
|
18
|
+
*/
|
|
19
|
+
export function simDynamoDbDocumentMarshallOptions(client) {
|
|
20
|
+
const config = isRecord(client) ? client["config"] : undefined;
|
|
21
|
+
const translateConfig = isRecord(config)
|
|
22
|
+
? config["translateConfig"]
|
|
23
|
+
: undefined;
|
|
24
|
+
const options = isRecord(translateConfig)
|
|
25
|
+
? translateConfig["marshallOptions"]
|
|
26
|
+
: undefined;
|
|
27
|
+
if (!isRecord(options)) {
|
|
28
|
+
return simDynamoDbDocumentMarshallDefaults;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
removeUndefinedValues: options["removeUndefinedValues"] === true,
|
|
32
|
+
convertEmptyValues: options["convertEmptyValues"] === true,
|
|
33
|
+
convertClassInstanceToMap: options["convertClassInstanceToMap"] === true,
|
|
34
|
+
allowImpreciseNumbers: options["allowImpreciseNumbers"] === true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
/**
|
|
3
4
|
* Read a native JavaScript value as the AttributeValue it stands for.
|
|
4
5
|
*
|
|
@@ -7,9 +8,9 @@ import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
|
7
8
|
* order, so a value that reaches a simulated table through the document client
|
|
8
9
|
* is the value that would have reached the real one.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The options are the ones the document client was built with. They decide
|
|
12
|
+
* what happens to a value the defaults refuse: an undefined member, an empty
|
|
13
|
+
* string or binary value, a class instance, and a number past the range a
|
|
14
|
+
* JavaScript number holds exactly.
|
|
14
15
|
*/
|
|
15
|
-
export declare function simDynamoDbDocumentAttributeValue(value: unknown, path: string): SimDynamoDbAttributeValue;
|
|
16
|
+
export declare function simDynamoDbDocumentAttributeValue(value: unknown, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SimDynamoDbDocumentValueError } from "../error/dynamodb.error.js";
|
|
2
|
-
import {
|
|
3
|
-
import { isSimDynamoDbDocumentNumberValue, simDynamoDbDocumentNumberAttribute, } from "./sim-dynamodb-document-number.js";
|
|
2
|
+
import { simDynamoDbDocumentScalarAttribute } from "./sim-dynamodb-document-scalar.js";
|
|
4
3
|
import { simDynamoDbDocumentSetAttribute } from "./sim-dynamodb-document-set.js";
|
|
5
4
|
/**
|
|
6
5
|
* Read a native JavaScript value as the AttributeValue it stands for.
|
|
@@ -10,61 +9,48 @@ import { simDynamoDbDocumentSetAttribute } from "./sim-dynamodb-document-set.js"
|
|
|
10
9
|
* order, so a value that reaches a simulated table through the document client
|
|
11
10
|
* is the value that would have reached the real one.
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* The options are the ones the document client was built with. They decide
|
|
13
|
+
* what happens to a value the defaults refuse: an undefined member, an empty
|
|
14
|
+
* string or binary value, a class instance, and a number past the range a
|
|
15
|
+
* JavaScript number holds exactly.
|
|
17
16
|
*/
|
|
18
|
-
export function simDynamoDbDocumentAttributeValue(value, path) {
|
|
17
|
+
export function simDynamoDbDocumentAttributeValue(value, path, options) {
|
|
19
18
|
if (value === undefined) {
|
|
20
|
-
throw new SimDynamoDbDocumentValueError(`${path} is undefined.
|
|
21
|
-
`
|
|
22
|
-
`
|
|
19
|
+
throw new SimDynamoDbDocumentValueError(`${path} is undefined. Build the document client with ` +
|
|
20
|
+
`removeUndefinedValues to drop it, which is what the real one asks ` +
|
|
21
|
+
`for, or leave the attribute out instead`);
|
|
23
22
|
}
|
|
24
23
|
if (value === null) {
|
|
25
24
|
return { NULL: true };
|
|
26
25
|
}
|
|
27
26
|
if (Array.isArray(value)) {
|
|
28
|
-
return { L: listMembers(value, path) };
|
|
27
|
+
return { L: listMembers(value, path, options) };
|
|
29
28
|
}
|
|
30
|
-
return containerOrScalar(value, path);
|
|
29
|
+
return containerOrScalar(value, path, options);
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
32
|
* Read a value that is not null, undefined or a list.
|
|
34
33
|
*/
|
|
35
|
-
function containerOrScalar(value, path) {
|
|
34
|
+
function containerOrScalar(value, path, options) {
|
|
36
35
|
if (value instanceof Set) {
|
|
37
|
-
return simDynamoDbDocumentSetAttribute(value, path);
|
|
36
|
+
return simDynamoDbDocumentSetAttribute(value, path, options);
|
|
38
37
|
}
|
|
39
38
|
if (value instanceof Map) {
|
|
40
|
-
return { M: mapEntries([...value], path) };
|
|
39
|
+
return { M: mapEntries([...value], path, options) };
|
|
41
40
|
}
|
|
42
41
|
if (isPlainObject(value)) {
|
|
43
|
-
return { M: mapEntries(Object.entries(value), path) };
|
|
42
|
+
return { M: mapEntries(Object.entries(value), path, options) };
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
* Read a value that stands for one attribute on its own.
|
|
49
|
-
*/
|
|
50
|
-
function scalar(value, path) {
|
|
51
|
-
if (isSimDynamoDbDocumentBinary(value)) {
|
|
52
|
-
return { B: value };
|
|
53
|
-
}
|
|
54
|
-
if (typeof value === "boolean") {
|
|
55
|
-
return { BOOL: value };
|
|
56
|
-
}
|
|
57
|
-
if (typeof value === "number") {
|
|
58
|
-
return simDynamoDbDocumentNumberAttribute(value, path);
|
|
44
|
+
const scalar = simDynamoDbDocumentScalarAttribute(value, path, options);
|
|
45
|
+
if (scalar !== undefined) {
|
|
46
|
+
return scalar;
|
|
59
47
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (typeof value === "
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (typeof value === "string") {
|
|
67
|
-
return { S: value };
|
|
48
|
+
// A class instance is read as a map only when the client asked for it, which
|
|
49
|
+
// is the last thing the real conversion tries before giving up. Null reached
|
|
50
|
+
// an answer of its own before any of this.
|
|
51
|
+
if (typeof value === "object" && options.convertClassInstanceToMap) {
|
|
52
|
+
const instance = value;
|
|
53
|
+
return { M: mapEntries(Object.entries(instance), path, options) };
|
|
68
54
|
}
|
|
69
55
|
throw new SimDynamoDbDocumentValueError(`${path} is a ${typeof value} the document client has no attribute type ` +
|
|
70
56
|
`for`);
|
|
@@ -72,27 +58,35 @@ function scalar(value, path) {
|
|
|
72
58
|
/**
|
|
73
59
|
* The members of a list, with the functions left out as the real one leaves
|
|
74
60
|
* them out.
|
|
61
|
+
*
|
|
62
|
+
* A dropped undefined member takes its position with it, so the members after
|
|
63
|
+
* it move up. That is what the real conversion does: it filters before it
|
|
64
|
+
* converts, rather than writing a NULL where the member was.
|
|
75
65
|
*/
|
|
76
|
-
function listMembers(values, path) {
|
|
66
|
+
function listMembers(values, path, options) {
|
|
77
67
|
return values
|
|
78
|
-
.filter((member) => typeof member !== "function"
|
|
79
|
-
|
|
68
|
+
.filter((member) => typeof member !== "function" &&
|
|
69
|
+
!(member === undefined && options.removeUndefinedValues))
|
|
70
|
+
.map((member, index) => simDynamoDbDocumentAttributeValue(member, `${path}[${index.toString()}]`, options));
|
|
80
71
|
}
|
|
81
72
|
/**
|
|
82
73
|
* The entries of a map, with the functions left out.
|
|
83
74
|
*/
|
|
84
|
-
function mapEntries(entries, path) {
|
|
75
|
+
function mapEntries(entries, path, options) {
|
|
85
76
|
const attributes = {};
|
|
86
77
|
for (const [name, member] of entries) {
|
|
87
78
|
if (typeof member === "function") {
|
|
88
79
|
continue;
|
|
89
80
|
}
|
|
81
|
+
if (member === undefined && options.removeUndefinedValues) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
90
84
|
const key = String(name);
|
|
91
85
|
// Defined rather than assigned, so an attribute named `__proto__` becomes
|
|
92
86
|
// an ordinary attribute instead of reaching the prototype setter. The real
|
|
93
87
|
// document client assigns, and so loses that attribute.
|
|
94
88
|
Object.defineProperty(attributes, key, {
|
|
95
|
-
value: simDynamoDbDocumentAttributeValue(member, `${path}.${key}
|
|
89
|
+
value: simDynamoDbDocumentAttributeValue(member, `${path}.${key}`, options),
|
|
96
90
|
enumerable: true,
|
|
97
91
|
writable: true,
|
|
98
92
|
configurable: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
/**
|
|
3
4
|
* A value carrying its own Number attribute, which is what lib-dynamodb's
|
|
4
5
|
* `NumberValue` is.
|
|
@@ -23,9 +24,10 @@ export declare function isSimDynamoDbDocumentNumberValue(value: unknown): value
|
|
|
23
24
|
* than storing one that has already lost digits. A simulated table holds the
|
|
24
25
|
* digits it is given exactly, so this refusal is the only thing standing
|
|
25
26
|
* between an application and a silently rounded identifier, which is why it is
|
|
26
|
-
* kept
|
|
27
|
+
* kept until a client asks for `allowImpreciseNumbers` and takes the rounding
|
|
28
|
+
* on. A decimal inside the range is written as it stands.
|
|
27
29
|
*/
|
|
28
|
-
export declare function simDynamoDbDocumentNumberAttribute(value: number, path: string): SimDynamoDbAttributeValue;
|
|
30
|
+
export declare function simDynamoDbDocumentNumberAttribute(value: number, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue;
|
|
29
31
|
/**
|
|
30
32
|
* Read a Number attribute back as the document client answers with it.
|
|
31
33
|
*
|
|
@@ -15,13 +15,15 @@ export function isSimDynamoDbDocumentNumberValue(value) {
|
|
|
15
15
|
* than storing one that has already lost digits. A simulated table holds the
|
|
16
16
|
* digits it is given exactly, so this refusal is the only thing standing
|
|
17
17
|
* between an application and a silently rounded identifier, which is why it is
|
|
18
|
-
* kept
|
|
18
|
+
* kept until a client asks for `allowImpreciseNumbers` and takes the rounding
|
|
19
|
+
* on. A decimal inside the range is written as it stands.
|
|
19
20
|
*/
|
|
20
|
-
export function simDynamoDbDocumentNumberAttribute(value, path) {
|
|
21
|
+
export function simDynamoDbDocumentNumberAttribute(value, path, options) {
|
|
21
22
|
if (!Number.isFinite(value)) {
|
|
22
23
|
throw new SimDynamoDbDocumentValueError(`${path} is ${value.toString()}, and DynamoDB has no such number`);
|
|
23
24
|
}
|
|
24
|
-
if (
|
|
25
|
+
if (!options.allowImpreciseNumbers &&
|
|
26
|
+
(value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER)) {
|
|
25
27
|
throw new SimDynamoDbDocumentValueError(`${path} is ${value.toString()}, which is outside the range a ` +
|
|
26
28
|
`JavaScript number holds exactly. Write it as a bigint, or as a ` +
|
|
27
29
|
`NumberValue from @aws-sdk/lib-dynamodb, so its digits survive`);
|
|
@@ -16,10 +16,6 @@ export interface SimDynamoDbDocumentPath {
|
|
|
16
16
|
*/
|
|
17
17
|
convert(value: unknown, conversion: SimDynamoDbDocumentConversion, path: string): unknown;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
* A path to one attribute value.
|
|
21
|
-
*/
|
|
22
|
-
export declare function simDynamoDbDocumentValue(): SimDynamoDbDocumentPath;
|
|
23
19
|
/**
|
|
24
20
|
* A path to a record or list whose every member is one attribute value, which
|
|
25
21
|
* is what an Item, a Key and a set of expression values are.
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { isRecord } from "../../../util/type-guard/record.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Every attribute value of an item, a key or a set of expression values.
|
|
4
|
+
*
|
|
5
|
+
* An attribute whose value is undefined is left out rather than converted, and
|
|
6
|
+
* that happens whatever the document client was built with. The real client
|
|
7
|
+
* drops it in the same place: `removeUndefinedValues` governs the values
|
|
8
|
+
* inside an attribute, and an item is not itself one of them.
|
|
4
9
|
*/
|
|
5
|
-
class
|
|
10
|
+
class SimDynamoDbDocumentValuesPath {
|
|
6
11
|
convert(value, conversion, path) {
|
|
7
|
-
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return value.map((member, index) => conversion(member, `${path}[${index.toString()}]`));
|
|
14
|
+
}
|
|
15
|
+
if (!isRecord(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return Object.fromEntries(Object.entries(value)
|
|
19
|
+
.filter(([, member]) => member !== undefined)
|
|
20
|
+
.map(([name, member]) => [name, conversion(member, `${path}.${name}`)]));
|
|
8
21
|
}
|
|
9
22
|
}
|
|
10
23
|
/**
|
|
@@ -55,18 +68,12 @@ class SimDynamoDbDocumentFieldsPath {
|
|
|
55
68
|
return converted;
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
|
-
/**
|
|
59
|
-
* A path to one attribute value.
|
|
60
|
-
*/
|
|
61
|
-
export function simDynamoDbDocumentValue() {
|
|
62
|
-
return new SimDynamoDbDocumentValuePath();
|
|
63
|
-
}
|
|
64
71
|
/**
|
|
65
72
|
* A path to a record or list whose every member is one attribute value, which
|
|
66
73
|
* is what an Item, a Key and a set of expression values are.
|
|
67
74
|
*/
|
|
68
75
|
export function simDynamoDbDocumentValues() {
|
|
69
|
-
return
|
|
76
|
+
return new SimDynamoDbDocumentValuesPath();
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* A path through every member of a record or list.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { simSdkCallerOptions, } from "../../../sdk/index.js";
|
|
2
|
+
import { simDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
import { simDynamoDbDocumentAttributeValue } from "./sim-dynamodb-document-marshall.js";
|
|
3
4
|
import { simDynamoDbDocumentNativeValue } from "./sim-dynamodb-document-unmarshall.js";
|
|
4
5
|
/**
|
|
@@ -23,7 +24,10 @@ export class SimDynamoDbDocumentRoute {
|
|
|
23
24
|
*/
|
|
24
25
|
route() {
|
|
25
26
|
return async (command, context) => {
|
|
26
|
-
|
|
27
|
+
// Read per send, as the real middleware reads it, so the same route
|
|
28
|
+
// serves document clients built with different options.
|
|
29
|
+
const options = simDynamoDbDocumentMarshallOptions(context.client);
|
|
30
|
+
const input = this.input.convert(command.input, (value, path) => simDynamoDbDocumentAttributeValue(value, path, options), "input");
|
|
27
31
|
const output = await this.send(input, simSdkCallerOptions(context));
|
|
28
32
|
return this.output.convert(output, (value, path) => simDynamoDbDocumentNativeValue(value, path), "output");
|
|
29
33
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a value that stands for one attribute on its own.
|
|
5
|
+
*
|
|
6
|
+
* The kinds are tried in the order the real conversion tries them. A value
|
|
7
|
+
* that is none of them answers with nothing, and what happens to it next is
|
|
8
|
+
* for the caller to decide.
|
|
9
|
+
*
|
|
10
|
+
* An empty string and a binary value holding no bytes are written as NULL when
|
|
11
|
+
* the client was built with `convertEmptyValues`, and as themselves otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function simDynamoDbDocumentScalarAttribute(value: unknown, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isSimDynamoDbDocumentBinary } from "./sim-dynamodb-document-binary.js";
|
|
2
|
+
import { isSimDynamoDbDocumentNumberValue, simDynamoDbDocumentNumberAttribute, } from "./sim-dynamodb-document-number.js";
|
|
3
|
+
/**
|
|
4
|
+
* Read a value that stands for one attribute on its own.
|
|
5
|
+
*
|
|
6
|
+
* The kinds are tried in the order the real conversion tries them. A value
|
|
7
|
+
* that is none of them answers with nothing, and what happens to it next is
|
|
8
|
+
* for the caller to decide.
|
|
9
|
+
*
|
|
10
|
+
* An empty string and a binary value holding no bytes are written as NULL when
|
|
11
|
+
* the client was built with `convertEmptyValues`, and as themselves otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export function simDynamoDbDocumentScalarAttribute(value, path, options) {
|
|
14
|
+
if (isSimDynamoDbDocumentBinary(value)) {
|
|
15
|
+
return isEmptyLength(value) && options.convertEmptyValues
|
|
16
|
+
? { NULL: true }
|
|
17
|
+
: { B: value };
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "boolean") {
|
|
20
|
+
return { BOOL: value };
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "number") {
|
|
23
|
+
return simDynamoDbDocumentNumberAttribute(value, path, options);
|
|
24
|
+
}
|
|
25
|
+
if (isSimDynamoDbDocumentNumberValue(value)) {
|
|
26
|
+
return { N: value.toAttributeValue().N };
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "bigint") {
|
|
29
|
+
return { N: value.toString() };
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
return value.length === 0 && options.convertEmptyValues
|
|
33
|
+
? { NULL: true }
|
|
34
|
+
: { S: value };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether a value holds nothing, by the length the real conversion reads.
|
|
40
|
+
*
|
|
41
|
+
* An ArrayBuffer reports `byteLength` rather than `length`, so an empty one is
|
|
42
|
+
* not empty by this measure, and the real conversion leaves it alone too.
|
|
43
|
+
*/
|
|
44
|
+
function isEmptyLength(value) {
|
|
45
|
+
return value.length === 0;
|
|
46
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
2
|
+
import type { SimDynamoDbDocumentMarshallOptions } from "./sim-dynamodb-document-marshall-options.js";
|
|
2
3
|
/**
|
|
3
4
|
* Read a JavaScript Set as one of DynamoDB's three set attributes.
|
|
4
5
|
*
|
|
@@ -8,7 +9,11 @@ import type { SimDynamoDbAttributeValue } from "../command/item/item.types.js";
|
|
|
8
9
|
* further down, where the table reads the value. That is what the real one
|
|
9
10
|
* does, so a set written this way behaves the same either side.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* An undefined member is dropped when the client was built with
|
|
13
|
+
* `removeUndefinedValues`, and refused otherwise. DynamoDB has no empty set, so
|
|
14
|
+
* a set with nothing left in it is written as NULL when the client was built
|
|
15
|
+
* with `convertEmptyValues`, and refused otherwise. The two are read in that
|
|
16
|
+
* order, so a set holding nothing but undefined is an empty set by the time its
|
|
17
|
+
* size is looked at, which is where the real conversion looks at it too.
|
|
13
18
|
*/
|
|
14
|
-
export declare function simDynamoDbDocumentSetAttribute(set: ReadonlySet<unknown>, path: string): SimDynamoDbAttributeValue;
|
|
19
|
+
export declare function simDynamoDbDocumentSetAttribute(set: ReadonlySet<unknown>, path: string, options: SimDynamoDbDocumentMarshallOptions): SimDynamoDbAttributeValue;
|
|
@@ -10,31 +10,38 @@ import { isSimDynamoDbDocumentNumberValue, simDynamoDbDocumentNumberAttribute, }
|
|
|
10
10
|
* further down, where the table reads the value. That is what the real one
|
|
11
11
|
* does, so a set written this way behaves the same either side.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* An undefined member is dropped when the client was built with
|
|
14
|
+
* `removeUndefinedValues`, and refused otherwise. DynamoDB has no empty set, so
|
|
15
|
+
* a set with nothing left in it is written as NULL when the client was built
|
|
16
|
+
* with `convertEmptyValues`, and refused otherwise. The two are read in that
|
|
17
|
+
* order, so a set holding nothing but undefined is an empty set by the time its
|
|
18
|
+
* size is looked at, which is where the real conversion looks at it too.
|
|
15
19
|
*/
|
|
16
|
-
export function simDynamoDbDocumentSetAttribute(set, path) {
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
export function simDynamoDbDocumentSetAttribute(set, path, options) {
|
|
21
|
+
const members = [...set].filter((member) => !(member === undefined && options.removeUndefinedValues));
|
|
22
|
+
if (!options.removeUndefinedValues && set.has(undefined)) {
|
|
23
|
+
throw new SimDynamoDbDocumentValueError(`${path} is a Set holding undefined. Build the document client with ` +
|
|
24
|
+
`removeUndefinedValues to drop it, which is what the real one asks for`);
|
|
19
25
|
}
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
if (members.length === 0) {
|
|
27
|
+
if (options.convertEmptyValues) {
|
|
28
|
+
return { NULL: true };
|
|
29
|
+
}
|
|
30
|
+
throw new SimDynamoDbDocumentValueError(`${path} is an empty Set, and DynamoDB has no empty set`);
|
|
24
31
|
}
|
|
25
|
-
return membersAttribute(
|
|
32
|
+
return membersAttribute(members, path, options);
|
|
26
33
|
}
|
|
27
34
|
/**
|
|
28
35
|
* Read the members of a set that is known to hold something.
|
|
29
36
|
*/
|
|
30
|
-
function membersAttribute(members, path) {
|
|
37
|
+
function membersAttribute(members, path, options) {
|
|
31
38
|
const first = members[0];
|
|
32
39
|
if (typeof first === "string") {
|
|
33
40
|
return { SS: members.map(String) };
|
|
34
41
|
}
|
|
35
42
|
if (isNumberMember(first)) {
|
|
36
43
|
return {
|
|
37
|
-
NS: members.map((member, index) => numberText(member, path, index)),
|
|
44
|
+
NS: members.map((member, index) => numberText(member, path, index, options)),
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
47
|
if (isSimDynamoDbDocumentBinary(first)) {
|
|
@@ -57,14 +64,14 @@ function isNumberMember(member) {
|
|
|
57
64
|
/**
|
|
58
65
|
* The digits one member of a number set is written with.
|
|
59
66
|
*/
|
|
60
|
-
function numberText(member, path, index) {
|
|
67
|
+
function numberText(member, path, index, options) {
|
|
61
68
|
if (typeof member === "bigint") {
|
|
62
69
|
return member.toString();
|
|
63
70
|
}
|
|
64
71
|
if (isSimDynamoDbDocumentNumberValue(member)) {
|
|
65
72
|
return member.toAttributeValue().N;
|
|
66
73
|
}
|
|
67
|
-
const attribute = simDynamoDbDocumentNumberAttribute(Number(member), `${path}[${index.toString()}]
|
|
74
|
+
const attribute = simDynamoDbDocumentNumberAttribute(Number(member), `${path}[${index.toString()}]`, options);
|
|
68
75
|
// A number attribute is the only thing that function answers with.
|
|
69
76
|
return attribute.N ?? "";
|
|
70
77
|
}
|
package/docs/sdk/README.md
CHANGED
|
@@ -282,7 +282,8 @@ service throws `SimSdkUnknownServiceError`.
|
|
|
282
282
|
- Simulated errors have SDK-shaped `name` and `$metadata` fields. They are separate classes from the
|
|
283
283
|
SDK exceptions, so match them by `error.name` instead of `instanceof`.
|
|
284
284
|
- The callback form of `send(command, callback)` is not supported. Use the promise form.
|
|
285
|
-
- Yulin
|
|
286
|
-
`DynamoDBDocumentClient.from(client, { marshallOptions, unmarshallOptions })
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
- Yulin reads the `marshallOptions` in
|
|
286
|
+
`DynamoDBDocumentClient.from(client, { marshallOptions, unmarshallOptions })` and ignores the
|
|
287
|
+
`unmarshallOptions`. A stored value comes back the way a document client built with no options of
|
|
288
|
+
its own reads it. See
|
|
289
|
+
[the DynamoDB docs](https://yulinsim.dev/services/dynamodb/#marshalling-options).
|
|
@@ -3040,6 +3040,84 @@ Intercept the document client itself. `DynamoDBDocumentClient.from(client)` buil
|
|
|
3040
3040
|
outside the `DynamoDBClient` class, so intercepting the base client leaves Commands sent through the
|
|
3041
3041
|
document one untouched. See [the SDK docs](https://yulinsim.dev/sdk/#the-dynamodb-document-client).
|
|
3042
3042
|
|
|
3043
|
+
### Marshalling options
|
|
3044
|
+
|
|
3045
|
+
`DynamoDBDocumentClient.from(client, { marshallOptions })` changes what the conversion does with a
|
|
3046
|
+
value the defaults refuse. Yulin reads those options off the client each Command was sent through.
|
|
3047
|
+
Two document clients over one simulation each convert by their own.
|
|
3048
|
+
|
|
3049
|
+
- `removeUndefinedValues` drops an `undefined` out of a map, a list or a `Set`. Without it, an
|
|
3050
|
+
`undefined` in any of the three is refused. The real client refuses it in the same place.
|
|
3051
|
+
- `convertEmptyValues` writes an empty string, an empty binary value and an empty `Set` as `NULL`.
|
|
3052
|
+
- `convertClassInstanceToMap` reads an object with behaviour as a map of its own properties.
|
|
3053
|
+
- `allowImpreciseNumbers` writes a number past `Number.MAX_SAFE_INTEGER` with the digits it has
|
|
3054
|
+
already been rounded to.
|
|
3055
|
+
|
|
3056
|
+
An `undefined` attribute of an item, a key or a set of expression values is left out whatever the
|
|
3057
|
+
client was built with. `removeUndefinedValues` governs the values held inside an attribute, and the
|
|
3058
|
+
attributes of an item sit a level above that (the real client drops an undefined one there without
|
|
3059
|
+
being asked to).
|
|
3060
|
+
|
|
3061
|
+
```typescript sim-dynamodb-document-marshall-options
|
|
3062
|
+
/**
|
|
3063
|
+
* Writing a partly filled object through a document client that drops
|
|
3064
|
+
* undefined values.
|
|
3065
|
+
*/
|
|
3066
|
+
|
|
3067
|
+
import { CreateTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
3068
|
+
import {
|
|
3069
|
+
DynamoDBDocumentClient,
|
|
3070
|
+
GetCommand,
|
|
3071
|
+
PutCommand,
|
|
3072
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
3073
|
+
|
|
3074
|
+
import { SimSdk } from "@kensio/yulin/sdk";
|
|
3075
|
+
|
|
3076
|
+
using simSdk = new SimSdk();
|
|
3077
|
+
|
|
3078
|
+
const documents = DynamoDBDocumentClient.from(
|
|
3079
|
+
new DynamoDBClient({ region: "eu-west-2" }),
|
|
3080
|
+
{ marshallOptions: { removeUndefinedValues: true } },
|
|
3081
|
+
);
|
|
3082
|
+
simSdk.intercept(documents);
|
|
3083
|
+
|
|
3084
|
+
await documents.send(
|
|
3085
|
+
new CreateTableCommand({
|
|
3086
|
+
TableName: "PagesTable",
|
|
3087
|
+
KeySchema: [{ AttributeName: "pageId", KeyType: "HASH" }],
|
|
3088
|
+
AttributeDefinitions: [{ AttributeName: "pageId", AttributeType: "S" }],
|
|
3089
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
3090
|
+
}),
|
|
3091
|
+
);
|
|
3092
|
+
await simSdk.simAws.backgroundTasksComplete();
|
|
3093
|
+
|
|
3094
|
+
// The summary was never filled in, and one section is still to be written.
|
|
3095
|
+
await documents.send(
|
|
3096
|
+
new PutCommand({
|
|
3097
|
+
TableName: "PagesTable",
|
|
3098
|
+
Item: {
|
|
3099
|
+
pageId: "page-1",
|
|
3100
|
+
meta: { title: "Home", summary: undefined },
|
|
3101
|
+
sections: ["intro", undefined, "outro"],
|
|
3102
|
+
},
|
|
3103
|
+
}),
|
|
3104
|
+
);
|
|
3105
|
+
|
|
3106
|
+
const read = await documents.send(
|
|
3107
|
+
new GetCommand({ TableName: "PagesTable", Key: { pageId: "page-1" } }),
|
|
3108
|
+
);
|
|
3109
|
+
|
|
3110
|
+
const meta = read.Item?.["meta"] as Record<string, string>;
|
|
3111
|
+
console.log(Object.keys(meta)); // [ 'title' ]
|
|
3112
|
+
|
|
3113
|
+
// A dropped member takes its position with it.
|
|
3114
|
+
const sections = read.Item?.["sections"] as string[];
|
|
3115
|
+
console.log(sections); // [ 'intro', 'outro' ]
|
|
3116
|
+
```
|
|
3117
|
+
|
|
3118
|
+
`unmarshallOptions` are ignored. A stored value comes back the way a document client built with no
|
|
3119
|
+
options of its own reads it.
|
|
3120
|
+
|
|
3043
3121
|
### Querying and scanning through the document client
|
|
3044
3122
|
|
|
3045
3123
|
`@aws-sdk/lib-dynamodb` names its `QueryCommand` and `ScanCommand` exactly as
|
|
@@ -3152,8 +3230,9 @@ A simulated table holds a number's digits exactly, but the document client conve
|
|
|
3152
3230
|
JavaScript numbers, and that is where digits are lost. It is the same loss AWS has. A test that
|
|
3153
3231
|
passes here is telling you something true about the real thing.
|
|
3154
3232
|
|
|
3155
|
-
- Writing a `number` outside the safe integer range is refused
|
|
3156
|
-
a `bigint`, or a `NumberValue` from `@aws-sdk/lib-dynamodb`, to keep
|
|
3233
|
+
- Writing a `number` outside the safe integer range is refused unless the client was built with
|
|
3234
|
+
`allowImpreciseNumbers`. Write a `bigint`, or a `NumberValue` from `@aws-sdk/lib-dynamodb`, to keep
|
|
3235
|
+
the digits.
|
|
3157
3236
|
- Reading a stored number outside the safe integer range gives a `bigint`.
|
|
3158
3237
|
- Reading a stored decimal with more digits than a JavaScript number carries gives a rounded
|
|
3159
3238
|
`number`. The table still holds every digit, and the rounding is the document client's. Read
|
package/package.json
CHANGED