├── .gitignore ├── README.md ├── __test__ ├── fromDynamoItemToModel.test.js └── fromModelToDynamoItem.test.js ├── example.js ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `json-schema-dynamo` 2 | 3 | If you are able to, please consider using the `DocumentClient` in the `AWS.DynamoDB` SDK as it makes this package mostly unnecessary. 4 | 5 | An easier way to transform objects into DynamoDB items 6 | 7 | var transformers = require('json-schema-dynamo') 8 | 9 | var schema = { 10 | type: 'object', 11 | properties: { 12 | id: { 13 | type: 'string' 14 | }, 15 | createDate: { 16 | type: 'date' 17 | }, 18 | name: { 19 | type: 'string' 20 | }, 21 | active: { 22 | type: 'boolean' 23 | }, 24 | likes: { 25 | type: 'number' 26 | }, 27 | types: { 28 | type: 'array', 29 | items: { 30 | type: 'string' 31 | } 32 | }, 33 | userIds: { 34 | type: 'array', 35 | items: { 36 | type: 'number' 37 | } 38 | } 39 | } 40 | } 41 | 42 | var model = { 43 | id: 'asdf', 44 | createDate: new Date(), 45 | name: 'asdffdas', 46 | likes: 1, 47 | active: true, 48 | types: ['qwerty', 'ytrewq'], 49 | userIds: [1, 2, 3, 4, 5, 6, 7] 50 | } 51 | 52 | var item = transformers.fromModelToDynamoItem(schema, model) 53 | console.log(item) 54 | /* 55 | { 56 | id: { 57 | S: 'asdf' 58 | }, 59 | createDate: { 60 | N: '1928383' 61 | }, 62 | name: { 63 | S: 'asdffdas' 64 | }, 65 | likes: { 66 | N: '1' 67 | }, 68 | active: { 69 | B: 'true' 70 | }, 71 | types: { 72 | SS: ['qwerty', 'ytrewq'] 73 | }, 74 | userIds: { 75 | SN: ['1', '2', '3', '4', '5', '6', '7'] 76 | } 77 | } 78 | */ 79 | 80 | var newModel = transformers.fromDynamoItemToModel(schema, item) 81 | console.log(newModel) 82 | /* 83 | { 84 | id: 'asdf', 85 | createDate: , 86 | name: 'asdffdas', 87 | active: true, 88 | likes: 1 89 | types: ['qwerty', 'ytrewq'], 90 | userIds: [1, 2, 3, 4, 5, 6, 7] 91 | } 92 | */ 93 | 94 | 95 | Currently it supports: string, number, integer, boolean, array of strings, array of numbers, and lists 96 | 97 | There is also custom support for dates. You can define an attribute as a date type which will be transformed into an `N` in Dynamo and then back into a date when retrieved. 98 | 99 | Both transforms will also validate your model against your schema as well 100 | -------------------------------------------------------------------------------- /__test__/fromDynamoItemToModel.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var transformer = require('..') 3 | 4 | describe('fromDynamoItemToModel', function () { 5 | 6 | describe('transforms', function () { 7 | var schema 8 | 9 | beforeEach(function() { 10 | schema = { 11 | type: 'object', 12 | properties: { 13 | string: { 14 | type: 'string' 15 | }, 16 | number: { 17 | type: 'number' 18 | }, 19 | integer: { 20 | type: 'integer' 21 | }, 22 | boolean: { 23 | type: 'boolean' 24 | }, 25 | date: { 26 | type: 'date' 27 | }, 28 | arrayString: { 29 | type: 'array', 30 | items: { 31 | type: 'string' 32 | } 33 | }, 34 | arrayNumber: { 35 | type: 'array', 36 | items: { 37 | type: 'number' 38 | } 39 | }, 40 | arrayDate: { 41 | type: 'array', 42 | items: { 43 | type: 'date' 44 | } 45 | }, 46 | arrayObject: { 47 | type: 'array', 48 | items: { 49 | anyOf: [ 50 | { 51 | type: 'number' 52 | }, 53 | { 54 | type: 'object', 55 | properties: { 56 | string: { 57 | type: 'string' 58 | } 59 | } 60 | } 61 | ] 62 | } 63 | }, 64 | nestedObject: { 65 | type: 'object', 66 | properties: { 67 | number: { 68 | type: 'number' 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | 76 | it('S', function () { 77 | var item = { 78 | string: { 79 | S: 'a string' 80 | } 81 | } 82 | var model = transformer.fromDynamoItemToModel(schema, item) 83 | assert(model.string === item.string.S) 84 | }) 85 | 86 | it('N', function () { 87 | var item = { 88 | number: { 89 | N: '12.34' 90 | }, 91 | integer: { 92 | N: '1234' 93 | } 94 | } 95 | 96 | var model = transformer.fromDynamoItemToModel(schema, item) 97 | assert(model.number === parseFloat(item.number.N)) 98 | assert(model.integer === parseInt(item.integer.N, 10)) 99 | }) 100 | 101 | it('N that is a date', function () { 102 | var item = { 103 | date: { 104 | N: +Date.now() 105 | } 106 | } 107 | var model = transformer.fromDynamoItemToModel(schema, item) 108 | assert(+model.date === parseInt(item.date.N, 10)) 109 | }) 110 | 111 | it('BOOL', function () { 112 | var item = { 113 | boolean: { 114 | BOOL: 'true' 115 | } 116 | } 117 | 118 | var model = transformer.fromDynamoItemToModel(schema, item) 119 | assert(model.boolean === Boolean(item.boolean.BOOL)) 120 | }) 121 | 122 | it('SS', function () { 123 | var item = { 124 | arrayString: { 125 | SS: ['asdf', 'fdsa'] 126 | } 127 | } 128 | 129 | var model = transformer.fromDynamoItemToModel(schema, item) 130 | assert(model.arrayString[0] === item.arrayString.SS[0]) 131 | assert(model.arrayString[1] === item.arrayString.SS[1]) 132 | }) 133 | 134 | it('NS', function () { 135 | var item = { 136 | arrayNumber: { 137 | NS: ['10', '20'] 138 | } 139 | } 140 | 141 | var model = transformer.fromDynamoItemToModel(schema, item) 142 | assert(model.arrayNumber[0] === parseInt(item.arrayNumber.NS[0], 10)) 143 | assert(model.arrayNumber[1] === parseInt(item.arrayNumber.NS[1], 10)) 144 | }) 145 | 146 | it('NS of dates', function () { 147 | var item = { 148 | arrayDate: { 149 | NS: ['1441127720385', '1441127720385'] 150 | } 151 | } 152 | var model = transformer.fromDynamoItemToModel(schema, item) 153 | assert(String(+model.arrayDate[0]) === item.arrayDate.NS[0]) 154 | assert(String(+model.arrayDate[1]) === item.arrayDate.NS[1]) 155 | }) 156 | 157 | it('L', function () { 158 | var item = { 159 | arrayObject: { 160 | L: [ 161 | { M: { string: { S: 'ewr' } } }, 162 | { M: { string: { S: 'wqe' } } } 163 | ] 164 | } 165 | } 166 | 167 | var model = transformer.fromDynamoItemToModel(schema, item) 168 | assert(model.arrayObject[0].string === item.arrayObject.L[0].M.string.S) 169 | assert(model.arrayObject[1].string === item.arrayObject.L[1].M.string.S) 170 | }) 171 | 172 | it('M', function () { 173 | var item = { 174 | nestedObject: { 175 | M: { number: { N: '7' } } 176 | } 177 | } 178 | 179 | var model = transformer.fromDynamoItemToModel(schema, item) 180 | assert(model.nestedObject.number === parseInt(item.nestedObject.M.number.N, 10)) 181 | }) 182 | 183 | it('NULL', function () { 184 | var item = { 185 | nullProperty: { 186 | NULL: true 187 | } 188 | } 189 | 190 | var model = transformer.fromDynamoItemToModel(schema, item) 191 | assert(model.nullProperty === null) 192 | }) 193 | }) 194 | 195 | }) 196 | -------------------------------------------------------------------------------- /__test__/fromModelToDynamoItem.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var transformer = require('..') 3 | 4 | describe('fromModelToDynamoItem', function() { 5 | 6 | describe('validation', function() { 7 | var schema 8 | var model 9 | 10 | beforeEach(function() { 11 | schema = { 12 | type: 'object', 13 | properties: { 14 | prop: { 15 | type: 'string' 16 | } 17 | } 18 | } 19 | 20 | model = { 21 | prop: 'a string' 22 | } 23 | }) 24 | 25 | it('fails if the schema and model data types do not match', function () { 26 | model.prop = 1234 27 | 28 | assert.throws(function () { 29 | transformer.fromModelToDynamoItem(schema, model) 30 | }) 31 | }) 32 | 33 | it('fails if a required property is missing', function () { 34 | delete model.prop 35 | schema.required = ['prop'] 36 | 37 | assert.throws(function () { 38 | transformer.fromModelToDynamoItem(schema, model) 39 | }) 40 | }) 41 | 42 | it('passes if an optional value is missing', function () { 43 | schema.properties.anotherProp = { 44 | type: 'number' 45 | } 46 | 47 | assert.doesNotThrow(function() { 48 | transformer.fromModelToDynamoItem(schema, model) 49 | }) 50 | }) 51 | 52 | it ('passes if there are attributes on the model not in the schema', function () { 53 | model.anotherProp = 99999 54 | 55 | assert.doesNotThrow(function() { 56 | transformer.fromModelToDynamoItem(schema, model) 57 | }) 58 | }) 59 | 60 | }) 61 | 62 | describe('doesnt transform', function () { 63 | var schema 64 | var model 65 | 66 | beforeEach(function() { 67 | schema = { 68 | type: 'object', 69 | properties: { 70 | prop: { 71 | type: 'string' 72 | } 73 | } 74 | } 75 | 76 | model = { 77 | prop: 'a string' 78 | } 79 | }) 80 | 81 | it('optional attributes that are not defined on the model', function () { 82 | schema.properties.anotherProp = { 83 | type: 'number' 84 | } 85 | 86 | var item = transformer.fromModelToDynamoItem(schema, model) 87 | assert(!item.hasOwnProperty('anotherProp')) 88 | }) 89 | 90 | it('attributes that arent in the schema', function () { 91 | model.anotherProp = 99999 92 | 93 | var item = transformer.fromModelToDynamoItem(schema, model) 94 | assert(!item.hasOwnProperty('anotherProp')) 95 | }) 96 | }) 97 | 98 | describe('transforms', function () { 99 | var schema 100 | 101 | beforeEach(function() { 102 | schema = { 103 | type: 'object', 104 | properties: { 105 | string: { 106 | type: 'string' 107 | }, 108 | number: { 109 | type: 'number' 110 | }, 111 | integer: { 112 | type: 'integer' 113 | }, 114 | boolean: { 115 | type: 'boolean' 116 | }, 117 | date: { 118 | type: 'date' 119 | }, 120 | arrayString: { 121 | type: 'array', 122 | items: { 123 | type: 'string' 124 | } 125 | }, 126 | arrayNumber: { 127 | type: 'array', 128 | items: { 129 | type: 'number' 130 | } 131 | }, 132 | arrayDate: { 133 | type: 'array', 134 | items: { 135 | type: 'date' 136 | } 137 | }, 138 | arrayObject: { 139 | type: 'array', 140 | items: { 141 | type: 'object', 142 | properties: { 143 | string: { 144 | type: 'string' 145 | } 146 | } 147 | } 148 | }, 149 | nestedObject: { 150 | type: 'object', 151 | properties: { 152 | number: { 153 | type: 'number' 154 | } 155 | } 156 | } 157 | } 158 | } 159 | }) 160 | 161 | it('strings', function () { 162 | var model = { 163 | string: 'a string' 164 | } 165 | var item = transformer.fromModelToDynamoItem(schema, model) 166 | assert(item.string.S === model.string) 167 | }) 168 | 169 | it('numbers', function () { 170 | var model = { 171 | number: 1234.1234 172 | } 173 | var item = transformer.fromModelToDynamoItem(schema, model) 174 | assert(item.number.N === String(model.number)) 175 | }) 176 | 177 | it('integers', function () { 178 | var model = { 179 | integer: 1234 180 | } 181 | var item = transformer.fromModelToDynamoItem(schema, model) 182 | assert(item.integer.N === String(model.integer)) 183 | }) 184 | 185 | it('booleans', function () { 186 | var model = { 187 | boolean: true 188 | } 189 | var item = transformer.fromModelToDynamoItem(schema, model) 190 | assert(item.boolean.BOOL === model.boolean) 191 | }) 192 | 193 | it('dates', function () { 194 | var model = { 195 | date: new Date() 196 | } 197 | var item = transformer.fromModelToDynamoItem(schema, model) 198 | assert(item.date.N === String(+model.date)) 199 | }) 200 | 201 | it('array of strings', function () { 202 | var model = { 203 | arrayString: ['asdf', 'fdas'] 204 | } 205 | var item = transformer.fromModelToDynamoItem(schema, model) 206 | assert(item.arrayString.SS[0] === model.arrayString[0]) 207 | assert(item.arrayString.SS[1] === model.arrayString[1]) 208 | }) 209 | 210 | it('array of numbers', function () { 211 | var model = { 212 | arrayNumber: [10, 20] 213 | } 214 | var item = transformer.fromModelToDynamoItem(schema, model) 215 | assert(item.arrayNumber.NS[0] === String(model.arrayNumber[0])) 216 | assert(item.arrayNumber.NS[1] === String(model.arrayNumber[1])) 217 | }) 218 | 219 | it('array of dates', function () { 220 | var model = { 221 | arrayDate: [new Date(), new Date()] 222 | } 223 | var item = transformer.fromModelToDynamoItem(schema, model) 224 | assert(item.arrayDate.NS[0] === String(+model.arrayDate[0])) 225 | assert(item.arrayDate.NS[1] === String(+model.arrayDate[1])) 226 | }) 227 | 228 | it('array of objects', function () { 229 | var model = { 230 | arrayObject: [ { string: 'a' }, { string: 'b' } ] 231 | } 232 | var item = transformer.fromModelToDynamoItem(schema, model) 233 | assert(item.arrayObject.L[0].M.string.S === model.arrayObject[0].string) 234 | assert(item.arrayObject.L[1].M.string.S === model.arrayObject[1].string) 235 | }) 236 | 237 | it('nested object', function () { 238 | var model = { 239 | nestedObject: { number: 42 } 240 | } 241 | var item = transformer.fromModelToDynamoItem(schema, model) 242 | assert(item.nestedObject.M.number.N === String(model.nestedObject.number)) 243 | }) 244 | }) 245 | 246 | }) 247 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var transformers = require('./') 2 | 3 | var schema = { 4 | id: 'schema', 5 | type: 'object', 6 | properties: { 7 | id: { 8 | type: 'string' 9 | }, 10 | createDate: { 11 | type: 'number' 12 | }, 13 | name: { 14 | type: 'string' 15 | }, 16 | active: { 17 | type: 'boolean' 18 | }, 19 | types: { 20 | type: 'array', 21 | items: { 22 | type: 'string' 23 | } 24 | }, 25 | userIds: { 26 | type: 'array', 27 | items: { 28 | type: 'number' 29 | } 30 | } 31 | } 32 | } 33 | 34 | var model = { 35 | id: 'asdf', 36 | createDate: 1928383, 37 | name: 'asdffdas', 38 | active: true, 39 | types: ['qwerty', 'ytrewq'], 40 | userIds: [1, 2, 3, 4, 5, 6, 7] 41 | } 42 | 43 | var dynamoItem = transformers.fromModelToDynamoItem(schema, model) 44 | console.log(dynamoItem) 45 | 46 | var newModel = transformers.fromDynamoItemToModel(schema, dynamoItem) 47 | console.log(newModel) 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Validator = require('jsonschema').Validator 2 | var _ = require('lodash') 3 | 4 | var toModel = { 5 | S: function (value) { 6 | return value 7 | }, 8 | N: function (value, itemSchema) { 9 | if (itemSchema && itemSchema.type === 'date') { 10 | return new Date(+value) 11 | } 12 | return +value 13 | }, 14 | BOOL: function (value) { 15 | switch (value) { 16 | case true: 17 | case 'true': 18 | case 1: 19 | case '1': 20 | case 'on': 21 | case 'yes': 22 | return true 23 | default: 24 | return false 25 | } 26 | }, 27 | L: function (value, schema) { 28 | return value.map(function (value) { 29 | var type = Object.keys(value)[0] 30 | return toModel[type](value[type], schema.items) 31 | }) 32 | }, 33 | M: function (value, schema) { 34 | return Object.keys(value).reduce((acc, key) => { 35 | var type = Object.keys(value[key])[0] 36 | var itemSchema = schema && schema.properties && schema.properties[key] 37 | acc[key] = toModel[type](value[key][type], itemSchema) 38 | return acc 39 | }, {}) 40 | }, 41 | SS: function (value) { 42 | return value.map(toModel.S) 43 | }, 44 | NS: function (value, schema) { 45 | return value.map(function (num) { 46 | return toModel.N(num, schema.items) 47 | }) 48 | }, 49 | NULL: function() { 50 | return null 51 | } 52 | } 53 | 54 | exports.fromDynamoItemToModel = function (schema, item) { 55 | var model = toModel.M(item, schema) 56 | 57 | var v = new Validator() 58 | var result = v.validate(model, schema) 59 | 60 | if (!result.valid) { 61 | throw new Error(result.errors) 62 | } 63 | 64 | return model 65 | } 66 | 67 | var toItem = { 68 | string: function (schema, value) { 69 | return { S: String(value) } 70 | }, 71 | number: function (schema, value) { 72 | return { N: String(+value) } 73 | }, 74 | boolean: function (schema, value) { 75 | return { BOOL: value } 76 | }, 77 | date: function (schema, value) { 78 | return { N: String(+value) } 79 | }, 80 | array: function (schema, value) { 81 | if (!Array.isArray(value)) { return null } 82 | 83 | var isDate = schema.items && schema.items.type === 'date' 84 | if (isDate) { 85 | return { NS: value.map(function(date) { return String(+date) })} 86 | } 87 | 88 | var isNum = (schema.items && ( 89 | schema.items.type === 'number' || schema.items.type === 'integer') 90 | ) 91 | if (isNum) { 92 | return { NS: value.map(String) } 93 | } 94 | 95 | var isStr = (schema.items && schema.items.type === 'string') 96 | if (isStr) { 97 | return { SS: value.map(String) } 98 | } 99 | 100 | return { L: value.map(toItem.any.bind(toItem, schema.items)) } 101 | }, 102 | object: function (schema, value) { 103 | if (typeof value !== 'object') { return null } 104 | 105 | var model = {} 106 | var properties = schema.properties 107 | Object.keys(properties).forEach(function (name) { 108 | var subvalue = toItem.any(properties[name], value[name]) 109 | if (subvalue != null) { model[name] = subvalue } 110 | }) 111 | return { M: model } 112 | }, 113 | any: function (schema, value) { 114 | if (value == null) { return null } 115 | switch (schema.type) { 116 | case 'string': 117 | return toItem.string(schema, value) 118 | case 'integer': 119 | case 'number': 120 | return toItem.number(schema, value) 121 | case 'date': 122 | return toItem.date(schema, value) 123 | case 'boolean': 124 | return toItem['boolean'](schema, value) 125 | case 'array': 126 | return toItem.array(schema, value) 127 | case 'object': 128 | return toItem.object(schema, value) 129 | default: 130 | break; 131 | } 132 | return null // couldn't determine a type 133 | } 134 | } 135 | 136 | exports.fromModelToDynamoItem = function (schema, model) { 137 | var v = new Validator() 138 | var result = v.validate(model, schema) 139 | 140 | if (!result.valid) { 141 | throw new Error(result.errors) 142 | } 143 | 144 | return toItem.object(schema, model).M 145 | } 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema-dynamo", 3 | "version": "0.6.0", 4 | "description": "Transform objects based on their JSON Schema definition back and forth between DynamoDB items", 5 | "main": "index.js", 6 | "author": "Matthew Sessions", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "jest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/shichongrui/json-schema-dynamo.git" 14 | }, 15 | "dependencies": { 16 | "jsonschema": "1.0.1" 17 | }, 18 | "devDependencies": { 19 | "jest": "^26.6.3" 20 | } 21 | } 22 | --------------------------------------------------------------------------------