Skip to content

Commit

Permalink
feat: support GraphQL IDs being internal
Browse files Browse the repository at this point in the history
Signed-off-by: Eric Dobbertin <[email protected]>
  • Loading branch information
aldeed committed May 14, 2020
1 parent 9d32bfe commit 3287241
Show file tree
Hide file tree
Showing 11 changed files with 2,008 additions and 680 deletions.
50 changes: 0 additions & 50 deletions .circleci/bin/docker-tags

This file was deleted.

7 changes: 7 additions & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Babel is used only for running Jest tests in API projects.
* If Jest adds support for ESM and import.meta, then Babel
* may become unnecessary.
*/

module.exports = require("./lib/configs/babel.config.cjs");
12 changes: 12 additions & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import envalid from "envalid";

const { bool } = envalid;

export default envalid.cleanEnv(process.env, {
REACTION_SHOULD_ENCODE_IDS: bool({
default: true,
desc: "Setting this to false makes the `encodeOpaqueId` function pass through the ID unchanged."
})
}, {
dotEnvPath: null
});
11 changes: 10 additions & 1 deletion lib/decodeOpaqueId.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
*/
export default function decodeOpaqueId(opaqueId) {
if (opaqueId === undefined || opaqueId === null) return null;
const unencoded = Buffer.from(opaqueId, "base64").toString("utf8");
const buffer = Buffer.from(opaqueId, "base64");

// As far as I can tell, round-tripping back to base64 is the
// only robust way to determine whether it was encoded as base64
// in the first place.
if (buffer.toString("base64") !== opaqueId) {
return { namespace: null, id: opaqueId };
}

const unencoded = buffer.toString("utf8");
const [namespace, id] = unencoded.split(":");
return { namespace, id };
}
17 changes: 17 additions & 0 deletions lib/decodeOpaqueId.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import decodeOpaqueId from "./decodeOpaqueId.js";

test("decodes base64", () => {
const encodedId = "cmVhY3Rpb24vc2hvcDpieTV3cGRnM25NcThnWDU0Yw==";
expect(decodeOpaqueId(encodedId)).toEqual({
id: "by5wpdg3nMq8gX54c",
namespace: "reaction/shop"
});
});

test("passes through non-base64", () => {
const id = "by5wpdg3nMq8gX54c";
expect(decodeOpaqueId(id)).toEqual({
id,
namespace: null
});
});
2 changes: 1 addition & 1 deletion lib/decodeOpaqueIdForNamespace.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const decodeOpaqueIdForNamespace = curry((namespace, opaqueId, error = new Error
const decodedId = decodeOpaqueId(opaqueId);
if (!decodedId) return null;
const { namespace: actualNamespace, id } = decodedId;
if (actualNamespace !== namespace) throw error;
if (actualNamespace && actualNamespace !== namespace) throw error;
return id;
});

Expand Down
16 changes: 16 additions & 0 deletions lib/decodeOpaqueIdForNamespace.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import decodeOpaqueIdForNamespace from "./decodeOpaqueIdForNamespace.js";

test("decodes base64", () => {
const encodedId = "cmVhY3Rpb24vc2hvcDpieTV3cGRnM25NcThnWDU0Yw==";
expect(decodeOpaqueIdForNamespace("reaction/shop", encodedId)).toBe("by5wpdg3nMq8gX54c");
});

test("passes through non-base64", () => {
const id = "by5wpdg3nMq8gX54c";
expect(decodeOpaqueIdForNamespace("reaction/shop", id)).toBe(id);
});

test("throws if base64 and namespace is wrong", () => {
const encodedId = "cmVhY3Rpb24vcHJvZHVjdDpieTV3cGRnM25NcThnWDU0Yw==";
expect(() => decodeOpaqueIdForNamespace("reaction/shop", encodedId)).toThrow("ID namespace must be reaction/shop");
});
6 changes: 5 additions & 1 deletion lib/encodeOpaqueId.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createRequire } from "module";
import config from "./config.js";

const require = createRequire(import.meta.url); // eslint-disable-line

Expand All @@ -8,12 +9,15 @@ const { curry } = require("ramda");
* @name encodeOpaqueId
* @method
* @memberof GraphQL/Transforms
* @summary Transforms an internal ID to an opaque ID
* @summary Transforms an internal ID to an opaque ID. Passes through the `id`
* unchanged if the `REACTION_SHOULD_ENCODE_IDS` environment variable
* is `false`
* @param {String} namespace The namespace of the ID
* @param {String} id The ID to transform
* @returns {String} An opaque ID
*/
const encodeOpaqueId = curry((namespace, id) => {
if (config.REACTION_SHOULD_ENCODE_IDS === false) return id;
if (typeof id !== "string" && typeof id !== "number") return id;
const unencoded = `${namespace}:${id}`;
return Buffer.from(unencoded).toString("base64");
Expand Down
22 changes: 22 additions & 0 deletions lib/encodeOpaqueId.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import config from "./config.js";
import encodeOpaqueId from "./encodeOpaqueId.js";

jest.mock("./config.js", () => ({
__esModule: true, // this property makes it work
default: {
REACTION_SHOULD_ENCODE_IDS: true
}
}));

const id = "by5wpdg3nMq8gX54c";
const encodedId = "cmVhY3Rpb24vc2hvcDpieTV3cGRnM25NcThnWDU0Yw==";

test("encodes to base64", () => {
expect(encodeOpaqueId("reaction/shop", id)).toBe(encodedId);
});

test("skips encoding in REACTION_SHOULD_ENCODE_IDS env is false", async () => {
config.REACTION_SHOULD_ENCODE_IDS = false;
expect(encodeOpaqueId("reaction/shop", id)).toBe(id);
config.REACTION_SHOULD_ENCODE_IDS = true;
});
Loading

0 comments on commit 3287241

Please sign in to comment.