UNPKG

@cyclic.sh/dynamodb

Version:

SDK for interacting with Cyclic.sh (https://cyclic.sh) app AWS DynamoDB databases

440 lines (378 loc) 12.9 kB
const {docClient} = require('./ddb_client') const { UpdateCommand, QueryCommand, DeleteCommand } = require("@aws-sdk/lib-dynamodb") const {ValidationError, RetryableError, validate_strings} = require('./cy_db_utils') let make_sub_expr = function(item, expr_type, expr_prefix=''){ let expression = [] let attr_names = {} let attr_vals = {} Object.keys(item).forEach((k,i)=>{ if(k=='pk' || k == 'sk' || item[k] === undefined){return true} let v = item[k] attr_names[`#k${expr_prefix}${i}`] = k attr_vals[`:v${expr_prefix}${i}`] = v expression.push(`#k${expr_prefix}${i} = :v${expr_prefix}${i}`) }) expression = `${expr_type} ${expression.join(', ')}` return { attr_names, attr_vals, expression } } let upsert = async function(item,opts){ let ops = [] let {attr_names, attr_vals, expression} = make_sub_expr(item,'set','s') let d = new Date().toISOString() attr_names['#kc'] = 'created' attr_names['#ku'] = 'updated' attr_vals[':kc'] = d attr_vals[':ku'] = d expression = `${expression}, #ku = :ku, #kc = if_not_exists(#kc,:kc)` if(opts['$unset']){ for (let k of Object.keys(opts.$unset)){ if(Object.keys(item).includes(k)){ throw `${k}: property can not appear in both set and $unset` } } d_expression = [] opts['$unset'].forEach((k,i)=>{ attr_names[`#dk${i}`] = k d_expression.push(`#dk${i}`) if(!item.sk.startsWith('fragment#index') && !item.sk.startsWith('index#')){ ops.push( docClient.send(new DeleteCommand({ TableName : process.env.CYCLIC_DB, Key: { pk: item.pk, sk: item.sk.startsWith('fragment') ? `fragment#index#${k}` : `index#${k}` } })) ) } }) expression = `${expression} remove ${d_expression.join(', ')}` } var record = { TableName : process.env.CYCLIC_DB, Key:{ pk: item.pk, sk: item.sk || item.pk }, UpdateExpression: expression, ExpressionAttributeNames: attr_names, ExpressionAttributeValues: attr_vals, ReturnConsumedCapacity:"TOTAL", ReturnValues:"ALL_OLD", } // console.log(record) if(opts.condition){ record.Expected = opts.condition } ops.push( docClient.send(new UpdateCommand(record)) ) try{ let res = await Promise.all(ops) return res }catch(e){ if(e.code == 'ConditionalCheckFailedException'){ throw new RetryableError(`${item.pk} ${e.code}`) } throw e } } const list_sks = async function(pk,sk_prefix = null){ let params = { TableName: process.env.CYCLIC_DB, ProjectionExpression:'pk,sk', KeyConditionExpression: 'pk = :pk', ExpressionAttributeValues:{ ':pk':pk, } }; if(sk_prefix){ params.KeyConditionExpression = `${params.KeyConditionExpression} and begins_with(sk,:sk)`, params.ExpressionAttributeValues[':sk'] = sk_prefix } let res = await docClient.send(new QueryCommand(params)) return res.Items.map(d=>{ return d.sk }) } const exclude_cy_keys = function(o){ delete o.pk delete o.sk delete o.keys_gsi delete o.keys_gsi_sk delete o.gsi_s delete o.gsi_s_sk delete o.gsi_1 delete o.gsi_2 delete o.gsi_s2 delete o.gsi_prj delete o.cy_meta return o } class CyclicItem{ constructor(collection,key, props={},opts={}){ validate_strings(collection, "Collection Name") validate_strings(key, "Item Key") this.collection = collection this.key = key this.props = exclude_cy_keys(props) if(opts.$index){ this.$index = opts.$index } } static from_dynamo(d){ if(d.sk.startsWith('fragment')){ return CyclicItemFragment.from_dynamo(d) } let [collection,key] = d.pk.split('#') let props = {...d} let opts = {} if(d.keys_gsi_sk){ props.updated = d.keys_gsi_sk } if(d.cy_meta && d.cy_meta.$i){ opts.$index = d.cy_meta.$i } return new CyclicItem(collection,key,props,opts) } async indexes(){ let indexes = await list_sks(`${this.collection}#${this.key}`, `index#`) return indexes.map(d=>{ return d.split('#').slice(-1)[0] }) } async fragments(){ let frags = await list_sks(`${this.collection}#${this.key}`, `fragment#`) return frags.map(d=>{ return d.split('#')[1] }).filter(d=>{return d !='index'}) } async delete(props={},opts={}){ let ops = [] let sks = await list_sks(`${this.collection}#${this.key}`) sks.forEach(sk=>{ ops.push( docClient.send(new DeleteCommand({ TableName : process.env.CYCLIC_DB, Key: { pk: `${this.collection}#${this.key}`, sk: sk } })) ) }) let res = await Promise.all(ops) return true } async get(){ let pk = `${this.collection}#${this.key}` let sk = `${this.collection}#${this.key}` let params = { TableName: process.env.CYCLIC_DB, KeyConditionExpression: 'pk = :pk and sk = :sk', ExpressionAttributeValues:{ ':pk':pk, ':sk':sk } }; let res = await docClient.send(new QueryCommand(params)) if(!res.Items.length){ return null } return CyclicItem.from_dynamo(res.Items[0]) // this.props = exclude_cy_keys(res.Items[0]) // return this } async set(props, opts={}){ this.props = {...this.props, ...props} let r = { pk: `${this.collection}#${this.key}`, sk: `${this.collection}#${this.key}`, keys_gsi: this.collection, keys_gsi_sk: new Date().toISOString(), cy_meta:{ c: this.collection, rt: 'item', }, ...props } let index_records = [] if(opts.$index){ this.$index = opts.$index r.cy_meta.$i = opts.$index opts.$index.forEach(idx=>{ let prop_keys = Object.keys(props) if(!prop_keys.includes(idx)){ throw new ValidationError(`index property "${idx}" does not exist in object properties ["${prop_keys.join('", "')}"]`) } }) index_records = opts.$index.map(idx=>{ let index = { name: idx, } let index_item = { pk: `${this.collection}#${this.key}`, sk: `index#${index.name}`, gsi_s: `${index.name}#${this.props[index.name]}`, gsi_s_sk: `${this.collection}#${this.key}`, cy_meta:{ c: this.collection, rt: 'item_index', $i: this.$index }, ...props } return upsert(index_item,opts) }) } let res = await Promise.all([ upsert(r, opts), ...index_records ]) return this } fragment(type, name = '', props = {}){ return new CyclicItemFragment(type, name, props, this) } } class CyclicItemFragment{ constructor(type, key, props ,parent, opts={}){ validate_strings(type, "Fragment Type") validate_strings(key, "Fragment Key") this.type = type this.key = key this.parent = parent this.props = exclude_cy_keys(props) if(opts.$index){ this.$index = opts.$index } } static from_dynamo(d){ // console.log(d) let [parent_collection,parent_key] = d.pk.split('#') let [type,key,index_name] = d.sk.replace('fragment#','').replace('index#','').split('#') let props = {...d} let opts = {} if(d.keys_gsi_sk){ props.updated = d.keys_gsi_sk } if(d.cy_meta && d.cy_meta.$i){ opts.$index = d.cy_meta.$i } let parent = new CyclicItem(parent_collection,parent_key) return new CyclicItemFragment(type,key,props,parent, opts) } async indexes(){ let indexes = await list_sks(`${this.parent.collection}#${this.parent.key}`, `fragment#index#`) return indexes.map(d=>{ return d.split('#').slice(-1)[0] }) } async delete(props={},opts={}){ let indexes = await this.indexes() let ops = [] ops.push(docClient.send(new DeleteCommand({ TableName : process.env.CYCLIC_DB, Key: { pk: `${this.parent.collection}#${this.parent.key}`, sk: `fragment#${this.type}#${this.key}` } }))) indexes.forEach(idx=>{ ops.push(docClient.send(new DeleteCommand({ TableName : process.env.CYCLIC_DB, Key: { pk: `${this.parent.collection}#${this.parent.key}`, sk: `fragment#index#${this.type}#${this.key}#${idx}`, } }))) }) await Promise.all(ops) return true } async set(props, opts={}){ this.props = {...this.props, ...props} let r = { pk: `${this.parent.collection}#${this.parent.key}`, sk: `fragment#${this.type}#${this.key}`, cy_meta:{ c: this.parent.collection, rt: 'fragment', }, ...props } let index_records = [] if(opts.$index){ this.$index = opts.$index r.cy_meta.$i = opts.$index index_records = opts.$index.map(idx=>{ let index = { name: idx, } let index_item = { pk: `${this.parent.collection}#${this.parent.key}`, sk: `fragment#index#${this.type}#${this.key}#${index.name}`, gsi_s: `${index.name}#${this.props[index.name]}`, gsi_s_sk: `${this.parent.collection}#${this.parent.key}`, cy_meta:{ c: this.parent.collection, rt: 'fragment_index', $i: this.$index }, ...props } return upsert(index_item,opts) }) } let res = await Promise.all([ upsert(r, opts), ...index_records ]) return this } async get(){ let pk = `${this.parent.collection}#${this.parent.key}` let sk = `fragment#${this.type}#${this.key}` let params = { TableName: process.env.CYCLIC_DB, KeyConditionExpression: 'pk = :pk and sk = :sk', ExpressionAttributeValues:{ ':pk':pk, ':sk':sk } }; let res = await docClient.send(new QueryCommand(params)) let results = res.Items.map(r=>{ return CyclicItemFragment.from_dynamo(r) }) if(this.key && this.key.length){ if(results.length){ return results[0] }else{ return null } } return results } async list(){ let pk = `${this.parent.collection}#${this.parent.key}` let sk = `fragment#${this.type}#` let params = { TableName: process.env.CYCLIC_DB, KeyConditionExpression: 'pk = :pk and begins_with(sk,:sk)', ExpressionAttributeValues:{ ':pk':pk, ':sk':sk } }; let res = await docClient.send(new QueryCommand(params)) return res.Items.map(r=>{ return CyclicItemFragment.from_dynamo(r) }) } } module.exports = CyclicItem