Skip to content

Commit

Permalink
Add encoding/decoding for Fauna Bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
ptpaterson committed May 6, 2024
1 parent 90499c5 commit b5df81b
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 19 deletions.
55 changes: 54 additions & 1 deletion __tests__/integration/template-format.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fql } from "../../src";
import { fql, TimeStub } from "../../src";
import { getClient } from "../client";

const client = getClient({
Expand Down Expand Up @@ -118,4 +118,57 @@ describe("query using template format", () => {
const response = await client.query(queryBuilder);
expect(response.data).toBe("Hello, Alice");
});

it("succeeds with a Date arg", async () => {
const date = new Date();
const queryBuilder = fql`${date}`;
const response = await client.query<TimeStub>(queryBuilder);
expect(response.data.isoString).toBe(date.toISOString());
});

it("succeeds with an ArrayBuffer variable", async () => {
const buf = new Uint8Array([1, 2, 3]).buffer;
const queryBuilder = fql`${buf}`;
const response = await client.query<ArrayBuffer>(queryBuilder);
expect(response.data).toEqual(buf);
expect(response.data.byteLength).toBe(3);
});

it("succeeds with ArrayBufferView variables", async () => {
const buf1 = new Uint8Array([1]);
const buf2 = new Int8Array([1, 2]);
const buf3 = new Uint16Array([1, 2, 3]);
const buf4 = new Int16Array([1, 2, 3, 4]);
const queryBuilder = fql`
[
${buf1},
${buf2},
${buf3},
${buf4},
]
`;
const response =
await client.query<[ArrayBuffer, ArrayBuffer, ArrayBuffer, ArrayBuffer]>(
queryBuilder,
);
expect(response.data[0]).toEqual(buf1.buffer);
expect(response.data[0].byteLength).toEqual(1);
expect(response.data[1]).toEqual(buf2.buffer);
expect(response.data[1].byteLength).toEqual(2);
expect(response.data[2]).toEqual(buf3.buffer);
expect(response.data[2].byteLength).toEqual(6);
expect(response.data[3]).toEqual(buf4.buffer);
expect(response.data[3].byteLength).toEqual(8);
});

it("succeeds using Node Buffer to encode strings", async () => {
const str =
"This is a test string 🚀 with various characters: !@#$%^&*()_+=-`~[]{}|;:'\",./<>?";
const buf = Buffer.from(str);
const queryBuilder = fql`${buf}`;
const response = await client.query<ArrayBuffer>(queryBuilder);

const decoded = Buffer.from(response.data).toString();
expect(decoded).toBe(str);
});
});
61 changes: 50 additions & 11 deletions __tests__/unit/tagged-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ import {
EmbeddedSet,
} from "../../src";

const testBytesString =
"This is a test string 🚀 with various characters: !@#$%^&*()_+=-`~[]{}|;:'\",./<>?";
const testBuffer = Buffer.from(testBytesString);
const testBytesBase64 = Buffer.from(testBytesString).toString("base64");

const testArrayBufferU8 = new ArrayBuffer(4);
const testArrayBufferViewU8 = new Uint8Array(testArrayBufferU8);
testArrayBufferViewU8[1] = 1;
testArrayBufferViewU8[2] = 2;
testArrayBufferViewU8[3] = 3;
testArrayBufferViewU8[4] = 4;

const testArrayBufferI8 = new ArrayBuffer(4);
const testArrayBufferViewI8 = new Int8Array(testArrayBufferI8);
testArrayBufferViewI8[1] = -1;
testArrayBufferViewI8[2] = -2;
testArrayBufferViewI8[3] = -3;
testArrayBufferViewI8[4] = -4;

