Skip to content

Commit

Permalink
Add encoding/decoding for Fauna Bytes (#260)
Browse files Browse the repository at this point in the history
  • Loading branch information
ptpaterson authored May 10, 2024
1 parent d7581de commit 5c503ee
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 19 deletions.
70 changes: 69 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 { ClientError, fql, TimeStub } from "../../src";
import { getClient } from "../client";

const client = getClient({
Expand Down Expand Up @@ -118,4 +118,72 @@ 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]);
const queryBuilder = fql`${buf.buffer}`;
const response = await client.query<Uint8Array>(queryBuilder);
expect(response.data.byteLength).toBe(3);
expect(response.data).toEqual(buf);
});

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

/**
* See table of various TypedArrays here
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects
*/
it.each`
ViewType
${Int8Array}
${Uint8ClampedArray}
${Int16Array}
${Uint16Array}
${Int32Array}
${Uint32Array}
${Float32Array}
${Float64Array}
`("fails with $ViewType variables", async ({ ViewType }) => {
const buf = new ViewType([1, 2]);

await expect(client.query(fql`${buf}`)).rejects.toThrow(ClientError);
});

/**
* See table of various TypedArrays here
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#typedarray_objects
*/
it.each`
ViewType
${BigInt64Array}
${BigUint64Array}
`("fails with $ViewType variables", async ({ ViewType }) => {
const buf = new ViewType([BigInt(1), BigInt(2)]);

await expect(client.query(fql`${buf}`)).rejects.toThrow(ClientError);
});

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<Uint8Array>(queryBuilder);

const decoded = Buffer.from(response.data).toString();
expect(decoded).toBe(str);
});
});
50 changes: 39 additions & 11 deletions __tests__/unit/tagged-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ 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;

describe.each`
long_type
${"number"}
Expand Down Expand Up @@ -79,7 +91,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 +138,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 +151,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 +204,17 @@ 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_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 +244,15 @@ 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_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 +292,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 +304,8 @@ describe.each`
JSON.stringify(
TaggedTypeFormat.encode({
"@foo": true,
})
)
}),
),
).toEqual('{"@object":{"@foo":true}}');
});

Expand Down Expand Up @@ -318,11 +346,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
13 changes: 10 additions & 3 deletions src/query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import type {
QueryOptions,
} from "./wire-protocol";

export type QueryArgument =
| QueryValue
| Query
| Date
| ArrayBuffer
| Uint8Array;

/**
* Creates a new Query. Accepts template literal inputs.
* @param queryFragments - a {@link TemplateStringsArray} that constitute
Expand All @@ -25,7 +32,7 @@ import type {
*/
export function fql(
queryFragments: ReadonlyArray<string>,
...queryArgs: (QueryValue | Query)[]
...queryArgs: QueryArgument[]
): Query {
return new Query(queryFragments, ...queryArgs);
}
Expand All @@ -37,11 +44,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
29 changes: 27 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 | Uint8Array): TaggedBytes => ({
"@bytes": bufferToBase64(value),
}),
};

const encode = (input: QueryValue): QueryValue => {
Expand Down Expand Up @@ -261,9 +269,26 @@ const encode = (input: QueryValue): QueryValue => {
return encodeMap["set"](input);
} else if (input instanceof StreamToken) {
return encodeMap["streamToken"](input);
} else if (input instanceof Uint8Array || input instanceof ArrayBuffer) {
return encodeMap["bytes"](input);
} else if (ArrayBuffer.isView(input)) {
throw new ClientError(
"Error encoding TypedArray to Fauna Bytes. Convert your TypedArray to Uint8Array or ArrayBuffer before passing it to Fauna. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray",
);
} else {
return encodeMap["object"](input);
}
}
// anything here would be unreachable code
};

function base64toBuffer(value: string): Uint8Array {
return base64.toByteArray(value);
}

function bufferToBase64(value: ArrayBuffer | Uint8Array): string {
const arr: Uint8Array =
value instanceof Uint8Array ? value : new Uint8Array(value);

return base64.fromByteArray(arr);
}
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
| Uint8Array;

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 5c503ee

Please sign in to comment.