Skip to content

Commit 1ae8d05

Browse files
committed
break up components (initial)
1 parent ed2b1a1 commit 1ae8d05

File tree

13 files changed

+741
-611
lines changed

13 files changed

+741
-611
lines changed

classes/Model.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
'use strict'
2+
3+
/**
4+
* DynamoDB Toolbox: A simple set of tools for working with Amazon DynamoDB
5+
* @author Jeremy Daly <jeremy@jeremydaly.com>
6+
* @license MIT
7+
*/
8+
9+
const parseModel = require('../lib/parseModel')
10+
const normalizeData = require('../lib/normalizeData')
11+
const formatItem = require('../lib/formatItem')
12+
const validateType = require('../lib/validateType')
13+
const getKey = require('../lib/getKey')
14+
const { hasValue, error } = require('../lib/utils')
15+
16+
17+
class Model {
18+
19+
constructor(name,model) {
20+
// TODO: add better validation
21+
if (typeof model !== 'object' || Array.isArray(model))
22+
error('Please provide a valid model definition')
23+
this.Model = parseModel(name,model)
24+
}
25+
26+
// returns the model object
27+
model() {
28+
return this.Model
29+
}
30+
31+
field(field) {
32+
// console.log(this.model.schema);
33+
return this.Model.schema[field] && this.Model.schema[field].mapped ? this.Model.schema[field].mapped
34+
: this.Model.schema[field] ? field
35+
: error(`'${field}' does not exist or is an invalid alias`)
36+
}
37+
38+
partitionKey() { return this.Model.partitionKey }
39+
sortKey() { return this.Model.sortKey }
40+
41+
parse(input,omit=[]) {
42+
// Load the schema
43+
let { schema, linked } = this.Model
44+
45+
// Assume standard response from DynamoDB
46+
let data = input.Item || input.Items || input
47+
48+
if (Array.isArray(data)) {
49+
return data.map(item => formatItem(schema,linked,item,omit))
50+
} else {
51+
return formatItem(schema,linked,data,omit)
52+
}
53+
}
54+
55+
get(item={},params={}) {
56+
// Extract schema and merge defaults
57+
let { schema, defaults, linked, partitionKey, sortKey, table } = this.Model
58+
let data = normalizeData(schema,linked,Object.assign({},defaults,item),true)
59+
60+
return Object.assign(
61+
{
62+
TableName: table,
63+
Key: getKey(data,schema,partitionKey,sortKey)
64+
},
65+
typeof params === 'object' ? params : {}
66+
)
67+
}
68+
69+
delete(item={},params={}) {
70+
return this.get(item,params)
71+
}
72+
73+
// Generate update expression
74+
update(item={},
75+
{
76+
SET=[],
77+
REMOVE=[],
78+
ADD=[],
79+
DELETE=[],
80+
ExpressionAttributeNames={},
81+
ExpressionAttributeValues={},
82+
...params
83+
} = {}) {
84+
85+
// Validate operation types
86+
if (!Array.isArray(SET)) error('SET must be an array')
87+
if (!Array.isArray(REMOVE)) error('REMOVE must be an array')
88+
if (!Array.isArray(ADD)) error('ADD must be an array')
89+
if (!Array.isArray(DELETE)) error('DELETE must be an array')
90+
91+
// Validate attribute names and values
92+
if (typeof ExpressionAttributeNames !== 'object'
93+
|| Array.isArray(ExpressionAttributeNames))
94+
error('ExpressionAttributeNames must be an object')
95+
if (typeof ExpressionAttributeValues !== 'object'
96+
|| Array.isArray(ExpressionAttributeValues))
97+
error('ExpressionAttributeValues must be an object')
98+
// if (ConditionExpression && typeof ConditionExpression !== 'string')
99+
// error(`ConditionExpression must be a string`)
100+
101+
// Extract schema and defaults
102+
let { schema, defaults, required, linked, partitionKey, sortKey, table } = this.Model
103+
104+
// Merge defaults
105+
let data = normalizeData(schema,linked,Object.assign({},defaults,item))
106+
107+
// Check for required fields
108+
Object.keys(required).forEach(field =>
109+
required[field] && !data[field] && error(`'${field}' is a required field`)
110+
) // end required field check
111+
112+
// Check for partition and sort keys
113+
let Key = getKey(data,schema,partitionKey,sortKey)
114+
115+
// Init names and values
116+
let names = {}
117+
let values = {}
118+
119+
// Loop through valid fields and add appropriate action
120+
Object.keys(data).forEach(function(field) {
121+
let mapping = schema[field]
122+
123+
// Remove null or empty fields
124+
if ((data[field] === null || String(data[field]).trim() === '') && (!mapping.link || mapping.save)) {
125+
REMOVE.push(`#${field}`)
126+
names[`#${field}`] = field
127+
} else if (
128+
field !== partitionKey
129+
&& field !== sortKey
130+
&& (mapping.save === undefined || mapping.save === true)
131+
&& (!mapping.link || (mapping.link && mapping.save === true))
132+
) {
133+
// If a number or a set and adding
134+
if (['number','set'].includes(mapping.type) && data[field].$add) {
135+
ADD.push(`#${field} :${field}`)
136+
values[`:${field}`] = validateType(mapping,field,data[field].$add,data)
137+
// Add field to names
138+
names[`#${field}`] = field
139+
// if a set and deleting items
140+
} else if (mapping.type === 'set' && data[field].$delete) {
141+
DELETE.push(`#${field} :${field}`)
142+
values[`:${field}`] = validateType(mapping,field,data[field].$delete,data)
143+
// Add field to names
144+
names[`#${field}`] = field
145+
// if a list and removing items by index
146+
} else if (mapping.type === 'list' && Array.isArray(data[field].$remove)) {
147+
data[field].$remove.forEach(i => {
148+
if (typeof i !== 'number') error(`Remove array for '${field}' must only contain numeric indexes`)
149+
REMOVE.push(`#${field}[${i}]`)
150+
})
151+
// Add field to names
152+
names[`#${field}`] = field
153+
// if list and appending or prepending
154+
} else if (mapping.type === 'list' && (data[field].$append || data[field].$prepend)) {
155+
if (data[field].$append) {
156+
SET.push(`#${field} = list_append(#${field},:${field})`)
157+
values[`:${field}`] = validateType(mapping,field,data[field].$append,data)
158+
} else {
159+
SET.push(`#${field} = list_append(:${field},#${field})`)
160+
values[`:${field}`] = validateType(mapping,field,data[field].$prepend,data)
161+
}
162+
// Add field to names
163+
names[`#${field}`] = field
164+
// if a list and updating by index
165+
} else if (mapping.type === 'list' && !Array.isArray(data[field]) && typeof data[field] === 'object') {
166+
Object.keys(data[field]).forEach(i => {
167+
if (String(parseInt(i)) !== i) error(`Properties must be numeric to update specific list items in '${field}'`)
168+
SET.push(`#${field}[${i}] = :${field}_${i}`)
169+
values[`:${field}_${i}`] = data[field][i]
170+
})
171+
// Add field to names
172+
names[`#${field}`] = field
173+
// if a map and updating by nested attribute/index
174+
} else if (mapping.type === 'map' && data[field].$set) {
175+
Object.keys(data[field].$set).forEach(f => {
176+
177+
// TODO: handle null values to remove
178+
179+
let props = f.split('.')
180+
let acc = [`#${field}`]
181+
props.forEach((prop,i) => {
182+
let id = `${field}_${props.slice(0,i+1).join('_')}`
183+
// Add names and values
184+
names[`#${id.replace(/\[(\d+)\]/,'')}`] = prop.replace(/\[(\d+)\]/,'')
185+
// if the final prop, add the SET and values
186+
if (i === props.length-1) {
187+
let input = data[field].$set[f]
188+
let path = `${acc.join('.')}.#${id}`
189+
let value = `${id.replace(/\[(\d+)\]/,'_$1')}`
190+
191+
if (input.$add) {
192+
ADD.push(`${path} :${value}`)
193+
values[`:${value}`] = input.$add
194+
} else if (input.$append) {
195+
SET.push(`${path} = list_append(${path},:${value})`)
196+
values[`:${value}`] = input.$append
197+
} else if (input.$prepend) {
198+
SET.push(`${path} = list_append(:${value},${path})`)
199+
values[`:${value}`] = input.$prepend
200+
} else if (input.$remove) {
201+
// console.log('REMOVE:',input.$remove);
202+
input.$remove.forEach(i => {
203+
if (typeof i !== 'number') error(`Remove array for '${field}' must only contain numeric indexes`)
204+
REMOVE.push(`${path}[${i}]`)
205+
})
206+
} else {
207+
SET.push(`${path} = :${value}`)
208+
values[`:${value}`] = input
209+
}
210+
211+
212+
if (input.$set) {
213+
Object.keys(input.$set).forEach(i => {
214+
if (String(parseInt(i)) !== i) error(`Properties must be numeric to update specific list items in '${field}'`)
215+
SET.push(`${path}[${i}] = :${value}_${i}`)
216+
values[`:${value}_${i}`] = input.$set[i]
217+
})
218+
}
219+
220+
221+
} else {
222+
acc.push(`#${id.replace(/\[(\d+)\]/,'')}`)
223+
}
224+
})
225+
})
226+
// Add field to names
227+
names[`#${field}`] = field
228+
// else add to SET
229+
} else {
230+
231+
let value = validateType(mapping,field,data[field],data)
232+
233+
// It's possible that defaults can purposely return undefined values
234+
if (hasValue(value)) {
235+
// Push the update to SET
236+
SET.push(mapping.default && !item[field] && !mapping.onUpdate ?
237+
`#${field} = if_not_exists(#${field},:${field})`
238+
: `#${field} = :${field}`)
239+
// Add names and values
240+
names[`#${field}`] = field
241+
values[`:${field}`] = value
242+
}
243+
}
244+
245+
} // end if null
246+
})
247+
248+
// Create the update expression
249+
let expression = (
250+
(SET.length > 0 ? 'SET ' + SET.join(', ') : '')
251+
+ (REMOVE.length > 0 ? ' REMOVE ' + REMOVE.join(', ') : '')
252+
+ (ADD.length > 0 ? ' ADD ' + ADD.join(', ') : '')
253+
+ (DELETE.length > 0 ? ' DELETE ' + DELETE.join(', ') : '')
254+
).trim()
255+
256+
257+
let attr_values = Object.assign(values,ExpressionAttributeValues)
258+
259+
// Return the parameters
260+
return Object.assign(
261+
{
262+
TableName: table,
263+
Key,
264+
UpdateExpression: expression,
265+
ExpressionAttributeNames: Object.assign(names,ExpressionAttributeNames)
266+
},
267+
typeof params === 'object' ? params : {},
268+
Object.keys(attr_values).length > 0 ? { ExpressionAttributeValues: attr_values } : {}
269+
) // end assign
270+
271+
} // end update
272+
273+
put(item={},params={}) {
274+
// Extract schema and defaults
275+
let { schema, defaults, required, linked, partitionKey, sortKey, table } = this.Model
276+
277+
// Merge defaults
278+
let data = normalizeData(schema,linked,Object.assign({},defaults,item))
279+
280+
// Check for required fields
281+
Object.keys(required).forEach(field =>
282+
required[field] !== undefined && !data[field] && error(`'${field}' is a required field`)
283+
) // end required field check
284+
285+
// Checks for partition and sort keys
286+
getKey(data,schema,partitionKey,sortKey)
287+
288+
// Loop through valid fields and add appropriate action
289+
return Object.assign(
290+
{
291+
TableName: table,
292+
Item: Object.keys(data).reduce((acc,field) => {
293+
let mapping = schema[field]
294+
let value = validateType(mapping,field,data[field],data)
295+
return hasValue(value)
296+
&& (mapping.save === undefined || mapping.save === true)
297+
&& (!mapping.link || (mapping.link && mapping.save === true))
298+
? Object.assign(acc, {
299+
[field]: value
300+
}) : acc
301+
},{})
302+
},
303+
typeof params === 'object' ? params : {}
304+
)
305+
}
306+
307+
} // end Model
308+
309+
module.exports = Model

classes/Table.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict'
2+
3+
/**
4+
* DynamoDB Toolbox: A simple set of tools for working with Amazon DynamoDB
5+
* @author Jeremy Daly <jeremy@jeremydaly.com>
6+
* @license MIT
7+
*/
8+
9+
class Table {
10+
constructor() {}
11+
}

classes/Transaction.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict'
2+
3+
/**
4+
* DynamoDB Toolbox: A simple set of tools for working with Amazon DynamoDB
5+
* @author Jeremy Daly <jeremy@jeremydaly.com>
6+
* @license MIT
7+
*/
8+
9+
class Transaction {
10+
constructor() {}
11+
}

0 commit comments

Comments
 (0)