Skip to content

Commit

Permalink
### Fixed
Browse files Browse the repository at this point in the history
- [Issue #464](#464); When specifing return attributes on retrieval methods, ElectroDB would unexpectly return null or missing values if the options chosen resulted in an empty object being returned. This behavor could be confused with no results being found. ElectroDB now returns the empty object in these cases.

### Added
- ElectroDB Error objects no contain a `params()` method. If your operation resulted in an error thrown by the DynamoDB client, you can call the `params()` method to get the compiled parameters sent to DynamoDB. This can be helpful for debugging. Note, that if the error was thrown prior to parameter creation (validation errors, invalid query errors, etc) then the `params()` method will return the value `null`.
  • Loading branch information
tywalch committed Jan 22, 2025
1 parent 1151425 commit acd8e98
Show file tree
Hide file tree
Showing 8 changed files with 287 additions and 9 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -550,4 +550,11 @@ All notable changes to this project will be documented in this file. Breaking ch

## [3.0.1]
### Fixed
- The execution option `{ compare: "attributes" }` used incorrect expression comparisons that impacted `lte` queries on indexes with a single composite key.
- The execution option `{ compare: "attributes" }` used incorrect expression comparisons that impacted `lte` queries on indexes with a single composite key.

## [3.1.0]
### Fixed
- [Issue #464](https://github.com/tywalch/electrodb/issues/464); When specifing return attributes on retrieval methods, ElectroDB would unexpectly return null or missing values if the options chosen resulted in an empty object being returned. This behavor could be confused with no results being found. ElectroDB now returns the empty object in these cases.

### Added
- ElectroDB Error objects no contain a `params()` method. If your operation resulted in an error thrown by the DynamoDB client, you can call the `params()` method to get the compiled parameters sent to DynamoDB. This can be helpful for debugging. Note, that if the error was thrown prior to parameter creation (validation errors, invalid query errors, etc) then the `params()` method will return the value `null`.
2 changes: 1 addition & 1 deletion RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ The approach for "Watch All" syntax for attributes (e.g. `{watch: "*"}`) weighed

### ExpressionAttributeValues Properties

To prevent clashes between `update` values and filter conditions, a change was made to how the property names of `ExpressionAttributeValues` are constructed. This is not a breaking change but if you have tests to specifically compare param results against static JSON you will need to update that JSON. Changes to JSON query parameters is not considered a breaking major version change.
To prevent clashes between `update` values and filter conditions, a change was made to how the property names of `ExpressionAttributeValues` are constructed. This is not a breaking change but if you have tests to specifically compare param results against static JSON you will need to update that JSON. Changes to JSON query parameters is not considered a breaking major version change.
5 changes: 3 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2935,9 +2935,10 @@ export class ElectroError<E extends Error = Error> extends Error {
readonly name: "ElectroError";
readonly code: number;
readonly date: number;
readonly isElectroError: boolean;
readonly cause: E | undefined;
ref: {
readonly isElectroError: boolean;
readonly params: () => Record<string, unknown> | null;
readonly ref: {
readonly code: number;
readonly section: string;
readonly name: string;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electrodb",
"version": "3.0.1",
"version": "3.1.0",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
"main": "index.js",
"scripts": {
Expand Down
17 changes: 14 additions & 3 deletions src/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,18 @@ class Entity {
if (err.__isAWSError) {
stackTrace.message = `Error thrown by DynamoDB client: "${err.message}" - For more detail on this error reference: https://electrodb.dev/en/reference/errors/#aws-error`;
stackTrace.cause = err;
e.applyParamsFn(stackTrace, err.__edb_params);
return Promise.reject(stackTrace);
} else if (err.isElectroError) {
e.applyParamsFn(err, err.__edb_params);
return Promise.reject(err);
} else {
stackTrace.message = new e.ElectroError(
e.ErrorCodes.UnknownError,
err.message,
err,
).message;
e.applyParamsFn(stackTrace, err.__edb_params);
return Promise.reject(stackTrace);
}
}
Expand Down Expand Up @@ -516,6 +519,10 @@ class Entity {
.catch((err) => {
notifyQuery();
notifyResults(err, false);
Object.defineProperty(err, '__edb_params', {
enumerable: false,
value: params,
})
err.__isAWSError = true;
throw err;
});
Expand Down Expand Up @@ -935,7 +942,7 @@ class Entity {
response.Item,
config,
);
if (Object.keys(results).length === 0) {
if (Object.keys(results).length === 0 && !config._objectOnEmpty) {
results = null;
}
} else if (!config._objectOnEmpty) {
Expand All @@ -957,7 +964,7 @@ class Entity {
item,
config,
);
if (Object.keys(record).length > 0) {
if (Object.keys(record).length > 0 || config._objectOnEmpty) {
results.push(record);
}
}
Expand All @@ -967,7 +974,7 @@ class Entity {
response.Attributes,
config,
);
if (Object.keys(results).length === 0) {
if (Object.keys(results).length === 0 && !config._objectOnEmpty) {
results = null;
}
} else if (config._objectOnEmpty) {
Expand Down Expand Up @@ -1646,6 +1653,7 @@ class Entity {
order: undefined,
hydrate: false,
hydrator: (_entity, _indexName, items) => items,
_objectOnEmpty: false,
_includeOnResponseItem: {},
};

Expand Down Expand Up @@ -1727,6 +1735,9 @@ class Entity {

if (Array.isArray(option.attributes)) {
config.attributes = config.attributes.concat(option.attributes);
if (config.attributes.length > 0) {
config._objectOnEmpty = true;
}
}

if (option.preserveBatchOrder === true) {
Expand Down
15 changes: 14 additions & 1 deletion src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ function makeMessage(message, section) {
}

class ElectroError extends Error {
constructor(code, message, cause) {
constructor(code, message, cause, params = null) {
super(message, { cause });
let detail = ErrorCodes.UnknownError;
if (code && code.sym === ErrorCode) {
Expand All @@ -298,9 +298,21 @@ class ElectroError extends Error {
this.code = detail.code;
this.date = Date.now();
this.isElectroError = true;
applyParamsFn(this, params);
}
}

function applyParamsFn(error, params = null) {
Object.defineProperty(error, 'params', {
enumerable: false,
writable: true,
configurable: true,
value: () => {
return params;
}
});
}

class ElectroValidationError extends ElectroError {
constructor(errors = []) {
const fields = [];
Expand Down Expand Up @@ -389,6 +401,7 @@ class ElectroAttributeValidationError extends ElectroError {
module.exports = {
ErrorCodes,
ElectroError,
applyParamsFn,
ElectroValidationError,
ElectroUserValidationError,
ElectroAttributeValidationError,
Expand Down
196 changes: 196 additions & 0 deletions test/ts_connected.client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,202 @@ describe("dynamodb sdk client compatibility", () => {
expect(results).to.be.an("array");
});

describe('electro error params', () => {
it('the params function should not be visible in console logs', async () => {
const entity = new Entity(
{
model: {
service: "tests",
entity: uuid(),
version: "1",
},
attributes: {
prop1: {
type: "string",
default: () => uuid(),
field: "p",
},
prop2: {
type: "string",
required: true,
field: "r",
},
prop3: {
type: "string",
required: true,
field: "a",
},
},
indexes: {
farm: {
pk: {
field: "pk",
composite: ["prop1"],
},
sk: {
field: "sk",
composite: ["prop2"],
},
},
},
},
{client, table: "electro"},
);

const prop1 = uuid();
const prop2 = uuid();
const prop3 = "abc";

let params: Record<string, unknown> | undefined = undefined;
await entity.create({prop1, prop2, prop3}).go();
const results = await entity.create({prop1, prop2, prop3}).go()
.then(() => null)
.catch((err: ElectroError) => err);

expect(results).to.not.be.null;

if (results) {
expect(JSON.parse(JSON.stringify(results))).to.not.have.keys('params');
expect(Object.keys(results).find(key => key === 'params')).to.be.undefined;
console.log(results);
}
});

it('should return null parameters if error occurs prior to compiling parameters', async () => {
const entity = new Entity(
{
model: {
service: "tests",
entity: uuid(),
version: "1",
},
attributes: {
prop1: {
type: "string",
default: () => uuid(),
field: "p",
},
prop2: {
type: "string",
required: true,
field: "r",
},
prop3: {
type: "string",
required: true,
field: "a",
validate: (val) => {
return val !== "abc";
}
},
},
indexes: {
farm: {
pk: {
field: "pk",
composite: ["prop1"],
},
sk: {
field: "sk",
composite: ["prop2"],
},
},
},
},
{client, table: "electro"},
);

const prop1 = uuid();
const prop2 = uuid();
const prop3 = "abc";

let params: Record<string, unknown> | undefined = undefined;


const results = await entity.create({prop1, prop2, prop3}).go({
logger: (event) => {
if (event.type === 'query') {
params = event.params;
}
}
})
.then(() => null)
.catch((err: ElectroError) => err);

expect(params).to.be.undefined;
expect(results).to.not.be.null;

if (results) {
expect(results.params()).to.be.null;
}
});

it('should return the parameters sent to DynamoDB if available', async () => {
const entity = new Entity(
{
model: {
service: "tests",
entity: uuid(),
version: "1",
},
attributes: {
prop1: {
type: "string",
default: () => uuid(),
field: "p",
},
prop2: {
type: "string",
required: true,
field: "r",
},
prop3: {
type: "string",
required: true,
field: "a",
},
},
indexes: {
farm: {
pk: {
field: "pk",
composite: ["prop1"],
},
sk: {
field: "sk",
composite: ["prop2"],
},
},
},
},
{client, table: "electro"},
);

const prop1 = uuid();
const prop2 = uuid();
const prop3 = "abc";

let params: Record<string, unknown> | undefined = undefined;
await entity.create({prop1, prop2, prop3}).go();
const results = await entity.create({prop1, prop2, prop3}).go({
logger: (event) => {
if (event.type === 'query') {
params = event.params;
}
}
})
.then(() => null)
.catch((err: ElectroError) => err);

expect(results).to.not.be.null;

if (results) {
expect(results.params()).to.not.be.undefined.and.not.to.be.null;
expect(results.params()).to.deep.equal(params);
}
});
});

it('should include original aws error as cause on thrown ElectroError', async () => {
const entity = new Entity(
{
Expand Down
Loading

0 comments on commit acd8e98

Please sign in to comment.