diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec1f7d7..99b67df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,19 +14,19 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: ["12", "13", "14", "15", "16"] + node-version: ["12", "13", "14", "15", "16.8"] os: [ubuntu-latest] edgedb-version: ["stable"] include: - os: ubuntu-latest - node-version: "16" + node-version: "16.8" edgedb-version: "nightly" - os: macos-latest - node-version: "16" + node-version: "16.8" edgedb-version: "stable" - os: windows-latest - node-version: "16" + node-version: "16.8" edgedb-version: "stable" steps: diff --git a/src/client.ts b/src/client.ts index 9fec3cf..6e8ca36 100644 --- a/src/client.ts +++ b/src/client.ts @@ -34,6 +34,8 @@ import {Set} from "./datatypes/set"; import LRU from "./lru"; import {EMPTY_TUPLE_CODEC, EmptyTupleCodec, TupleCodec} from "./codecs/tuple"; import {NamedTupleCodec} from "./codecs/namedtuple"; +import {ObjectCodec} from "./codecs/object"; +import {NULL_CODEC, NullCodec} from "./codecs/codecs"; import { ALLOW_MODIFICATIONS, INNER, @@ -61,7 +63,7 @@ import { import {Transaction as LegacyTransaction} from "./legacy_transaction"; import {Transaction, START_TRANSACTION_IMPL} from "./transaction"; -const PROTO_VER: ProtocolVersion = [0, 11]; +const PROTO_VER: ProtocolVersion = [0, 12]; const PROTO_VER_MIN: ProtocolVersion = [0, 9]; enum AuthenticationStatuses { @@ -1222,16 +1224,32 @@ export class ConnectionImpl { } private _encodeArgs(args: QueryArgs, inCodec: ICodec): Buffer { - if (inCodec === EMPTY_TUPLE_CODEC && !args) { - return EmptyTupleCodec.BUFFER; - } + if (versionGreaterThanOrEqual(this.protocolVersion, [0, 12])) { + if (inCodec === NULL_CODEC && !args) { + return NullCodec.BUFFER; + } - if (inCodec instanceof NamedTupleCodec || inCodec instanceof TupleCodec) { - return inCodec.encodeArgs(args); - } + if (inCodec instanceof ObjectCodec) { + return inCodec.encodeArgs(args); + } - // Shouldn't ever happen. - throw new Error("invalid input codec"); + // Shouldn't ever happen. + throw new Error("invalid input codec"); + } else { + if (inCodec === EMPTY_TUPLE_CODEC && !args) { + return EmptyTupleCodec.BUFFER; + } + + if ( + inCodec instanceof NamedTupleCodec || + inCodec instanceof TupleCodec + ) { + return inCodec.encodeArgs(args); + } + + // Shouldn't ever happen. + throw new Error("invalid input codec"); + } } protected async _executeFlow( @@ -1573,9 +1591,13 @@ export class RawConnection extends ConnectionImpl { public async rawExecute(encodedArgs: Buffer | null = null): Promise { const result = new WriteBuffer(); + let inCodec = EMPTY_TUPLE_CODEC; + if (versionGreaterThanOrEqual(this.protocolVersion, [0, 12])) { + inCodec = NULL_CODEC; + } await this._executeFlow( encodedArgs, // arguments - EMPTY_TUPLE_CODEC, // inCodec -- to encode lack of arguments. + inCodec, // inCodec -- to encode lack of arguments. EMPTY_TUPLE_CODEC, // outCodec -- does not matter, it will not be used. result ); diff --git a/src/codecs/codecs.ts b/src/codecs/codecs.ts index 4b2c2a5..8309749 100644 --- a/src/codecs/codecs.ts +++ b/src/codecs/codecs.ts @@ -45,6 +45,7 @@ import {KNOWN_TYPENAMES, NULL_CODEC_ID} from "./consts"; /////////////////////////////////////////////////////////////////////////////// export class NullCodec extends Codec implements ICodec { + static BUFFER: Buffer = new WriteBuffer().writeInt32(0).unwrap(); encode(_buf: WriteBuffer, _object: any): void { throw new Error("null codec cannot used to encode data"); } diff --git a/src/codecs/consts.ts b/src/codecs/consts.ts index dee5076..4201e7b 100644 --- a/src/codecs/consts.ts +++ b/src/codecs/consts.ts @@ -52,3 +52,9 @@ export const KNOWN_TYPENAMES = (() => { } return res; })(); + +export const NO_RESULT = 0x6e; +export const AT_MOST_ONE = 0x6f; +export const ONE = 0x41; +export const MANY = 0x6d; +export const AT_LEAST_ONE = 0x4d; diff --git a/src/codecs/object.ts b/src/codecs/object.ts index 69497a2..b1bb7c2 100644 --- a/src/codecs/object.ts +++ b/src/codecs/object.ts @@ -18,6 +18,7 @@ import {ICodec, Codec, uuid, CodecKind} from "./ifaces"; import {ReadBuffer, WriteBuffer} from "../buffer"; +import {ONE, AT_LEAST_ONE} from "./consts"; import { generateType, ObjectConstructor, @@ -27,9 +28,17 @@ import { export class ObjectCodec extends Codec implements ICodec { private codecs: ICodec[]; private names: string[]; + private namesSet: Set; + private cardinalities: number[]; private objectType: ObjectConstructor; - constructor(tid: uuid, codecs: ICodec[], names: string[], flags: number[]) { + constructor( + tid: uuid, + codecs: ICodec[], + names: string[], + flags: number[], + cards: number[] + ) { super(tid); this.codecs = codecs; @@ -43,6 +52,8 @@ export class ObjectCodec extends Codec implements ICodec { } } this.names = newNames; + this.namesSet = new Set(newNames); + this.cardinalities = cards; this.objectType = generateType(newNames, flags); } @@ -50,6 +61,105 @@ export class ObjectCodec extends Codec implements ICodec { throw new Error("Objects cannot be passed as arguments"); } + encodeArgs(args: any): Buffer { + if (this.names[0] === "0") { + return this._encodePositionalArgs(args); + } + return this._encodeNamedArgs(args); + } + + _encodePositionalArgs(args: any): Buffer { + if (!Array.isArray(args)) { + throw new Error("an array of arguments was expected"); + } + + const codecs = this.codecs; + const codecsLen = codecs.length; + + if (args.length !== codecsLen) { + throw new Error( + `expected ${codecsLen} argument${codecsLen === 1 ? "" : "s"}, got ${ + args.length + }` + ); + } + + const elemData = new WriteBuffer(); + for (let i = 0; i < codecsLen; i++) { + elemData.writeInt32(0); // reserved + const arg = args[i]; + if (arg == null) { + const card = this.cardinalities[i]; + if (card === ONE || card === AT_LEAST_ONE) { + throw new Error( + `argument ${this.names[i]} is required, but received ${arg}` + ); + } + elemData.writeInt32(-1); + } else { + const codec = codecs[i]; + codec.encode(elemData, arg); + } + } + + const elemBuf = elemData.unwrap(); + const buf = new WriteBuffer(); + buf.writeInt32(4 + elemBuf.length); + buf.writeInt32(codecsLen); + buf.writeBuffer(elemBuf); + return buf.unwrap(); + } + + _encodeNamedArgs(args: any): Buffer { + if (args == null) { + throw new Error( + "a named arguments was expected, got a null value instead" + ); + } + + const keys = Object.keys(args); + const names = this.names; + const namesSet = this.namesSet; + const codecs = this.codecs; + const codecsLen = codecs.length; + + if (keys.length > codecsLen) { + const extraKeys = keys.filter((key) => !namesSet.has(key)); + throw new Error( + `unexpected named argument${ + extraKeys.length === 1 ? "" : "s" + }: "${extraKeys.join('", "')}"` + ); + } + + const elemData = new WriteBuffer(); + for (let i = 0; i < codecsLen; i++) { + const key = names[i]; + const val = args[key]; + + elemData.writeInt32(0); // reserved bytes + if (val == null) { + const card = this.cardinalities[i]; + if (card === ONE || card === AT_LEAST_ONE) { + throw new Error( + `argument ${this.names[i]} is required, but received ${val}` + ); + } + elemData.writeInt32(-1); + } else { + const codec = codecs[i]; + codec.encode(elemData, val); + } + } + + const elemBuf = elemData.unwrap(); + const buf = new WriteBuffer(); + buf.writeInt32(4 + elemBuf.length); + buf.writeInt32(codecsLen); + buf.writeBuffer(elemBuf); + return buf.unwrap(); + } + decode(buf: ReadBuffer): any { const codecs = this.codecs; const names = this.names; diff --git a/src/codecs/registry.ts b/src/codecs/registry.ts index a27d7cd..b8afd1d 100644 --- a/src/codecs/registry.ts +++ b/src/codecs/registry.ts @@ -317,14 +317,17 @@ export class CodecsRegistry { const codecs: ICodec[] = new Array(els); const names: string[] = new Array(els); const flags: number[] = new Array(els); + const cards: number[] = new Array(els); for (let i = 0; i < els; i++) { let flag: number; + let card: number; if (versionGreaterThanOrEqual(protocolVersion, [0, 11])) { flag = frb.readUInt32(); - frb.discard(1); // cardinality + card = frb.readUInt8(); // cardinality } else { flag = frb.readUInt8(); + card = 0; } const strLen = frb.readUInt32(); @@ -339,9 +342,10 @@ export class CodecsRegistry { codecs[i] = subCodec; names[i] = name; flags[i] = flag; + cards[i] = card; } - res = new ObjectCodec(tid, codecs, names, flags); + res = new ObjectCodec(tid, codecs, names, flags, cards); break; }