import { EnumShape, ShapeGuards, ShapeVisitor, TypeShape, UnionShape, Value } from '@punchcard/shape'; import { ArrayShape, MapShape, SetShape } from '@punchcard/shape/lib/collection'; import { AnyShape, BinaryShape, bool, BoolShape, NothingShape, number, NumberShape, string, StringShape, timestamp, TimestampShape } from '@punchcard/shape/lib/primitive'; import { Shape } from '@punchcard/shape/lib/shape'; import { AttributeValue } from './attribute'; import { Mapper } from './mapper'; import { Writer } from './writer'; // tslint:disable: ban-types // we have a thing named Object inside Query, so stash this here. const Objekt = Object; export namespace DSL { export type Tag = typeof Tag; export const Tag = Symbol.for('@punchcard/shape-dynamodb.Query.Tag'); export type Of = T extends BinaryShape ? DSL.Binary : T extends BoolShape ? DSL.Bool : T extends AnyShape ? DSL.Any : T extends NumberShape ? DSL.Number : /* { type: 'string' } */ T extends StringShape ? DSL.String : T extends TimestampShape ? DSL.Timestamp : T extends UnionShape ? U extends 1 ? { [i in Extract]: Of }[1] : NothingShape extends Extract], NothingShape> ? U['length'] extends 1 ? DSL.Object : U['length'] extends 2 ? Exclude<{ [i in Extract]: Of; }[Extract], DSL.Object> : DSL.Union : DSL.Union : T extends EnumShape ? Enum : T extends TypeShape ? DSL.Struct : T extends MapShape ? DSL.Map : T extends SetShape ? DSL.Set : T extends ArrayShape ? DSL.List : T extends { [Tag]: infer Q } ? Q : DSL.Object ; export type Root> = Struct['fields']; export function of>(shape: T): Root { const result: any = {}; for (const [name, member] of Objekt.entries(shape.Members)) { result[name] = (member as Shape).visit(DslVisitor, new RootProperty(member as Shape, name)); } return result; } export function _of(shape: T, expr: ExpressionNode): Of { return shape.visit(DslVisitor as any, expr) as Of; } export const DslVisitor: ShapeVisitor> = { enumShape: (shape, expr) => { return new Enum(shape, expr); }, literalShape: (shape, expression) => { return shape.Type.visit(DslVisitor as any, expression); }, unionShape: (shape, expression) => { const items = shape.Items.filter(i => !ShapeGuards.isNothingShape(i)); if (items.length === 1) { return _of(items[0], expression); } return new Union(shape, expression); }, functionShape: (() => { throw new Error(`functionShape is not valid on a DynamoDB DSL`); }) as any, neverShape: (() => { throw new Error(`neverShape is not valid on a DynamoDB DSL`); }) as any, nothingShape: (shape: NothingShape, expression: ExpressionNode): Object => { return new Object(shape, expression); }, anyShape: (shape: AnyShape, expression: ExpressionNode): Any => { return new Any(shape, expression); }, binaryShape: (shape: BinaryShape, expression: ExpressionNode): Binary => { return new Binary(shape, expression); }, arrayShape: (shape: ArrayShape, expression: ExpressionNode): List => { return new Proxy(new List(shape, expression), { get: (target, prop) => { if (typeof prop === 'string') { if (!isNaN(prop as any)) { return target.get(parseInt(prop, 10)); } } else if (typeof prop === 'number' && prop % 1 === 0) { return target.get(prop); } return (target as any)[prop]; } }); }, boolShape: (shape: BoolShape, expression: ExpressionNode): Bool => { return new Bool(expression, shape); }, recordShape: (shape: TypeShape, expression: ExpressionNode): Struct => { return new Struct(shape, expression); }, mapShape: (shape: MapShape, expression: ExpressionNode): Map => { return new Proxy(new Map(shape, expression), { get: (target, prop) => { if (typeof prop === 'string') { if (typeof (target as any)[prop] === 'function') { return (target as any)[prop]; } return target.get(prop); } return (target as any)[prop]; } }); }, numberShape: (shape: NumberShape, expression: ExpressionNode): Number => { return new DSL.Number(expression, shape); }, setShape: (shape: SetShape, expression: ExpressionNode): Set => { return new Set(shape.Items, expression); }, stringShape: (shape: StringShape, expression: ExpressionNode): Node => { // tslint:disable: no-construct return new String(expression, shape); }, timestampShape: (shape: TimestampShape, expression: ExpressionNode): Node => { return new Timestamp(expression, shape); } }; } export namespace DSL { export const isNode = (a: any): a is Node => a[NodeType] !== undefined; export type Expression = ExpressionNode | Value.Of; export const NodeType = Symbol.for('@punchcard/shape-dynamodb.DSL.NodeType'); export const SubNodeType = Symbol.for('@punchcard/shape-dynamodb.DSL.SubNodeType'); export const DataType = Symbol.for('@punchcard/shape-dynamodb.DSL.DataType'); export const InstanceExpression = Symbol.for('@punchcard/shape-dynamodb.DSL.InstanceExpression'); export const Synthesize = Symbol.for('@punchcard/shape-dynamodb.DSL.Synthesize'); export abstract class Node { public readonly [NodeType]: T; constructor(nodeType: T) { this[NodeType] = nodeType; } public abstract [Synthesize](writer: Writer): void; } export function isStatementNode(a: any): a is StatementNode { return a[NodeType] === 'statement'; } export abstract class StatementNode extends Node<'statement'> { public abstract [SubNodeType]: string; constructor() { super('statement'); } } export abstract class ExpressionNode extends Node<'expression'> { public readonly [DataType]: S; public abstract readonly [SubNodeType]: string; constructor(shape: S) { super('expression'); this[DataType] = shape; } } export class Id extends ExpressionNode { public readonly [SubNodeType] = 'identifier'; constructor(public readonly value: string) { super(string); } public [Synthesize](writer: Writer): void { writer.writeName(this.value); } } export function isLiteral(a: any): a is Literal { return a[SubNodeType] === 'literal'; } export class Literal extends ExpressionNode { public readonly [SubNodeType] = 'literal'; constructor(type: T, public readonly value: Value.Of>) { super(type); } public [Synthesize](writer: Writer): void { writer.writeValue(this.value as AWS.DynamoDB.AttributeValue); } } export class RootProperty extends ExpressionNode { public [SubNodeType] = 'root-property'; constructor(type: T, public readonly name: string) { super(type); } public [Synthesize](writer: Writer): void { writer.writeName(this.name); } } function resolveExpression(type: T, expression: Expression | Computation): ExpressionNode { return isComputation(expression) ? new ComputationExpression(type, expression) : isNode(expression) ? expression : new Literal(type, Mapper.of(type).write(expression as any) as any) ; } export class FunctionCall extends ExpressionNode { public [SubNodeType] = 'function-call'; constructor( public readonly name: string, public readonly returnType: T, public readonly parameters: ExpressionNode[] ) { super(returnType); } public [Synthesize](writer: Writer): void { writer.writeToken(this.name); writer.writeToken('('); if (this.parameters.length > 0) { this.parameters.forEach(p => { p[Synthesize](writer); writer.writeToken(','); }); writer.pop(); } writer.writeToken(')'); } } export class Object extends ExpressionNode { public readonly [SubNodeType] = 'object'; public readonly [InstanceExpression]: ExpressionNode; constructor(type: T, instanceExpression: ExpressionNode) { super(type); this[InstanceExpression] = instanceExpression; } public [Synthesize](writer: Writer): void { this[InstanceExpression][Synthesize](writer); } public equals(other: Expression): Bool { return new Bool(new Object.Equals(this, resolveExpression(this[DataType], other))); } public get size(): Number { return new Number(new FunctionCall('size', number, [this])); } public set(value: Expression | Computation): Action { return new Action(ActionType.SET, new Object.Assign(this, resolveExpression(this[DataType], value))); } public exists(): Bool { return new Bool(new FunctionCall('attribute_exists', bool, [this])); } public notExists(): Bool { return new Bool(new FunctionCall('attribute_not_exists', bool, [this])); } } export namespace Object { export function beginsWith(lhs: Expression, rhs: Expression) { return new Bool(new String.BeginsWith(resolveExpression(string, lhs), resolveExpression(string, rhs))); } export class Assign extends StatementNode { public [SubNodeType] = 'assign'; constructor(private readonly instance: Object, private readonly value: Node) { super(); } public [Synthesize](writer: Writer): void { this.instance[Synthesize](writer); writer.writeToken('='); this.value[Synthesize](writer); } } export abstract class Comparison extends ExpressionNode { protected abstract operator: string; constructor(public readonly left: ExpressionNode, public readonly right: ExpressionNode) { super(bool); } public [Synthesize](writer: Writer): void { this.left[Synthesize](writer); writer.writeToken(this.operator); this.right[Synthesize](writer); } } export class Equals extends Object.Comparison { protected readonly operator: '=' = '='; public readonly [SubNodeType] = 'equals'; } } export function size(path: Object) { return new Size(path); } export class Size extends FunctionCall { constructor(path: Object) { super('size', number, [path]); } } export class Bool extends Object { constructor(expression: ExpressionNode, boolShape?: BoolShape) { super(boolShape || bool, expression); } public and(...conditions: Expression[]): Bool { return new Bool(new Bool.And([this, ...(conditions.map(c => resolveExpression(bool, c)))])); } public or(...conditions: Expression[]): Bool { return new Bool(new Bool.Or([this, ...(conditions.map(c => resolveExpression(bool, c)))])); } public not(): Bool { return new Bool(new Bool.Not(this), this[DataType]); } } export namespace Bool { export abstract class Operands extends ExpressionNode { public abstract readonly operator: string; constructor(public readonly operands: ExpressionNode[]) { super(bool); } public [Synthesize](writer: Writer): void { writer.writeToken('('); for (const op of this.operands) { op[Synthesize](writer); writer.writeToken(` ${this.operator} `); } writer.pop(); writer.writeToken(')'); } } export class And extends Operands { public readonly operator = 'AND'; public [SubNodeType]: 'and' = 'and'; } export class Or extends Operands { public readonly operator = 'OR'; public [SubNodeType]: 'or' = 'or'; } export class Not extends ExpressionNode { public [SubNodeType]: 'not' = 'not'; constructor(public readonly operand: ExpressionNode) { super(bool); } public [Synthesize](writer: Writer): void { writer.writeToken('NOT'); writer.writeToken('('); this.operand[Synthesize](writer); writer.writeToken(')'); } } } export function or(...operands: ExpressionNode[]): Bool { return new Bool(new Bool.Or(operands)); } export function and(...operands: ExpressionNode[]): Bool { return new Bool(new Bool.And(operands)); } export function not(operand: ExpressionNode): Bool { return new Bool(new Bool.Not(operand)); } export class Ord extends Object { public greaterThan(other: Expression): Bool { return new Bool(new Ord.Gt(this, resolveExpression(number, other))); } public greaterThanOrEqual(other: Expression): Bool { return new Bool(new Ord.Gte(this, resolveExpression(number, other))); } public lessThan(other: Expression): Bool { return new Bool(new Ord.Lt(this, resolveExpression(number, other))); } public lessThanOrEqual(other: Expression): Bool { return new Bool(new Ord.Lte(this, resolveExpression(number, other))); } public between(lowerBound: Expression, upperBound: Expression): Bool { return new Bool(new Ord.Between(this, resolveExpression(this[DataType], lowerBound), resolveExpression(this[DataType], upperBound))); } } export namespace Ord { export class Gt extends Object.Comparison { protected readonly operator: '>' = '>'; public readonly [SubNodeType] = 'greaterThan'; } export class Gte extends Object.Comparison { protected readonly operator: '>=' = '>='; public readonly [SubNodeType] = 'greaterThanOrEqual'; } export class Lt extends Object.Comparison { protected readonly operator: '<' = '<'; public readonly [SubNodeType] = 'lessThan'; } export class Lte extends Object.Comparison { protected readonly operator: '<=' = '<='; public readonly [SubNodeType] = 'lessThanOrEqual'; } export class Between extends ExpressionNode { public readonly [SubNodeType] = 'between'; constructor( public readonly lhs: ExpressionNode, public readonly lowerBound: ExpressionNode, public readonly upperBound: ExpressionNode ) { super(bool); } public [Synthesize](writer: Writer): void { this.lhs[Synthesize](writer); writer.writeToken(' BETWEEN '); this.lowerBound[Synthesize](writer); writer.writeToken(' AND '); this.upperBound[Synthesize](writer); } } } export function isComputation(a: any): a is Computation { return isStatementNode(a) && a[SubNodeType] === 'computation'; } /** * Computations are not Expressions, although they do represent a value. * * This is because they are not usable within Query or Filter expressions. * * E.g. this is impossible: * ``` * table.putIf(.., item => item.plus(1).equals(2)) * ``` */ export abstract class Computation extends StatementNode { public readonly [SubNodeType] = 'computation'; public abstract readonly operator: string; constructor(public readonly lhs: ExpressionNode, public readonly rhs: ExpressionNode) { super(); } public [Synthesize](writer: Writer): void { this.lhs[Synthesize](writer); writer.writeToken(this.operator); this.rhs[Synthesize](writer); } } export class ComputationExpression extends ExpressionNode { public [SubNodeType] = 'computation-expression'; constructor(shape: T, public readonly computation: Computation) { super(shape); } public [Synthesize](writer: Writer): void { this.computation[Synthesize](writer); } } export enum ActionType { SET = 'SET' } export class Action { constructor(public readonly actionType: ActionType, public readonly statement: StatementNode) {} } /** * Represents a number in a DynamoDB Filter, Query or Update expression. */ export class Number extends Ord { constructor(expression: ExpressionNode, shape?: NumberShape) { super(shape || number, expression); } public decrement(value?: Expression) { return this.set(this.minus(value === undefined ? 1 : value)); } public increment(value?: Expression) { return this.set(this.plus(value === undefined ? 1 : value)); } public minus(value: Expression): Number.Minus { return new Number.Minus(this, resolveExpression(this[DataType], value)); } public plus(value: Expression): Number.Plus { return new Number.Plus(this, resolveExpression(this[DataType], value)); } } export namespace Number { export class Plus extends Computation { public operator: '+' = '+'; } export class Minus extends Computation { public operator: '-' = '-'; } } export class Binary extends Object {} export class StringLike extends Ord { public beginsWith(value: Expression): Bool { return String.beginsWith(this, value); } public get length() { return this.size; } } export class String extends StringLike { constructor(expression: ExpressionNode, shape?: StringShape) { super(shape || string, expression); } } export namespace String { export class BeginsWith extends FunctionCall { public readonly [SubNodeType] = 'string-begins-with'; constructor(lhs: ExpressionNode, rhs: ExpressionNode) { super('begins_with', bool, [lhs, rhs]); } } export function beginsWith(lhs: Expression, rhs: Expression) { return new Bool(new String.BeginsWith(resolveExpression(string, lhs), resolveExpression(string, rhs))); } } export class Timestamp extends Object { constructor(expression: ExpressionNode, shape?: TimestampShape) { super(shape || timestamp, expression); } } export class Enum extends StringLike {} export class List extends Object> { constructor(type: ArrayShape, expression: ExpressionNode>) { super(type, expression); } [index: number]: Of; public get length() { return this.size; } public get(index: Expression): Of { return this[DataType].Items.visit(DSL.DslVisitor as any, new List.Item(this, resolveExpression(number, index))); } public push(item: Expression): Action { return new Action(ActionType.SET, new Object.Assign(this.get(1) as any, resolveExpression(this[DataType].Items, item))); } public concat(list: Expression>): Action { return new Action(ActionType.SET, new Object.Assign(this, new List.Append(this, resolveExpression(this[DataType], list) as List))); } } export namespace List { export class Item extends ExpressionNode { public readonly [SubNodeType] = 'list-item'; constructor(public readonly list: List, public readonly index: ExpressionNode) { super(list[DataType].Items as T); } public [Synthesize](writer: Writer): void { this.list[Synthesize](writer); writer.writeToken('['); if (isLiteral(this.index)) { // indexing a list should not write the literal as an attribute value writer.writeToken((this.index as any).value.N); } else { this.index[Synthesize](writer); } writer.writeToken(']'); } } export class Append extends FunctionCall> { public [SubNodeType]: 'list-append' = 'list-append'; constructor(public readonly list: List, public readonly values: List) { super('list_append', list[DataType], [list, values]); } } } export class Set extends Object> { public contains(value: Expression): Bool { return new Bool(new Set.Contains(this, resolveExpression(this[DataType].Items, value))); } } export namespace Set { export class Contains extends FunctionCall { constructor(set: Set, value: ExpressionNode) { super('contains', bool, [set, value]); } } } export class Map extends Object> { public get(key: Expression): Of { return this[DataType].Items.visit(DSL.DslVisitor as any, typeof key === 'string' ? new Map.GetValue(this, new Id(key)) as any : new Map.GetValue(this, resolveExpression(string, key))); } public put(key: Expression, value: Expression): Action { return new Action(ActionType.SET, new Object.Assign(this.get(key) as any, resolveExpression(this[DataType].Items, value))); } } export namespace Map { export class GetValue extends ExpressionNode { public readonly [SubNodeType] = 'map-value'; constructor(public readonly map: Map, public readonly key: ExpressionNode) { super(map[DataType].Items as T); } public [Synthesize](writer: Writer): void { this.map[Synthesize](writer); writer.writeToken('.'); this.key[Synthesize](writer); } } } export class Struct> extends Object { public readonly fields: { [fieldName in keyof T['Members']]: Of; }; constructor(type: T, expression: ExpressionNode) { super(type, expression); this.fields = {} as any; for (const [name, prop] of Objekt.entries(type.Members)) { (this.fields as any)[name] = (prop as Shape).visit(DslVisitor, new Struct.Field(this, prop as Shape, name)); } } } export namespace Struct { export class Field extends ExpressionNode { public readonly [SubNodeType] = 'struct-field'; constructor(public readonly struct: Struct, type: T, public readonly name: string) { super(type); } public [Synthesize](writer: Writer): void { this.struct[Synthesize](writer); writer.writeToken('.'); writer.writeName(this.name); } } } export class Any extends Object { public as(shape: S): DSL.Of { return shape.visit(DslVisitor as any, this); } public equals(args: never): never { throw new Error('equals is not supported on a dynamic type, you must first cast with `as(shape)`'); } public set(args: never): never { throw new Error('equals is not supported on a dynamic type, you must first cast with `as(shape)`'); } } export class Union> extends Object { public as]>(shape: S): DSL.Of { return shape.visit(DslVisitor as any, this); } } }