UNPKG

@env0/dynamo-easy

Version:

DynamoDB client for NodeJS and browser with a fluent api to build requests. We take care of the type mapping between JS and DynamoDB, customizable trough typescript decorators.

316 lines 12.1 kB
import * as tslib_1 from "tslib"; /** * @module mapper */ import { v4 as uuidv4 } from 'uuid'; import { hasSortKey } from '../decorator/metadata/metadata'; import { metadataForModel } from '../decorator/metadata/metadata-for-model.function'; import { hasType } from '../decorator/metadata/property-metadata.model'; import { BooleanMapper } from './for-type/boolean.mapper'; import { CollectionMapper } from './for-type/collection.mapper'; import { NullMapper } from './for-type/null.mapper'; import { NumberMapper } from './for-type/number.mapper'; import { ObjectMapper } from './for-type/object.mapper'; import { StringMapper } from './for-type/string.mapper'; import { Binary } from './type/binary.type'; import { NullType } from './type/null.type'; import { UndefinedType } from './type/undefined.type'; import { getPropertyPath, typeOf, typeOfFromDb } from './util'; /** * @hidden */ var mapperForType = new Map(); /** * mapps an item according to given model constructor [its meta data] to attributes */ export function toDb(item, modelConstructor) { var mapped = {}; if (modelConstructor) { var metadata = metadataForModel(modelConstructor); /* * initialize possible properties with auto generated uuid */ if (metadata) { metadata.getKeysWithUUID().forEach(function (propertyMetadata) { if (!Reflect.get(item, propertyMetadata.name)) { Reflect.set(item, propertyMetadata.name, uuidv4()); } }); } } item = getES6ClassProperties(item); var propertyNames = Object.getOwnPropertyNames(item) || []; propertyNames.forEach(function (propertyKey) { /* * 1) get the value of the property */ var propertyValue = getPropertyValue(item, propertyKey); var attributeValue; if (propertyValue === undefined || propertyValue === null) { // noop ignore because we can't map it } else { /* * 2) decide how to map the property depending on type or value */ var propertyMetadata = void 0; if (modelConstructor) { propertyMetadata = metadataForModel(modelConstructor).forProperty(propertyKey); } if (propertyMetadata) { if (propertyMetadata.transient) { // 3a_1) skip transient property } else { // 3a_2) property metadata is defined and property is not marked not transient attributeValue = toDbOne(propertyValue, getPropertyPath(modelConstructor, propertyKey), propertyMetadata); } } else { // 3b) no metadata found attributeValue = toDbOne(propertyValue, getPropertyPath(modelConstructor, propertyKey)); } if (attributeValue === undefined) { // no-op transient field, just ignore it } else if (attributeValue === null) { // empty values will be ignored too } else { ; mapped[propertyMetadata ? propertyMetadata.nameDb : propertyKey] = attributeValue; } } }); return mapped; } export function toDbOne(propertyValue, propertyPathOrMetadata, propertyMetadata) { var propertyPath = propertyPathOrMetadata && typeof propertyPathOrMetadata === 'string' ? propertyPathOrMetadata : null; propertyMetadata = propertyPathOrMetadata && typeof propertyPathOrMetadata !== 'string' ? propertyPathOrMetadata : propertyMetadata; var explicitType = hasType(propertyMetadata) ? propertyMetadata.typeInfo.type : null; var type = explicitType || typeOf(propertyValue, propertyPath); var mapper = propertyMetadata && propertyMetadata.mapper ? propertyMetadata.mapper() : forType(type); var attrValue = explicitType ? mapper.toDb(propertyValue, propertyMetadata) : mapper.toDb(propertyValue); // some basic validation if (propertyMetadata && propertyMetadata.key) { if (attrValue === null) { throw new Error(propertyMetadata.name.toString() + " is null but is a " + propertyMetadata.key.type + " key"); } if (!('S' in attrValue) && !('N' in attrValue) && !('B' in attrValue)) { throw new Error("DynamoDb only allows string, number or binary type for RANGE and HASH key. Make sure to define a custom mapper for '" + propertyMetadata.name.toString() + "' which returns a string, number or binary value for partition key, type " + type + " cannot be used as partition key, value = " + JSON.stringify(propertyValue)); } } return attrValue; } /** * @hidden */ function testForKey(p) { return !!p.key; } /** * returns the function for the given ModelConstructor to create the AttributeMap with HASH (and RANGE) Key of a given item. * @param modelConstructor */ export function createToKeyFn(modelConstructor) { var metadata = metadataForModel(modelConstructor); var properties = metadata.modelOptions.properties; if (!properties) { throw new Error('metadata properties is not defined'); } var keyProperties = properties.filter(testForKey); return function (item) { return keyProperties.reduce(function (key, propMeta) { if (item[propMeta.name] === null || item[propMeta.name] === undefined) { throw new Error("there is no value for property " + propMeta.name.toString() + " but is " + propMeta.key.type + " key"); } var propertyValue = getPropertyValue(item, propMeta.name); key[propMeta.nameDb] = toDbOne(propertyValue, propMeta); return key; }, {}); }; } /** * creates toKeyFn and applies item to it. * @see {@link createToKeyFn} */ export function toKey(item, modelConstructor) { return createToKeyFn(modelConstructor)(item); } /** * @hidden */ export function createKeyAttributes(metadata, partitionKey, sortKey) { var _a; var partitionKeyProp = metadata.getPartitionKey(); var partitionKeyMetadata = metadata.forProperty(partitionKeyProp); if (!partitionKeyMetadata) { throw new Error('metadata for partition key must be defined'); } var keyAttributeMap = (_a = {}, _a[partitionKeyMetadata.nameDb] = toDbOne(partitionKey, partitionKeyMetadata), _a); if (hasSortKey(metadata)) { if (sortKey === null || sortKey === undefined) { throw new Error("please provide the sort key for attribute " + metadata.getSortKey()); } var sortKeyProp = metadata.getSortKey(); var sortKeyMetadata = metadata.forProperty(sortKeyProp); if (!sortKeyMetadata) { throw new Error('metadata for sort key must be defined'); } keyAttributeMap[sortKeyMetadata.nameDb] = toDbOne(sortKey, sortKeyMetadata); } return keyAttributeMap; } /** * parses attributes to a js item according to the given model constructor [its meta data] */ export function fromDb(attributeMap, modelConstructor) { var model = {}; Object.getOwnPropertyNames(attributeMap).forEach(function (attributeName) { /* * 1) get the value of the property */ var attributeValue = attributeMap[attributeName]; /* * 2) decide how to map the property depending on type or value */ var modelValue; var propertyMetadata; if (modelConstructor) { propertyMetadata = metadataForModel(modelConstructor).forProperty(attributeName); } if (propertyMetadata) { if (propertyMetadata.transient) { // skip transient property } else { /* * 3a) property metadata is defined */ if (propertyMetadata && propertyMetadata.mapper) { // custom mapper modelValue = propertyMetadata.mapper().fromDb(attributeValue, propertyMetadata); } else { modelValue = fromDbOne(attributeValue, propertyMetadata); } } } else { modelValue = fromDbOne(attributeValue); } if (modelValue !== null && modelValue !== undefined) { Reflect.set(model, propertyMetadata ? propertyMetadata.name : attributeName, modelValue); } }); return model; } /** * parses an attribute to a js value according to the given property metadata */ export function fromDbOne(attributeValue, propertyMetadata) { var explicitType = hasType(propertyMetadata) ? propertyMetadata.typeInfo.type : null; var type = explicitType || typeOfFromDb(attributeValue); if (explicitType) { return forType(type).fromDb(attributeValue, propertyMetadata); } else { return forType(type).fromDb(attributeValue); } } /** * @hidden */ export function forType(type) { var mapper = mapperForType.get(type); if (!mapper) { switch (type) { case String: mapper = StringMapper; break; case Number: mapper = NumberMapper; break; case Boolean: mapper = BooleanMapper; break; case Map: // Maps support complex types as keys, we only support String & Number as Keys, otherwise a .toString() method should be implemented, // so we now how to save a key // mapperForType = new MapMapper() throw new Error('Map is not supported to be mapped for now'); case Array: mapper = CollectionMapper; break; case Set: mapper = CollectionMapper; break; case NullType: mapper = NullMapper; break; case Binary: throw new Error('no mapper for binary type implemented yet'); case UndefinedType: mapper = ObjectMapper; break; case Object: default: // return ObjectMapper as default to support nested @Model decorated classes (nested complex classes) // just note that the property still needs @Property decoration to get the metadata of the complex type mapper = ObjectMapper; } mapperForType.set(type, mapper); } return mapper; } /** * @hidden */ export function getPropertyValue(item, propertyKey) { var propertyDescriptor = Object.getOwnPropertyDescriptor(item, propertyKey); // use get accessor if available otherwise use value property of descriptor if (propertyDescriptor) { if (propertyDescriptor.get) { return propertyDescriptor.get(); } else { return propertyDescriptor.value; } } else { throw new Error("there is no property descriptor for item " + JSON.stringify(item) + " and property key " + propertyKey); } } /** * @hidden */ function getES6ClassProperties(item) { var e_1, _a; var jsonObj = tslib_1.__assign({}, item); var proto = Object.getPrototypeOf(item); try { for (var _b = tslib_1.__values(Object.getOwnPropertyNames(proto)), _c = _b.next(); !_c.done; _c = _b.next()) { var key = _c.value; var desc = Object.getOwnPropertyDescriptor(proto, key); var hasGetter = desc && typeof desc.get === 'function'; if (hasGetter) { jsonObj[key] = item[key]; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } return jsonObj; } //# sourceMappingURL=mapper.js.map