Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3.1.0 #466

Merged
merged 3 commits into from
Jan 26, 2025
Merged

3.1.0 #466

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: <T = Record<string, unknown>>() => T | null;
readonly ref: {
readonly code: number;
readonly section: string;
readonly name: string;
Expand Down
2 changes: 2 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
expectNotAssignable,
} from "tsd";

import './test/tests.test-d';

type Resolve<T> = T extends Function | string | number | boolean
? T
: { [Key in keyof T]: Resolve<T[Key]> };
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
Loading