describe.each`
long_type
${"number"}
Expand Down Expand Up @@ -79,7 +98,8 @@ describe.each`
}
},
"page": { "@set": { "data": ["a", "b"] } },
"embeddedSet": { "@set": "abc123" }
"embeddedSet": { "@set": "abc123" },
"bytes": { "@bytes": "${testBytesBase64}" }
}`;

const bugs_mod = new Module("Bugs");
Expand Down Expand Up @@ -125,7 +145,9 @@ describe.each`
expect(result.measurements[1].time).toBeInstanceOf(TimeStub);
expect(result.molecules).toEqual(
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
long_type === "number" ? 999999999999999999 : BigInt("999999999999999999")
long_type === "number"
? 999999999999999999
: BigInt("999999999999999999"),
);
expect(result.null).toBeNull();
expect(result.mod).toStrictEqual(bugs_mod);
Expand All @@ -136,6 +158,7 @@ describe.each`
expect(result.nullDoc).toStrictEqual(nullDoc);
expect(result.page).toStrictEqual(page);
expect(result.embeddedSet).toStrictEqual(embeddedSet);
expect(Buffer.from(result.bytes).toString()).toEqual(testBytesString);
});

it("can be encoded", () => {
Expand Down Expand Up @@ -188,14 +211,18 @@ describe.each`
}),
nullDoc: new NullDocument(
new DocumentReference({ coll: bugs_mod, id: "123" }),
"not found"
"not found",
),
bytes_array_buffer: testArrayBufferU8,
bytes_array_buffer_view_u8: testArrayBufferViewU8,
bytes_array_buffer_view_i8: testArrayBufferViewI8,
bytes_from_string: testBuffer,
// 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"];
Expand Down Expand Up @@ -225,6 +252,18 @@ describe.each`
expect(backToObj.nullDoc).toStrictEqual({
"@ref": { coll: { "@mod": "Bugs" }, id: "123" },
});
expect(backToObj.bytes_array_buffer).toStrictEqual({
"@bytes": Buffer.from(testArrayBufferU8).toString("base64"),
});
expect(backToObj.bytes_array_buffer_view_u8).toStrictEqual({
"@bytes": Buffer.from(testArrayBufferViewU8).toString("base64"),
});
expect(backToObj.bytes_array_buffer_view_i8).toStrictEqual({
"@bytes": Buffer.from(testArrayBufferViewI8).toString("base64"),
});
expect(backToObj.bytes_from_string).toStrictEqual({
"@bytes": testBytesBase64,
});
// Set types
// TODO: uncomment to add test once core accepts `@set` tagged values
// expect(backToObj.page).toStrictEqual({ "@set": { data: ["a", "b"] } });
Expand Down Expand Up @@ -264,10 +303,10 @@ describe.each`
"@time": new Date("2022-12-02T02:00:00.000Z"),
},
},
})
)
}),
),
).toEqual(
'{"@object":{"@date":{"@object":{"@date":{"@object":{"@time":{"@time":"2022-12-02T02:00:00.000Z"}}}}}}}'
'{"@object":{"@date":{"@object":{"@date":{"@object":{"@time":{"@time":"2022-12-02T02:00:00.000Z"}}}}}}}',
);
});

Expand All @@ -276,8 +315,8 @@ describe.each`
JSON.stringify(
TaggedTypeFormat.encode({
"@foo": true,
})
)
}),
),
).toEqual('{"@object":{"@foo":true}}');
});

Expand Down Expand Up @@ -318,11 +357,11 @@ describe.each`
expect(encodedKey).toEqual(tag);
const decoded = TaggedTypeFormat.decode(
JSON.stringify(encoded),
decodeOptions
decodeOptions,
);
expect(typeof decoded).toBe(expectedType);
expect(decoded).toEqual(expected);
}
},
);

