Repository: jeremydaly/dynamodb-toolbox
Pull request #18 (2019-12-13 21:52:07)

Add delimiter option for composite sortKey generation
#17 


Patch:
diff --git a/README.md b/README.md
index 7d902307..043395b1 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ timestamps | `Boolean` | no | Automatically add and manage `created` and `modifi
 created | `string` | no | Override default `created` field name |
 modified | `string` | no | Override default `modified` field name |
 schema | `object` | yes | Complex type that specifies the schema for the model (see below) |
+delimiter | `string` | no | The string that separates composite values in your sort key (`#` default) |
 
 ### Schema Definition
 
diff --git a/index.js b/index.js
index 35f42681..f841f69e 100644
--- a/index.js
+++ b/index.js
@@ -1,5 +1,3 @@
-// TODO: Add delimiter option
-
 // TODO: prevent reserved field names ?
 
 // TODO: Add date type
@@ -33,22 +31,22 @@ class Model {
 
   parse(input,omit=[]) {
     // Load the schema
-    let { schema, linked } = this.Model
+    let { schema, linked, delimiter } = this.Model
 
     // Assume standard response from DynamoDB
     let data = input.Item || input.Items || input
 
     if (Array.isArray(data)) {
-      return data.map(item => formatItem(schema,linked,item,omit))
+      return data.map(item => formatItem(schema,linked,item,omit,delimiter))
     } else {
-      return formatItem(schema,linked,data,omit)
+      return formatItem(schema,linked,data,omit,delimiter)
     }
   }
 
   get(item={},params={}) {
     // Extract schema and merge defaults
-    let { schema, defaults, linked, partitionKey, sortKey, table } = this.Model
-    let data = normalizeData(schema,linked,Object.assign({},defaults,item),true)
+    let { schema, defaults, linked, partitionKey, sortKey, table, delimiter } = this.Model
+    let data = normalizeData(schema,linked,Object.assign({},defaults,item),delimiter,true)
 
     return Object.assign(
       {
@@ -92,10 +90,10 @@ class Model {
     //     error(`ConditionExpression must be a string`)
 
     // Extract schema and defaults
-    let { schema, defaults, required, linked, partitionKey, sortKey, table } = this.Model
+    let { schema, defaults, required, linked, partitionKey, sortKey, table, delimiter } = this.Model
 
     // Merge defaults
-    let data = normalizeData(schema,linked,Object.assign({},defaults,item))
+    let data = normalizeData(schema,linked,Object.assign({},defaults,item),delimiter)
 
     // Check for required fields
     Object.keys(required).forEach(field =>
@@ -265,10 +263,10 @@ class Model {
 
   put(item={},params={}) {
     // Extract schema and defaults
-    let { schema, defaults, required, linked, partitionKey, sortKey, table } = this.Model
+    let { schema, defaults, required, linked, partitionKey, sortKey, table, delimiter } = this.Model
 
     // Merge defaults
-    let data = normalizeData(schema,linked,Object.assign({},defaults,item))
+    let data = normalizeData(schema,linked,Object.assign({},defaults,item),delimiter)
 
     // Check for required fields
     Object.keys(required).forEach(field =>
@@ -338,6 +336,11 @@ const parseModel = (name,model) => {
   let schema = typeof model.schema === 'object' && !Array.isArray(model.schema) ?
     model.schema : error(`Please provide a valid 'schema'`)
 
+  let delimiter = '#'
+  if (typeof model.delimiter === 'string' && model.delimiter.trim().length > 0) {
+    delimiter = model.delimiter.trim()
+  } else if (model.delimiter) error(`'delimiter' must be string value with length >= 1`)
+
   // Add model_field
   if (model_field) {
     schema[model_field] = { type: 'string', default: model_name, hidden: true }
@@ -362,6 +365,7 @@ const parseModel = (name,model) => {
     table,
     partitionKey,
     sortKey,
+    delimiter,
     schema: Object.keys(schema).reduce((acc,field) => {
       if (typeof schema[field] === 'string') {
         return validTypes.includes(schema[field]) ?
@@ -551,14 +555,14 @@ const getKey = (data,schema,partitionKey,sortKey) => {
 
 
 // Format item based on schema
-const formatItem = (schema,linked,item,omit) => {
+const formatItem = (schema,linked,item,omit,delimiter) => {
   return Object.keys(item).reduce((acc,field) => {
 
     if (linked[field]) {
       Object.assign(acc, linked[field].reduce((acc,f,i) => {
         if (schema[f].save || schema[f].hidden || omit.includes(f)) return acc
         return Object.assign(acc,{
-          [schema[f].alias || f]: validateType(schema[f],f,item[field].split('#')[i])
+          [schema[f].alias || f]: validateType(schema[f],f,item[field].split(delimiter)[i])
         })
       },{}))
     }
@@ -571,7 +575,7 @@ const formatItem = (schema,linked,item,omit) => {
 }
 
 
-const normalizeData = (schema,linked,data,filter=false) => {
+const normalizeData = (schema,linked,data,delimiter,filter=false) => {
   let _data = Object.keys(data).reduce((acc,field) => {
     return Object.assign(acc,
       schema[field] ? { [schema[field].mapped || field] : data[field] }
@@ -592,7 +596,7 @@ const normalizeData = (schema,linked,data,filter=false) => {
     //   error(`${linked[field].join(', ')} are all required for composite key`)
     // } else
     if (values.length === linked[field].length) {
-      return Object.assign(acc, { [field]: values.join('#') })
+      return Object.assign(acc, { [field]: values.join(delimiter) })
     } else {
       return acc
     }
diff --git a/package.json b/package.json
index e6f80471..5595f130 100644
--- a/package.json
+++ b/package.json
@@ -40,4 +40,4 @@
     "eslint": "^6.7.1",
     "jest": "^24.9.0"
   }
-}
+}
\ No newline at end of file


Test patch:
diff --git a/__tests__/model-creation.unit.test.js b/__tests__/model-creation.unit.test.js
index 020362b0..0afc4101 100644
--- a/__tests__/model-creation.unit.test.js
+++ b/__tests__/model-creation.unit.test.js
@@ -28,6 +28,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true },
         __model: { type: 'string', default: 'Default', coerce: true, hidden: true }
@@ -54,6 +55,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true }
       },
@@ -81,6 +83,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true },
         __model: { type: 'string', default: 'Default', hidden: true, coerce: true },
@@ -115,6 +118,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true },
         my_model_field: { type: 'string', default: 'Default', hidden: true, coerce: true }
@@ -147,6 +151,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true },
         __model: { type: 'string', default: 'Default', hidden: true, coerce: true },
@@ -182,6 +187,7 @@ describe('model creation', ()=> {
       table: 'test-table',
       partitionKey: 'pk',
       sortKey: null,
+      delimiter: '#',
       schema: {
         pk: { type: 'string', coerce: true },
         __model: { type: 'string', default: 'Default', coerce: true, hidden: true },
@@ -259,6 +265,18 @@ describe('model creation', ()=> {
     expect(result).toThrow(`'sortKey' must be string value`)
   })
 
+  it('fails when creating a model with a non-string delimiter', () => {
+    let result = () => new Model('TestModel',{
+      table: 'test-table',
+      partitionKey: 'pk',
+      delimiter: 123,
+      schema: {
+        pk: 'string'
+      }
+    })
+    expect(result).toThrow(`'delimiter' must be string value`)
+  })
+
   it('fails when creating a model without a table', () => {
     let result = () => new Model('TestModel',{
       partitionKey: 'pk',
diff --git a/__tests__/models/simple-model.js b/__tests__/models/simple-model.js
index c1d74ef4..fecf2395 100644
--- a/__tests__/models/simple-model.js
+++ b/__tests__/models/simple-model.js
@@ -21,5 +21,8 @@ module.exports = {
     test_composite: ['sk',0, { save: true }],
     test_composite2: ['sk',1],
     test_undefined: { default: () => undefined }
-  }
+  },
+  
+  // Define delimiter
+  delimiter: '|'
 }
diff --git a/__tests__/parse.unit.test.js b/__tests__/parse.unit.test.js
index 57b8f3cf..96502c3e 100644
--- a/__tests__/parse.unit.test.js
+++ b/__tests__/parse.unit.test.js
@@ -71,7 +71,7 @@ describe('parse',()=>{
   })
 
   it('parses composite field', ()=>{
-    let item = SimpleModel.parse({ pk: 'test@test.com', sk: 'active#email', test_composite: 'test' })
+    let item = SimpleModel.parse({ pk: 'test@test.com', sk: 'active|email', test_composite: 'test' })
     expect(item).toEqual({
       pk: 'test@test.com',
       test_composite: 'test',
diff --git a/__tests__/put.unit.test.js b/__tests__/put.unit.test.js
index fd3cfb65..2951ebb5 100644
--- a/__tests__/put.unit.test.js
+++ b/__tests__/put.unit.test.js
@@ -84,7 +84,7 @@ describe('put',()=>{
       test_composite2: 'test2'
     })
     expect(Item.pk).toBe('test-pk')
-    expect(Item.sk).toBe('test#test2')
+    expect(Item.sk).toBe('test|test2')
     expect(Item.test_composite).toBe('test')
     expect(Item).not.toHaveProperty('test_composite2')
   })