Skip to content

Commit 51fd6e1

Browse files
committed
Restructure the transformer
Signed-off-by: moznion <moznion@mail.moznion.net>
1 parent 32ba100 commit 51fd6e1

File tree

2 files changed

+293
-243
lines changed

2 files changed

+293
-243
lines changed

lib/dynamodb_record_transformer.ts

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import ts from 'typescript';
2+
import {
3+
DynamodbPrimitiveTypes,
4+
dynamodbPrimitiveTypeFromTypeFlag,
5+
dynamodbPrimitiveTypeFromName,
6+
} from './dynamodb_primitive_types';
7+
import {
8+
ArrayField,
9+
DynamodbItemField,
10+
KeyValuePairMapField,
11+
MapField,
12+
PrimitiveField,
13+
SetField,
14+
} from './dynamodb_item_field';
15+
import { warn } from './logger';
16+
17+
export class DynamodbRecordTransformer {
18+
public static readonly dynamodbRecordFuncName = 'dynamodbRecord';
19+
private static readonly shouldLenientTypeCheck = !!process.env['TS_DYNAMODB_ATTR_TRANSFORMER_LENIENT_TYPE_CHECK'];
20+
21+
public static visitNode(node: ts.CallExpression, typeChecker: ts.TypeChecker): ts.Node | undefined {
22+
if (node.typeArguments === undefined || node.typeArguments.length !== 1 || !node.typeArguments[0]) {
23+
throw new Error(
24+
`No type argument on ${DynamodbRecordTransformer.dynamodbRecordFuncName}(). Please put a type argument on the function`,
25+
);
26+
}
27+
28+
const typeName = node.typeArguments[0].getText();
29+
30+
if (node.arguments.length !== 1 || !node.arguments[0]) {
31+
throw new Error(
32+
`No argument on ${DynamodbRecordTransformer.dynamodbRecordFuncName}(). Please put an argument that has ${typeName} type on the function`,
33+
);
34+
}
35+
36+
const argVarNameIdent = ts.factory.createIdentifier('arg');
37+
const type = typeChecker.getTypeFromTypeNode(node.typeArguments[0]);
38+
const properties = typeChecker.getPropertiesOfType(type);
39+
const objectProps = properties
40+
.map((prop): DynamodbItemField | undefined => {
41+
if (
42+
!prop.valueDeclaration ||
43+
!ts.canHaveModifiers(prop.valueDeclaration) ||
44+
!DynamodbRecordTransformer.isPropertyModifierInArgumentSuitableForDynamodbAttr(
45+
ts.getModifiers(prop.valueDeclaration),
46+
)
47+
) {
48+
// skip it
49+
return undefined;
50+
}
51+
52+
const propName = prop.name;
53+
54+
if (
55+
prop.valueDeclaration.kind !== ts.SyntaxKind.Parameter &&
56+
prop.valueDeclaration.kind !== ts.SyntaxKind.PropertySignature
57+
) {
58+
const msg = `a property "${propName}" of the type "${typeName}" doesn't have parameter kind; maybe the ${typeName} is not class of interface`;
59+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
60+
warn(msg);
61+
return undefined;
62+
}
63+
throw new Error(msg);
64+
}
65+
66+
const valueDeclSymbol = typeChecker.getTypeAtLocation(prop.valueDeclaration).symbol;
67+
const valueDeclSymbolName = valueDeclSymbol?.name;
68+
69+
if (valueDeclSymbolName === 'Array') {
70+
const typeArgs = typeChecker.getTypeArguments(
71+
typeChecker.getTypeAtLocation(prop.valueDeclaration) as ts.TypeReference,
72+
);
73+
const valueType = dynamodbPrimitiveTypeFromTypeFlag(typeArgs[0]?.flags);
74+
if (valueType === undefined) {
75+
const valueTypeName = typeArgs[0]?.symbol?.name;
76+
if (valueTypeName === 'Uint8Array') {
77+
return new ArrayField(propName, DynamodbPrimitiveTypes.Binary);
78+
}
79+
if (valueTypeName === 'BigInt') {
80+
return new ArrayField(propName, DynamodbPrimitiveTypes.Number);
81+
}
82+
83+
const msg = `a property "${propName}" of the type "${typeName}" has unsupported type: "${valueTypeName}"`;
84+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
85+
warn(msg);
86+
return undefined;
87+
}
88+
throw new Error(msg);
89+
}
90+
return new ArrayField(propName, valueType);
91+
}
92+
if (valueDeclSymbolName == 'Set') {
93+
const typeArgs = typeChecker.getTypeArguments(
94+
typeChecker.getTypeAtLocation(prop.valueDeclaration) as ts.TypeReference,
95+
);
96+
const valueType = dynamodbPrimitiveTypeFromTypeFlag(typeArgs[0]?.flags);
97+
if (valueType === undefined) {
98+
const valueTypeName = typeArgs[0]?.symbol?.name;
99+
if (valueTypeName === 'Uint8Array') {
100+
return new SetField(
101+
propName,
102+
DynamodbPrimitiveTypes.Binary,
103+
DynamodbRecordTransformer.shouldLenientTypeCheck,
104+
);
105+
}
106+
if (valueTypeName === 'BigInt') {
107+
return new SetField(
108+
propName,
109+
DynamodbPrimitiveTypes.Number,
110+
DynamodbRecordTransformer.shouldLenientTypeCheck,
111+
);
112+
}
113+
114+
const msg = `a property "${propName}" of the type "${typeName}" has unsupported type: ${valueTypeName}`;
115+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
116+
warn(msg);
117+
return undefined;
118+
}
119+
throw new Error(msg);
120+
}
121+
return new SetField(propName, valueType, DynamodbRecordTransformer.shouldLenientTypeCheck);
122+
}
123+
if (valueDeclSymbolName == 'Map') {
124+
const typeArgs = typeChecker.getTypeArguments(
125+
typeChecker.getTypeAtLocation(prop.valueDeclaration) as ts.TypeReference,
126+
);
127+
128+
const keyType = dynamodbPrimitiveTypeFromTypeFlag(typeArgs[0]?.flags);
129+
if (keyType === undefined) {
130+
const msg = `a Map type property "${propName}" of the type "${typeName}" has non-string key type`;
131+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
132+
warn(msg);
133+
return undefined;
134+
}
135+
throw new Error(msg);
136+
}
137+
138+
const valueType = dynamodbPrimitiveTypeFromTypeFlag(typeArgs[1]?.flags);
139+
if (valueType === undefined) {
140+
const valueTypeName = typeArgs[1]?.symbol?.name;
141+
if (valueTypeName === 'Uint8Array') {
142+
return new MapField(
143+
propName,
144+
keyType,
145+
DynamodbPrimitiveTypes.Binary,
146+
DynamodbRecordTransformer.shouldLenientTypeCheck,
147+
);
148+
}
149+
if (valueTypeName === 'BigInt') {
150+
return new MapField(
151+
propName,
152+
keyType,
153+
DynamodbPrimitiveTypes.Number,
154+
DynamodbRecordTransformer.shouldLenientTypeCheck,
155+
);
156+
}
157+
158+
const msg = `a property "${propName}" of the type "${typeName}" has unsupported type: "${valueTypeName}"`;
159+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
160+
warn(msg);
161+
return undefined;
162+
}
163+
throw new Error(msg);
164+
}
165+
166+
return new MapField(propName, keyType, valueType, DynamodbRecordTransformer.shouldLenientTypeCheck);
167+
}
168+
if ((valueDeclSymbol?.flags & ts.SymbolFlags.TypeLiteral) === ts.SymbolFlags.TypeLiteral) {
169+
// for key-value pair map notation
170+
const kvType = DynamodbRecordTransformer.extractKeyValueTypesFromKeyValuePairMapSyntax(
171+
prop.valueDeclaration.getChildren(),
172+
);
173+
174+
const keyTypeName = kvType?.[0];
175+
const keyType = dynamodbPrimitiveTypeFromName(keyTypeName);
176+
if (keyType === undefined) {
177+
const msg = `a Map type property "${propName}" of the type "${typeName}" has non-string key type: "${keyTypeName}"`;
178+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
179+
warn(msg);
180+
return undefined;
181+
}
182+
throw new Error(msg);
183+
}
184+
185+
const valueTypeName = kvType?.[1];
186+
const valueType = dynamodbPrimitiveTypeFromName(valueTypeName);
187+
if (valueType === undefined) {
188+
const msg = `a property "${propName}" of the type "${typeName}" has unsupported type: "${valueTypeName}"`;
189+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
190+
warn(msg);
191+
return undefined;
192+
}
193+
throw new Error(msg);
194+
}
195+
196+
return new KeyValuePairMapField(
197+
propName,
198+
keyType,
199+
valueType,
200+
DynamodbRecordTransformer.shouldLenientTypeCheck,
201+
);
202+
}
203+
204+
// primitive types
205+
if (valueDeclSymbolName === 'Uint8Array') {
206+
return new PrimitiveField(propName, DynamodbPrimitiveTypes.Binary);
207+
}
208+
209+
let colonTokenCame = false;
210+
for (const propNode of prop.valueDeclaration.getChildren()) {
211+
if (colonTokenCame) {
212+
const fieldType = dynamodbPrimitiveTypeFromName(propNode.getText());
213+
if (fieldType === undefined) {
214+
const msg = `a property "${propName}" of the type "${typeName}" has unsupported type: "${propNode.getText()}"`;
215+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
216+
warn(msg);
217+
return undefined;
218+
}
219+
throw new Error(msg);
220+
}
221+
return new PrimitiveField(propName, fieldType);
222+
}
223+
colonTokenCame = propNode.kind == ts.SyntaxKind.ColonToken;
224+
}
225+
226+
// should never reach here
227+
228+
const msg = `unexpected error: a property "${propName}" of the type "${typeName}" has unsupported type`;
229+
if (DynamodbRecordTransformer.shouldLenientTypeCheck) {
230+
warn(msg);
231+
return undefined;
232+
}
233+
throw new Error(msg);
234+
})
235+
.map(field => {
236+
return field?.generateCode(argVarNameIdent.text);
237+
})
238+
.filter((c): c is ts.ObjectLiteralElementLike => !!c);
239+
240+
return ts.factory.createImmediatelyInvokedFunctionExpression(
241+
[ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(objectProps, true))] as ts.Statement[],
242+
ts.factory.createParameterDeclaration(
243+
[],
244+
undefined,
245+
argVarNameIdent,
246+
undefined,
247+
node.typeArguments[0],
248+
undefined,
249+
),
250+
node.arguments[0],
251+
);
252+
}
253+
254+
private static extractKeyValueTypesFromKeyValuePairMapSyntax(nodes: ts.Node[]): [string, string] | undefined {
255+
for (const node of nodes) {
256+
if (node.kind === ts.SyntaxKind.TypeLiteral && node.getChildCount() === 3) {
257+
const kvTypeDeclNode = node.getChildAt(1);
258+
if (kvTypeDeclNode.kind === ts.SyntaxKind.SyntaxList && kvTypeDeclNode.getChildCount() === 1) {
259+
const kvTypeSignatureNode = kvTypeDeclNode.getChildAt(0);
260+
if (kvTypeSignatureNode.kind === ts.SyntaxKind.IndexSignature && kvTypeSignatureNode.getChildCount() === 5) {
261+
const valueType = kvTypeSignatureNode.getChildAt(4).getText();
262+
263+
const keyTypeDeclNode = kvTypeSignatureNode.getChildAt(1);
264+
if (keyTypeDeclNode.kind === ts.SyntaxKind.SyntaxList && keyTypeDeclNode.getChildCount() === 1) {
265+
const keyTypeSignatureNode = keyTypeDeclNode.getChildAt(0);
266+
if (keyTypeSignatureNode.kind === ts.SyntaxKind.Parameter && keyTypeSignatureNode.getChildCount() === 3) {
267+
return [keyTypeSignatureNode.getChildAt(2).getText(), valueType];
268+
}
269+
}
270+
}
271+
}
272+
break;
273+
}
274+
}
275+
return undefined;
276+
}
277+
278+
private static isPropertyModifierInArgumentSuitableForDynamodbAttr(
279+
modifiers: readonly ts.Modifier[] | undefined,
280+
): boolean {
281+
if (modifiers === undefined) {
282+
return true;
283+
}
284+
return !modifiers.map(m => m.kind).includes(ts.SyntaxKind.PrivateKeyword);
285+
}
286+
}

0 commit comments

Comments
 (0)