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.19 to 0.9.0

src/clauses.js

9

.vscode/launch.json

@@ -10,9 +10,14 @@ {

"request": "launch",
"name": "Launch Program",
"name": "Mocha test",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/test/debug.js"
"args": [
"--inspect-brk",
"${workspaceFolder}/test/connected.service.spec.js"
],
"port": 9229,
"program": "${workspaceRoot}/node_modules/mocha/bin/mocha"
}
]
}
const { Entity } = require("./src/entity");
const { Service } = require("./src/Service");
module.exports = { Entity };
module.exports = { Entity, Service };
{
"name": "electrodb",
"version": "0.8.19",
"version": "0.9.00",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",

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

@@ -0,1 +1,2 @@

# ElectroDB

@@ -14,17 +15,11 @@

- **Simple Filter Composition**: Easily create complex readable filters for Dynamo queries without worrying about the implementation of `ExpressionAttributeNames`, `ExpressionAttributeValues`.
- **Easily Cross Entity Queries**: Define "collections" to create powerful/peformant queries that return multiple entities in a single request.
Turn this:
```javascript
let MallStores = new Entity(model, client);
let mallId = "EastPointe";
let stateDate = "2020-04-01";
let endDate = "2020-07-01";
let maxRent = "5000.00";
let minRent = "2000.00";
let promotion = "1000.00";
let stores = MallStores.query
.leases({ mallId })
.between({ leaseEndDate: stateDate }, { leaseEndDate: endDate })
MallStores.query
.leases({ mallId: "EastPointe" })
.between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
.filter(({rent, discount}) => `
${rent.between(minRent, maxRent)} AND ${discount.lte(promotion)}
${rent.between("2000.00", "5000.00")} AND ${discount.lte("1000.00")}
`)

@@ -56,54 +51,80 @@ .params();

```
Table of Contents
=================
- [ElectroDB](#electrodb)
- [Features](#features)
* [Features](#features)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Usage](#usage)
- [Entities](#entities)
- [Model](#model)
- [Attributes](#attributes)
- [Indexes](#indexes)
- [Facets](#facets)
- [Facet Arrays](#facet-arrays)
- [Facet Templates](#facet-templates)
- [Filters](#filters)
- [Defined on the model](#defined-on-the-model)
- [Defined via "Filter" method after query operators](#defined-via-filter-method-after-query-operators)
- [Multiple Filters](#multiple-filters)
- [Entities and Services](#entities-and-services)
- [Entities](#entities)
- [Services](#services)
* [Model](#model)
+ [Model Properties:](#model-properties-)
+ [Service Properties](#service-properties)
+ [Model/Service Options](#model-service-options)
* [Attributes](#attributes)
* [Indexes](#indexes)
* [Facets](#facets)
+ [Facet Arrays](#facet-arrays)
+ [Facet Templates](#facet-templates)
* [Collections](#collections)
* [Filters](#filters)
+ [Defined on the model](#defined-on-the-model)
+ [Defined via "Filter" method after query operators](#defined-via--filter--method-after-query-operators)
+ [Multiple Filters](#multiple-filters)
- [Building Queries](#building-queries)
- [Sort Key Operations](#sort-key-operations)
- [Using facets to make hierarchical keys](#using-facets-to-make-hierarchical-keys)
- [Shopping Mall Stores](#shopping-mall-stores)
- [Query Chains](#query-chains)
- [`Get` Method](#get-method)
- [`Delete` Method](#delete-method)
- [`Put` Record](#put-record)
- [`Update` Record](#update-record)
- [`Scan` Records](#scan-records)
- [`Query` Records](#query-records)
- [Partition Key Facets](#partition-key-facets)
- [Execute Query `.go() and .params()`](#execute-query-go-and-params)
- [`.params()`](#params)
- [`.go()`](#go)
- [Query Chain Examples](#query-chain-examples)
- [Query Options](#query-options)
+ [Sort Key Operations](#sort-key-operations)
+ [Using facets to make hierarchical keys](#using-facets-to-make-hierarchical-keys)
- [Shopping Mall Stores](#shopping-mall-stores)
* [Query Chains](#query-chains)
+ [`Get` Method](#-get--method)
+ [`Delete` Method](#-delete--method)
+ [`Put` Record](#-put--record)
+ [`Update` Record](#-update--record)
+ [`Scan` Records](#-scan--records)
+ [`Query` Records](#-query--records)
- [Partition Key Facets](#partition-key-facets)
* [Collection Chains](#collection-chains)
* [Execute Query `.go() and .params()`](#execute-query--go---and-params---)
+ [`.params()`](#-params---)
+ [`.go()`](#-go---)
* [Query Examples](#query-examples)
* [Query Options](#query-options)
- [Examples](#examples)
- [Shopping Mall Property Management App](#shopping-mall-property-management-app)
- [Shopping Mall Requirements](#shopping-mall-requirements)
- [Access Patterns are accessible on the StoreLocation](#access-patterns-are-accessible-on-the-storelocation)
- [`PUT` Record](#put-record-1)
- [Add a new Store to the Mall:](#add-a-new-store-to-the-mall)
- [`UPDATE` Record](#update-record-1)
- [Change the Store's Lease Date:](#change-the-stores-lease-date)
- [`GET` Record](#get-record)
- [Retrieve a specific Store in a Mall](#retrieve-a-specific-store-in-a-mall)
- [`DELETE` Record](#delete-record)
- [Remove a Store location from the Mall](#remove-a-store-location-from-the-mall)
- [`Query` Records](#query-records-1)
- [Find Stores that match core access patterns](#find-stores-that-match-core-access-patterns)
- [Coming Soon:](#coming-soon)
* [Employee App](#employee-app)
+ [Employee App Requirements](#employee-app-requirements)
+ [Entities](#entities-1)
+ [`Query` Records](#-query--records-1)
- [All tasks and employee information for a given employee](#all-tasks-and-employee-information-for-a-given-employee--requirement--1---employee-app-requirements-)
- [Find all employees and office details for a given office](#find-all-employees-and-office-details-for-a-given-office--requirement--2---employee-app-requirements-)
- [Tasks for a given employee](#tasks-for-a-given-employee--requirement--3---employee-app-requirements-)
- [Tasks for a given project](#tasks-for-a-given-project--requirement--4---employee-app-requirements-)
- [Find office locations](#find-office-locations--requirement--5---employee-app-requirements-)
- [Find employee salaries and titles](#find-employee-salaries-and-titles--requirement--6---employee-app-requirements-)
- [Find employee birthday/anniversary](#find-employee-birthday-anniversary--requirement--7---employee-app-requirements-)
- [Find direct reports](#find-direct-reports--requirement--8---employee-app-requirements-)
* [Shopping Mall Property Management App](#shopping-mall-property-management-app)
+ [Shopping Mall Requirements](#shopping-mall-requirements)
+ [Access Patterns are accessible on the StoreLocation](#access-patterns-are-accessible-on-the-storelocation)
+ [`PUT` Record](#-put--record)
- [Add a new Store to the Mall](#add-a-new-store-to-the-mall-)
+ [`UPDATE` Record](#-update--record)
- [Change the Store's Lease Date](#change-the-store-s-lease-date-)
+ [`GET` Record](#-get--record)
- [Retrieve a specific Store in a Mall](#retrieve-a-specific-store-in-a-mall)
+ [`DELETE` Record](#-delete--record)
- [Remove a Store location from the Mall](#remove-a-store-location-from-the-mall)
+ [`Query` Records](#-query--records-2)
- [All Stores in a particular mall](#all-stores-in-a-particular-mall---requirement--1---shopping-mall-requirements--)
- [All Stores in a particular mall building](#all-stores-in-a-particular-mall-building---requirement--1---shopping-mall-requirements--)
- [What store is located in unit "B47"?)](#what-store-is-located-in-unit--b47-----requirement--1---shopping-mall-requirements--)
- [Stores by Category at Mall](#stores-by-category-at-mall---requirement--2---shopping-mall-requirements--)
- [Stores by upcoming lease](#stores-by-upcoming-lease---requirement--3---shopping-mall-requirements--)
- [Stores will renewals for Q4](#stores-will-renewals-for-q4---requirement--3---shopping-mall-requirements--)
- [Spite-stores with release renewals this year](#spite-stores-with-release-renewals-this-year----requirement--3---shopping-mall-requirements--)
- [All Latte Larry's in a particular mall building](#all-latte-larry-s-in-a-particular-mall-building--crazy-for-any-store-except-a-coffee-shop-)
- [Coming Soon:](#coming-soon-)
# Installation

@@ -117,102 +138,234 @@

# Usage
Unlike in traditional sql databases, a single *DynamoDB* table will include multiple entities along side each other. Additionally, *DynamoDB* utilizes *Partition* and *Sort Keys* to query records to allow for hierarchical relationships. ElectroDB allows you to make the most of these concepts with less headaches.
Require or import `Entity` or `Service` from `electrodb`:
```javascript
const {Entity, Service} = require("electrodb");
```
## Entities
# Entities and Services
> To see full examples of ***ElectroDB*** in action, go to the [Examples](#examples) section.
In ***ElectroDB*** an `Entity` is a single record that represents a single business object. For example, in a simple contact application, one entity could represent a Person and another entity might represent a Contact method for that person (email, phone, etc.).
`Entity` allows you to create separate and individual business objects in a *DynamoDB* table. When queried your results will not include other Entities that exist the same table. For more detail, read [Entities](#entities).
`Service` allows you to build a relationships across Entities. A service imports Entity [Models](#model), builds individual Entities and builds [Collections](#collections) for cross Entity querying. For more detail, read [Services](#services).
You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries `join` multiple Entities.
# Entities
In ***ElectroDB*** an `Entity` is represents a single business object. For example, in a simple task tracking application, one Entity could represent an Employee and another Entity might represent a the Task that the employee is assigned to.
Require or import `Entity` from `electrodb`:
```javascript
const {Entity} = require("electrodb");
const {Entity, Service} = require("electrodb");
```
## Model
# Services
In ***ElectroDB*** a `Service` represents a collection of Entities and also allows you to build queries span across Entities. Similar to Entities, Services can coexist on a single table without collision. You can use Entities independent of Services, you do not need to import models into a Service to use them individually. However, you do you need to use a Service if you intend make queries `join` multiple Entities.
Create an Entity's schema
## Model
Create an Entity's schema. In the below example.
```javascript
const DynamoDB = require("aws-sdk/clients/dynamodb");
const {Entity} = require("electrodb");
const {Entity, Service} = require("electrodb");
const client = new DynamoDB.DocumentClient();
let model = {
service: "ClientRelationshipApp",
entity: "UserContacts",
table: "ClientContactTable",
version: "1",
const EmployeesModel = {
entity: "employees",
version: "1",
service: "taskapp",
table: "projectmanagement",
attributes: {
clientId: {
employee: {
type: "string",
required: true,
},
userId: {
type: "string",
required: true,
default: () => uuidv4(),
},
type: {
type: ["email", "phone", "social"],
required: true,
firstName: {
type: "string",
},
value: {
type: "string",
required: true,
lastName: {
type: "string",
},
canContactWeekends: {
type: "boolean",
required: false
office: {
type: "string",
},
maxContactFrequency: {
type: "number",
required: true,
default: 5
}
title: {
type: "string",
},
team: {
type: ["development", "marketing", "finance", "product"],
},
salary: {
type: "string",
},
manager: {
type: "string",
},
dateHired: {
type: "string",
},
birthday: {
type: "string",
},
},
indexes: {
contact: {
employee: {
pk: {
field: "PK",
facets: ["value"]
field: "pk",
facets: ["employee"],
},
sk: {
field: "SK",
facets: ["clientId", "userId"]
}
field: "sk",
facets: [],
},
},
clientContact: {
index: "GSI1PK-GSI1SK-Index",
coworkers: {
index: "gsi1pk-gsi1sk-index",
collection: "workplaces",
pk: {
field: "GSI1PK",
facets: ["clientId"]
field: "gsi1pk",
facets: ["office"],
},
sk: {
field: "GSI1SK",
facets: ["value", "userId"]
}
field: "gsi1sk",
facets: ["team", "title", "employee"],
},
},
userContact: {
index: "GSI2PK-GSI2SK-Index",
teams: {
index: "gsi2pk-gsi2sk-index",
pk: {
field: "GSI2PK",
facets: ["userId"]
field: "gsi2pk",
facets: ["team"],
},
sk: {
field: "GSI2SK",
facets: ["userId", "value"]
}
}
field: "gsi2sk",
facets: ["title", "salary", "employee"],
},
},
employeeLookup: {
collection: "assignments",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
facets: ["employee"],
},
sk: {
field: "gsi3sk",
facets: [],
},
},
roles: {
index: "gsi4pk-gsi4sk-index",
pk: {
field: "gsi4pk",
facets: ["title"],
},
sk: {
field: "gsi4sk",
facets: ["salary", "employee"],
},
},
directReports: {
index: "gsi5pk-gsi5sk-index",
pk: {
field: "gsi5pk",
facets: ["manager"],
},
sk: {
field: "gsi5sk",
facets: ["team", "office", "employee"],
},
},
},
filters: {
weekendFrequency: (attributes, max, canContact) => {
let {maxContactFrequency, canContactWeekends} = attributes;
return `
${maxContactFrequency.lte(max)} AND ${canContactWeekends.eq(canContact)}
`
}
}
upcomingCelebrations: (attributes, startDate, endDate) => {
let { dateHired, birthday } = attributes;
return `${dateHired.between(startDate, endDate)} OR ${birthday.between(
startDate,
endDate,
)}`;
},
},
};
const UserContacts = new Entity(model, {client});
const TasksModel = {
entity: "tasks",
version: "1",
service: "taskapp",
table: "projectmanagement",
attributes: {
task: {
type: "string",
default: () => uuidv4(),
},
project: {
type: "string",
},
employee: {
type: "string",
},
description: {
type: "string",
},
},
indexes: {
task: {
pk: {
field: "pk",
facets: ["task"],
},
sk: {
field: "sk",
facets: ["project", "employee"],
},
},
project: {
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
facets: ["project"],
},
sk: {
field: "gsi1sk",
facets: ["employee", "task"],
},
},
assigned: {
collection: "assignments",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
facets: ["employee"],
},
sk: {
field: "gsi3sk",
facets: ["project", "task"],
},
},
},
};
```
Create individual Entities with the Models or `join` them via a Service:
```javascript
// Independent Models
let employees = new Entity(EmployeesModel, { client });
let tasks = new Entity(TasksModel, { client });
```
```javascript
// Joined via a Service
let TaskApp = new Service({
version: "1",
service: "TaskApp",
table: "projectmanagement",
},
{ client },
);
TaskApp.join(EmployeesModel); // TaskApp.entities.employees
TaskApp.join(TasksModel); // TaskApp.entities.tasks
```
### Model Properties:
| Property | Description |

@@ -228,2 +381,16 @@ | ----------- | ----------- |

### Service Properties
| Property | Description |
| ----------- | ----------- |
| service | Name of the service, used to namespace all joined entities, will override the model definition. |
table | Name of the dynamodb table in aws, will override the model definition. |
version | (optional) The version number of the schema, used to namespace keys, will override the model definition. |
### Model/Service Options
Optional second parameter
| Property | Description |
| ----------- | ----------- |
| client | (optional) A docClient instance for use when querying a DynamoDB table. This is optional if you wish to only use the `params` functionality, but required if you actually need to query against a database.
## Attributes

@@ -238,12 +405,12 @@ **Attributes** define an **Entity** record. The `propertyName` represents the value your code will use to represent an attribute.

<AttributeName>: {
"type": <string|string[]>,
"required": [boolean],
"default": [value|() => value]
"validate": [RegExp|() => boolean]
"field": [string]
"readOnly": [boolean]
"label": [string]
"cast": ["number"|"string"|"boolean"],
"get": (attribute, schema) => value,
"set": (attribute, schema) => value
"type": string|string[],
"required"?: boolean,
"default"?: value|() => value
"validate"?: RegExp|() => boolean
"field"?: string
"readOnly"?: boolean
"label"?: string
"cast"?: "number"|"string"|"boolean",
"get"?: (attribute, schema) => value,
"set"?: (attribute, schema) => value
}

@@ -278,7 +445,8 @@ }

},
"sk": {
"sk"?: {
"field": <string>
"facets": <AttributesName[]>
},
"index": [string]
"index"?: string
"collection"?: string
}

@@ -296,3 +464,4 @@ }

`sk.field` | `string` | yes | The name of the attribute as it exists dynamo, if named differently in the schema attributes. |
`index` | `string` | yes | Used only when the `Index` defined is a *Global Secondary Index*; this is left blank for the table's primary index.
`index` | `string` | no | Required when the `Index` defined is a *Secondary Index*; but is left blank for the table's primary index. |
`collection` | `string` | no | Used when models are joined to a `Service`. When two entities share a `collection` on the same `index`, they can be queried with one request to DynamoDB. The name of the collection should represent what the query would return as a pseudo `Entity`. (see [Collections](#collections) below for more on this functionality).

@@ -382,6 +551,5 @@ ## Facets

> ElectroDB will not prefix templated keys with the Entity, Project, Version, or Collection. This will give you greater control of your keys but will limit ElectroDBs ability to prevent leaking entities with some queries.
> ***ElectroDB*** will not prefix templated keys with the Entity, Project, Version, or Collection. This will give you greater control of your keys but will limit ***ElectroDB's*** ability to prevent leaking entities with some queries.
Facet Templates have some "gotchas" to consider:
1. Keys only allow for one instance of an attribute, the template `:prop1#:prop1` will be interpreted the same as `:prop1#`.

@@ -435,2 +603,31 @@ 2. ElectoDB will continue to always add a trailing delimiter to facets with keys are partially supplied. (More documentation coming on this soon)

## Collections
A Collection is a grouping of Entities with the same Partition Key and allows you to make efficient query across multiple entities. If you background is SQL, imagine Partition Keys as Foreign Keys, a Collection represents a View with multiple joined Entities.
Collections are defined on an Index, and the name of the collection should represent what the query would return as a pseudo `Entity`.
> **Note**: `collection` should be unique to a single common index across entities.
Using the TaskApp Models defined in [Models](#model), these models share a `collection` called `assignments` on the index `gsi3pk-gsi3sk-index`
```javascript
let TaskApp = new Service({
version: "1",
service: "TaskApp",
table: "projectmanagement"
}, { client });
TaskApp.join(EmployeesModel); // TaskApp.entities.employees
TaskApp.join(TasksModel); // TaskApp.entities.tasks
TaskApp.collections.assignments({employee: "JExotic"}).params();
// Results
{
TableName: 'projectmanagement',
ExpressionAttributeNames: { '#pk': 'gsi3pk', '#sk1': 'gsi3sk' },
ExpressionAttributeValues: { ':pk': '$taskapp_1#employee_joeexotic', ':sk1': '$assignments' },
KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
IndexName: 'gsi3pk-gsi3sk-index'
}
```
## Filters

@@ -479,6 +676,3 @@ 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.

let MallStores = new Entity(model, client);
let mallId = "EastPointe";
let stateDate = "2020-04-01";
let endDate = "2020-07-01";
let MallStores = new Entity("MallStores", model);
let maxRent = "5000.00";

@@ -488,4 +682,4 @@ let minRent = "2000.00";

let stores = MallStores.query
.stores({ mallId })
.between({ leaseEndDate: stateDate }, { leaseEndDate: endDate })
.stores({ mallId: "EastPointe" })
.between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
.rentPromotions(minRent, maxRent, promotion)

@@ -518,6 +712,3 @@ .params();

```javascript
let MallStores = new Entity(model, client);
let mallId = "EastPointe";
let stateDate = "2020-04-01";
let endDate = "2020-07-01";
let MallStores = new Entity("MallStores", model);
let maxRent = "5000.00";

@@ -527,4 +718,4 @@ let minRent = "2000.00";

let stores = MallStores.query
.leases({ mallId })
.between({ leaseEndDate: stateDate }, { leaseEndDate: endDate })
.leases({ mallId: "EastPointe" })
.between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
.filter(({rent, discount}) => `

@@ -580,14 +771,8 @@ ${rent.between(minRent, maxRent)} AND ${discount.lte(promotion)}

```javascript
let MallStores = new Entity(model, client);
let mallId = "EastPointe";
let stateDate = "2020-04-01";
let endDate = "2020-07-01";
let maxRent = "5000.00";
let minRent = "2000.00";
let promotion = "1000.00";
let MallStores = new Entity("MallStores", model);
let stores = MallStores.query
.leases({ mallId })
.between({ leaseEndDate: stateDate }, { leaseEndDate: endDate })
.leases({ mallId: "EastPointe" })
.between({ leaseEndDate: "2020-04-01" }, { leaseEndDate: "2020-07-01" })
.filter(({ rent, discount }) => `
${rent.between(minRent, maxRent)} AND ${discount.eq(promotion)}
${rent.between("2000.00", "5000.00")} AND ${discount.eq("1000.00")}
`)

@@ -761,7 +946,8 @@ .filter(({ category }) => `

```javascript
let storeId = "LatteLarrys";
let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
await StoreLocations.get({storeId, mallId, buildingId, unitId}).go();
await StoreLocations.get({
storeId: "LatteLarrys",
mallId: "EastPointe",
buildingId: "BuildingA1",
unitId: "B47"
}).go();
```

@@ -772,7 +958,8 @@ ### `Delete` Method

```javascript
let storeId = "LatteLarrys";
let mallId = "EastPointe";
let buildingId = "BuildingA1";
let unitId = "B47";
await StoreLocations.delete({storeId, mallId, buildingId, unitId}).go();
await StoreLocations.delete({
storeId: "LatteLarrys",
mallId: "EastPointe",
buildingId: "BuildingA1",
unitId: "B47"
}).go();
```

@@ -862,2 +1049,54 @@

## Collection Chains
Collections allow you to query across Entities. To use them you need to `join` your Models onto a `Service` instance.
> Using the TaskApp Models defined in [Models](#model), these models share a `collection` called `assignments` on the index `gsi3pk-gsi3sk-index`
```javascript
let TaskApp = new Service({
version: "1",
service: "TaskApp",
table: "projectmanagement"
}, { client });
TaskApp.join(EmployeesModel); // TaskApp.entities.employees
TaskApp.join(TasksModel); // TaskApp.entities.tasks
```
Available on your Service are two objects: `entites` and `collections`. Entities available on `entities` have the same capabilities as they would if created individually. When a Model added to a Service with `join` however, its Collections are automatically added and validated with the other Models joined to that Service. These Collections are available on `collections`.
```javascript
TaskApp.collections.assignments({employee: "JExotic"}).params();
// Results
{
TableName: 'projectmanagement',
ExpressionAttributeNames: { '#pk': 'gsi3pk', '#sk1': 'gsi3sk' },
ExpressionAttributeValues: { ':pk': '$taskapp_1#employee_joeexotic', ':sk1': '$assignments' },
KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
IndexName: 'gsi3pk-gsi3sk-index'
}
```
Collections do not have the same `query` functionality and as an Entity, though it does allow for inline filters like an Entity. The `attributes` available on the filter object include **all** attributes across entities.
```javascript
TaskApp.collections
.assignments({employee: "CBaskin"})
.filter((attributes) => `
${attributes.project.notExists()} OR ${attributes.project.contains("murder")}
`)
// Results
{
TableName: 'projectmanagement',
ExpressionAttributeNames: { '#project': 'project', '#pk': 'gsi3pk', '#sk1': 'gsi3sk' },
ExpressionAttributeValues: {
':project1': 'murder',
':pk': '$taskapp_1#employee_carolbaskin',
':sk1': '$assignments'
},
KeyConditionExpression: '#pk = :pk and begins_with(#sk1, :sk1)',
IndexName: 'gsi3pk-gsi3sk-index',
FilterExpression: '\n\t\tattribute_not_exists(#project) OR contains(#project, :project1)\n\t'
}
```
## Execute Query `.go() and .params()`

@@ -916,3 +1155,3 @@ Lastly, all query chains end with either a `.go()` or a `.params()` method invocation. These will either execute the query to DynamoDB (`.go()`) or return formatted parameters for use with the DynamoDB docClient (`.params()`).

## Query Chain Examples
## Query Examples
Below are _all_ chain possibilities available, given the `MallStore` model.

@@ -981,5 +1220,459 @@

### Shopping Mall Property Management App
## Employee App
For an example, lets look at the needs of application used to manage Employees. The application Looks at employees, offices, tasks, and projects.
### Employee App Requirements
1. As Project Manager I need to find all tasks and details on a specific employee.
2. As a Regional Manager I need to see all details about an office and its employees
3. As an Employee I need to see all my Tasks.
4. As a Product Manager I need to see all the tasks for a project.
5. As a Client I need to find a physical office close to me.
6. As a Hiring manager I need to find employees with comparable salaries.
7. As HR I need to find upcoming employee birthdays/anniversaries
8. As HR I need to find all the employees that report to a specific manager
### Entities
```javascript
const EmployeesModel = {
entity: "employees",
version: "1",
service: "taskapp",
table: "projectmanagement",
attributes: {
employee: {
type: "string",
},
firstName: {
type: "string",
},
lastName: {
type: "string",
},
office: {
type: "string",
},
title: {
type: "string",
},
team: {
type: ["development", "marketing", "finance", "product"],
},
salary: {
type: "string",
},
manager: {
type: "string",
},
dateHired: {
type: "string",
},
birthday: {
type: "string",
},
},
indexes: {
employee: {
pk: {
field: "pk",
facets: ["employee"],
},
sk: {
field: "sk",
facets: [],
},
},
coworkers: {
index: "gsi1pk-gsi1sk-index",
collection: "workplaces",
pk: {
field: "gsi1pk",
facets: ["office"],
},
sk: {
field: "gsi1sk",
facets: ["team", "title", "employee"],
},
},
teams: {
index: "gsi2pk-gsi2sk-index",
pk: {
field: "gsi2pk",
facets: ["team"],
},
sk: {
field: "gsi2sk",
facets: ["title", "salary", "employee"],
},
},
employeeLookup: {
collection: "assignements",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
facets: ["employee"],
},
sk: {
field: "gsi3sk",
facets: [],
},
},
roles: {
index: "gsi4pk-gsi4sk-index",
pk: {
field: "gsi4pk",
facets: ["title"],
},
sk: {
field: "gsi4sk",
facets: ["salary", "employee"],
},
},
directReports: {
index: "gsi5pk-gsi5sk-index",
pk: {
field: "gsi5pk",
facets: ["manager"],
},
sk: {
field: "gsi5sk",
facets: ["team", "office", "employee"],
},
},
},
filters: {
upcomingCelebrations: (attributes, startDate, endDate) => {
let { dateHired, birthday } = attributes;
return `${dateHired.between(startDate, endDate)} OR ${birthday.between(
startDate,
endDate,
)}`;
},
},
};
const TasksModel = {
entity: "tasks",
version: "1",
service: "taskapp",
table: "projectmanagement",
attributes: {
task: {
type: "string",
},
project: {
type: "string",
},
employee: {
type: "string",
},
description: {
type: "string",
},
},
indexes: {
task: {
pk: {
field: "pk",
facets: ["task"],
},
sk: {
field: "sk",
facets: ["project", "employee"],
},
},
project: {
index: "gsi1pk-gsi1sk-index",
pk: {
field: "gsi1pk",
facets: ["project"],
},
sk: {
field: "gsi1sk",
facets: ["employee", "task"],
},
},
assigned: {
collection: "assignements",
index: "gsi3pk-gsi3sk-index",
pk: {
field: "gsi3pk",
facets: ["employee"],
},
sk: {
field: "gsi3sk",
facets: ["project", "task"],
},
},
},
};
const OfficesModel = new Entity({
entity: "offices",
version: "1",
table: "electro",
service: "electrotest",
attributes: {
office: {
type: "string",
},
country: {
type: "string",
},
state: {
type: "string",
},
city: {
type: "string",
},
zip: {
type: "string",
},
address: {
type: "string",
},
},
indexes: {
locations: {
pk: {
field: "pk",
facets: ["country", "state"],
},
sk: {
field: "sk",
facets: ["city", "zip", "office"],
},
},
office: {
index: "gsi1pk-gsi1sk-index",
collection: "workplaces",
pk: {
field: "gsi1pk",
facets: ["office"],
},
sk: {
field: "gsi1sk",
facets: [],
},
},
},
});
```
Join models on a new `Service` called `EmployeeApp`
```javascript
const DynamoDB = require("aws-sdk/clients/dynamodb");
const client = new DynamoDB.DocumentClient({
region: "us-east-1",
});
const { Service } = require("electrodb");
let EmployeeApp = new Service(
{
version: "1",
service: "EmployeeApp",
table: "projectmanagement",
},
{ client },
);
EmployeeApp.join(EmployeesModel); // EmployeeApp.entities.employees
EmployeeApp.join(TasksModel); // EmployeeApp.entities.tasks
EmployeeApp.join(OfficesModel); // EmployeeApp.entities.tasks
```
### `Query` Records
#### All tasks and employee information for a given employee [Requirement #1](#employee-app-requirements)
```javascript
EmployeeApp.collections.assignements({employee: "CBaskin"}).go();
```
Returns the following:
```javascript
{
employees: [{
employee: "cbaskin",
firstName: "carol",
lastName: "baskin",
office: "big cat rescue",
title: "owner",
team: "cool cats and kittens",
salary: "1,000,000",
manager: "",
dateHired: "1992-11-04",
birthday: "1961-06-06",
}].
tasks: [{
task: "Feed tigers",
description: "Prepare food for tigers to eat",
project: "Keep tigers alive",
employee: "cbaskin"
}, {
task: "Fill water bowls",
description: "Ensure the tigers have enough water",
project: "Keep tigers alive",
employee: "cbaskin"
}]
}
```
#### Find all employees and office details for a given office [Requirement #2](#employee-app-requirements)
```javascript
EmployeeApp.collections.workplaces({office: "big cat rescue"}).go()
```
Returns the following:
```javascript
{
employees: [{
employee: "cbaskin",
firstName: "carol",
lastName: "baskin",
office: "big cat rescue",
title: "owner",
team: "cool cats and kittens",
salary: "1,000,000",
manager: "",
dateHired: "1992-11-04",
birthday: "1961-06-06",
}],
offices: [{
office: "big cat rescue",
country: "usa",
state: "florida",
city: "tampa"
zip: "12345"
address: "123 Kitty Cat Lane"
}]
}
```
#### Tasks for a given employee [Requirement #3](#employee-app-requirements)
```javascript
EmployeeApp.entities.tasks.query.assigned({employee: "cbaskin"}).go();
```
Returns the following:
```javascript
[
{
task: "Feed tigers",
description: "Prepare food for tigers to eat",
project: "Keep tigers alive",
employee: "cbaskin"
}, {
task: "Fill water bowls",
description: "Ensure the tigers have enough water",
project: "Keep tigers alive",
employee: "cbaskin"
}
]
```
#### Tasks for a given project [Requirement #4](#employee-app-requirements)
```javascript
EmployeeApp.entities.tasks.query.project({project: "Murder Carol"}).go();
```
Returns the following:
```javascript
[
{
task: "Hire hitman",
description: "Find someone to murder Carol",
project: "Murder Carol",
employee: "jexotic"
}
];
```
#### Find office locations [Requirement #5](#employee-app-requirements)
```javascript
EmployeeApp.entities.office.locations({country: "usa", state: "florida"}).go()
```
Returns the following:
```javascript
[
{
office: "big cat rescue",
country: "usa",
state: "florida",
city: "tampa"
zip: "12345"
address: "123 Kitty Cat Lane"
}
]
```
#### Find employee salaries and titles [Requirement #6](#employee-app-requirements)
```javascript
EmployeeApp.entities.employees
.roles({title: "animal wrangler"})
.lte({salary: "150.00"})
.go()
```
Returns the following:
```javascript
[
{
employee: "ssaffery",
firstName: "saff",
lastName: "saffery",
office: "gw zoo",
title: "animal wrangler",
team: "keepers",
salary: "105.00",
manager: "jexotic",
dateHired: "1999-02-23",
birthday: "1960-07-11",
}
]
```
#### Find employee birthday/anniversary [Requirement #7](#employee-app-requirements)
```javascript
EmployeeApp.entities.employees
.workplaces({office: "gw zoo"})
.upcomingCelebrations("2020-05-01", "2020-06-01")
.go()
```
Returns the following:
```javascript
[
{
employee: "jexotic",
firstName: "joe",
lastName: "maldonado-passage",
office: "gw zoo",
title: "tiger king",
team: "founders",
salary: "10000.00",
manager: "jlowe",
dateHired: "1999-02-23",
birthday: "1963-03-05",
}
]
```
#### Find direct reports [Requirement #8](#employee-app-requirements)
```javascript
EmployeeApp.entities.employees
.reports({manager: "jlowe"})
.go()
```
Returns the following:
```javascript
[
{
employee: "jexotic",
firstName: "joe",
lastName: "maldonado-passage",
office: "gw zoo",
title: "tiger king",
team: "founders",
salary: "10000.00",
manager: "jlowe",
dateHired: "1999-02-23",
birthday: "1963-03-05",
}
]
```
## Shopping Mall Property Management App
For an example, lets look at the needs of application used to manage Shopping Mall properties. The application assists employees in the day-to-day operations of multiple Shopping Malls.
#### Shopping Mall Requirements
### Shopping Mall Requirements
1. As a Maintenance Worker I need to know which stores are currently in each Mall down to the Building they are located.

@@ -997,4 +1690,4 @@ 2. As a Helpdesk Employee I need to locate related stores in Mall locations by Store Category.

Access Patterns are accessible on the StoreLocation
---
### Access Patterns are accessible on the StoreLocation
### `PUT` Record

@@ -1087,5 +1780,4 @@ #### Add a new Store to the Mall:

### `Query` Records
#### Find Stores that match core access patterns
All Stores in a particular mall ([Requirement #1](#shopping-mall-requirements))
#### All Stores in a particular mall ([Requirement #1](#shopping-mall-requirements))
```javascript

@@ -1096,3 +1788,3 @@

```
All Stores in a particular mall building ([Requirement #1](#shopping-mall-requirements))
#### All Stores in a particular mall building ([Requirement #1](#shopping-mall-requirements))
```javascript

@@ -1104,3 +1796,3 @@ let mallId = "EastPointe";

What store is located in unit "B47"? ([Requirement #1](#shopping-mall-requirements))
#### What store is located in unit "B47"? ([Requirement #1](#shopping-mall-requirements))
```javascript

@@ -1112,3 +1804,3 @@ let mallId = "EastPointe";

```
Stores by Category at Mall ([Requirement #2](#shopping-mall-requirements))
#### Stores by Category at Mall ([Requirement #2](#shopping-mall-requirements))
```javascript

@@ -1119,3 +1811,3 @@ let mallId = "EastPointe";

```
Stores by upcoming lease ([Requirement #3](#shopping-mall-requirements))
#### Stores by upcoming lease ([Requirement #3](#shopping-mall-requirements))
```javascript

@@ -1126,3 +1818,3 @@ let mallId = "EastPointe";

```
Stores will renewals for Q4 ([Requirement #3](#shopping-mall-requirements))
#### Stores will renewals for Q4 ([Requirement #3](#shopping-mall-requirements))
```javascript

@@ -1138,3 +1830,3 @@ let mallId = "EastPointe";

```
Spite-stores with release renewals this year ([Requirement #3](#shopping-mall-requirements))
#### Spite-stores with release renewals this year ([Requirement #3](#shopping-mall-requirements))
```javascript

@@ -1153,3 +1845,3 @@ let mallId = "EastPointe";

All Latte Larry's in a particular mall building (crazy for any store except a coffee shop)
#### All Latte Larry's in a particular mall building (crazy for any store except a coffee shop)
```javascript

@@ -1164,5 +1856,4 @@

# Coming Soon:
- `Collection` class for relating and querying across multiple entities, configuring/enforcing relationships
- `.page()` finish method (like `.params()` and `.go()`) to allow for easier pagination of results
- Additional query options like `limit`, `pages`, `attributes`, `sort` and more for easier querying.
- Default query options defined on the `model` to give more general control of interactions with the Entity.

@@ -6,239 +6,6 @@ "use strict";

const validations = require("./validations");
const { clauses } = require("./clauses");
let clauses = {
index: {
action(entity, state = {}, facets = {}) {
// todo: maybe all key info is passed on the subsequent query identifiers?
// todo: look for article/list of all dynamodb query limitations
return state;
},
children: ["get", "delete", "update", "query", "put", "scan"],
},
scan: {
action(entity, state) {
state.query.method = MethodTypes.scan;
return state;
},
children: ["params", "go"],
},
get: {
action(entity, state = {}, facets = {}) {
state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk);
state.query.method = MethodTypes.get;
state.query.type = QueryTypes.eq;
if (state.hasSortKey) {
let queryFacets = entity._buildQueryFacets(
facets,
state.query.facets.sk,
);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
}
return state;
},
children: ["params", "go"],
},
delete: {
action(entity, state = {}, facets = {}) {
state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk);
state.query.method = MethodTypes.delete;
state.query.type = QueryTypes.eq;
if (state.hasSortKey) {
let queryFacets = entity._buildQueryFacets(
facets,
state.query.facets.sk,
);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
}
return state;
},
children: ["params", "go"],
},
put: {
action(entity, state = {}, payload = {}) {
let record = entity.model.schema.checkCreate({ ...payload });
state.query.keys.pk = entity._expectFacets(record, state.query.facets.pk);
state.query.method = MethodTypes.put;
state.query.type = QueryTypes.eq;
if (state.hasSortKey) {
let queryFacets = entity._buildQueryFacets(
record,
state.query.facets.sk,
);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
}
state.query.put.data = Object.assign({}, record);
return state;
},
children: ["params", "go"],
},
update: {
action(entity, state = {}, facets = {}) {
state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk);
state.query.method = MethodTypes.update;
state.query.type = QueryTypes.eq;
if (state.hasSortKey) {
let queryFacets = entity._buildQueryFacets(
facets,
state.query.facets.sk,
);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
}
return state;
},
children: ["set"],
},
set: {
action(entity, state = {}, data) {
let record = entity.model.schema.checkUpdate({ ...data });
state.query.update.set = Object.assign(
{},
state.query.update.set,
record,
);
return state;
},
children: ["set", "go", "params"],
},
query: {
action(entity, state = {}, facets = {}) {
state.query.keys.pk = entity._expectFacets(facets, state.query.facets.pk);
entity._expectFacets(facets, Object.keys(facets), `"query" facets`);
state.query.method = MethodTypes.query;
state.query.type = QueryTypes.begins;
let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
return state;
},
children: ["between", "gte", "gt", "lte", "lt", "params", "go"],
},
between: {
action(entity, state = {}, startingFacets = {}, endingFacets = {}) {
entity._expectFacets(
startingFacets,
Object.keys(startingFacets),
`"between" facets`,
);
entity._expectFacets(
endingFacets,
Object.keys(endingFacets),
`"and" facets`,
);
state.query.type = QueryTypes.between;
let queryEndingFacets = entity._buildQueryFacets(
endingFacets,
state.query.facets.sk,
);
let queryStartingFacets = entity._buildQueryFacets(
startingFacets,
state.query.facets.sk,
);
state.query.keys.sk.push({
type: QueryTypes.and,
facets: queryEndingFacets,
});
state.query.keys.sk.push({
type: QueryTypes.between,
facets: queryStartingFacets,
});
return state;
},
children: ["go", "params"],
},
gt: {
action(entity, state = {}, facets = {}) {
entity._expectFacets(facets, Object.keys(facets), `"gt" facets`);
state.query.type = QueryTypes.gt;
let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
return state;
},
children: ["go", "params"],
},
gte: {
action(entity, state = {}, facets = {}) {
entity._expectFacets(facets, Object.keys(facets), `"gte" facets`);
state.query.type = QueryTypes.gte;
let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
return state;
},
children: ["go", "params"],
},
lt: {
action(entity, state = {}, facets = {}) {
entity._expectFacets(facets, Object.keys(facets), `"lt" facets`);
state.query.type = QueryTypes.lt;
let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
return state;
},
children: ["go", "params"],
},
lte: {
action(entity, state = {}, facets = {}) {
entity._expectFacets(facets, Object.keys(facets), `"lte" facets`);
state.query.type = QueryTypes.lte;
let queryFacets = entity._buildQueryFacets(facets, state.query.facets.sk);
state.query.keys.sk.push({
type: state.query.type,
facets: queryFacets,
});
return state;
},
children: ["go", "params"],
},
params: {
action(entity, state = {}, options) {
if (state.query.method === MethodTypes.query) {
return entity._queryParams(state.query, options);
} else {
return entity._params(state.query, options);
}
},
children: [],
},
go: {
action(entity, state = {}, options) {
if (entity.client === undefined) {
throw new Error("No client defined on model");
}
let params = {};
if (state.query.method === MethodTypes.query) {
params = entity._queryParams(state.query, options);
} else {
params = entity._params(state.query, options);
}
return entity.go(state.query.method, params, options);
},
children: [],
},
};
const utilities = {
structureFacets: function(
structureFacets: function (
structure,

@@ -265,5 +32,5 @@ { index, type, name } = {},

class Entity {
constructor(model = {}, { client } = {}) {
constructor(model = {}, config = {}) {
this._validateModel(model);
this.client = client;
this.client = config.client;
this.model = this._parseModel(model);

@@ -275,3 +42,4 @@ this._filterBuilder = new FilterFactory(

this.query = {};
let clausesWithFilters = this._injectFiltersIntoClauses(
let clausesWithFilters = this._filterBuilder.injectFilterClauses(
clauses,

@@ -297,34 +65,35 @@ this.model.filters,

_injectFiltersIntoClauses(clauses = {}, filters = {}) {
let injected = { ...clauses };
let filterParents = Object.entries(injected)
.filter(clause => {
let [name, { children }] = clause;
return children.includes("go");
})
.map(([name]) => name);
let modelFilters = Object.keys(filters);
let filterChildren = [];
for (let [name, filter] of Object.entries(filters)) {
filterChildren.push(name);
injected[name] = {
action: this._filterBuilder.buildClause(filter),
children: ["params", "go", "filter", ...modelFilters],
};
}
filterChildren.push("filter");
injected["filter"] = {
action: (entity, state, fn) => {
return this._filterBuilder.buildClause(fn)(entity, state);
},
children: ["params", "go", "filter", ...modelFilters],
};
for (let parent of filterParents) {
injected[parent] = { ...injected[parent] };
injected[parent].children = [
...filterChildren,
...injected[parent].children,
];
}
return injected;
// _rearrangeModel(model = {}, config = {}) {
// model.client = model.client || {};
// model.service = model.service || {};
// if (model.table) {
// console.log(`Warning: Defining the "table" directly on the model will be depricated in version 1.0. Please see the README for additional direction.`)
// model.client.table = model.table;
// }
// if (config.client) {
// console.log(`Warning: Defining the "version" directly on the model will be depricated in version 1.0. Please see the README for additional direction.`)
// model.client.docClient = config.client;
// }
// if (model.service) {
// console.log(`Warning: Defining the "service" directly on the model will be depricated in version 1.0. Please see the README for additional direction.`)
// model.service.name = model.service;
// }
// if (model.version) {
// console.log(`Warning: Defining the "version" directly on the model will be depricated in version 1.0. Please see the README for additional direction.`)
// model.service.version = model.version;
// }
// }
collection(collection = "", clauses = {}, facets = {}) {
let index = this.model.translations.collections.fromCollectionToIndex[
collection
];
return this._makeChain(index, clauses, clauses.index).collection(
collection,
facets,
);
}

@@ -373,3 +142,3 @@

_makeChain(index = "", clauses, rootClause) {
_makeChain(index = "", clauses, rootClause, options = {}) {
let facets = this.model.facets.byIndex[index];

@@ -394,4 +163,4 @@ let state = {

},
collectionOnly: !!options.collectionOnly,
hasSortKey: this.model.lookup.indexHasSortKeys[index],
indexComplete: false,
};

@@ -401,3 +170,3 @@ return this._chain(state, clauses, rootClause);

_cleanseRetrievedData(item = {}, options = {}) {
cleanseRetrievedData(item = {}, options = {}) {
let { includeKeys } = options;

@@ -417,2 +186,42 @@ let data = {};

formatResponse(response, config = {}) {
let stackTrace = new Error();
try {
if (config.raw) {
if (response.TableName) {
// a VERY hacky way to deal with PUTs
return {};
} else {
return response;
}
}
let data = {};
if (response.Item) {
data = this.cleanseRetrievedData(response.Item);
} else if (response.Items) {
data = response.Items.map((item) =>
this.cleanseRetrievedData(item, config),
);
}
let appliedGets;
if (Array.isArray(data)) {
appliedGets = data.map((item) =>
this.model.schema.applyAttributeGetters(item),
);
} else {
appliedGets = this.model.schema.applyAttributeGetters(data);
}
return appliedGets;
} catch (err) {
if (config.originalErr) {
throw err;
} else {
stackTrace.message = err.message;
throw stackTrace;
}
}
}
async go(method, params = {}, options = {}) {

@@ -434,27 +243,8 @@ let config = {

let response = await this.client[method](params).promise();
if (config.raw) {
return response;
}
let data = {};
if (method === "put") {
data = this._cleanseRetrievedData(params.Item, config);
} else if (response.Item) {
data = this._cleanseRetrievedData(response.Item);
} else if (response.Items) {
data = response.Items.map(item =>
this._cleanseRetrievedData(item, config),
);
}
let appliedGets;
if (Array.isArray(data)) {
appliedGets = data.map(item =>
this.model.schema.applyAttributeGetters(item),
);
// a VERY hacky way to deal with PUTs
return this.formatResponse(params, config);
} else {
appliedGets = this.model.schema.applyAttributeGetters(data);
return this.formatResponse(response, config);
}
// let appliedGets = this.model.schema.applyAttributeGetters(data);
return appliedGets;
} catch (err) {

@@ -571,3 +361,2 @@ if (config.originalErr) {

let setAttributes = this.model.schema.applyAttributeSetters(data);
let { updatedKeys } = this._getUpdatedKeys(pk, sk, setAttributes);

@@ -579,2 +368,3 @@ let transatedFields = this.model.schema.translateToFields(setAttributes);

...updatedKeys,
__edb_e__: this.model.entity,
},

@@ -649,3 +439,3 @@ TableName: this.model.table,

let props = Object.keys(item);
let missing = require.filter(prop => !props.includes(prop));
let missing = require.filter((prop) => !props.includes(prop));
if (!missing) {

@@ -688,3 +478,3 @@ throw new Error(`Item is missing attributes: ${missing.join(", ")}`);

_queryParams(chainState) {
_queryParams(chainState = {}, options = {}) {
let conlidatedQueryFacets = this._consolidateQueryFacets(

@@ -706,2 +496,9 @@ chainState.keys.sk,

);
case QueryTypes.collection:
return this._makeBeginsWithQueryParams(
chainState.index,
chainState.filter,
pk,
this._getCollectionSk(chainState.collection),
);
case QueryTypes.between:

@@ -848,7 +645,7 @@ return this._makeBetweenQueryParams(

`Incomplete facets: Without the facets ${incomplete
.filter(val => val !== undefined)
.filter((val) => val !== undefined)
.join(
", ",
)} the following access patterns ${incompleteAccessPatterns
.filter(val => val !== undefined)
.filter((val) => val !== undefined)
.join(", ")}cannot be updated.`,

@@ -930,3 +727,3 @@ );

...pk.filter(
attr =>
(attr) =>
!impacted[KeyTypes.pk].includes(attr) &&

@@ -941,3 +738,3 @@ !includedFacets.includes(attr),

...sk.filter(
attr =>
(attr) =>
!impacted[KeyTypes.sk].includes(attr) &&

@@ -1010,3 +807,3 @@ !includedFacets.includes(attr),

_findProperties(obj = {}, properties = []) {
return properties.map(name => [name, obj[name]]);
return properties.map((name) => [name, obj[name]]);
}

@@ -1040,3 +837,11 @@

_getPrefixes({collection = "", customFacets = {}} = {}) {
_getCollectionSk(collection = "") {
if (typeof collection && collection.length) {
return `$${collection}`.toLowerCase();
} else {
return "";
}
}
_getPrefixes({ collection = "", customFacets = {} } = {}) {
/*

@@ -1053,13 +858,13 @@ Collections will prefix the sort key so they can be queried with

prefix: "",
isCustom: false
isCustom: false,
},
sk: {
prefix: "",
isCustom: false
}
isCustom: false,
},
};
if (collection) {
keys.pk.prefix = this.model.prefixes.pk
keys.sk.prefix = `$${collection}#${this.model.entity}`
keys.pk.prefix = this.model.prefixes.pk;
keys.sk.prefix = `$${collection}#${this.model.entity}`;
} else {

@@ -1087,7 +892,14 @@ keys.pk.prefix = this.model.prefixes.pk;

let prefixes = this._getPrefixes(facets);
let pk = this._makeKey(prefixes.pk.prefix, facets.pk, pkFacets, prefixes.pk);
let pk = this._makeKey(
prefixes.pk.prefix,
facets.pk,
pkFacets,
prefixes.pk,
);
let sk = [];
if (this.model.lookup.indexHasSortKeys[index]) {
for (let skFacet of skFacets) {
sk.push(this._makeKey(prefixes.sk.prefix, facets.sk, skFacet, prefixes.sk));
sk.push(
this._makeKey(prefixes.sk.prefix, facets.sk, skFacet, prefixes.sk),
);
}

@@ -1098,6 +910,6 @@ }

_makeKey(prefix = "", facets = [], supplied = {}, {isCustom} = {}) {
_makeKey(prefix = "", facets = [], supplied = {}, { isCustom } = {}) {
let key = prefix;
for (let i = 0; i < facets.length; i++) {
let facet = facets[i];
let facet = facets[i];
let { label, name } = this.model.schema.attributes[facet];

@@ -1214,2 +1026,6 @@ if (isCustom) {

};
let collectionIndexTranslation = {
fromCollectionToIndex: {},
fromIndexToCollection: {},
};
let collections = {};

@@ -1226,2 +1042,3 @@ let facets = {

};
let seenIndexes = {};

@@ -1234,8 +1051,17 @@ let accessPatterns = Object.keys(indexes);

let indexName = index.index || "";
if (seenIndexes[indexName] !== undefined) {
throw new Error(
`Duplicate index defined in model: ${accessPattern} (${
indexName || "PRIMARY INDEX"
})`,
);
}
seenIndexes[indexName] = indexName;
let hasSk = !!index.sk;
let inCollection = !!index.collection;
let collection = index.collection || "";
let customFacets = {
pk: false,
sk: false
}
sk: false,
};
indexHasSortKeys[indexName] = hasSk;

@@ -1272,5 +1098,5 @@ let parsedPKFacets = this._parseFacets(index.pk.facets);

if (inCollection) {
collections[index.collection] = index.collection;
}
// if (inCollection) {
// collections[index.collection] = index.collection;
// }

@@ -1280,9 +1106,26 @@ let definition = {

sk,
collection,
customFacets,
index: indexName,
collection: index.collection
collection: index.collection,
};
if (inCollection) {
if (collections[collection] !== undefined) {
throw new Error(
`Duplicate collection, "${collection}" is defined across multiple indexes "${collections[collection]}" and "${accessPattern}". Collections must be unique names across indexes for an Entity.`,
);
} else {
collections[collection] = accessPattern;
}
collectionIndexTranslation.fromCollectionToIndex[
collection
] = indexName;
collectionIndexTranslation.fromIndexToCollection[
indexName
] = collection;
}
let attributes = [
...pk.facets.map(name => ({
...pk.facets.map((name) => ({
name,

@@ -1292,3 +1135,3 @@ index: indexName,

})),
...(sk.facets || []).map(name => ({
...(sk.facets || []).map((name) => ({
name,

@@ -1337,3 +1180,4 @@ index: indexName,

indexAccessPattern: indexAccessPatternTransaction,
collections: Object.keys(collections)
indexCollection: collectionIndexTranslation,
collections: Object.keys(collections),
};

@@ -1369,2 +1213,3 @@ }

indexAccessPattern,
indexCollection,
} = this._normalizeIndexes(model.indexes);

@@ -1390,2 +1235,3 @@ let schema = new Schema(model.attributes, facets);

indexes: indexAccessPattern,
collections: indexCollection,
},

@@ -1392,0 +1238,0 @@

@@ -19,3 +19,3 @@ let queryChildren = [

},
strict: true,
strict: false,
},

@@ -186,4 +186,38 @@ gt: {

}
injectFilterClauses(clauses = {}, filters = {}) {
let injected = { ...clauses };
let filterParents = Object.entries(injected)
.filter(clause => {
let [name, { children }] = clause;
return children.includes("go");
})
.map(([name]) => name);
let modelFilters = Object.keys(filters);
let filterChildren = [];
for (let [name, filter] of Object.entries(filters)) {
filterChildren.push(name);
injected[name] = {
action: this.buildClause(filter),
children: ["params", "go", "filter", ...modelFilters],
};
}
filterChildren.push("filter");
injected["filter"] = {
action: (entity, state, fn) => {
return this.buildClause(fn)(entity, state);
},
children: ["params", "go", "filter", ...modelFilters],
};
for (let parent of filterParents) {
injected[parent] = { ...injected[parent] };
injected[parent].children = [
...filterChildren,
...injected[parent].children,
];
}
return injected;
}
}
module.exports = { FilterFactory, FilterTypes };

@@ -7,4 +7,2 @@ const KeyTypes = {

const QueryTypes = {
begins: "begins",
between: "between",
and: "and",

@@ -16,2 +14,5 @@ gte: "gte",

eq: "eq",
begins: "begins",
between: "between",
collection: "collection"
};

@@ -18,0 +19,0 @@

const Validator = require("jsonschema").Validator;
Validator.prototype.customFormats.isFunction = function(input) {
Validator.prototype.customFormats.isFunction = function (input) {
return typeof input === "function";
};
Validator.prototype.customFormats.isFunctionOrString = function(input) {
Validator.prototype.customFormats.isFunctionOrString = function (input) {
return typeof input === "function" || typeof input === "string";
};
Validator.prototype.customFormats.isFunctionOrRegexp = function(input) {
Validator.prototype.customFormats.isFunctionOrRegexp = function (input) {
return typeof input === "function" || input instanceof RegExp;

@@ -91,3 +91,2 @@ };

type: ["array", "string"],
minItems: 1,
required: true,

@@ -164,3 +163,3 @@ items: {

errors
.map(err => {
.map((err) => {
let message = `${err.property}`;

@@ -167,0 +166,0 @@ switch (err.argument) {

@@ -12,3 +12,3 @@ const { Entity } = require("../src/entity");

setTimeout(resolve, ms);
})
});
}

@@ -63,3 +63,3 @@ let model = {

required: true,
validate: date =>
validate: (date) =>
moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format",

@@ -207,10 +207,8 @@ },

stores = await Promise.all(stores);
expect(stores)
.to.be.an("array")
.and.have.length(10);
expect(stores).to.be.an("array").and.have.length(10);
let mallOne = malls[0];
let mallOneIds = stores
.filter(store => store.mall === mallOne)
.map(store => store.id);
.filter((store) => store.mall === mallOne)
.map((store) => store.id);

@@ -223,3 +221,3 @@ let mallOneStores = await MallStores.query

let mallOneMatches = mallOneStores.every(store =>
let mallOneMatches = mallOneStores.every((store) =>
mallOneIds.includes(store.id),

@@ -229,5 +227,3 @@ );

expect(mallOneMatches);
expect(mallOneStores)
.to.be.an("array")
.and.have.length(5);
expect(mallOneStores).to.be.an("array").and.have.length(5);

@@ -245,3 +241,3 @@ let first = stores[0];

.go();
let buildingsAfterBStores = stores.filter(store => {
let buildingsAfterBStores = stores.filter((store) => {
return (

@@ -260,3 +256,3 @@ store.mall === mallOne &&

let buildingsBetweenBHStores = stores.filter(store => {
let buildingsBetweenBHStores = stores.filter((store) => {
return (

@@ -314,3 +310,3 @@ store.mall === mallOne &&

},
get: prop1 => `${prop1} GET`,
get: (prop1) => `${prop1} GET`,
},

@@ -391,4 +387,4 @@ prop2: {

required: true,
set: val => val + " wham",
get: val => val + " bam",
set: (val) => val + " wham",
get: (val) => val + " bam",
},

@@ -420,2 +416,3 @@ },

Item: {
__edb_e__: entity,
id,

@@ -437,2 +434,3 @@ date,

{
__edb_e__: entity,
id,

@@ -492,3 +490,3 @@ date,

let max = "50";
let filteredStores = stores.filter(store => {
let filteredStores = stores.filter((store) => {
return store.mall === mall && store.rent <= max;

@@ -550,7 +548,7 @@ });

.record({ date })
.filter(attr => attr.property.eq(property))
.filter((attr) => attr.property.eq(property))
.go();
let foundParams = db.query
.record({ date })
.filter(attr => attr.property.eq(property))
.filter((attr) => attr.property.eq(property))
.params();

@@ -566,3 +564,2 @@ expect(foundParams.ExpressionAttributeNames["#property"]).to.equal(

it("Should allow for multiple filters", async () => {
let entity = uuidv4();

@@ -613,6 +610,7 @@ let id = uuidv4();

let expectedMembers = records.filter(
record => record.color !== "green" && record.property !== "A",
(record) => record.color !== "green" && record.property !== "A",
);
// sleep gives time for eventual consistency
let found = await db.query.record({id})
let found = await db.query
.record({ id })
.filter(({ property }) => property.gt("A"))

@@ -619,0 +617,0 @@ .filter(

@@ -55,3 +55,3 @@ const { Entity, clauses } = require("../src/entity");

required: true,
validate: date =>
validate: (date) =>
moment(date, "YYYY-MM-DD").isValid() ? "" : "Invalid date format",

@@ -70,3 +70,3 @@ },

filters: {
rentsLeaseEndFilter: function(
rentsLeaseEndFilter: function (
attr,

@@ -177,4 +177,4 @@ { lowRent, beginning, end, location } = {},

type: "string",
validate: /^\d{4}-\d{2}-\d{2}$/gi
}
validate: /^\d{4}-\d{2}-\d{2}$/gi,
},
},

@@ -185,31 +185,38 @@ indexes: {

field: "test",
facets: ["regexp"]
}
}
}
facets: ["regexp"],
},
},
},
});
expect(() => Test.put({regexp: "1533-15-44"}).params()).to.not.throw();
expect(() => Test.put({regexp: "1533-1a-44"}).params()).to.throw(`Invalid value for attribute "regexp": Failed user defined regex.`)
expect(() => Test.put({ regexp: "1533-15-44" }).params()).to.not.throw();
expect(() => Test.put({ regexp: "1533-1a-44" }).params()).to.throw(
`Invalid value for attribute "regexp": Failed user defined regex.`,
);
});
it("Should not allow for an invalid schema type", () => {
expect(() => new Entity({
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
regexp: {
type: "raccoon",
}
},
indexes: {
test: {
pk: {
field: "test",
facets: ["regexp"]
}
}
}
})).to.throw(`Invalid "type" property for attribute: "regexp". Acceptable types include string, number, boolean, enum`)
})
expect(
() =>
new Entity({
service: "MallStoreDirectory",
entity: "MallStores",
table: "StoreDirectory",
version: "1",
attributes: {
regexp: {
type: "raccoon",
},
},
indexes: {
test: {
pk: {
field: "test",
facets: ["regexp"],
},
},
},
}),
).to.throw(
`Invalid "type" property for attribute: "regexp". Acceptable types include string, number, boolean, enum`,
);
});
it("Should prevent the update of the main partition key without the user needing to define the property as read-only in their schema", () => {

@@ -438,2 +445,3 @@ let id = uuidV4();

Item: {
__edb_e__: "MallStores",
storeLocationId: put.Item.storeLocationId,

@@ -733,2 +741,3 @@ mall,

Item: {
__edb_e__: "MallStores",
storeLocationId: "IDENTIFIER",

@@ -744,5 +753,5 @@ dateTime: "DATE",

});
/* This test was removed because facet templates was refactored to remove all electrodb opinions. */
//
//
// it("Should throw on invalid characters in facet template (string)", () => {

@@ -834,2 +843,3 @@ // const schema = {

Item: {
__edb_e__: "MallStores",
storeLocationId: "IDENTIFIER",

@@ -868,3 +878,3 @@ dateTime: "DATE",

type: "string",
}
},
},

@@ -881,3 +891,3 @@ indexes: {

},
collection: "testing"
collection: "testing",
},

@@ -893,3 +903,3 @@ },

prop2: "PROPERTY2",
prop3: "PROPERTY3"
prop3: "PROPERTY3",
})

@@ -899,2 +909,3 @@ .params();

Item: {
__edb_e__: "MallStores",
storeLocationId: "IDENTIFIER",

@@ -1080,3 +1091,3 @@ dateTime: "DATE",

let facets = MallStores.model.facets.byIndex[index];
let all = facets.all.map(facet => facet.name);
let all = facets.all.map((facet) => facet.name);
let allMatches = MallStores._expectFacets(

@@ -1101,3 +1112,3 @@ { store, mall, building, unit },

let facets = MallStores.model.facets.byIndex[index];
let all = facets.all.map(facet => facet.name);
let all = facets.all.map((facet) => facet.name);
let allMatches = () => MallStores._expectFacets({ store }, all);

@@ -1138,3 +1149,3 @@ let pkMatches = () =>

}
let injected = MallStores._injectFiltersIntoClauses(clauses, {
let injected = MallStores._filterBuilder.injectFilterClauses(clauses, {
rentsLeaseEndFilter,

@@ -1141,0 +1152,0 @@ });

@@ -166,18 +166,18 @@ const moment = require("moment");

});
it("Should validate the attributes passed when strict", () => {
function byCategory(attr, { category }) {
return attr.category.eq(category);
}
let filter = new FilterFactory(
MallStores.model.schema.attributes,
FilterTypes,
);
let clause = filter.buildClause(byCategory);
let category = "BAD_CATEGORY";
let results = () =>
clause(MallStores, { query: { filter: {} } }, { category });
expect(results).to.throw(
"Value not found in set of acceptable values: food/coffee, food/meal, clothing, electronics, department, misc",
);
});
// it("Should validate the attributes passed when strict", () => {
// function byCategory(attr, { category }) {
// return attr.category.eq(category);
// }
// let filter = new FilterFactory(
// MallStores.model.schema.attributes,
// FilterTypes,
// );
// let clause = filter.buildClause(byCategory);
// let category = "BAD_CATEGORY";
// let results = () =>
// clause(MallStores, { query: { filter: {} } }, { category });
// expect(results).to.throw(
// "Value not found in set of acceptable values: food/coffee, food/meal, clothing, electronics, department, misc",
// );
// });
it("Shouldnt validate the attributes passed when not strict", () => {

@@ -184,0 +184,0 @@ function byCategory(attr, { category }) {

@@ -15,2 +15,8 @@ tests:

features:
- find method
- find method
collections
- add custom column to flag collections?
- index by collection on entity
- TEST FOR MISSING PRIMARY INDEX;
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