it.each`
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,8 @@
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
},
"dependencies": {
"base64-js": "^1.5.1"
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export {
ServiceInternalError,
ThrottlingError,
} from "./errors";
export { type Query, fql } from "./query-builder";
export { type Query, type QueryArgument, fql } from "./query-builder";
export { LONG_MAX, LONG_MIN, TaggedTypeFormat } from "./tagged-type";
export {
type QueryValueObject,
Expand Down
8 changes: 5 additions & 3 deletions src/query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
QueryOptions,
} from "./wire-protocol";

export type QueryArgument = QueryValue | Query | Date | ArrayBufferView;

/**
* Creates a new Query. Accepts template literal inputs.
* @param queryFragments - a {@link TemplateStringsArray} that constitute
Expand All @@ -25,7 +27,7 @@ import type {
*/
export function fql(
queryFragments: ReadonlyArray<string>,
...queryArgs: (QueryValue | Query)[]
...queryArgs: QueryArgument[]
): Query {
return new Query(queryFragments, ...queryArgs);
}
Expand All @@ -37,11 +39,11 @@ export function fql(
*/
export class Query {
readonly #queryFragments: ReadonlyArray<string>;
readonly #queryArgs: (QueryValue | Query)[];
readonly #queryArgs: QueryArgument[];

constructor(
queryFragments: ReadonlyArray<string>,
...queryArgs: (QueryValue | Query)[]
...queryArgs: QueryArgument[]
) {
if (
queryFragments.length === 0 ||
Expand Down
30 changes: 28 additions & 2 deletions src/tagged-type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import base64 from "base64-js";

import { ClientError } from "./errors";
import {
DateStub,
Expand Down Expand Up @@ -98,13 +100,16 @@ Returning as Number with loss of precision. Use long_type 'bigint' instead.`);
return value["@object"];
} else if (value["@stream"]) {
return new StreamToken(value["@stream"]);
} else if (value["@bytes"]) {
return base64toBuffer(value["@bytes"]);
}

return value;
});
}
}

type TaggedBytes = { "@bytes": string };
type TaggedDate = { "@date": string };
type TaggedDouble = { "@double": string };
type TaggedInt = { "@int": string };
Expand All @@ -127,7 +132,7 @@ const encodeMap = {
bigint: (value: bigint): TaggedLong | TaggedInt => {
if (value < LONG_MIN || value > LONG_MAX) {
throw new RangeError(
"BigInt value exceeds max magnitude for a 64-bit Fauna long. Use a 'number' to represent doubles beyond that limit."
"BigInt value exceeds max magnitude for a 64-bit Fauna long. Use a 'number' to represent doubles beyond that limit.",
);
}
if (value >= INT_MIN && value <= INT_MAX) {
Expand Down Expand Up @@ -201,7 +206,7 @@ const encodeMap = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
set: (value: Page<QueryValue> | EmbeddedSet) => {
throw new ClientError(
"Page could not be encoded. Fauna does not accept encoded Set values, yet. Use Page.data and Page.after as arguments, instead."
"Page could not be encoded. Fauna does not accept encoded Set values, yet. Use Page.data and Page.after as arguments, instead.",
);
// TODO: uncomment to encode Pages once core starts accepting `@set` tagged values
// if (value.data === undefined) {
Expand All @@ -215,6 +220,9 @@ const encodeMap = {
// TODO: encode as a tagged value if provided as a query arg?
// streamToken: (value: StreamToken): TaggedStreamToken => ({ "@stream": value.token }),
streamToken: (value: StreamToken): string => value.token,
bytes: (value: ArrayBuffer | ArrayBufferView): TaggedBytes => ({
"@bytes": bufferToBase64(value),
}),
};

const encode = (input: QueryValue): QueryValue => {
Expand Down Expand Up @@ -261,9 +269,27 @@ const encode = (input: QueryValue): QueryValue => {
return encodeMap["set"](input);
} else if (input instanceof StreamToken) {
return encodeMap["streamToken"](input);
} else if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
return encodeMap["bytes"](input);
} else {
return encodeMap["object"](input);
}
}
// anything here would be unreachable code
};

function bufferToBase64(value: ArrayBuffer | ArrayBufferView): string {
if (value instanceof ArrayBuffer) {
return base64.fromByteArray(new Uint8Array(value));
} else if (value instanceof Uint8Array) {
return base64.fromByteArray(value);
} else {
return base64.fromByteArray(
new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
);
}
}

function base64toBuffer(value: string): ArrayBuffer {
return base64.toByteArray(value).buffer;
}
3 changes: 2 additions & 1 deletion src/wire-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ export type QueryValue =
| NullDocument
| Page<QueryValue>
| EmbeddedSet
| StreamToken;
| StreamToken
| ArrayBuffer;

export type StreamRequest = {
token: string;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==

base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==

brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
Expand Down

0 comments on commit b5df81b

Please sign in to comment.