Skip to content

Commit

Permalink
Merge fc90245 into 5404b3d
Browse files Browse the repository at this point in the history
  • Loading branch information
tywalch committed Jan 19, 2023
2 parents 5404b3d + fc90245 commit b594d92
Show file tree
Hide file tree
Showing 18 changed files with 1,828 additions and 570 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,10 @@ All notable changes to this project will be documented in this file. Breaking ch
### Fixed
- Fixes issue that resulted in provided undefined values from becoming involuntarily set via updates
- Updated documentation links in error message to direct traffic to https://electrodb.dev

## [2.4.0] - 2022-01-19
### Added
- Added the new filter expression methods: `size()`, `type()` and `escape` [[read more]](https://electrodb.dev/en/queries/filters/#operations)
- Add the `createSchema()` function for helping create and type ElectroDB schemas without instantiating an Entity [[read more]](https://electrodb.dev/en/reference/typscript/#createSchema)
### Changed
- ElectroDB will now filter out empty `Set` type attributes from being passed to the DocumentClient. This was a frequently requested feature for convenience.
29 changes: 28 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,18 @@ export type IsolatedCollectionAttributes<E extends {[name: string]: Entity<any,
}[keyof E]
}

type DynamoDBAttributeType =
| 'S'
| 'SS'
| 'N'
| 'NS'
| 'B'
| 'BS'
| 'BOOL'
| 'NULL'
| 'L'
| 'M'

export interface CollectionWhereOperations {
eq: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: T) => string;
ne: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: T) => string;
Expand All @@ -131,6 +143,12 @@ export interface CollectionWhereOperations {
notContains: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: T) => string;
value: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: T) => string;
name: <T, A extends WhereAttributeSymbol<T>>(attr: A) => string;
size: <T, A extends WhereAttributeSymbol<T>>(attr: A) => string;
type: <T, A extends WhereAttributeSymbol<T>>(attr: A, type: DynamoDBAttributeType) => string;
escape: <T extends string | number | boolean>(value: T) => T extends string ? string
: T extends number ? number
: T extends boolean ? boolean
: never;
}

export type CollectionWhereCallback<E extends {[name: string]: Entity<any, any, any, any>}, I extends Partial<AllEntityAttributes<E>>> =
Expand Down Expand Up @@ -2199,6 +2217,13 @@ export interface WhereOperations<A extends string, F extends string, C extends s
notContains: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: T) => string;
value: <T, A extends WhereAttributeSymbol<T>>(attr: A, value: A extends WhereAttributeSymbol<infer V> ? V : never) => A extends WhereAttributeSymbol<infer V> ? V : never;
name: <A extends WhereAttributeSymbol<any>>(attr: A) => string;
size: <T, A extends WhereAttributeSymbol<T>>(attr: A) => number;
type: <T, A extends WhereAttributeSymbol<T>>(attr: A, type: DynamoDBAttributeType) => string;
escape: <T extends string | number | boolean>(value: T) =>
T extends string ? string
: T extends number ? number
: T extends boolean ? boolean
: never;
}

