Skip to content

Commit

Permalink
Issue/114 (tywalch#117)
Browse files Browse the repository at this point in the history
* Adding new concept: "Listener"
Listeners open the door to some really cool tooling that isn't currently possible with how ElectroDB currently obscures raw DynamoDB responses.

* Tested related cleanup

* Improved default handling, defaults stop when default value not set or default callback returns undefined

* Adding more color to descriptions for default/required properties
  • Loading branch information
tywalch authored Mar 30, 2022
1 parent fa6ec49 commit f6c5dd7
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 21 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,8 @@ All notable changes to this project will be documented in this file. Breaking ch
- Expected typings for the injected v2 client now include methods for `transactWrite` and `transactGet`
### Changed
- Map attributes will now always resolve to least an empty object on a `create` and `put` methods (instead of just the root map)
- In the past, default values for property attributes on maps only resolves when a user provided an object to place the values on. Now default values within maps attributes will now always resolve onto the object on `create` and `put` methods.
- In the past, default values for property attributes on maps only resolves when a user provided an object to place the values on. Now default values within maps attributes will now always resolve onto the object on `create` and `put` methods.

## [1.8.1] - 2022-03-29
### Fixed
- Solidifying default application methodology: default values for nested properties will be applied up until an undefined default occurs or default callback returns undefined
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ const EmployeesModel = {
attributes: {
employee: {
type: "string",
default: () => uuidv4(),
default: () => uuid(),
},
firstName: {
type: "string",
Expand Down Expand Up @@ -686,9 +686,9 @@ attributes: {
Property | Type | Required | Types | Description
------------ | :--------------------------------------------------------: | :------: | :-------: | -----------
`type` | `string`, `ReadonlyArray<string>`, `string[]` | yes | all | Accepts the values: `"string"`, `"number"` `"boolean"`, `"map"`, `"list"`, `"set"`, an array of strings representing a finite list of acceptable values: `["option1", "option2", "option3"]`, or `"any"` which disables value type checking on that attribute.
`required` | `boolean` | no | all | Flag an attribute as required to be present when creating a record. This attribute also acts as a type of `NOT NULL` flag, preventing it from being removed directly.
`required` | `boolean` | no | all | Flag an attribute as required to be present when creating a record. This attribute also acts as a type of `NOT NULL` flag, preventing it from being removed directly. When applied to nested properties, be mindful that default map values can cause required child attributes to fail validation.
`hidden` | `boolean` | no | all | Flag an attribute as hidden to remove the property from results before they are returned.
`default` | `value`, `() => value` | no | all | Either the default value itself or a synchronous function that returns the desired value. Applied before `set` and before `required` check.
`default` | `value`, `() => value` | no | all | Either the default value itself or a synchronous function that returns the desired value. Applied before `set` and before `required` check. In the case of nested attributes, default values will apply defaults to children attributes until an undefined value is reached
`validate` | `RegExp`, `(value: any) => void`, `(value: any) => string` | no | all | Either regex or a synchronous callback to return an error string (will result in exception using the string as the error's message), or thrown exception in the event of an error.
`field` | `string` | no | all | The name of the attribute as it exists in DynamoDB, if named differently in the schema attributes. Defaults to the `AttributeName` as defined in the schema.
`readOnly` | `boolean` | no | all | Prevents an attribute from being updated after the record has been created. Attributes used in the composition of the table's primary Partition Key and Sort Key are read-only by default. The one exception to `readOnly` is for properties that also use the `watch` property, read [attribute watching](#attribute-watching) for more detail.
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": "1.8.0",
"version": "1.8.1",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ class MapAttribute extends Attribute {
const valueType = getValueType(data);

if (data === undefined) {
data = {};
return data;
} else if (valueType !== "object") {
throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`);
}
Expand Down
4 changes: 2 additions & 2 deletions test/offline.entity.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -991,8 +991,8 @@ describe("Entity", () => {
},
value: undefined
},
fail: false,
// message: `Invalid value type at entity path: "data". Value is required.`,
fail: true,
message: `Invalid value type at entity path: "data". Value is required.`,
}, {
input: {
data: {
Expand Down
2 changes: 1 addition & 1 deletion test/ts_connected.client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = "1";
import { Entity, Service } from "../index";
import { expect } from "chai";
import {v3, v4 as uuid} from "uuid";
import { v4 as uuid } from "uuid";
import { DocumentClient as V2Client } from "aws-sdk/clients/dynamodb";
import { DynamoDBClient as V3Client } from '@aws-sdk/client-dynamodb';

Expand Down
7 changes: 3 additions & 4 deletions test/ts_connected.complex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ function getEntity(helper: Helper) {
},
attributes: {
emptyNestedMap: {
default: {},
type: 'map',
properties: {
nestedMap: {
default: {},
type: 'map',
properties: {
deeplyNestedMap: {
default: {},
type: 'map',
properties: {
deeplyNestedString: {
Expand Down Expand Up @@ -602,10 +605,6 @@ describe("Simple Crud On Complex Entity", () => {
};
const putItem = await entity.put(data).go();
const item = await entity.get({stringVal, stringVal2}).go();
console.log({
item: item?.emptyNestedMap,
putItem: putItem.emptyNestedMap,
})
expect(item).to.deep.equal(putItem);
});

Expand Down
231 changes: 223 additions & 8 deletions test/ts_connected.update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,200 @@ const StoreLocations = new Entity({

describe("Update Item", () => {
describe('updating deeply nested attributes', () => {
it('should apply nested defaults on creation', () => {
const customers = new Entity(
{
model: {
entity: "customer",
service: "company",
version: "1"
},
attributes: {
id: { type: "string"},
email: {type: "string" },
name: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string" },
}
}
}
},
name2: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge' },
}
}
}
},
name3: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge' },
},
default: {}
}
}
},
name4: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge', required: true },
}
}
}
},
name5: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge', required: true },
},
}
}
},
name6: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge', required: true },
},
default: {},
}
}
},
name7: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", default: 'jorge', required: true },
},
default: {},
}
},
required: true,
default: () => ({})
},
name8: {
type: "map",
properties: {
legal: {
type: "map",
properties: {
first: { type: "string" },
middle: { type: "string", required: true },
},
default: {},
}
}
}
},
indexes: {
primary: {
pk: {
field: "pk",
composite: ["id"]
},
sk: {
field: "sk",
composite: []
}
}
}
},
{ table }
);

const p1 = customers.create({
id: "test",
email: "[email protected]",

name: { // should save as is
legal: {}
},
name2: {}, // should save as is
name3: {}, // should walk up defaults
name4: {}, // should stop when defaults stop
name5: {legal: {}}, // should be fine with nested required flag on 'middle' because 'middle' has default
name6: {}, // no typing issue with default missing because it
// name:7 does not need to be included despite being 'required', will be set by defaults
}).params();

expect(p1).to.deep.equal({
"Item": {
"id": "test",
"email": "[email protected]",
"name": {
"legal": {}
},
"name2": {},
"name3": {
"legal": {
"middle": "jorge"
}
},
"name4": {},
"name5": {
"legal": {
"middle": "jorge"
}
},
"name6": {
"legal": {
"middle": "jorge"
}
},
"name7": {
"legal": {
"middle": "jorge"
}
},
"name8": {},
"pk": "$company#id_test",
"sk": "$customer_1",
"__edb_e__": "customer",
"__edb_v__": "1"
},
"TableName": "electro",
"ConditionExpression": "attribute_not_exists(#pk) AND attribute_not_exists(#sk)",
"ExpressionAttributeNames": {
"#pk": "pk",
"#sk": "sk"
}
});

expect(() => customers.create({
id: "test",
email: "[email protected]",
name8: {}, // unfortunate combination, user defined illogical defaults that resulted in non-typed validation error
}).params()).to.throw('Invalid value type at entity path: "name8.legal.middle". Value is required. - For more detail on this error reference: https://github.com/tywalch/electrodb#invalid-attribute');
});
it('should not clobber a deeply nested attribute when updating', async () => {
const customers = new Entity(
{
Expand Down Expand Up @@ -422,29 +616,50 @@ describe("Update Item", () => {
{ table, client }
);

const id = uuid();
const id1 = uuid();
const id2 = uuid();
const email = "[email protected]";

await customers.create({ id, email }).go();
await customers.create({ id: id1, email }).go();
await customers.create({ id: id2, email, name: {legal: {}} }).go();

const retrieved1 = await customers.get({id: id1}).go();
const retrieved2 = await customers.get({id: id2}).go();

const retrieved = await customers.get({id}).go();
expect(retrieved1).to.deep.equal({
email,
id: id1,
name: {}
});

expect(retrieved).to.deep.equal({
id,
expect(retrieved2).to.deep.equal({
email,
id: id2,
name: {
legal: {}
}
});

const updated = await customers.patch({id})
const updated1 = await customers.patch({id: id1})
.data((attr, op) => {
op.set(attr.name.legal.first, 'joe');
op.set(attr.name.legal.last, 'exotic');
})
.go({response: 'all_new'})
.then((data) => ({success: true, result: data}))
.catch(err => ({success: false, result: err}));

expect(updated1.success).to.be.false;
expect(updated1.result.message).to.equal('The document path provided in the update expression is invalid for update - For more detail on this error reference: https://github.com/tywalch/electrodb#aws-error');

const updated2 = await customers.patch({id: id2})
.data((attr, op) => {
op.set(attr.name.legal.first, 'joe');
op.set(attr.name.legal.last, 'exotic');
}).go({response: 'all_new'});

expect(updated).to.deep.equal({
id,
expect(updated2).to.deep.equal({
id: id2,
email,
name: {
legal: {
Expand Down

0 comments on commit f6c5dd7

Please sign in to comment.