Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

electrodb

Package Overview
Dependencies
Maintainers
1
Versions
163
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

electrodb - npm Package Compare versions

Comparing version 0.8.10 to 0.8.11

2

package.json
{
"name": "electrodb",
"version": "0.8.10",
"version": "0.8.11",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -27,2 +27,4 @@ # ElectroDB

- [Facets](#facets)
- [Facet Arrays](#facet-arrays)
- [Facet Templates](#facet-templates)
- [Filters](#filters)

@@ -243,3 +245,3 @@ - [Defined on the model](#defined-on-the-model)

| `sk` | `object` | no | Configuration for the sk of that index or table |
`sk.facets` | `boolean` | no | An array that represents the order in which attributes are concatenated to facets the key (see [Facets](#facets) below for more on this functionality). |
`sk.facets` | `array | string` | no | Either an Array that represents the order in which attributes are concatenated to facets the key, or a String for a facet template. (see [Facets](#facets) below for more on this functionality). |
`sk.field` | `string` | yes | The name of the attribute as it exists dynamo, if named differently in the schema attributes. |

@@ -251,5 +253,48 @@ `index` | `string` | yes | Used only when the `Index` defined is a *Global Secondary Index*; this is left blank for the table's primary index.

There are two ways to provide facets:
1. As a [Facet Array](#facet-arrays)
2. As a [Facet Template](#facet-templates)
For example, in the following **Access Pattern**, "`locations`" is made up of the facets `storeId`, `mallId`, `buildingId` and `unitId` which map to defined attributes in the `schema`:
```
// Input
{
storeId: "STOREVALUE",
mallId: "MALLVALUE",
buildingId: "BUILDINGVALUE",
unitId: "UNITVALUE"
};
// Output:
{
pk: '$mallstoredirectory_1#storeId_storevalue',
sk: '$mallstores#mallid_mallvalue#buildingid_buildingvalue#unitid_unitvalue'
}
```
For `PK` values, the `service` and `version` values from the model are prefixed onto the key.
For `SK` values, the `entity` value from the model is prefixed onto the key.
### Facet Arrays
In a Facet Array, each element is the name of the corresponding Attribute defined in the Model. If the Attribute has a `label` property, that will be used to prefix the facets, otherwise the full Attribute name will be used.
```javascript
attributes: {
storeId: {
type: "string",
label: "sid",
},
mallId: {
type: "string",
label: "mid",
},
buildingId: {
type: "string",
label: "bid",
},
unitId: {
type: "string",
label: "uid",
}
},
indexes: {

@@ -267,4 +312,67 @@ locations: {

}
// Input
{
storeId: "STOREVALUE",
mallId: "MALLVALUE",
buildingId: "BUILDINGVALUE",
unitId: "UNITVALUE"
};
// Output:
{
pk: '$mallstoredirectory_1#sid_storevalue',
sk: '$mallstores#mid_mallvalue#bid_buildingvalue#uid_unitvalue'
}
```
### Facet Templates
In a Facet Template, you provide a formatted template for ElectroDB to use when making keys.
The syntax to a Facet Template is simple using the following rules:
1. Only alphanumeric, underscore, colons, and hash symbols are valid. the following regex is used to determine validity: `/^[A-Z1-9:#_]+$/gi`
2. Attributes are identified by a prefixed colon and the attributes name. For example, the syntax `:storeId` will matches `storeId` attribute in the `model`
3. Convention for a composing a key use the `#` symbol to separate attributes, and for labels to attach with underscore. For example, when composing both the `mallId` and `buildingId` would be expressed as `mid_:mallId#bid_:buildingId`.
```javascript
attributes: {
storeId: {
type: "string"
},
mallId: {
type: "string"
},
buildingId: {
type: "string"
},
unitId: {
type: "string"
}
},
indexes: {
locations: {
pk: {
field: "pk",
facets: "sid_:storeId"
},
sk: {
field: "sk",
facets: "mid_:mallId#bid_:buildingId#uid_:unitId"]
}
}
}
// Input
{
storeId: "STOREVALUE",
mallId: "MALLVALUE",
buildingId: "BUILDINGVALUE",
unitId: "UNITVALUE"
};
// Output:
{
pk: '$mallstoredirectory_1#sid_storevalue',
sk: '$mallstores#mid_mallvalue#bid_buildingvalue#uid_unitvalue'
}
```
## Filters

@@ -271,0 +379,0 @@ Building thoughtful indexes can make queries simple and performant. Sometimes you need to filter results down further. By adding Filters to your model, you can extend your queries with custom filters. Below is the traditional way you would add a filter to Dynamo's DocumentClient directly along side how you would accomplish the same using a Filter function.

@@ -1065,2 +1065,38 @@ "use strict";

_parseComposedKey(key = "") {
if (!key.match(/^[A-Z1-9:#_]+$/gi)) {
throw new Error(`Invalid key facet template. Allowed characters include only "A-Z", "a-z", "1-9", ":", "_", "#". Received: ${key}`);
}
let facets = {};
let names = key.match(/:[A-Z1-9]+/gi);
if (!names) {
throw new Error(`Invalid key facet template. No facets provided, expected at least one facet with the format ":attributeName". Received: ${key}`)
}
let labels = key.split(/:[A-Z1-9]+/gi);
for (let i = 0; i < names.length; i++) {
let name = names[i].replace(":", "");
let label = labels[i];
if (name !== "") {
facets[name] = label.replace(/#|_/gi, "");
}
}
return facets;
}
_parseFacets(facets) {
if (Array.isArray(facets)) {
return {
facetLabels: {},
facetArray: facets,
}
} else {
let facetLabels = this._parseComposedKey(facets);
return {
facetLabels,
facetArray: Object.keys(facetLabels),
}
}
}
_normalizeIndexes(indexes = {}) {

@@ -1083,2 +1119,3 @@ let normalized = {};

attributes: [],
labels: {},
};

@@ -1094,8 +1131,11 @@

indexHasSortKeys[indexName] = hasSk;
let {facetArray, facetLabels} = this._parseFacets(index.pk.facets);
facets.labels = Object.assign({}, facets.labels, facetLabels);
let pk = {
accessPattern,
facetLabels,
index: indexName,
type: KeyTypes.pk,
field: index.pk.field || "",
facets: [...index.pk.facets],
facets: [...facetArray],
};

@@ -1105,3 +1145,6 @@ let sk = {};

if (hasSk) {
let {facetArray, facetLabels} = this._parseFacets(index.sk.facets);
facets.labels = Object.assign({}, facets.labels, facetLabels);
sk = {
facetLabels,
accessPattern,

@@ -1111,3 +1154,3 @@ index: indexName,

field: index.sk.field || "",
facets: [...index.sk.facets],
facets: [...facetArray],
};

@@ -1174,2 +1217,19 @@ facets.fields.push(sk.field);

_normalizeFilters(filters = {}) {
let normalized = {};
let invalidFilterNames = ["go", "params", "filter"];
for (let [name, fn] of Object.entries(filters)) {
if (invalidFilterNames.includes(name)) {
throw new Error(
`Invalid filter name. Filter cannot be named "go", "params", or "filter"`,
);
} else {
normalized[name] = fn;
}
}
return normalized;
}
_parseModel(model = {}) {

@@ -1186,3 +1246,3 @@ let { service, entity, table, version = "1" } = model;

let schema = new Schema(model.attributes, facets);
let filters = model.filters || {};
let filters = this._normalizeFilters(model.filters);
return {

@@ -1189,0 +1249,0 @@ service,

@@ -218,2 +218,3 @@ const { KeyTypes } = require("./types");

name,
label: facets.labels[name] || attribute.label,
required: !!attribute.required,

@@ -246,5 +247,11 @@ field: attribute.field || name,

}
let missingFacetAttributes = facets.attributes.filter(({name}) => {
return !normalized[name]
}).map(facet => `"${facet.type}: ${facet.name}"`);
if (missingFacetAttributes.length) {
throw new Error(`Invalid key facet template. The following facet attributes were described in the key facet template but were not included model's attributes: ${missingFacetAttributes.join(", ")}`);
}
if (invalidProperties.length) {
let message = invalidProperties.map(
prop =>
prop =>
`Schema Validation Error: Attribute "${prop.name}" property "${prop.property}". Received: "${prop.value}", Expected: "${prop.expected}"`,

@@ -251,0 +258,0 @@ );

@@ -72,3 +72,3 @@ const Validator = require("jsonschema").Validator;

facets: {
type: "array",
type: ["array", "string"],
minItems: 1,

@@ -91,3 +91,3 @@ items: {

facets: {
type: "array",
type: ["array", "string"],
minItems: 1,

@@ -167,10 +167,11 @@ required: true,

let message = `${err.property}`;
if (err.argument === "isFunction") {
return `${message} must be a function`;
} else if (err.argument === "isFunctionOrString") {
return `${message} must be either a function or string`;
} else if (err.argument === "isFunctionOrRegexp") {
return `${message} must be either a function or Regexp`;
} else {
return `${message} ${err.message}`;
switch (err.argument) {
case "isFunction":
return `${message} must be a function`;
case "isFunctionOrString":
return `${message} must be either a function or string`;
case "isFunctionOrRegexp":
return `${message} must be either a function or Regexp`;
default:
return `${message} ${err.message}`;
}

@@ -177,0 +178,0 @@ })

@@ -276,2 +276,159 @@ const { Entity } = require("../src/entity");

});
describe("Getters/Setters", async () => {
let db = new Entity(
{
service: "testing",
entity: "tester",
table: "electro",
version: "1",
attributes: {
id: {
type: "string",
default: () => uuidv4(),
},
date: {
type: "string",
default: () => moment.utc().format(),
},
prop1: {
type: "string",
field: "prop1Field",
set: (prop1, { id }) => {
if (id) {
return `${prop1} SET ${id}`;
} else {
return `${prop1} SET`;
}
},
get: prop1 => `${prop1} GET`,
},
prop2: {
type: "string",
field: "prop2Field",
get: (prop2, { id }) => `${prop2} GET ${id}`,
},
},
indexes: {
record: {
pk: {
field: "pk",
facets: ["date"],
},
sk: {
field: "sk",
facets: ["id"],
},
},
},
},
{ client },
);
it("Should show getter/setter values on put", async () => {
let date = moment.utc().format();
let id = uuidv4();
let prop1 = "aaa";
let prop2 = "bbb";
let record = await db.put({ date, id, prop1, prop2 }).go();
expect(record).to.deep.equal({
id,
date,
prop1: `${prop1} SET ${id} GET`,
prop2: `${prop2} GET ${id}`,
});
let fetchedRecord = await db.get({ date, id }).go();
expect(fetchedRecord).to.deep.equal({
id,
date,
prop1: `${prop1} SET ${id} GET`,
prop2: `${prop2} GET ${id}`,
});
let updatedProp1 = "ZZZ";
let updatedRecord = await db
.update({ date, id })
.set({ prop1: updatedProp1 })
.go();
expect(updatedRecord).to.deep.equal({});
let getUpdatedRecord = await db.get({ date, id }).go();
expect(getUpdatedRecord).to.deep.equal({
id,
date,
prop1: "ZZZ SET GET",
prop2: "bbb GET " + id,
});
});
});
describe("Query Options", async () => {
let db = new Entity(
{
service: "testing",
entity: "tester",
table: "electro",
version: "1",
attributes: {
id: {
type: "string",
},
date: {
type: "string",
},
someValue: {
type: "string",
required: true,
set: val => val + " wham",
get: val => val + " bam",
},
},
indexes: {
record: {
pk: {
field: "pk",
facets: ["date"],
},
sk: {
field: "sk",
facets: ["id"],
},
},
},
},
{ client },
);
it("Should return the originally returned results", async () => {
let id = uuidv4();
let date = moment.utc().format();
let someValue = "ABDEF";
let putRecord = await db.put({ id, date, someValue }).go({ raw: true });
expect(putRecord).to.deep.equal({});
let getRecord = await db.get({ id, date }).go({ raw: true });
expect(getRecord).to.deep.equal({
Item: {
id,
date,
someValue: someValue + " wham",
sk: `$tester#id_${id}`.toLowerCase(),
pk: `$testing_1#date_${date}`.toLowerCase(),
},
});
let updateRecord = await db
.update({ id, date })
.set({ someValue })
.go({ raw: true });
expect(updateRecord).to.deep.equal({});
let queryRecord = await db.query.record({ id, date }).go({ raw: true });
expect(queryRecord).to.deep.equal({
Items: [
{
id,
date,
someValue: someValue + " wham",
sk: `$tester#id_${id}`.toLowerCase(),
pk: `$testing_1#date_${date}`.toLowerCase(),
},
],
Count: 1,
ScannedCount: 1,
});
});
});
describe("Filters", async () => {

@@ -372,2 +529,4 @@ it("Should filter results with custom user filter", async () => {

let property = "ABDEF";
let recordParams = db.put({ date, property }).params();
expect(recordParams.Item.propertyVal).to.equal(property);
let record = await db.put({ date, property }).go();

@@ -378,4 +537,9 @@ let found = await db.query

.go();
console.log("RECORD", record);
console.log("FOUND", found);
let foundParams = db.query
.record({ date })
.filter(attr => attr.property.eq(property))
.params();
expect(foundParams.ExpressionAttributeNames["#property"]).to.equal(
"propertyVal",
);
expect(found)

@@ -382,0 +546,0 @@ .to.be.an("array")

@@ -133,5 +133,22 @@ const { Entity, clauses } = require("../src/entity");

describe("Entity", () => {
describe("Schema parsing", () => {
describe("'client' validation", () => {
let mall = "EastPointe";
let store = "LatteLarrys";
let building = "BuildingA";
let category = "food/coffee";
let unit = "B54";
let leaseEnd = "2020-01-20";
let rent = "0.00";
let MallStores = new Entity(schema);
// console.log(JSON.stringify(MallStores.schema));
expect(() =>
MallStores.put({
store,
mall,
building,
rent,
category,
leaseEnd,
unit,
}).go(),
).to.throw("No client defined on model");
});

@@ -588,2 +605,222 @@ describe("Schema validation", () => {

});
it("Should allow facets to be a facet template (string)", () => {
const schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
field: "storeLocationId",
},
date: {
type: "string",
field: "dateTime"
},
prop1: {
type: "string",
},
prop2: {
type: "string",
}
},
indexes: {
record: {
pk: {
field: "pk",
facets: `id_:id#p1_:prop1`
},
sk: {
field: "sk",
facets: `d_:date#p2_:prop2`
}
}
}
}
let mallStore = new Entity(schema);
let putParams = mallStore.put({
id: "IDENTIFIER",
date: "DATE",
prop1: "PROPERTY1",
prop2: "PROPERTY2"
}).params();
expect(putParams).to.deep.equal({
Item: {
storeLocationId: 'IDENTIFIER',
dateTime: 'DATE',
prop1: 'PROPERTY1',
prop2: 'PROPERTY2',
pk: '$mallstoredirectory_1#id_identifier#p1_property1',
sk: '$mallstores#d_date#p2_property2'
},
TableName: 'StoreDirectory'
});
});
it("Should throw on invalid characters in facet template (string)", () => {
const schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
field: "storeLocationId",
},
date: {
type: "string",
field: "dateTime"
},
prop1: {
type: "string",
},
prop2: {
type: "string",
}
},
indexes: {
record: {
pk: {
field: "pk",
facets: `id_:id#p1_:prop1`
},
sk: {
field: "sk",
facets: `d_:date|p2_:prop2`
}
}
}
}
expect(() => new Entity(schema)).to.throw(`Invalid key facet template. Allowed characters include only "A-Z", "a-z", "1-9", ":", "_", "#". Received: d_:date|p2_:prop2`);
});
it("Should default labels to facet attribute names in facet template (string)", () => {
const schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
field: "storeLocationId",
},
date: {
type: "string",
field: "dateTime"
},
prop1: {
type: "string",
},
prop2: {
type: "string",
}
},
indexes: {
record: {
pk: {
field: "pk",
facets: `id_:id#:prop1`
},
sk: {
field: "sk",
facets: `:date#p2_:prop2`
}
}
}
}
let mallStore = new Entity(schema);
let putParams = mallStore.put({
id: "IDENTIFIER",
date: "DATE",
prop1: "PROPERTY1",
prop2: "PROPERTY2"
}).params();
expect(putParams).to.deep.equal({
Item: {
storeLocationId: 'IDENTIFIER',
dateTime: 'DATE',
prop1: 'PROPERTY1',
prop2: 'PROPERTY2',
pk: '$mallstoredirectory_1#id_identifier#prop1_property1',
sk: '$mallstores#date_date#p2_property2'
},
TableName: 'StoreDirectory'
})
});
it("Should throw on invalid characters in facet template (string)", () => {
const schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
field: "storeLocationId",
},
date: {
type: "string",
field: "dateTime"
},
prop1: {
type: "string",
},
prop2: {
type: "string",
}
},
indexes: {
record: {
pk: {
field: "pk",
facets: `id_:id#p1_:prop1`
},
sk: {
field: "sk",
facets: `dbsfhdfhsdshfshf`
}
}
}
}
expect(() => new Entity(schema)).to.throw(`Invalid key facet template. No facets provided, expected at least one facet with the format ":attributeName". Received: dbsfhdfhsdshfshf`);
});
it("Should throw when defined facets are not in attributes: facet template and facet array", () => {
const schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
field: "storeLocationId",
},
date: {
type: "string",
field: "dateTime"
},
prop1: {
type: "string",
},
prop2: {
type: "string",
}
},
indexes: {
record: {
pk: {
field: "pk",
facets: ["id", "prop5"]
},
sk: {
field: "sk",
facets: `:date#p3_:prop3#p4_:prop4`
}
}
}
}
expect(() => new Entity(schema)).to.throw(`Invalid key facet template. The following facet attributes were described in the key facet template but were not included model's attributes: "pk: prop5", "sk: prop3", "sk: prop4"`);
});
});

@@ -590,0 +827,0 @@

@@ -182,3 +182,39 @@ const moment = require("moment");

});
it("Should not allow filters named 'go', 'params', or 'filter'", () => {
let schema = {
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
id: {
type: "string",
default: () => uuidV4(),
facets: "storeLocationId",
},
},
indexes: {
record: {
pk: {
field: "pk",
facets: ["id"],
},
},
},
filters: {},
};
schema.filters = { go: () => "" };
expect(() => new Entity(schema)).to.throw(
`Invalid filter name. Filter cannot be named "go", "params", or "filter"`,
);
schema.filters = { params: () => "" };
expect(() => new Entity(schema)).to.throw(
`Invalid filter name. Filter cannot be named "go", "params", or "filter"`,
);
schema.filters = { filter: () => "" };
expect(() => new Entity(schema)).to.throw(
`Invalid filter name. Filter cannot be named "go", "params", or "filter"`,
);
});
});
});

@@ -6,6 +6,3 @@ tests:

entity
- test for connected delete test w/ sortKey
- test for each option
- raw
- original error

@@ -19,8 +16,4 @@ - includeKeys

- test for bad "isValid" attribute result
features:
- if no client passed, no calling "go" method
- find method
- scan method
- go, params, filter as invalid filter names
- scan method
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc