From f6fc89828f3848c0a360880f1865e6ed673e87a7 Mon Sep 17 00:00:00 2001 From: "Tyler W. Walch" Date: Mon, 6 Jul 2020 21:22:31 -0400 Subject: [PATCH] Feature/patchandcreate (#16) * adding patch and create methods. both automatically add ConditionExpressions to either make sure the record exists before updating (patch) or make sure the record doesnt already exist (create) * adding tests, not full working right now * query had a defect preventing queries from being ran on tables without an sk. yikes. * bumping version --- package.json | 2 +- src/clauses.js | 68 +++++++-- src/entity.js | 266 +++++++++++++++++++++-------------- src/filters.js | 10 +- src/types.js | 2 + test/connected.crud.spec.js | 66 +++++++++ test/offline.entity.spec.js | 33 +++++ test/offline.service.spec.js | 65 +++++++++ 8 files changed, 393 insertions(+), 119 deletions(-) diff --git a/package.json b/package.json index 90e56826..05b0a1c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electrodb", - "version": "0.9.7", + "version": "0.9.8", "description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb", "main": "index.js", "scripts": { diff --git a/src/clauses.js b/src/clauses.js index d29fd552..4caf6b60 100644 --- a/src/clauses.js +++ b/src/clauses.js @@ -8,12 +8,13 @@ let clauses = { // // todo: look for article/list of all dynamodb query limitations // // return state; // }, - children: ["get", "delete", "update", "query", "put", "scan", "collection"], + children: ["get", "delete", "update", "query", "put", "scan", "collection", "create", "patch"], }, collection: { name: "collection", - action(entity, state, collection /* istanbul ignore next */ = "", facets /* istanbul ignore next */ = {}) { + /* istanbul ignore next */ + action(entity, state, collection = "", facets /* istanbul ignore next */ = {}) { state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); entity._expectFacets(facets, Object.keys(facets), `"query" facets`); state.query.collection = collection; @@ -33,6 +34,7 @@ let clauses = { }, get: { name: "get", + /* istanbul ignore next */ action(entity, state, facets = {}) { state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); state.query.method = MethodTypes.get; @@ -53,6 +55,7 @@ let clauses = { }, delete: { name: "delete", + /* istanbul ignore next */ action(entity, state, facets = {}) { state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); state.query.method = MethodTypes.delete; @@ -73,7 +76,8 @@ let clauses = { }, put: { name: "put", - action(entity, state, payload = {}) { + /* istanbul ignore next */ + action(entity, state, payload) { let record = entity.model.schema.checkCreate({ ...payload }); state.query.keys.pk = entity._expectFacets(record, state.query.facets.pk); state.query.method = MethodTypes.put; @@ -94,9 +98,51 @@ let clauses = { }, children: ["params", "go"], }, + create: { + name: "create", + action(entity, state, payload) { + let record = entity.model.schema.checkCreate({ ...payload }); + state.query.keys.pk = entity._expectFacets(record, state.query.facets.pk); + state.query.method = MethodTypes.put; + state.query.type = QueryTypes.eq; + if (state.hasSortKey) { + let queryFacets = entity._buildQueryFacets( + record, + state.query.facets.sk, + ); + state.query.keys.sk.push({ + type: state.query.type, + facets: queryFacets, + }); + } + state.query.put.data = Object.assign({}, record); + return state; + }, + children: ["params", "go"], + }, + patch: { + name: "patch", + action(entity, state, facets) { + state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); + state.query.method = MethodTypes.update; + state.query.type = QueryTypes.eq; + if (state.hasSortKey) { + let queryFacets = entity._buildQueryFacets( + facets, + state.query.facets.sk, + ); + state.query.keys.sk.push({ + type: state.query.type, + facets: queryFacets, + }); + } + return state; + }, + children: ["set"], + }, update: { name: "update", - action(entity, state, facets = {}) { + action(entity, state, facets) { state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); state.query.method = MethodTypes.update; state.query.type = QueryTypes.eq; @@ -129,16 +175,18 @@ let clauses = { }, query: { name: "query", - action(entity, state, facets = {}, options = {}) { + action(entity, state, facets, options = {}) { state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk); entity._expectFacets(facets, Object.keys(facets), `"query" facets`); state.query.method = MethodTypes.query; state.query.type = QueryTypes.begins; - let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk); - state.query.keys.sk.push({ - type: state.query.type, - facets: queryFacets, - }); + if (state.query.facets.sk) { + let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk); + state.query.keys.sk.push({ + type: state.query.type, + facets: queryFacets, + }); + } return state; }, children: ["between", "gte", "gt", "lte", "lt", "params", "go", "page"], diff --git a/src/entity.js b/src/entity.js index c50d0500..b764c148 100644 --- a/src/entity.js +++ b/src/entity.js @@ -8,7 +8,7 @@ const { clauses } = require("./clauses"); const utilities = { structureFacets: function ( structure, - { index, type, name } = {}, + { index, type, name }, i, attributes, indexSlot, @@ -24,23 +24,11 @@ const utilities = { structure.byFacet[name][i].push(facet); structure.bySlot[i] = structure.bySlot[i] || []; structure.bySlot[i][indexSlot] = facet; - }, - safeParse(str = "") { - try { - if (typeof str === "string") { - - } - } catch(err) { - - } - }, - safeStringify() { - } }; class Entity { - constructor(model = {}, config = {}) { + constructor(model, config = {}) { this._validateModel(model); this.client = config.client; this.model = this._parseModel(model); @@ -48,43 +36,66 @@ class Entity { this.model.schema.attributes, FilterTypes, ); - this.query = {}; - - let clausesWithFilters = this._filterBuilder.injectFilterClauses( + this._clausesWithFilters = this._filterBuilder.injectFilterClauses( clauses, this.model.filters, ); - // this.find = (facets = {}) => { - // let index = this._findBestIndexKeyMatch(facets); - // return this._makeChain(index, clausesWithFilters, clauses.index).query( - // facets, - // ); - // }; - this.scan = this._makeChain("", clausesWithFilters, clauses.index).scan(); + this.scan = this._makeChain("", this._clausesWithFilters, clauses.index).scan(); + this.query = {}; for (let accessPattern in this.model.indexes) { let index = this.model.indexes[accessPattern].index; this.query[accessPattern] = (...values) => { - return this._makeChain(index, clausesWithFilters, clauses.index).query( + return this._makeChain(index, this._clausesWithFilters, clauses.index).query( ...values, ); }; } } + find(facets = {}) { + let match = this._findBestIndexKeyMatch(facets); + if (match.shouldScan) { + return this._makeChain("", this._clausesWithFilters, clauses.index).scan().filter(attr => { + let eqFilters = []; + for (let facet of Object.keys(facets)) { + if (attr[facet]) { + eqFilters.push(attr[facet].eq(facets[facet])); + } + } + return eqFilters.join(" AND"); + }) + } else { + return this._makeChain(match.index, this._clausesWithFilters, clauses.index).query( + facets, + ).filter(attr => { + let eqFilters = []; + for (let facet of Object.keys(facets)) { + if (attr[facet]) { + eqFilters.push(attr[facet].eq(facets[facet])); + } + } + return eqFilters.join(" AND"); + }); + } + } + collection(collection = "", clauses = {}, facets = {}) { let index = this.model.translations.collections.fromCollectionToIndex[ collection ]; + if (index === undefined) { + throw new Error(`Invalid collection: ${collection}`); + } return this._makeChain(index, clauses, clauses.index).collection( collection, facets, ); } - _validateModel(model = {}) { + _validateModel(model) { return validations.model(model); } - + get(facets = {}) { let index = ""; return this._makeChain(index, clauses, clauses.index).get(facets); @@ -100,12 +111,32 @@ class Entity { return this._makeChain(index, clauses, clauses.index).put(attributes); } + create(attributes = {}) { + let index = ""; + let options = { + params: { + ConditionExpression: this._makeCreateConditions(index) + } + } + return this._makeChain(index, clauses, clauses.index, options).create(attributes); + } + update(facets = {}) { let index = ""; return this._makeChain(index, clauses, clauses.index).update(facets); } - _chain(state = {}, clauses, clause = {}) { + patch(facets = {}) { + let index = ""; + let options = { + params: { + ConditionExpression: this._makePatchConditions(index) + } + } + return this._makeChain(index, clauses, clauses.index, options).patch(facets); + } + + _chain(state, clauses, clause) { let current = {}; for (let child of clause.children) { current[child] = (...args) => { @@ -121,7 +152,7 @@ class Entity { } return current; } - + /* istanbul ignore next */ _makeChain(index = "", clauses, rootClause, options = {}) { let facets = this.model.facets.byIndex[index]; let state = { @@ -141,8 +172,8 @@ class Entity { sk: [], }, filter: {}, + options, }, - collectionOnly: !!options.collectionOnly, hasSortKey: this.model.lookup.indexHasSortKeys[index], }; return this._chain(state, clauses, rootClause); @@ -212,24 +243,38 @@ class Entity { } } - _applyParameterOptions(params = {}, options = {}) { + _applyParameterOptions(params, ...options) { let config = { - includeKeys: options.includeKeys, - originalErr: options.originalErr, - raw: options.raw, - params: options.params || {}, - page: options.page, - pager: !!options.pager + includeKeys: false, + originalErr: false, + raw: false, + params: {}, + page: {}, + pager: false }; + + config = options.reduce((config, option) => { + if (option.includeKeys) config.includeKeys = true; + if (option.originalErr) config.originalErr = true; + if (option.raw) config.raw = true; + if (option.pager) config.pager = true; + config.page = Object.assign({}, config.page, option.page); + config.params = Object.assign({}, config.params, option.params); + return config; + }, config); + let parameters = Object.assign({}, params); + for (let customParameter of Object.keys(config.params)) { if (config.params[customParameter] !== undefined) { parameters[customParameter] = config.params[customParameter]; } } + if (Object.keys(config.page || {}).length) { parameters.ExclusiveStartKey = config.page; } + return parameters; } @@ -247,7 +292,7 @@ class Entity { let stackTrace = new Error(); try { let response = await this.client[method](parameters).promise(); - if (method === "put") { + if (method === "put" || method === "create") { // a VERY hacky way to deal with PUTs return this.formatResponse(parameters, config); } else { @@ -263,18 +308,43 @@ class Entity { } } - _params({ keys = {}, method = "", put = {}, update = {}, filter = {} } = {}, options = {}) { + _makeCreateConditions(index) { + let filter = [`attribute_not_exists(pk)`] + if (this.model.lookup.indexHasSortKeys[index]) { + filter.push(`attribute_not_exists(sk)`); + } + return filter.join(" AND "); + } + + _makePatchConditions(index) { + let filter = [`attribute_exists(pk)`] + if (this.model.lookup.indexHasSortKeys[index]) { + filter.push(`attribute_exists(sk)`); + } + return filter.join(" AND "); + } + /* istanbul ignore next */ + _params({ keys = {}, method = "", put = {}, update = {}, filter = {}, options = {} }, config = {}) { let conlidatedQueryFacets = this._consolidateQueryFacets(keys.sk); - let params = {} + let params = {}; switch (method) { case MethodTypes.get: case MethodTypes.delete: params = this._makeSimpleIndexParams(keys.pk, ...conlidatedQueryFacets); break; case MethodTypes.put: + case MethodTypes.create: params = this._makePutParams(put, keys.pk, ...keys.sk); break; case MethodTypes.update: + case MethodTypes.patch: + params = this._makeUpdateParams( + update, + keys.pk, + ...conlidatedQueryFacets, + ); + break; + case MethodTypes.patch: params = this._makeUpdateParams( update, keys.pk, @@ -284,10 +354,11 @@ class Entity { case MethodTypes.scan: params = this._makeScanParam(filter); break; + /* istanbul ignore next */ default: throw new Error(`Invalid method: ${method}`); } - return this._applyParameterOptions(params, options); + return this._applyParameterOptions(params, options, config); } _makeParameterKey(index, pk, sk) { @@ -302,10 +373,9 @@ class Entity { } return key; } - + + /* istanbul ignore next */ _makeScanParam(filter = {}) { - // let _makeKey - // let { pk, sk } = this._makeIndexKeys(); let indexBase = ""; let hasSortKey = this.model.lookup.indexHasSortKeys[indexBase]; let facets = this.model.facets.byIndex[indexBase]; @@ -347,6 +417,7 @@ class Entity { return params; } + /* istanbul ignore next */ _makeUpdateParams({ set } = {}, pk = {}, sk = {}) { let setAttributes = this.model.schema.applyAttributeSetters(set); let { indexKey, updatedKeys } = this._getUpdatedKeys(pk, sk, setAttributes); @@ -371,6 +442,7 @@ class Entity { return params; } + /* istanbul ignore next */ _makePutParams({ data } = {}, pk, sk) { let setAttributes = this.model.schema.applyAttributeSetters(data); let { updatedKeys } = this._getUpdatedKeys(pk, sk, setAttributes); @@ -386,7 +458,7 @@ class Entity { return params; } - _updateExpressionBuilder(data = {}) { + _updateExpressionBuilder(data) { let skip = [ ...this.model.schema.getReadOnly(), ...this.model.facets.fields, @@ -394,10 +466,11 @@ class Entity { return this._expressionAttributeBuilder(data, { skip }); } - _queryKeyExpressionAttributeBuilder(index = "", pk, ...sks) { + _queryKeyExpressionAttributeBuilder(index, pk, ...sks) { let translate = { ...this.model.translations.keys[index] }; let restrict = ["pk"]; let keys = { pk }; + sks = sks.filter(sk => sk !== undefined); for (let i = 0; i < sks.length; i++) { let id = `sk${i + 1}`; keys[id] = sks[i]; @@ -410,22 +483,13 @@ class Entity { }); return { - ExpressionAttributeNames: Object.assign( - {}, - keyExpressions.ExpressionAttributeNames, - ), - ExpressionAttributeValues: Object.assign( - {}, - keyExpressions.ExpressionAttributeValues, - ), + ExpressionAttributeNames: Object.assign({}, keyExpressions.ExpressionAttributeNames), + ExpressionAttributeValues: Object.assign({}, keyExpressions.ExpressionAttributeValues), }; } - _expressionAttributeBuilder( - item = {}, - options = {}, - { noDuplicateNames } = {}, - ) { + /* istanbul ignore next */ + _expressionAttributeBuilder(item = {}, options = {}, {noDuplicateNames} = {}) { let { require = [], reject = [], @@ -486,7 +550,8 @@ class Entity { )}`; return expressions; } - + + /* istanbul ignore next */ _queryParams(chainState = {}, options = {}) { let conlidatedQueryFacets = this._consolidateQueryFacets( chainState.keys.sk, @@ -540,7 +605,7 @@ class Entity { return this._applyParameterOptions(parameters, options); } - _makeBetweenQueryParams(index = "", filter = {}, pk = {}, ...sk) { + _makeBetweenQueryParams(index, filter, pk, ...sk) { let keyExpressions = this._queryKeyExpressionAttributeBuilder( index, pk, @@ -568,23 +633,17 @@ class Entity { return params; } - _makeBeginsWithQueryParams(index = "", filter = {}, pk = {}, sk = {}) { - let keyExpressions = this._queryKeyExpressionAttributeBuilder( - index, - pk, - sk, - ); + _makeBeginsWithQueryParams(index, filter, pk, sk) { + let keyExpressions = this._queryKeyExpressionAttributeBuilder(index, pk, sk); + let KeyConditionExpression = "#pk = :pk"; + if (this.model.lookup.indexHasSortKeys[index]) { + KeyConditionExpression = `${KeyConditionExpression} and begins_with(#sk1, :sk1)`; + } let params = { + KeyConditionExpression, TableName: this.model.table, - ExpressionAttributeNames: this._mergeExpressionsAttributes( - filter.ExpressionAttributeNames, - keyExpressions.ExpressionAttributeNames, - ), - ExpressionAttributeValues: this._mergeExpressionsAttributes( - filter.ExpressionAttributeValues, - keyExpressions.ExpressionAttributeValues, - ), - KeyConditionExpression: `#pk = :pk and begins_with(#sk1, :sk1)`, + ExpressionAttributeNames: this._mergeExpressionsAttributes(filter.ExpressionAttributeNames, keyExpressions.ExpressionAttributeNames), + ExpressionAttributeValues: this._mergeExpressionsAttributes(filter.ExpressionAttributeValues, keyExpressions.ExpressionAttributeValues), }; if (index) { params["IndexName"] = index; @@ -604,14 +663,9 @@ class Entity { } return merged; } - - _makeComparisonQueryParams( - index = "", - comparison = "", - filter = {}, - pk = {}, - sk = {}, - ) { + + /* istanbul ignore next */ + _makeComparisonQueryParams(index = "", comparison = "", filter = {}, pk = {}, sk = {}) { let operator = Comparisons[comparison]; if (!operator) { throw new Error( @@ -646,7 +700,7 @@ class Entity { return params; } - _expectIndexFacets(attributes = {}, facets = {}) { + _expectIndexFacets(attributes, facets) { let [isIncomplete, { incomplete, complete }] = this._getIndexImpact( attributes, facets, @@ -669,7 +723,7 @@ class Entity { return complete; } - _makeKeysFromAttributes(indexes, attributes = {}) { + _makeKeysFromAttributes(indexes, attributes) { let indexKeys = {}; for (let index of indexes) { indexKeys[index] = this._makeIndexKeys(index, attributes, attributes); @@ -677,7 +731,7 @@ class Entity { return indexKeys; } - _getUpdatedKeys(pk = {}, sk = {}, set = {}) { + _getUpdatedKeys(pk, sk, set) { let updateIndex = ""; let keyTranslations = this.model.translations.keys; let keyAttributes = { ...sk, ...pk }; @@ -710,7 +764,8 @@ class Entity { } return { indexKey, updatedKeys }; } - + + /* istanbul ignore next */ _getIndexImpact(attributes = {}, included = {}) { let includedFacets = Object.keys(included); let impactedIndexes = {}; @@ -799,7 +854,8 @@ class Entity { ); return { ...queryFacets }; } - + + /* istanbul ignore next */ _expectFacets(obj = {}, properties = [], type = "key facets") { let [incompletePk, missing, matching] = this._expectProperties( obj, @@ -816,11 +872,11 @@ class Entity { } } - _findProperties(obj = {}, properties = []) { + _findProperties(obj, properties) { return properties.map((name) => [name, obj[name]]); } - _expectProperties(obj = {}, properties = []) { + _expectProperties(obj, properties) { let missing = []; let matching = {}; this._findProperties(obj, properties).forEach(([name, value]) => { @@ -833,27 +889,28 @@ class Entity { return [!!missing.length, missing, matching]; } - _makeKeyPrefixes(service = "", entity = "", version = "1") { + _makeKeyPrefixes(service, entity, version = "1") { return { pk: `$${service}_${version}`, sk: `$${entity}`, }; } - _validateIndex(index = "") { + _validateIndex(index) { if (!this.model.facets.byIndex[index]) { throw new Error(`Invalid index: ${index}`); } } - _getCollectionSk(collection = "") { + _getCollectionSk(collection) { if (typeof collection && collection.length) { return `$${collection}`.toLowerCase(); } else { return ""; } } - + + /* istanbul ignore next */ _getPrefixes({ collection = "", customFacets = {} } = {}) { /* Collections will prefix the sort key so they can be queried with @@ -894,7 +951,8 @@ class Entity { return keys; } - + + /* istanbul ignore next */ _makeIndexKeys(index = "", pkFacets = {}, ...skFacets) { this._validateIndex(index); let facets = this.model.facets.byIndex[index]; @@ -916,6 +974,7 @@ class Entity { return { pk, sk }; } + /* istanbul ignore next */ _makeKey(prefix = "", facets = [], supplied = {}, { isCustom } = {}) { let key = prefix; for (let i = 0; i < facets.length; i++) { @@ -935,10 +994,10 @@ class Entity { return key.toLowerCase(); } - _findBestIndexKeyMatch(attributes = {}) { + _findBestIndexKeyMatch(attributes) { let candidates = this.model.facets.bySlot.map((val, i) => i); let facets = this.model.facets.bySlot; - let match = ""; + let match; let keys = {}; for (let i = 0; i < facets.length; i++) { let currentMatches = []; @@ -972,7 +1031,6 @@ class Entity { break; } } else if (i === 0) { - match = ""; break; } else { match = @@ -983,9 +1041,11 @@ class Entity { return { keys: keys[match] || [], index: match || "", + shouldScan: match === undefined }; } - + + /* istanbul ignore next */ _parseComposedKey(key = "") { let facets = {}; let names = key.match(/:[A-Z1-9]+/gi); @@ -1023,7 +1083,7 @@ class Entity { } } - _normalizeIndexes(indexes = {}) { + _normalizeIndexes(indexes) { let normalized = {}; let indexFieldTranslation = {}; let indexHasSortKeys = {}; @@ -1187,7 +1247,7 @@ class Entity { collections: Object.keys(collections), }; } - + _normalizeFilters(filters = {}) { let normalized = {}; let invalidFilterNames = ["go", "params", "filter"]; @@ -1205,7 +1265,7 @@ class Entity { return normalized; } - _parseModel(model = {}) { + _parseModel(model) { let { service, entity, table, version = "1" } = model; let prefixes = this._makeKeyPrefixes(service, entity, version); let { diff --git a/src/filters.js b/src/filters.js index b89c9894..30a58ee6 100644 --- a/src/filters.js +++ b/src/filters.js @@ -142,9 +142,9 @@ class FilterFactory { if (existingNeedsParens) { existingExpression = `(${existingExpression})`; } - if (newNeedsParens) { - newExpression = `(${newExpression})`; - } + // if (newNeedsParens) { + // newExpression = `(${newExpression})`; + // } return `${existingExpression} AND ${newExpression}`; } else { return newExpression; @@ -199,7 +199,7 @@ class FilterFactory { filterChildren.push(name); injected[name] = { action: this.buildClause(filter), - children: ["params", "go", "filter", ...modelFilters], + children: ["params", "go", "page", "filter", ...modelFilters], }; } filterChildren.push("filter"); @@ -207,7 +207,7 @@ class FilterFactory { action: (entity, state, fn) => { return this.buildClause(fn)(entity, state); }, - children: ["params", "go", "filter", ...modelFilters], + children: ["params", "go", "page", "filter", ...modelFilters], }; for (let parent of filterParents) { injected[parent] = { ...injected[parent] }; diff --git a/src/types.js b/src/types.js index 2b7fa90c..a44a703e 100644 --- a/src/types.js +++ b/src/types.js @@ -23,6 +23,8 @@ const MethodTypes = { update: "update", delete: "delete", scan: "scan", + patch: "patch", + create: "create" }; const Comparisons = { diff --git a/test/connected.crud.spec.js b/test/connected.crud.spec.js index 361572f6..639078d2 100644 --- a/test/connected.crud.spec.js +++ b/test/connected.crud.spec.js @@ -275,8 +275,74 @@ describe("Entity", async () => { let secondStoreAfterUpdate = await MallStores.get(secondStore).go(); expect(secondStoreAfterUpdate.rent).to.equal(newRent); }).timeout(20000); + + it("Should not create a overwrite existing record", async () => { + let id = uuidv4(); + let mall = "EastPointe"; + let store = "LatteLarrys"; + let sector = "A1"; + let category = "food/coffee"; + let leaseEnd = "2020-01-20"; + let rent = "0.00"; + let building = "BuildingZ"; + let unit = "G1"; + let record = { + id, + mall, + store, + sector, + category, + leaseEnd, + rent, + building, + unit + }; + let recordOne = await MallStores.create(record).go(); + expect(recordOne).to.deep.equal(record); + let recordTwo = null; + try { + recordTwo = await MallStores.create(record).go(); + } catch(err) { + expect(err.message).to.be.equal("The conditional request failed"); + } + expect(recordTwo).to.be.null + }); + + it("Should only update a record if it already exists", async () => { + let id = uuidv4(); + let mall = "EastPointe"; + let store = "LatteLarrys"; + let sector = "A1"; + let category = "food/coffee"; + let leaseEnd = "2020-01-20"; + let rent = "0.00"; + let building = "BuildingZ"; + let unit = "G1"; + let record = { + id, + mall, + store, + sector, + category, + leaseEnd, + rent, + building, + unit + }; + let recordOne = await MallStores.create(record).go(); + expect(recordOne).to.deep.equal(record); + let patchResultsOne = await MallStores.patch({sector, id}).set({rent: "100.00"}).go(); + let patchResultsTwo = null; + try { + patchResultsTwo = await MallStores.patch({sector, id: `${id}-2`}).set({rent: "200.00"}).go(); + } catch(err) { + expect(err.message).to.be.equal("The conditional request failed"); + } + expect(patchResultsTwo).to.be.null + }) }); + describe("Delete records", async () => { it("Should create then delete a record", async () => { let record = new Entity( diff --git a/test/offline.entity.spec.js b/test/offline.entity.spec.js index c711644c..45d52785 100644 --- a/test/offline.entity.spec.js +++ b/test/offline.entity.spec.js @@ -1083,6 +1083,18 @@ describe("Entity", () => { let leaseEnd = "123"; it("Should match on the primary index", () => { let { index, keys } = MallStores._findBestIndexKeyMatch({ id }); + let params = MallStores.find({id}).params(); + console.log("params", params); + expect(params).to.be.deep.equal({ + TableName: 'StoreDirectory', + ExpressionAttributeNames: { '#id': 'storeLocationId', '#pk': 'pk'}, + ExpressionAttributeValues: { + ':id1': '123', + ':pk': '$mallstoredirectory_1#id_123', + }, + KeyConditionExpression: '#pk = :pk', + FilterExpression: '#id = :id1' + }); expect(keys).to.be.deep.equal([{ name: "id", type: "pk" }]); expect(index).to.be.equal(""); }); @@ -1092,6 +1104,27 @@ describe("Entity", () => { building, unit, }); + let params = MallStores.find({mall, building, unit}).params(); + expect(params).to.be.deep.equal({ + KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)', + TableName: 'StoreDirectory', + ExpressionAttributeNames: { + '#mall': 'mall', + '#building': 'buildingId', + '#unit': 'unitId', + '#pk': 'gsi1pk', + '#sk1': 'gsi1sk' + }, + ExpressionAttributeValues: { + ':mall1': '123', + ':building1': '123', + ':unit1': '123', + ':pk': '$mallstoredirectory_1#mall_123', + ':sk1': '$mallstores#building_123#unit_123#store_' + }, + IndexName: 'gsi1pk-gsi1sk-index', + FilterExpression: '#mall = :mall1 AND#building = :building1 AND#unit = :unit1' + }); expect(keys).to.be.deep.equal([ { name: "mall", type: "pk" }, { name: "building", type: "sk" }, diff --git a/test/offline.service.spec.js b/test/offline.service.spec.js index 1970934e..a35b9fa0 100644 --- a/test/offline.service.spec.js +++ b/test/offline.service.spec.js @@ -508,6 +508,71 @@ describe("Misconfiguration exceptions", () => { database.join(entityOne); expect(() => database.join(entityTwo)).to.throw(`Partition Key Facets provided "prop4" do not match established facets "prop1"`); }); + it("Should validate that attributes with the same have the same field also listed", () => { + let entityOne = { + entity: "entityOne", + attributes: { + prop1: { + type: "string", + field: "abc" + }, + prop2: { + type: "string" + }, + prop3: { + type: "string" + } + }, + indexes: { + index1: { + pk: { + field: "pk", + facets: ["prop1"], + }, + sk: { + field: "sk", + facets: ["prop2", "prop3"], + }, + collection: "collectionA", + } + } + } + let entityTwo = { + entity: "entityOne", + attributes: { + prop1: { + type: "string", + field: "def" + }, + prop4: { + type: "string" + }, + prop5: { + type: "string" + } + }, + indexes: { + index1: { + pk: { + field: "pk", + facets: ["prop1"], + }, + sk: { + field: "sk", + facets: ["prop1", "prop5"], + }, + collection: "collectionA", + }, + } + } + let database = new Service({ + version: "1", + table: "electro", + service: "electrotest", + }); + database.join(entityOne); + expect(() => database.join(entityTwo)).to.throw(`Attribute provided "prop1" with Table Field "def" does not match established Table Field "abc"`); + }); it("Should validate the PK field matches on all added schemas", () => { let entityOne = { entity: "entityOne",