diff --git a/__tests__/integration/query.test.ts b/__tests__/integration/query.test.ts index 15a1dbb1..52234e09 100644 --- a/__tests__/integration/query.test.ts +++ b/__tests__/integration/query.test.ts @@ -499,70 +499,4 @@ describe("query can encode / decode QueryValue correctly", () => { } } }); - - it("symbol arguments throw a TypeError", async () => { - expect.assertions(2); - // whack in a symbol - // @ts-expect-error Type 'symbol' is not assignable to type 'QueryValue' - let symbolValue: QueryValue = Symbol("foo"); - try { - await client.query(fql`{ foo: ${symbolValue} }`); - } catch (e) { - if (e instanceof TypeError) { - expect(e.name).toBe("TypeError"); - expect(e.message).toBe( - "Passing symbol as a QueryValue is not supported" - ); - } - } - }); - - it("function arguments throw a TypeError", async () => { - expect.assertions(2); - // whack in a function - let fnValue: QueryValue = () => {}; - try { - await client.query(fql`{ foo: ${fnValue} }`); - } catch (e) { - if (e instanceof TypeError) { - expect(e.name).toBe("TypeError"); - expect(e.message).toBe( - "Passing function as a QueryValue is not supported" - ); - } - } - }); - - it("symbol arguments throw a TypeError in arguments", async () => { - expect.assertions(2); - // whack in a symbol - // @ts-expect-error Type 'symbol' is not assignable to type 'QueryValue' - let symbolValue: QueryValue = Symbol("foo"); - try { - await client.query(fql`foo`, { arguments: { foo: symbolValue } }); - } catch (e: any) { - if (e instanceof ClientError && e.cause instanceof TypeError) { - expect(e.cause.name).toBe("TypeError"); - expect(e.cause.message).toBe( - "Passing symbol as a QueryValue is not supported" - ); - } - } - }); - - it("function arguments throw a TypeError in arguments", async () => { - expect.assertions(2); - // whack in a function - let fnValue: QueryValue = () => {}; - try { - await client.query(fql`foo`, { arguments: { foo: fnValue } }); - } catch (e: any) { - if (e instanceof ClientError && e.cause instanceof TypeError) { - expect(e.cause.name).toBe("TypeError"); - expect(e.cause.message).toBe( - "Passing function as a QueryValue is not supported" - ); - } - } - }); }); diff --git a/__tests__/integration/template-format.test.ts b/__tests__/integration/template-format.test.ts index 264f2eba..71bfb60a 100644 --- a/__tests__/integration/template-format.test.ts +++ b/__tests__/integration/template-format.test.ts @@ -109,64 +109,6 @@ describe("query using template format", () => { expect(response.data).toBe(true); }); - it("succeeds with deep nested expressions - example 2", async () => { - const str = "foo"; - const otherStr = "bar"; - const num = 6; - const otherNum = 3; - const deepFirst = fql`(${str} + ${otherStr})`; - const deeperBuilder = fql`(${num} + 3)`; - const innerQuery = fql`(${deeperBuilder} + ${otherNum})`; - const queryBuilder = fql`${deepFirst}.length + ${innerQuery}`; - const response = await client.query(queryBuilder); - expect(response.data).toBe(18); - }); - - it("succeeds with expressions nested within objects", async () => { - const arg = { - a: fql`1`, - b: fql`2`, - }; - const queryBuilder = fql`${arg}`; - const response = await client.query(queryBuilder); - expect(response.data).toStrictEqual({ a: 1, b: 2 }); - }); - - it("succeeds with expressions nested within arrays", async () => { - const arg = [fql`1`, fql`2`]; - const queryBuilder = fql`${arg}`; - const response = await client.query(queryBuilder); - expect(response.data).toEqual([1, 2]); - }); - - it("succeeds with expressions nested within arrays and objects combined", async () => { - const arg = [ - [fql`1`], - { - a: fql`1`, - b: fql`2`, - }, - ]; - const queryBuilder = fql`${arg}`; - const response = await client.query(queryBuilder); - expect(response.data).toEqual([[1], { a: 1, b: 2 }]); - }); - - it("succeeds with multiple layers of nesting of arrays and objects", async () => { - const other = { a: fql`3`, b: fql`4` }; - const arg = [ - [fql`1 + ${fql`2`}`], - { - a: fql`1`, - b: fql`2`, - c: other, - }, - ]; - const queryBuilder = fql`${arg}`; - const response = await client.query(queryBuilder); - expect(response.data).toEqual([[3], { a: 1, b: 2, c: { a: 3, b: 4 } }]); - }); - it("succeeds with FQL string interpolation", async () => { const codeName = "Alice"; const queryBuilder = fql` diff --git a/__tests__/unit/query-builder.test.ts b/__tests__/unit/query-builder.test.ts index 68204c84..701b872d 100644 --- a/__tests__/unit/query-builder.test.ts +++ b/__tests__/unit/query-builder.test.ts @@ -5,7 +5,7 @@ describe("fql method producing Querys", () => { const queryBuilder = fql`'foo'.length`; const queryRequest = queryBuilder.toQuery(); expect(queryRequest.query).toEqual({ fql: ["'foo'.length"] }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with a string variable", () => { @@ -15,7 +15,7 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: [{ value: "foo" }, ".length"], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with a number variable", () => { @@ -25,7 +25,7 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: ["'foo'.length == ", { value: { "@int": "8" } }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with a boolean variable", () => { @@ -35,7 +35,7 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: ["val.enabled == ", { value: true }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with a null variable", () => { @@ -44,7 +44,7 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: ["value: ", { value: null }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with an object variable", () => { @@ -52,12 +52,9 @@ describe("fql method producing Querys", () => { const queryBuilder = fql`value: ${obj}`; const queryRequest = queryBuilder.toQuery(); expect(queryRequest.query).toEqual({ - fql: [ - "value: ", - { object: { bar: { value: "baz" }, foo: { value: "bar" } } }, - ], + fql: ["value: ", { value: { bar: "baz", foo: "bar" } }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with an object variable having a toQuery property", () => { @@ -65,18 +62,9 @@ describe("fql method producing Querys", () => { const queryBuilder = fql`value: ${obj}`; const queryRequest = queryBuilder.toQuery(); expect(queryRequest.query).toEqual({ - fql: [ - "value: ", - { - object: { - bar: { value: "baz" }, - foo: { value: "bar" }, - toQuery: { value: "hehe" }, - }, - }, - ], + fql: ["value: ", { value: { bar: "baz", foo: "bar", toQuery: "hehe" } }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with an array variable", () => { @@ -86,16 +74,10 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: [ "value: ", - { - array: [ - { value: { "@int": "1" } }, - { value: { "@int": "2" } }, - { value: { "@int": "3" } }, - ], - }, + { value: [{ "@int": "1" }, { "@int": "2" }, { "@int": "3" }] }, ], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with multiple variables", () => { @@ -106,7 +88,7 @@ describe("fql method producing Querys", () => { expect(queryRequest.query).toEqual({ fql: [{ value: "bar" }, ".length == ", { value: { "@int": "20" } }], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses nested expressions", () => { @@ -122,7 +104,7 @@ describe("fql method producing Querys", () => { { fql: ["Math.add(", { value: { "@int": "17" } }, ", 3)"] }, ], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses deep nested expressions", () => { @@ -150,7 +132,38 @@ describe("fql method producing Querys", () => { }, ], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); + }); + + it("adds headers if passed in", () => { + const str = "baz"; + const num = 17; + const innerQuery = fql`Math.add(${num}, 3)`; + const queryBuilder = fql`${str}.length == ${innerQuery}`; + const queryRequest = queryBuilder.toQuery({ + linearized: true, + query_timeout_ms: 600, + max_contention_retries: 4, + query_tags: { a: "tag" }, + traceparent: "00-750efa5fb6a131eb2cf4db39f28366cb-5669e71839eca76b-00", + typecheck: false, + }); + expect(queryRequest).toMatchObject({ + linearized: true, + query_timeout_ms: 600, + max_contention_retries: 4, + query_tags: { a: "tag" }, + traceparent: "00-750efa5fb6a131eb2cf4db39f28366cb-5669e71839eca76b-00", + typecheck: false, + }); + expect(queryRequest.query).toEqual({ + fql: [ + { value: "baz" }, + ".length == ", + { fql: ["Math.add(", { value: { "@int": "17" } }, ", 3)"] }, + ], + }); + expect(queryRequest.arguments).toStrictEqual({}); }); it("parses with FQL string interpolation", async () => { @@ -167,6 +180,6 @@ describe("fql method producing Querys", () => { '\n "Hello, #{name}"\n ', ], }); - expect(queryRequest.arguments).toBeUndefined(); + expect(queryRequest.arguments).toStrictEqual({}); }); }); diff --git a/__tests__/unit/tagged-format.test.ts b/__tests__/unit/tagged-format.test.ts index bdff8862..573181f1 100644 --- a/__tests__/unit/tagged-format.test.ts +++ b/__tests__/unit/tagged-format.test.ts @@ -13,7 +13,6 @@ import { TimeStub, EmbeddedSet, } from "../../src"; -import { ObjectFragment, ValueFragment } from "../../src/wire-protocol"; describe.each` long_type @@ -143,196 +142,94 @@ describe.each` const bugs_mod = new Module("Bugs"); const collection_mod = new Module("Collection"); - const encoded = TaggedTypeFormat.encode({ - // literals - double: 4.14, - int: 32, - name: "Hello, World", - null: null, - number: 48, - // objects and arrays - child: { more: { itsworking: DateStub.from("1983-04-15") } }, - extra: [ - { - id: 1, - time: new Date(), - }, - { - id: 2, - time: new Date(), - }, - ], - "@foobar": { - date: DateStub.from("1888-08-08"), - }, - // dates and times - date: DateStub.from("1923-05-13"), - time: TimeStub.from("2023-03-20T00:00:00Z"), - datetime: new Date("2023-03-20T00:00:00Z"), - // Document types - mod: bugs_mod, - docReference: new DocumentReference({ coll: bugs_mod, id: "123" }), - doc: new Document({ - coll: bugs_mod, - id: "123", - ts: TimeStub.from("2023-03-20T00:00:00Z"), - }), - namedDocReference: new NamedDocumentReference({ - coll: collection_mod, - name: "Bugs", - }), - namedDoc: new NamedDocument({ - coll: collection_mod, - name: "Bugs", - ts: TimeStub.from("2023-03-20T00:00:00Z"), - }), - nullDoc: new NullDocument( - new DocumentReference({ coll: bugs_mod, id: "123" }), - "not found" - ), - // Set types - // TODO: uncomment to add test once core accepts `@set` tagged values - // page: new Page({ data: ["a", "b"] }), - // TODO: uncomment to add test once core accepts `@set` tagged values - // page_string: new Page({ after: "abc123" }), - }); - - expect(encoded).toMatchObject({ - "@object": { + const result = JSON.stringify( + TaggedTypeFormat.encode({ // literals - double: { "@double": "4.14" }, - int: { "@int": "32" }, + double: 4.14, + int: 32, name: "Hello, World", null: null, + number: 48, // objects and arrays - child: { more: { itsworking: { "@date": "1983-04-15" } } }, - extra: [{ id: { "@int": "1" } }, { id: { "@int": "2" } }], - "@foobar": { date: { "@date": "1888-08-08" } }, - // Document types - mod: { "@mod": "Bugs" }, - docReference: { "@ref": { coll: { "@mod": "Bugs" }, id: "123" } }, - doc: { "@ref": { coll: { "@mod": "Bugs" }, id: "123" } }, - namedDocReference: { - "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" }, - }, - namedDoc: { "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" } }, - nullDoc: { "@ref": { coll: { "@mod": "Bugs" }, id: "123" } }, - }, - // Set types - // TODO: expect set types to be encoded as `@set` tagged values - }); - }); - - it("can be encoded as interpolation query", () => { - const bugs_mod = new Module("Bugs"); - const collection_mod = new Module("Collection"); - - const encoded = TaggedTypeFormat.encodeInterpolation({ - // literals - double: 4.14, - int: 32, - name: "Hello, World", - null: null, - number: 48, - // objects and arrays - child: { more: { itsworking: DateStub.from("1983-04-15") } }, - extra: [ - { - id: 1, - time: new Date(), - }, - { - id: 2, - time: new Date(), - }, - ], - "@foobar": { - date: DateStub.from("1888-08-08"), - }, - // dates and times - date: DateStub.from("1923-05-13"), - time: TimeStub.from("2023-03-20T00:00:00Z"), - datetime: new Date("2023-03-20T00:00:00Z"), - // Document types - mod: bugs_mod, - docReference: new DocumentReference({ coll: bugs_mod, id: "123" }), - doc: new Document({ - coll: bugs_mod, - id: "123", - ts: TimeStub.from("2023-03-20T00:00:00Z"), - }), - namedDocReference: new NamedDocumentReference({ - coll: collection_mod, - name: "Bugs", - }), - namedDoc: new NamedDocument({ - coll: collection_mod, - name: "Bugs", - ts: TimeStub.from("2023-03-20T00:00:00Z"), - }), - nullDoc: new NullDocument( - new DocumentReference({ coll: bugs_mod, id: "123" }), - "not found" - ), - // Set types - // TODO: uncomment to add test once core accepts `@set` tagged values - // page: new Page({ data: ["a", "b"] }), - // TODO: uncomment to add test once core accepts `@set` tagged values - // page_string: new Page({ after: "abc123" }), - }); - - expect(encoded).toMatchObject({ - object: { - // literals - double: { value: { "@double": "4.14" } }, - int: { value: { "@int": "32" } }, - name: { value: "Hello, World" }, - null: { value: null }, - // objects and arrays - child: { - object: { - more: { - object: { itsworking: { value: { "@date": "1983-04-15" } } }, - }, + child: { more: { itsworking: DateStub.from("1983-04-15") } }, + extra: [ + { + id: 1, + time: new Date(), }, - }, - extra: { array: expect.arrayContaining([]) }, - "@foobar": { - object: { - date: { value: { "@date": "1888-08-08" } }, + { + id: 2, + time: new Date(), }, + ], + "@foobar": { + date: DateStub.from("1888-08-08"), }, + // dates and times + date: DateStub.from("1923-05-13"), + time: TimeStub.from("2023-03-20T00:00:00Z"), + datetime: new Date("2023-03-20T00:00:00Z"), // Document types - mod: { value: { "@mod": "Bugs" } }, - docReference: { - value: { - "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, - }, - }, - doc: { - value: { - "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, - }, - }, - namedDocReference: { - value: { - "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" }, - }, - }, - namedDoc: { - value: { - "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" }, - }, - }, - nullDoc: { - value: { - "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, - }, - }, - }, - // Set types - // TODO: expect set types to be encoded as `@set` tagged values + mod: bugs_mod, + docReference: new DocumentReference({ coll: bugs_mod, id: "123" }), + doc: new Document({ + coll: bugs_mod, + id: "123", + ts: TimeStub.from("2023-03-20T00:00:00Z"), + }), + namedDocReference: new NamedDocumentReference({ + coll: collection_mod, + name: "Bugs", + }), + namedDoc: new NamedDocument({ + coll: collection_mod, + name: "Bugs", + ts: TimeStub.from("2023-03-20T00:00:00Z"), + }), + nullDoc: new NullDocument( + new DocumentReference({ coll: bugs_mod, id: "123" }), + "not found" + ), + // Set types + // TODO: uncomment to add test once core accepts `@set` tagged values + // page: new Page({ data: ["a", "b"] }), + // TODO: uncomment to add test once core accepts `@set` tagged values + // page_string: new Page({ after: "abc123" }), + }) + ); + + const backToObj = JSON.parse(result)["@object"]; + + // literals + expect(backToObj.double).toStrictEqual({ "@double": "4.14" }); + expect(backToObj.null).toBeNull(); + // objects and arrays + expect(backToObj.child.more.itsworking).toStrictEqual({ + "@date": "1983-04-15", + }); + expect(backToObj.extra).toHaveLength(2); + // Document types + expect(backToObj.mod).toStrictEqual({ "@mod": "Bugs" }); + expect(backToObj.docReference).toStrictEqual({ + "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, + }); + expect(backToObj.doc).toStrictEqual({ + "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, + }); + expect(backToObj.namedDocReference).toStrictEqual({ + "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" }, + }); + expect(backToObj.namedDoc).toStrictEqual({ + "@ref": { coll: { "@mod": "Collection" }, name: "Bugs" }, + }); + expect(backToObj.nullDoc).toStrictEqual({ + "@ref": { coll: { "@mod": "Bugs" }, id: "123" }, }); + // Set types + // TODO: uncomment to add test once core accepts `@set` tagged values + // expect(backToObj.page).toStrictEqual({ "@set": { data: ["a", "b"] } }); + // TODO: uncomment to add test once core accepts `@set` tagged values + // expect(backToObj.page_string).toStrictEqual({ "@set": "abc123" }); }); it("handles conflicts", () => { @@ -342,97 +239,46 @@ describe.each` int: { "@int": 1 }, long: { "@long": BigInt("99999999999999999") }, double: { "@double": 1.99 }, - }) as ObjectFragment; - - expect(result).toMatchObject({ - date: { "@object": { "@date": { "@date": "2022-11-01" } } }, - time: { "@object": { "@time": { "@time": "2022-11-02T05:00:00.000Z" } } }, - int: { "@object": { "@int": { "@int": "1" } } }, - long: { "@object": { "@long": { "@long": "99999999999999999" } } }, - double: { "@object": { "@double": { "@double": "1.99" } } }, }); - }); - - it("handles conflicts in interpolation queries", () => { - const result = TaggedTypeFormat.encodeInterpolation({ - date: { "@date": DateStub.from("2022-11-01") }, - time: { "@time": TimeStub.from("2022-11-02T05:00:00.000Z") }, - int: { "@int": 1 }, - long: { "@long": BigInt("99999999999999999") }, - double: { "@double": 1.99 }, - }) as ObjectFragment; - - expect(result).toMatchObject({ - object: { - date: { object: { "@date": { value: { "@date": "2022-11-01" } } } }, - time: { - object: { - "@time": { value: { "@time": "2022-11-02T05:00:00.000Z" } }, - }, - }, - int: { object: { "@int": { value: { "@int": "1" } } } }, - long: { - object: { "@long": { value: { "@long": "99999999999999999" } } }, - }, - double: { object: { "@double": { value: { "@double": "1.99" } } } }, - }, + expect(result["date"]["@object"]["@date"]).toStrictEqual({ + "@date": "2022-11-01", }); - }); - - it("handles nested conflict types", () => { - const encoded = TaggedTypeFormat.encode({ - "@date": { - "@date": { - "@time": new Date("2022-12-02T02:00:00.000Z"), - }, - }, + expect(result["time"]["@object"]["@time"]).toStrictEqual({ + "@time": "2022-11-02T05:00:00.000Z", }); - - expect(encoded).toMatchObject({ - "@object": { - "@date": { - "@object": { - "@date": { - "@object": { - "@time": { "@time": "2022-12-02T02:00:00.000Z" }, - }, - }, - }, - }, - }, + expect(result["int"]["@object"]["@int"]).toStrictEqual({ "@int": "1" }); + expect(result["long"]["@object"]["@long"]).toStrictEqual({ + "@long": "99999999999999999", }); - }); - - it("handles nested conflict types in interpolation queries", () => { - const encoded = TaggedTypeFormat.encodeInterpolation({ - "@date": { - "@date": { - "@time": new Date("2022-12-02T02:00:00.000Z"), - }, - }, + expect(result["double"]["@object"]["@double"]).toEqual({ + "@double": "1.99", }); + }); - expect(encoded).toMatchObject({ - object: { - "@date": { - object: { + it("handles nested conflict types", () => { + expect( + JSON.stringify( + TaggedTypeFormat.encode({ + "@date": { "@date": { - object: { - "@time": { value: { "@time": "2022-12-02T02:00:00.000Z" } }, - }, + "@time": new Date("2022-12-02T02:00:00.000Z"), }, }, - }, - }, - }); + }) + ) + ).toEqual( + '{"@object":{"@date":{"@object":{"@date":{"@object":{"@time":{"@time":"2022-12-02T02:00:00.000Z"}}}}}}}' + ); }); it("wraps user-provided `@` fields", () => { - const encoded = TaggedTypeFormat.encodeInterpolation({ - "@foo": true, - }); - - expect(encoded).toMatchObject({ object: { "@foo": { value: true } } }); + expect( + JSON.stringify( + TaggedTypeFormat.encode({ + "@foo": true, + }) + ) + ).toEqual('{"@object":{"@foo":true}}'); }); it.each` @@ -467,13 +313,11 @@ describe.each` } } testCase; - const encoded = TaggedTypeFormat.encodeInterpolation( - input - ) as ValueFragment; - const encodedKey = Object.keys(encoded.value as Object)[0]; + const encoded = TaggedTypeFormat.encode(input); + const encodedKey = Object.keys(encoded)[0]; expect(encodedKey).toEqual(tag); const decoded = TaggedTypeFormat.decode( - JSON.stringify(encoded.value), + JSON.stringify(encoded), decodeOptions ); expect(typeof decoded).toBe(expectedType); @@ -488,6 +332,6 @@ describe.each` ${Number.NEGATIVE_INFINITY} | ${"NEGATIVE_INFINITY"} ${Number.POSITIVE_INFINITY} | ${"POSITIVE_INFINITY"} `("Throws if BigInt value is $testCase", async ({ input }) => { - expect(() => TaggedTypeFormat.encodeInterpolation(input)).toThrow(); + expect(() => TaggedTypeFormat.encode(input)).toThrow(); }); }); diff --git a/package.json b/package.json index 510b578d..6d5086f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fauna", - "version": "1.3.0", + "version": "1.3.1", "description": "A driver to query Fauna databases in browsers, Node.js, and other Javascript runtimes", "homepage": "https://fauna.com", "bugs": { diff --git a/src/client.ts b/src/client.ts index 0171daac..624ee986 100644 --- a/src/client.ts +++ b/src/client.ts @@ -34,7 +34,6 @@ import { type QueryOptions, type QuerySuccess, type QueryValue, - QueryValueObject, } from "./wire-protocol"; type RequiredClientConfig = ClientConfiguration & @@ -436,11 +435,7 @@ in an environmental variable named FAUNA_SECRET or pass it to the Client\ const queryArgs = requestConfig.arguments ? isTaggedFormat - ? // Type cast safety: requestConfig.arguments is an object, so - // encoding creates a subtype of QueryValueObject - (TaggedTypeFormat.encode( - requestConfig.arguments - ) as QueryValueObject) + ? TaggedTypeFormat.encode(requestConfig.arguments) : requestConfig.arguments : undefined; diff --git a/src/query-builder.ts b/src/query-builder.ts index d9ce39dd..e8353379 100644 --- a/src/query-builder.ts +++ b/src/query-builder.ts @@ -5,7 +5,6 @@ import type { QueryInterpolation, QueryRequest, QueryOptions, - FQLFragment, } from "./wire-protocol"; /** @@ -70,37 +69,42 @@ export class Query { * { query: { fql: ["'foo'.length == ", { value: { "@int": "8" } }, ""] }} * ``` */ - toQuery(requestHeaders: QueryOptions = {}): QueryRequest { - return { - query: this.#render_query(), - arguments: requestHeaders.arguments, - }; + toQuery(requestHeaders: QueryOptions = {}): QueryRequest { + return { ...this.#render(requestHeaders), ...requestHeaders }; } - #render_query(): FQLFragment { + #render(requestHeaders: QueryOptions): QueryRequest { if (this.#queryFragments.length === 1) { - return { fql: [this.#queryFragments[0]] }; + return { query: { fql: [this.#queryFragments[0]] }, arguments: {} }; } - let renderedFragments: (string | QueryInterpolation)[] = + let resultArgs: QueryValueObject = {}; + const renderedFragments: (string | QueryInterpolation)[] = this.#queryFragments.flatMap((fragment, i) => { // There will always be one more fragment than there are arguments if (i === this.#queryFragments.length - 1) { return fragment === "" ? [] : [fragment]; } - // arguments in the template format must always be encoded, regardless - // of the "x-format" request header - // TODO: catch and rethrow Errors, indicating bad user input const arg = this.#queryArgs[i]; - const encoded = TaggedTypeFormat.encodeInterpolation(arg); + let subQuery: string | QueryInterpolation; + if (arg instanceof Query) { + const request = arg.toQuery(requestHeaders); + subQuery = request.query; + resultArgs = { ...resultArgs, ...request.arguments }; + } else { + // arguments in the template format must always be encoded, regardless + // of the "x-format" request header + // TODO: catch and rethrow Errors, indicating bad user input + subQuery = { value: TaggedTypeFormat.encode(arg) }; + } - return [fragment, encoded]; + return [fragment, subQuery].filter((x) => x !== ""); }); - // We don't need to send empty-string fragments over the wire - renderedFragments = renderedFragments.filter((x) => x !== ""); - - return { fql: renderedFragments }; + return { + query: { fql: renderedFragments }, + arguments: resultArgs, + }; } } diff --git a/src/tagged-type.ts b/src/tagged-type.ts index e74a11a7..c7f29a87 100644 --- a/src/tagged-type.ts +++ b/src/tagged-type.ts @@ -1,5 +1,4 @@ import { ClientError } from "./errors"; -import { Query } from "./query-builder"; import { DateStub, Document, @@ -12,15 +11,7 @@ import { NullDocument, EmbeddedSet, } from "./values"; -import { - QueryValueObject, - QueryValue, - FQLFragment, - ObjectFragment, - ArrayFragment, - QueryInterpolation, - ValueFragment, -} from "./wire-protocol"; +import { QueryValueObject, QueryValue } from "./wire-protocol"; export interface DecodeOptions { long_type: "number" | "bigint"; @@ -31,23 +22,13 @@ export interface DecodeOptions { */ export class TaggedTypeFormat { /** - * Encode the value to the Tagged Type format for Fauna + * Encode the Object to the Tagged Type format for Fauna * - * @param input - value that will be encoded + * @param obj - Object that will be encoded * @returns Map of result */ - static encode(input: QueryValue): TaggedType { - return encode(input); - } - - /** - * Encode the value to a QueryInterpolation to send to Fauna - * - * @param input - value that will be encoded - * @returns Map of result - */ - static encodeInterpolation(input: QueryValue): QueryInterpolation { - return encodeInterpolation(input); + static encode(obj: any): any { + return encode(obj); } /** @@ -126,7 +107,7 @@ type TaggedDouble = { "@double": string }; type TaggedInt = { "@int": string }; type TaggedLong = { "@long": string }; type TaggedMod = { "@mod": string }; -type TaggedObject = { "@object": EncodedObject }; +type TaggedObject = { "@object": QueryValueObject }; type TaggedRef = { "@ref": { id: string; coll: TaggedMod } | { name: string; coll: TaggedMod }; }; @@ -134,22 +115,6 @@ type TaggedRef = { // type TaggedSet = { "@set": { data: QueryValue[]; after?: string } }; type TaggedTime = { "@time": string }; -type EncodedObject = { [key: string]: TaggedType }; -type TaggedType = - | string - | boolean - | null - | TaggedDate - | TaggedDouble - | TaggedInt - | TaggedLong - | TaggedMod - | TaggedObject - | TaggedRef - | TaggedTime - | EncodedObject - | TaggedType[]; - export const LONG_MIN = BigInt("-9223372036854775808"); export const LONG_MAX = BigInt("9223372036854775807"); export const INT_MIN = -(2 ** 31); @@ -193,9 +158,9 @@ const encodeMap = { string: (value: string): string => { return value; }, - object: (input: QueryValueObject): TaggedObject | EncodedObject => { + object: (input: QueryValueObject): TaggedObject | QueryValueObject => { let wrapped = false; - const _out: EncodedObject = {}; + const _out: QueryValueObject = {}; for (const k in input) { if (k.startsWith("@")) { @@ -207,7 +172,11 @@ const encodeMap = { } return wrapped ? { "@object": _out } : _out; }, - array: (input: QueryValue[]): TaggedType[] => input.map(encode), + array: (input: Array): Array => { + const _out: QueryValue = []; + for (const i in input) _out.push(encode(input[i])); + return _out; + }, date: (dateValue: Date): TaggedTime => ({ "@time": dateValue.toISOString(), }), @@ -242,7 +211,10 @@ const encodeMap = { }, }; -const encode = (input: QueryValue): TaggedType => { +const encode = (input: QueryValue): QueryValue => { + if (input === undefined) { + throw new TypeError("Passing undefined as a QueryValue is not supported"); + } switch (typeof input) { case "bigint": return encodeMap["bigint"](input); @@ -281,78 +253,9 @@ const encode = (input: QueryValue): TaggedType => { return encodeMap["set"](input); } else if (input instanceof EmbeddedSet) { return encodeMap["set"](input); - } else if (input instanceof Query) { - throw new TypeError( - "Cannot encode instance of type 'Query'. Try using TaggedTypeFormat.encodeInterpolation instead." - ); } else { return encodeMap["object"](input); } - default: - // catch "undefined", "symbol", and "function" - throw new TypeError( - `Passing ${typeof input} as a QueryValue is not supported` - ); } // anything here would be unreachable code }; - -const encodeInterpolation = (input: QueryValue): QueryInterpolation => { - switch (typeof input) { - case "bigint": - case "string": - case "number": - case "boolean": - return encodeValueInterpolation(encode(input)); - case "object": - if ( - input == null || - input instanceof Date || - input instanceof DateStub || - input instanceof TimeStub || - input instanceof Module || - input instanceof DocumentReference || - input instanceof NamedDocumentReference || - input instanceof Page || - input instanceof EmbeddedSet - ) { - return encodeValueInterpolation(encode(input)); - } else if (input instanceof NullDocument) { - return encodeInterpolation(input.ref); - } else if (input instanceof Query) { - return encodeQueryInterpolation(input); - } else if (Array.isArray(input)) { - return encodeArrayInterpolation(input); - } else { - return encodeObjectInterpolation(input); - } - default: - // catch "undefined", "symbol", and "function" - throw new TypeError( - `Passing ${typeof input} as a QueryValue is not supported` - ); - } -}; - -const encodeObjectInterpolation = (input: QueryValueObject): ObjectFragment => { - const _out: QueryValueObject = {}; - - for (const k in input) { - if (input[k] !== undefined) { - _out[k] = encodeInterpolation(input[k]); - } - } - return { object: _out }; -}; - -const encodeArrayInterpolation = (input: Array): ArrayFragment => { - const encodedItems = input.map(encodeInterpolation); - return { array: encodedItems }; -}; - -const encodeQueryInterpolation = (value: Query): FQLFragment => - value.toQuery().query; - -const encodeValueInterpolation = (value: QueryValue): ValueFragment => ({ - value, -}); diff --git a/src/util/package-version.ts b/src/util/package-version.ts index 079a0731..c24da044 100644 --- a/src/util/package-version.ts +++ b/src/util/package-version.ts @@ -1,4 +1,4 @@ //THIS FILE IS AUTOGENERATED. DO NOT EDIT. SEE .husky/pre-commit /** The current package version. */ -export const packageVersion = "1.3.0"; +export const packageVersion = "1.3.1"; diff --git a/src/wire-protocol.ts b/src/wire-protocol.ts index 644e3dd3..8682b1f7 100644 --- a/src/wire-protocol.ts +++ b/src/wire-protocol.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { Query, fql } from "./query-builder"; +import { fql } from "./query-builder"; import { DateStub, Document, @@ -16,9 +16,9 @@ import { /** * A request to make to Fauna. */ -export interface QueryRequest { +export interface QueryRequest { /** The query */ - query: T; + query: string | QueryInterpolation; /** Optional arguments. Variables in the query will be initialized to the * value associated with an argument key. @@ -218,11 +218,7 @@ export const isQueryResponse = (res: any): res is QueryResponse => * @see {@link ValueFragment} and {@link FQLFragment} for additional * information */ -export type QueryInterpolation = - | FQLFragment - | ValueFragment - | ObjectFragment - | ArrayFragment; +export type QueryInterpolation = FQLFragment | ValueFragment; /** * A piece of an interpolated query that represents an actual value. Arguments @@ -244,40 +240,6 @@ export type QueryInterpolation = */ export type ValueFragment = { value: QueryValue }; -/** - * A piece of an interpolated query that represents an object. Arguments - * are passed to fauna using ObjectFragments so that query arguments can be - * nested within javascript objects. - * - * ObjectFragments must always be encoded with tags, regardless of the "x-format" - * request header sent. - * @example - * ```typescript - * const arg = { startDate: DateStub.from("2023-09-01") }; - * const query = fql`${arg})`; - * // produces - * { fql: [{ object: { startDate: { "@date": "2023-09-01" } } }] } - * ``` - */ -export type ObjectFragment = { object: QueryValueObject }; - -/** - * A piece of an interpolated query that represents an array. Arguments - * are passed to fauna using ArrayFragments so that query arguments can be - * nested within javascript arrays. - * - * ArrayFragments must always be encoded with tags, regardless of the "x-format" - * request header sent. - * @example - * ```typescript - * const arg = [1, 2]; - * const query = fql`${arg})`; - * // produces - * { fql: [{ array: [{ "@int": 1 }, { "@int": 2 }] }] } - * ``` - */ -export type ArrayFragment = { array: QueryValue[] }; - /** * A piece of an interpolated query. Interpolated Queries can be safely composed * together without concern of query string injection. @@ -344,7 +306,6 @@ export type QueryValue = | boolean | QueryValueObject | Array - | Date // client-provided classes | DateStub | TimeStub @@ -355,5 +316,4 @@ export type QueryValue = | NamedDocumentReference | NullDocument | Page - | EmbeddedSet - | Query; + | EmbeddedSet;