electrodb
Advanced tools
Comparing version 0.8.19 to 0.9.0
@@ -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", |
1047
README.md
@@ -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; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
272159
19
5270
1831
1