export interface DataUpdateOperations<A extends string, F extends string, C extends string, S extends Schema<A,F,C>, I extends UpdateData<A,F,C,S>> {
Expand Down Expand Up @@ -2347,4 +2372,6 @@ declare function CustomAttributeType<T>(
: 'any'
): T extends string | number | boolean
? OpaquePrimitiveTypeName<T>
: CustomAttributeTypeName<T>;
: CustomAttributeTypeName<T>;

declare function createSchema<A extends string, F extends string, C extends string, S extends Schema<A,F,C>>(schema: S): S
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const { Entity } = require("./src/entity");
const { Service } = require("./src/service");
const { createCustomAttribute, CustomAttributeType } = require('./src/schema');
const { createCustomAttribute, CustomAttributeType, createSchema } = require('./src/schema');
const { ElectroError, ElectroValidationError, ElectroUserValidationError, ElectroAttributeValidationError } = require('./src/errors');

module.exports = {
Entity,
Service,
ElectroError,
createSchema,
CustomAttributeType,
createCustomAttribute,
ElectroValidationError,
Expand Down
2 changes: 1 addition & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ const item: Item = {

type AttributeNames = "attr1" | "attr2" | "attr3" | "attr4" | "attr5" | "attr6" | "attr7" | "attr8" | "attr9";
const AttributeName = "" as AttributeNames;
type OperationNames = "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "between" | "begins" | "exists" | "notExists" | "contains" | "notContains" | "value" | "name";
type OperationNames = "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "between" | "begins" | "exists" | "notExists" | "contains" | "notContains" | "value" | "name" | 'size' | 'escape' | 'type';


type WithSKMyIndexCompositeAttributes = {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "electrodb",
"version": "2.3.5",
"version": "2.4.0",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
"main": "index.js",
"scripts": {
Expand Down
115 changes: 88 additions & 27 deletions src/operations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes} = require("./types");
const {AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes, DynamoDBAttributeTypes} = require("./types");
const e = require("./errors");
const u = require("./util");

Expand Down Expand Up @@ -159,8 +159,26 @@ const UpdateOperations = {
}

const FilterOperations = {
escape: {
template: function escape(options, attr) {
return `${attr}`;
},
noAttribute: true,
},
size: {
template: function size(options, attr, name) {
return `size(${name})`
},
strict: false,
},
type: {
template: function attributeType(options, attr, name, value) {
return `attribute_type(${name}, ${value})`;
},
strict: false
},
ne: {
template: function eq(options, attr, name, value) {
template: function ne(options, attr, name, value) {
return `${name} <> ${value}`;
},
strict: false,
Expand Down Expand Up @@ -379,31 +397,61 @@ class AttributeOperationProxy {

static buildOperations(builder, operations) {
let ops = {};
let seen = new Set();
let seen = new Map();
for (let operation of Object.keys(operations)) {
let {template, canNest} = operations[operation];
let {template, canNest, noAttribute} = operations[operation];
Object.defineProperty(ops, operation, {
get: () => {
return (property, ...values) => {
if (property === undefined) {
throw new e.ElectroError(e.ErrorCodes.InvalidWhere, `Invalid/Unknown property passed in where clause passed to operation: '${operation}'`);
}
if (property.__is_clause__ === AttributeProxySymbol) {
const {paths, root, target} = property();
if (property[AttributeProxySymbol]) {
const {commit, target} = property();
const fixedValues = values.map((value) => target.applyFixings(value))
.filter(value => value !== undefined);
const isFilterBuilder = builder.type === BuilderTypes.filter;
const takesValueArgument = template.length > 3;
const isAcceptableValue = fixedValues.every(value => {
const seenAttributes = seen.get(value);
if (seenAttributes) {
return seenAttributes.every(v => target.acceptable(v))
}
return target.acceptable(value);
});

const shouldCommit =
// if it is a filterBuilder than we don't care what they pass because the user needs more freedom here
isFilterBuilder ||
// if the operation does not take a value argument then not committing here could cause problems.
// this should be revisited to make more robust, we could hypothetically store the commit in the
// "seen" map for when the value is used, but that's a lot of new complexity
!takesValueArgument ||
// if the operation takes a value, we should determine if that value is acceptable. For
// example, in the cases of a "set" we check to see if it is empty, or if the value is
// undefined, we should not commit. The "fixedValues" length check is because the
// "fixedValues" array has been filtered for undefined, so no length there indicates an
// undefined value was passed.
(takesValueArgument && isAcceptableValue && fixedValues.length > 0);

if (!shouldCommit) {
return '';
}

const paths = commit();
const attributeValues = [];
let hasNestedValue = false;
for (let value of values) {
value = target.applyFixings(value);
// template.length is to see if function takes value argument
if (template.length > 3) {
if (seen.has(value)) {
attributeValues.push(value);
hasNestedValue = true;
} else {
let attributeValueName = builder.setValue(target.name, value);
builder.setPath(paths.json, {value, name: attributeValueName});
attributeValues.push(attributeValueName);
}
for (let fixedValue of fixedValues) {
if (seen.has(fixedValue)) {
attributeValues.push(fixedValue);
hasNestedValue = true;
} else {
let attributeValueName = builder.setValue(target.name, fixedValue);
builder.setPath(paths.json, {
value: fixedValue,
name: attributeValueName
});
attributeValues.push(attributeValueName);
}
}

Expand All @@ -414,15 +462,26 @@ class AttributeOperationProxy {
const formatted = template(options, target, paths.expression, ...attributeValues);
builder.setImpacted(operation, paths.json, target);
if (canNest) {
seen.add(paths.expression);
seen.add(formatted);
seen.set(paths.expression, attributeValues);
seen.set(formatted, attributeValues);
}

if (builder.type === BuilderTypes.update && formatted && typeof formatted.operation === "string" && typeof formatted.expression === "string") {
builder.add(formatted.operation, formatted.expression);
return formatted.expression;
}

return formatted;
} else if (noAttribute) {
// const {json, expression} = builder.setName({}, property, property);
let attributeValueName = builder.setValue(property, property);
builder.setPath(property, {
value: property,
name: attributeValueName,
});
const formatted = template({}, attributeValueName);
seen.set(attributeValueName, [property]);
seen.set(formatted, [property]);
return formatted;
} else {
throw new e.ElectroError(e.ErrorCodes.InvalidWhere, `Invalid Attribute in where clause passed to operation '${operation}'. Use injected attributes only.`);
Expand All @@ -437,15 +496,15 @@ class AttributeOperationProxy {
static pathProxy(build) {
return new Proxy(() => build(), {
get: (_, prop, o) => {
if (prop === "__is_clause__") {
return AttributeProxySymbol;
if (prop === AttributeProxySymbol) {
return true;
} else {
return AttributeOperationProxy.pathProxy(() => {
const { paths, root, target, builder } = build();
const { commit, root, target, builder } = build();
const attribute = target.getChild(prop);
let field;
if (attribute === undefined) {
throw new Error(`Invalid attribute "${prop}" at path "${paths.json}".`);
throw new Error(`Invalid attribute "${prop}" at path "${target.path}.${prop}"`);
} else if (attribute === root && attribute.type === AttributeTypes.any) {
// This function is only called if a nested property is called. If this attribute is ultimately the root, don't use the root's field name
field = prop;
Expand All @@ -457,7 +516,10 @@ class AttributeOperationProxy {
root,
builder,
target: attribute,
paths: builder.setName(paths, prop, field),
commit: () => {
const paths = commit();
return builder.setName(paths, prop, field);
},
}
});
}
Expand All @@ -471,12 +533,11 @@ class AttributeOperationProxy {
Object.defineProperty(attr, name, {
get: () => {
return AttributeOperationProxy.pathProxy(() => {
const paths = builder.setName({}, attribute.name, attribute.field);
return {
paths,
root: attribute,
target: attribute,
builder,
commit: () => builder.setName({}, attribute.name, attribute.field)
}
});
}
Expand Down
25 changes: 22 additions & 3 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,11 @@ class Attribute {

_makeApplyFixings({ prefix = "", postfix = "", casing= KeyCasing.none } = {}) {
return (value) => {
if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type) && value !== undefined) {
if (value === undefined) {
return;
}

if ([AttributeTypes.string, AttributeTypes.enum].includes(this.type)) {
value = `${prefix}${value}${postfix}`;
}

Expand Down Expand Up @@ -305,6 +309,10 @@ class Attribute {
};
}

acceptable(val) {
return val !== undefined;
}

getPathType(type, parentType) {
if (parentType === AttributeTypes.list || parentType === AttributeTypes.set) {
return PathTypes.item;
Expand Down Expand Up @@ -876,12 +884,18 @@ class SetAttribute extends Attribute {
value = Array.isArray(value)
? Array.from(new Set(value))
: value;
return this.client.createSet(value, {validate: true});
return this.client.createSet(value, { validate: true });
} else {
return new DynamoDBSet(value, this.items.type);
}
}

acceptable(val) {
return Array.isArray(val)
? val.length > 0
: this.items.acceptable(val);
}

toDDBSet(value) {
const valueType = getValueType(value);
let array;
Expand Down Expand Up @@ -1474,11 +1488,16 @@ function CustomAttributeType(base) {
return base;
}

function createSchema(schema) {
return v.model(schema);
}

module.exports = {
Schema,
Attribute,
SetAttribute,
CastTypes,
SetAttribute,
createSchema,
CustomAttributeType,
createCustomAttribute,
};
18 changes: 18 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,23 @@ const ResultOrderOption = {

const ResultOrderParam = 'ScanIndexForward';

const DynamoDBAttributeTypes = Object.entries({
string: 'S',
stringSet: 'SS',
number: 'N',
numberSet: 'NS',
binary: 'B',
binarySet: 'BS',
boolean: 'BOOL',
null: 'NULL',
list: 'L',
map: 'M',
}).reduce((obj, [name, type]) => {
obj[name] = type;
obj[type] = type;
return obj;
}, {});

module.exports = {
Pager,
KeyTypes,
Expand Down Expand Up @@ -303,6 +320,7 @@ module.exports = {
ElectroInstanceTypes,
MethodTypeTranslation,
EventSubscriptionTypes,
DynamoDBAttributeTypes,
AttributeMutationMethods,
AllPages,
ResultOrderOption,
Expand Down

0 comments on commit b594d92

Please sign in to comment.