diff --git a/src/module/dy/create-model.js b/src/module/dy/create-model.js index d265c5d1..c41bbc5f 100644 --- a/src/module/dy/create-model.js +++ b/src/module/dy/create-model.js @@ -1,4 +1,5 @@ import get from 'lodash.get'; +import objectScan from 'object-scan'; import getFirst from './get-first.js'; import validateKwargs from './validate-kwargs.js'; @@ -61,7 +62,9 @@ export default (kwargs) => { name, timestamps: false, attributes: Object.fromEntries(Object.entries(attributes).map(([k, v]) => { - const { validate, ...prunedV } = v; + const { + validate, marshall, unmarshall, ...prunedV + } = v; if (prunedV.type === 'set' && Array.isArray(prunedV.default) && prunedV.default.length === 0) { const { default: _, ...newV } = prunedV; return [k, newV]; @@ -110,9 +113,35 @@ export default (kwargs) => { BillingMode: 'PAY_PER_REQUEST' }; + const generateRewriter = (fn) => { + const entries = Object + .entries(attributes) + .filter(([k, v]) => typeof v?.[fn] === 'function') + .map(([k, v]) => [`{[*].${k},${k}}`, v]); + if (entries.length === 0) { + return (e) => e; + } + const logic = Object.fromEntries(entries); + return (itemOrItems) => { + objectScan(Object.keys(logic), { + filterFn: ({ + parent, property, value, matchedBy + }) => { + matchedBy.forEach((m) => { + // eslint-disable-next-line no-param-reassign + parent[property] = logic[m][fn](value); + }); + } + })(itemOrItems); + return itemOrItems; + }; + }; + return { schema, table, - entity + entity, + marshall: generateRewriter('marshall'), + unmarshall: generateRewriter('unmarshall') }; }; diff --git a/src/module/dy/fns/get-item.js b/src/module/dy/fns/get-item.js index 00c1d26c..5e17f084 100644 --- a/src/module/dy/fns/get-item.js +++ b/src/module/dy/fns/get-item.js @@ -36,5 +36,5 @@ export default (model, onNotFound_, setDefaults) => async (...args) => { .forEach((k) => { delete item[k]; }); - return item; + return model.unmarshall(item); }; diff --git a/src/module/dy/fns/query.js b/src/module/dy/fns/query.js index d3551e2c..94689152 100644 --- a/src/module/dy/fns/query.js +++ b/src/module/dy/fns/query.js @@ -155,6 +155,9 @@ export default (model, validateSecondaryIndex, setDefaults, getSortKeyByIndex, c if (toReturn !== null) { Retainer(toReturn)(items); } - return { items, page }; + return { + items: model.unmarshall(items), + page + }; }; }; diff --git a/src/module/dy/fns/scan.js b/src/module/dy/fns/scan.js index 6fba9f89..18329425 100644 --- a/src/module/dy/fns/scan.js +++ b/src/module/dy/fns/scan.js @@ -34,7 +34,9 @@ export default (model, validateSecondaryIndex, setDefaults) => async (...args) = entity: model.table.name }); return { - items: result.Items.map((item) => setDefaults(item, toReturn)), + items: model.unmarshall( + result.Items.map((item) => setDefaults(item, toReturn)) + ), ...(result.LastEvaluatedKey === undefined ? {} : { lastEvaluatedKey: result.LastEvaluatedKey }) }; }; diff --git a/src/module/dy/util.js b/src/module/dy/util.js index be59da94..29a98f53 100644 --- a/src/module/dy/util.js +++ b/src/module/dy/util.js @@ -109,7 +109,7 @@ export default ({ let result; try { result = await model.entity[fn]( - itemRewriterByFn[fn](item), + model.marshall(itemRewriterByFn[fn](item)), { returnValues: 'all_old', ...(conditions === null ? {} : { conditions }) @@ -130,7 +130,7 @@ export default ({ } const didNotExist = result.Attributes === undefined; const mergedItem = mergeAttributes( - (didNotExist || fn === 'put') ? {} : result.Attributes, + (didNotExist || fn === 'put') ? {} : model.unmarshall(result.Attributes), item ); const resultItem = setDefaults(mergedItem, null); diff --git a/src/module/dy/validate-kwargs.js b/src/module/dy/validate-kwargs.js index b6a0b6fd..0f95af54 100644 --- a/src/module/dy/validate-kwargs.js +++ b/src/module/dy/validate-kwargs.js @@ -8,6 +8,8 @@ const schema = Joi.object().keys({ type: Joi.string().valid('string', 'boolean', 'number', 'list', 'map', 'binary', 'set'), partitionKey: Joi.boolean().valid(true).optional(), sortKey: Joi.boolean().valid(true).optional(), + marshall: Joi.function().arity(1).optional(), + unmarshall: Joi.function().arity(1).optional(), default: Joi.alternatives().try( Joi.string(), Joi.number(), diff --git a/test/module/dy/create-model.spec.js b/test/module/dy/create-model.spec.js index cc8edcfe..6961215a 100644 --- a/test/module/dy/create-model.spec.js +++ b/test/module/dy/create-model.spec.js @@ -30,7 +30,7 @@ describe('Testing create-model.js', () => { Table, Entity }); - expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity']); + expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity', 'marshall', 'unmarshall']); }); it('Testing creation without indices', () => { @@ -45,7 +45,7 @@ describe('Testing create-model.js', () => { Table, Entity }); - expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity']); + expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity', 'marshall', 'unmarshall']); }); it('Testing creation different attribute types', () => { @@ -61,7 +61,7 @@ describe('Testing create-model.js', () => { Table, Entity }); - expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity']); + expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity', 'marshall', 'unmarshall']); }); it('Testing attribute not supported for indexing error', () => { @@ -95,6 +95,6 @@ describe('Testing create-model.js', () => { Table, Entity }); - expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity']); + expect(Object.keys(r)).to.deep.equal(['schema', 'table', 'entity', 'marshall', 'unmarshall']); }); }); diff --git a/test/module/dy/fns/get-item.spec.js b/test/module/dy/fns/get-item.spec.js index 1d051c91..bc26acb9 100644 --- a/test/module/dy/fns/get-item.spec.js +++ b/test/module/dy/fns/get-item.spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { describe } from 'node-tdd'; +import zlib from 'zlib'; import { LocalTable, buildModel, createItems } from '../../../dy-helper.js'; import { ModelNotFound } from '../../../../src/resources/errors.js'; import nockReqHeaderOverwrite from '../../../req-header-overwrite.js'; @@ -99,4 +100,27 @@ describe('Testing get-item', { result.ids.push(1, 2, 3); expect(def).to.deep.equal([]); }); + + it('Testing getItem with custom marshalling', async () => { + await generateTable({ + extraAttrs: { + bin: { + type: 'binary', + default: () => [], + marshall: (item) => zlib.gzipSync(JSON.stringify(item), { level: 9 }), + unmarshall: (item) => JSON.parse(zlib.gunzipSync(item)) + } + } + }); + + const { item } = await model.create({ + id: '123', + name: 'name', + age: 50, + bin: [{ a: 1 }] + }); + const key = { id: item.id, name: item.name }; + const result = await model.getItem(key, { toReturn: ['bin'] }); + expect(result).to.deep.equal({ bin: [{ a: 1 }] }); + }); }); diff --git a/test/module/dy/fns/get-item.spec.js__cassettes/testingGetItem_testingGetItemWithCustomMarshalling_recording.json b/test/module/dy/fns/get-item.spec.js__cassettes/testingGetItem_testingGetItemWithCustomMarshalling_recording.json new file mode 100644 index 00000000..03b0357c --- /dev/null +++ b/test/module/dy/fns/get-item.spec.js__cassettes/testingGetItem_testingGetItemWithCustomMarshalling_recording.json @@ -0,0 +1,397 @@ +[ + { + "scope": "http://dynamodb-local:8000", + "method": "POST", + "path": "/", + "body": { + "TableName": "table-name", + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "name", + "AttributeType": "S" + }, + { + "AttributeName": "age", + "AttributeType": "N" + } + ], + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "targetIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "idIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "ageIndex", + "KeySchema": [ + { + "AttributeName": "age", + "KeyType": "HASH" + }, + { + "AttributeName": "id", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "BillingMode": "PAY_PER_REQUEST" + }, + "status": 200, + "response": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "name", + "AttributeType": "S" + }, + { + "AttributeName": "age", + "AttributeType": "N" + } + ], + "TableName": "table-name", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "TableStatus": "ACTIVE", + "CreationDateTime": 1708647040.173, + "ProvisionedThroughput": { + "LastIncreaseDateTime": 0, + "LastDecreaseDateTime": 0, + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "TableSizeBytes": 0, + "ItemCount": 0, + "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name", + "BillingModeSummary": { + "BillingMode": "PAY_PER_REQUEST", + "LastUpdateToPayPerRequestDateTime": 1708647040.173 + }, + "GlobalSecondaryIndexes": [ + { + "IndexName": "targetIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 0, + "ItemCount": 0, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/targetIndex" + }, + { + "IndexName": "ageIndex", + "KeySchema": [ + { + "AttributeName": "age", + "KeyType": "HASH" + }, + { + "AttributeName": "id", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 0, + "ItemCount": 0, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/ageIndex" + }, + { + "IndexName": "idIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 0, + "ItemCount": 0, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/idIndex" + } + ], + "DeletionProtectionEnabled": false + } + }, + "reqheaders": {}, + "responseIsBinary": false + }, + { + "scope": "http://dynamodb-local:8000", + "method": "POST", + "path": "/", + "body": { + "ConditionExpression": "attribute_not_exists(#attr1)", + "ExpressionAttributeNames": { + "#attr1": "id" + }, + "Item": { + "age": { + "N": "50" + }, + "bin": { + "B": "H4sIAAAAAAACA4uuVkpUsjKsjQUAfXPL8gkAAAA=" + }, + "id": { + "S": "123" + }, + "name": { + "S": "name" + } + }, + "ReturnValues": "ALL_OLD", + "TableName": "table-name" + }, + "status": 200, + "response": {}, + "reqheaders": {}, + "responseIsBinary": false + }, + { + "scope": "http://dynamodb-local:8000", + "method": "POST", + "path": "/", + "body": { + "ConsistentRead": true, + "ExpressionAttributeNames": { + "#proj1": "bin", + "#proj2": "id", + "#proj3": "name" + }, + "Key": { + "id": { + "S": "123" + }, + "name": { + "S": "name" + } + }, + "ProjectionExpression": "#proj1,#proj2,#proj3", + "TableName": "table-name" + }, + "status": 200, + "response": { + "Item": { + "name": { + "S": "name" + }, + "bin": { + "B": "H4sIAAAAAAACA4uuVkpUsjKsjQUAfXPL8gkAAAA=" + }, + "id": { + "S": "123" + } + } + }, + "reqheaders": {}, + "responseIsBinary": false + }, + { + "scope": "http://dynamodb-local:8000", + "method": "POST", + "path": "/", + "body": { + "TableName": "table-name" + }, + "status": 200, + "response": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + }, + { + "AttributeName": "name", + "AttributeType": "S" + }, + { + "AttributeName": "age", + "AttributeType": "N" + } + ], + "TableName": "table-name", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "TableStatus": "ACTIVE", + "CreationDateTime": 1708647040.173, + "ProvisionedThroughput": { + "LastIncreaseDateTime": 0, + "LastDecreaseDateTime": 0, + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "TableSizeBytes": 50, + "ItemCount": 1, + "TableArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name", + "BillingModeSummary": { + "BillingMode": "PAY_PER_REQUEST", + "LastUpdateToPayPerRequestDateTime": 1708647040.173 + }, + "GlobalSecondaryIndexes": [ + { + "IndexName": "targetIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + }, + { + "AttributeName": "name", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 50, + "ItemCount": 1, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/targetIndex" + }, + { + "IndexName": "ageIndex", + "KeySchema": [ + { + "AttributeName": "age", + "KeyType": "HASH" + }, + { + "AttributeName": "id", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 50, + "ItemCount": 1, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/ageIndex" + }, + { + "IndexName": "idIndex", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "IndexStatus": "ACTIVE", + "ProvisionedThroughput": { + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "IndexSizeBytes": 50, + "ItemCount": 1, + "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/table-name/index/idIndex" + } + ], + "DeletionProtectionEnabled": false + } + }, + "reqheaders": {}, + "responseIsBinary": false + } +] \ No newline at end of file