baqend
Version:
Baqend JavaScript SDK
465 lines (416 loc) • 11.5 kB
text/typescript
import { UpdateOperation } from './UpdateOperation';
import { Json, JsonMap } from '../util';
import { Entity } from '../binding';
const ALLOWED_OPERATIONS = [
'$add',
'$and',
'$currentDate',
'$dec',
'$inc',
'$max',
'$min',
'$mul',
'$or',
'$pop',
'$push',
'$put',
'$remove',
'$rename',
'$replace',
'$set',
'$shift',
'$unshift',
'$xor',
];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export interface PartialUpdateBuilder<T extends Entity> {
/**
* Increments a field by a given value
*
* @param field The field to increment
* @param by The number to increment by, defaults to 1
* @return
*/
increment(field: string, by?: number): this;
/**
* Decrements a field by a given value
*
* @param field The field to decrement
* @param by The number to decrement by, defaults to 1
* @return
*/
decrement(field: string, by?: number): this;
/**
* Multiplies a field by a given number
*
* @param field The field to multiply
* @param multiplicator The number to multiply by
* @return
*/
multiply(field: string, multiplicator: number): this;
/**
* Divides a field by a given number
*
* @param field The field to divide
* @param divisor The number to divide by
* @return
*/
divide(field: string, divisor: number): this;
/**
* Sets the highest possible value of a field
*
* @param field The field to compare with
* @param value The highest possible value
* @return
*/
atMost(field: string, value: number): this;
/**
* Sets the smallest possible value of a field
*
* @param field The field to compare with
* @param value The smalles possible value
* @return
*/
atLeast(field: string, value: number): this;
/**
* Sets a datetime field to the current moment
*
* @method
* @param field The field to perform the operation on
* @return
*/
toNow(field: string): this;
}
export class PartialUpdateBuilder<T extends Entity> {
public operations: UpdateOperation[] = [];
/**
* @param operations
*/
constructor(operations: JsonMap) {
if (operations) {
this.addOperations(operations);
}
}
/**
* Sets a field to a given value
*
* @param field The field to set
* @param value The value to set to
* @return
*/
set(field: string, value: any) : this {
let val = value;
if (val instanceof Set) {
val = Array.from(val);
} else if (val instanceof Map) {
const newValue: { [key: string]: any } = {};
val.forEach((v: any, k: string) => {
newValue[k] = v;
});
val = newValue;
}
return this.addOperation(field, '$set', val);
}
/**
* Increments a field by a given value
*
* @param field The field to increment
* @param by The number to increment by, defaults to 1
* @return
*/
inc(field: string, by?: number) : this {
return this.addOperation(field, '$inc', typeof by === 'number' ? by : 1);
}
/**
* Decrements a field by a given value
*
* @param field The field to decrement
* @param by The number to decrement by, defaults to 1
* @return
*/
dec(field: string, by?: number) : this {
return this.inc(field, typeof by === 'number' ? -by : -1);
}
/**
* Multiplies a field by a given number
*
* @param field The field to multiply
* @param multiplicator The number to multiply by
* @return
*/
mul(field: string, multiplicator: number) : this {
if (typeof multiplicator !== 'number') {
throw new Error('Multiplicator must be a number.');
}
return this.addOperation(field, '$mul', multiplicator);
}
/**
* Divides a field by a given number
*
* @param field The field to divide
* @param divisor The number to divide by
* @return
*/
div(field: string, divisor: number) : this {
if (typeof divisor !== 'number') {
throw new Error('Divisor must be a number.');
}
return this.addOperation(field, '$mul', 1 / divisor);
}
/**
* Sets the highest possible value of a field
*
* @param field The field to compare with
* @param value The highest possible value
* @return
*/
min(field: string, value: number) : this {
if (typeof value !== 'number') {
throw new Error('Value must be a number');
}
return this.addOperation(field, '$min', value);
}
/**
* Sets the smallest possible value of a field
*
* @param field The field to compare with
* @param value The smalles possible value
* @return
*/
max(field: string, value: number) : this {
if (typeof value !== 'number') {
throw new Error('Value must be a number');
}
return this.addOperation(field, '$max', value);
}
/**
* Removes an item from an array or map
*
* @param field The field to perform the operation on
* @param item The item to add
* @return
*/
remove(field: string, item: any) : this {
return this.addOperation(field, '$remove', item);
}
/**
* Puts an item from an array or map
*
* @param field The field to perform the operation on
* @param key The map key to put the value to or an object of arguments
* @param [value] The value to put if a key was used
* @return
*/
put(field: string, key: string | number | { [key: string]: any }, value?: any) : this {
const obj: { [key: string]: any } = {};
if (typeof key === 'string' || typeof key === 'number') {
obj[key] = value;
} else if (typeof key === 'object') {
Object.assign(obj, key);
}
return this.addOperation(field, '$put', obj);
}
/**
* Pushes an item into a list
*
* @param field The field to perform the operation on
* @param item The item to add
* @return
*/
push(field: string, item: any) : this {
return this.addOperation(field, '$push', item);
}
/**
* Unshifts an item into a list
*
* @param field The field to perform the operation on
* @param item The item to add
* @return
*/
unshift(field: string, item: any) : this {
return this.addOperation(field, '$unshift', item);
}
/**
* Pops the last item out of a list
*
* @param field The field to perform the operation on
* @return
*/
pop(field: string) : this {
return this.addOperation(field, '$pop');
}
/**
* Shifts the first item out of a list
*
* @param field The field to perform the operation on
* @return
*/
shift(field: string) : this {
return this.addOperation(field, '$shift');
}
/**
* Adds an item to a set
*
* @param field The field to perform the operation on
* @param item The item to add
* @return
*/
add(field: string, item: any) : this {
return this.addOperation(field, '$add', item);
}
/**
* Replaces an item at a given index
*
* @param path The path to perform the operation on
* @param index The index where the item will be replaced
* @param item The item to replace with
* @return
*/
replace(path: string, index: number, item: any): this {
if (this.hasOperationOnPath(path)) {
throw new Error(`You cannot update ${path} multiple times`);
}
return this.addOperation(`${path}.${index}`, '$replace', item);
}
/**
* Sets a datetime field to the current moment
*
* @param field The field to perform the operation on
* @return
*/
currentDate(field: string) : this {
return this.addOperation(field, '$currentDate');
}
/**
* Performs a bitwise AND on a path
*
* @param path The path to perform the operation on
* @param bitmask The bitmask taking part in the operation
* @return
*/
and(path: string, bitmask: number): this {
return this.addOperation(path, '$and', bitmask);
}
/**
* Performs a bitwise OR on a path
*
* @param path The path to perform the operation on
* @param bitmask The bitmask taking part in the operation
* @return
*/
or(path: string, bitmask: number): this {
return this.addOperation(path, '$or', bitmask);
}
/**
* Performs a bitwise XOR on a path
*
* @param path The path to perform the operation on
* @param bitmask The bitmask taking part in the operation
* @return
*/
xor(path: string, bitmask: number): this {
return this.addOperation(path, '$xor', bitmask);
}
/**
* Renames a field
*
* @param oldPath The old field name
* @param newPath The new field name
* @return
*/
rename(oldPath: string, newPath: string) {
return this.addOperation(oldPath, '$rename', newPath);
}
/**
* Returns a JSON representation of this partial update
*
* @return
*/
toJSON(): Json {
return this.operations.reduce((json, operation: UpdateOperation) => ({
...json,
[operation.name]: {
...json[operation.name],
[operation.path]: operation.value,
},
}), {} as { [path: string]: any });
}
/**
* Executes the partial update
*
* @return The promise resolves when the partial update has been executed successfully
* @abstract
*/
execute(): Promise<T> {
throw new Error('Cannot call "execute" on abstract PartialUpdateBuilder');
}
/**
* Adds an update operation on the partial update
*
* @param path The path which gets modified by the operation
* @param operator The operator of the operation to add
* @param [value] The value used to execute the operation
* @return
* @private
*/
addOperation(path: string, operator: string, value?: any): this {
if (typeof path !== 'string') {
throw new Error('Path must be a string');
}
if (ALLOWED_OPERATIONS.indexOf(operator) === -1) {
throw new Error(`Operation invalid: ${operator}`);
}
if (this.hasOperationOnPath(path)) {
throw new Error(`You cannot update ${path} multiple times`);
}
// Check for illegal values
if (typeof value === 'number') {
if (Number.isNaN(value)) {
throw new Error('NaN is not a supported value');
}
if (!Number.isFinite(value)) {
throw new Error('Infinity is not a supported value');
}
}
// Add the new operation
const normalizedValue = typeof value === 'undefined' ? null : value;
const updateOperation = new UpdateOperation(operator, path, normalizedValue);
this.operations.push(updateOperation);
return this;
}
/**
* Adds initial operations
*
* @param json
* @private
*/
addOperations(json: JsonMap) {
Object.keys(json).forEach((key) => {
const pathValueDictionary = json[key] as JsonMap;
Object.keys(pathValueDictionary).forEach((path) => {
const value = pathValueDictionary[path];
this.operations.push(new UpdateOperation(key, path, value));
});
});
}
/**
* Checks whether an operation on the field exists already
*
* @param path The path where the operation is executed on
* @return True, if the operation does exist
* @private
*/
hasOperationOnPath(path: string): boolean {
return this.operations.some((op) => op.path === path);
}
}
// aliases
Object.assign(PartialUpdateBuilder.prototype, {
increment: PartialUpdateBuilder.prototype.inc,
decrement: PartialUpdateBuilder.prototype.dec,
multiply: PartialUpdateBuilder.prototype.mul,
divide: PartialUpdateBuilder.prototype.div,
atMost: PartialUpdateBuilder.prototype.min,
atLeast: PartialUpdateBuilder.prototype.max,
toNow: PartialUpdateBuilder.prototype.currentDate,
});