Skip to content

Commit ace43e3

Browse files
authored
Fixes issue 530 (#531)
* Fixes issue 530 Fixed issue where ElectroDB would attempt to set a table index field's value on `upsert` if an index key used an attribute's name as it's field name. This would cause the `upsert` to fail because DyanmoDB prevents set operations on table index fields. * Fixes table initialization
1 parent a7feefd commit ace43e3

File tree

7 files changed

+155
-19
lines changed

7 files changed

+155
-19
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,4 +598,8 @@ All notable changes to this project will be documented in this file. Breaking ch
598598

599599
## [3.4.6]
600600
### Fixed
601-
- [Issue #343]((https://github.com/tywalch/electrodb/issues/343)); Fixed issue where ElectroDB would attempt to double set an attribute's value on `upsert` if an index key used the attribute's name as it's field name. This would cause the `upsert` to fail because DyanmoDB prevents duplicate set operations to the same field.
601+
- [Issue #343]((https://github.com/tywalch/electrodb/issues/343)); Fixed issue where ElectroDB would attempt to double set an attribute's value on `upsert` if an index key used the attribute's name as it's field name. This would cause the `upsert` to fail because DyanmoDB prevents duplicate set operations to the same field.
602+
603+
## [3.4.7]
604+
### Fixed
605+
- [Issue #530]((https://github.com/tywalch/electrodb/issues/530)); Fixed issue where ElectroDB would attempt to set a table index field's value on `upsert` if an index key used an attribute's name as it's field name. This would cause the `upsert` to fail because DyanmoDB prevents set operations on table index fields.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "electrodb",
3-
"version": "3.4.6",
3+
"version": "3.4.7",
44
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
55
"main": "index.js",
66
"scripts": {

src/clauses.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ const {
1515
AttributeOperationProxy,
1616
UpdateOperations,
1717
FilterOperationNames,
18-
UpdateOperationNames,
1918
} = require("./operations");
2019
const { UpdateExpression } = require("./update");
2120
const { FilterExpression } = require("./where");
@@ -472,6 +471,8 @@ let clauses = {
472471
const value = updatedKeys[key];
473472
if (indexKey[key] === undefined) {
474473
setFields[key] = value;
474+
} else {
475+
delete setFields[key];
475476
}
476477
}
477478

test/connected.issues.spec.ts

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
// @ts-nocheck
1+
// @ts-expect-error
22
process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1;
33
import DynamoDB from "aws-sdk/clients/dynamodb";
44
import { v4 as uuid } from "uuid";
55
import { expect } from "chai";
66
import { Entity } from "../";
7+
import { putTable } from "./table";
78
const table = "electro";
89
const client = new DynamoDB.DocumentClient({
910
region: "us-east-1",
@@ -14,6 +15,15 @@ const client = new DynamoDB.DocumentClient({
1415
},
1516
});
1617

18+
const dynamodb = new DynamoDB({
19+
region: "us-east-1",
20+
endpoint: "http://localhost:8000",
21+
credentials: {
22+
accessKeyId: "test",
23+
secretAccessKey: "test",
24+
},
25+
});
26+
1727
describe("Issue #343", () => {
1828
function createEntity() {
1929
return new Entity({
@@ -96,18 +106,18 @@ describe("Issue #343", () => {
96106
"#gsi1sk": "gsi1sk",
97107
},
98108
"ExpressionAttributeValues": {
99-
":__edb_e___u0": inventory.model.entity,
100-
":__edb_v___u0": inventory.model.version,
109+
":__edb_e___u0": inventory.schema.model.entity,
110+
":__edb_v___u0": inventory.schema.model.version,
101111
":accountId_u0": accountId,
102112
":transactionId_u0": transactionId,
103113
":date_u0": date,
104114
":value_u0": value,
105-
":gsi1pk_u0": `$${inventory.model.service}#transactionid_${transactionId}`,
106-
":gsi1sk_u0": `$${inventory.model.entity}_${inventory.model.version}`
115+
":gsi1pk_u0": `$${inventory.schema.model.service}#transactionid_${transactionId}`,
116+
":gsi1sk_u0": `$${inventory.schema.model.entity}_${inventory.schema.model.version}`
107117
},
108118
"Key": {
109-
"pk": `$${inventory.model.service}#accountid_${accountId}`,
110-
"sk": `$${inventory.model.entity}_${inventory.model.version}#date_${date}#transactionid_${transactionId}`
119+
"pk": `$${inventory.schema.model.service}#accountid_${accountId}`,
120+
"sk": `$${inventory.schema.model.entity}_${inventory.schema.model.version}#date_${date}#transactionid_${transactionId}`
111121
}
112122
});
113123

@@ -135,16 +145,16 @@ describe("Issue #343", () => {
135145
"#value": "value",
136146
},
137147
"ExpressionAttributeValues": {
138-
":__edb_e___u0": inventory.model.entity,
139-
":__edb_v___u0": inventory.model.version,
148+
":__edb_e___u0": inventory.schema.model.entity,
149+
":__edb_v___u0": inventory.schema.model.version,
140150
":accountId_u0": accountId,
141151
":transactionId_u0": transactionId,
142152
":date_u0": date,
143153
":value_u0": value,
144154
},
145155
"Key": {
146-
"pk": `$${inventory.model.service}#accountid_${accountId}`,
147-
"sk": `$${inventory.model.entity}_${inventory.model.version}#date_${date}#transactionid_${transactionId}`
156+
"pk": `$${inventory.schema.model.service}#accountid_${accountId}`,
157+
"sk": `$${inventory.schema.model.entity}_${inventory.schema.model.version}#date_${date}#transactionid_${transactionId}`
148158
}
149159
});
150160
});
@@ -178,18 +188,89 @@ describe("Issue #343", () => {
178188
"#sk": "sk",
179189
},
180190
"ExpressionAttributeValues": {
181-
":__edb_e___u0": inventory.model.entity,
182-
":__edb_v___u0": inventory.model.version,
191+
":__edb_e___u0": inventory.schema.model.entity,
192+
":__edb_v___u0": inventory.schema.model.version,
183193
":accountId_u0": accountId,
184194
":transactionId_u0": transactionId,
185195
":date_u0": date,
186196
":value_u0": value,
187197
},
188198
"ConditionExpression": "attribute_exists(#pk) AND attribute_exists(#sk)",
189199
"Key": {
190-
"pk": `$${inventory.model.service}#accountid_${accountId}`,
191-
"sk": `$${inventory.model.entity}_${inventory.model.version}#date_${date}#transactionid_${transactionId}`
200+
"pk": `$${inventory.schema.model.service}#accountid_${accountId}`,
201+
"sk": `$${inventory.schema.model.entity}_${inventory.schema.model.version}#date_${date}#transactionid_${transactionId}`
192202
}
193203
});
194204
});
195-
});
205+
});
206+
207+
describe("Issue #530", () => {
208+
it("should upsert item without apply set to attribute key name", async () => {
209+
const table = `issue530`;
210+
const Log = new Entity(
211+
{
212+
model: {
213+
entity: "log_record",
214+
version: "1",
215+
service: "log",
216+
},
217+
attributes: {
218+
ip_addr: {
219+
type: "string",
220+
required: true,
221+
readOnly: true,
222+
},
223+
expires_ttl: {
224+
type: "number",
225+
},
226+
app: {
227+
type: "string",
228+
},
229+
click_id: {
230+
type: "string",
231+
},
232+
timestamp: {
233+
type: "string",
234+
},
235+
},
236+
indexes: {
237+
byIpAddr: {
238+
pk: {
239+
field: "ip_addr",
240+
composite: ["ip_addr"],
241+
},
242+
},
243+
},
244+
},
245+
{ table, client }
246+
);
247+
248+
const record = {
249+
ip_addr: "127.0.0.1",
250+
app: "something",
251+
};
252+
253+
const params = Log.upsert(record).params({});
254+
expect(params).to.deep.equal({
255+
TableName: table,
256+
UpdateExpression: 'SET #__edb_e__ = :__edb_e___u0, #__edb_v__ = :__edb_v___u0, #app = :app_u0',
257+
ExpressionAttributeNames: {
258+
'#__edb_e__': '__edb_e__',
259+
'#__edb_v__': '__edb_v__',
260+
'#app': 'app'
261+
},
262+
ExpressionAttributeValues: {
263+
':__edb_e___u0': 'log_record',
264+
':__edb_v___u0': '1',
265+
':app_u0': 'something'
266+
},
267+
Key: {
268+
ip_addr: '127.0.0.1'
269+
},
270+
});
271+
272+
await Log.upsert(record).where((attr, op) => op.notExists(attr.ip_addr)).go()
273+
const result = await Log.get({ ip_addr: record.ip_addr }).go();
274+
expect(result.data).to.deep.equal(record);
275+
})
276+
})

test/definitions/issue530.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"TableName": "issue530",
3+
"KeySchema": [
4+
{
5+
"AttributeName": "ip_addr",
6+
"KeyType": "HASH"
7+
}
8+
],
9+
"AttributeDefinitions": [
10+
{
11+
"AttributeName": "ip_addr",
12+
"AttributeType": "S"
13+
}
14+
],
15+
"BillingMode": "PAY_PER_REQUEST"
16+
}

test/init.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const localSecondaryIndexes = require("./definitions/localsecondaryindexes.json"
1111
const keysOnly = require("./definitions/keysonly.json");
1212
const castKeys = require("./definitions/castkeys.json");
1313
const reverseIndex = require("./definitions/reverseindex.json");
14+
const issue530 = require("./definitions/issue530.json");
1415
const shouldDestroy = process.argv.includes("--recreate");
1516

1617
if (
@@ -84,6 +85,7 @@ async function main() {
8485
createTable(dynamodb, "electro_keysonly", keysOnly),
8586
createTable(dynamodb, "electro_castkeys", castKeys),
8687
createTable(dynamodb, "electro_reverseindex", reverseIndex),
88+
createTable(dynamodb, "issue530", issue530),
8789
]);
8890
}
8991

test/table.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { DynamoDB } from "aws-sdk";
2+
3+
export interface TableManager {
4+
exists(): Promise<boolean>;
5+
drop(): Promise<void>;
6+
create(definition: DynamoDB.CreateTableInput): Promise<void>;
7+
}
8+
9+
export function createTableManager(dynamodb: DynamoDB, table: string): TableManager {
10+
return {
11+
async exists() {
12+
let tables = await dynamodb.listTables().promise();
13+
return (tables.TableNames || []).includes(table);
14+
},
15+
async drop() {
16+
await dynamodb.deleteTable({ TableName: table }).promise();
17+
},
18+
async create(definition: DynamoDB.CreateTableInput) {
19+
await dynamodb
20+
.createTable({ ...definition, TableName: table })
21+
.promise();
22+
},
23+
};
24+
}
25+
26+
export async function putTable(dynamodb: DynamoDB, definition: DynamoDB.CreateTableInput) {
27+
const table = createTableManager(dynamodb, definition.TableName);
28+
if (await table.exists()) {
29+
await table.drop();
30+
}
31+
return await table.create(definition);
32+
}

0 commit comments

Comments
 (0)