Skip to content

Commit

Permalink
feat(request): add GraphQLRequest object and types
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Oct 11, 2022
1 parent 6d1965c commit 8054e8e
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 0 deletions.
7 changes: 7 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { GraphQLRequest } from "./request.ts";
export {
type GraphQLRequestOptions,
type GraphQLRequestParams,
type RequestOptions,
type RequestParams,
} from "./types.ts";
130 changes: 130 additions & 0 deletions request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { mergeInit } from "./utils.ts";
import {
GraphQLRequestOptions,
GraphQLRequestParams,
RequestOptions,
RequestParams,
} from "./types.ts";

const ACCEPT =
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
CONTENT_TYPE = "application/json;charset=UTF-8",
DEFAULT_METHOD = "POST";

/** `Request` object with GraphQL request parameters.
* @throws {TypeError} When the input is Invalid URL or Header name is invalid.
*
* @example
* ```ts
* import { GraphQLRequest } from "https://deno.land/x/gql_request@$VERSION/mod.ts";
*
* const query = `query {
* person(personID: "1") {
* name
* }
* }`;
* const request = new GraphQLRequest("https://graphql.org/swapi-graphql", query);
* fetch(request);
* ```
*/
export class GraphQLRequest extends Request {
constructor(
input: string | URL,
query: string,
options?: RequestOptions,
) {
const {
method = DEFAULT_METHOD,
extensions,
operationName,
variables,
...rest
} = options ?? {};
const [url, init] = createRequestInit({ input, query, method }, {
extensions,
operationName,
variables,
});

const requestInit = mergeInit(init, rest);

super(url, requestInit);
}
}

/**
* @throws {TypeError}
*/
function createRequestInit(
{ input, query, method }: RequestParams & { method: string },
{ variables, operationName, extensions }: GraphQLRequestOptions,
): [url: string | URL, requestInit: RequestInit] {
switch (method) {
case "GET": {
const url = addQueryString(input, {
query,
operationName,
variables,
extensions,
});

const requestInit: RequestInit = {
method,
headers: {
Accept: ACCEPT,
},
};

return [url, requestInit];
}
case "POST": {
const body = JSON.stringify({
query,
variables,
operationName,
extensions,
});
const headers: HeadersInit = {
"content-type": CONTENT_TYPE,
accept: ACCEPT,
};
const requestInit: RequestInit = {
method,
body,
headers,
};

return [input, requestInit];
}
default: {
return [input, {}];
}
}
}

/**
* @throws {TypeError}
*/
function addQueryString(
url: string | URL,
{ query, variables, operationName, extensions }:
& GraphQLRequestParams
& GraphQLRequestOptions,
): URL {
url = new URL(url);
url.searchParams.set("query", query);

if (variables) {
const data = JSON.stringify(variables);
url.searchParams.set("variables", data);
}
if (operationName) {
url.searchParams.set("operationName", operationName);
}
if (extensions) {
const data = JSON.stringify(extensions);
url.searchParams.set("extensions", data);
}

return url;
}
216 changes: 216 additions & 0 deletions request_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { GraphQLRequest } from "./request.ts";
import {
assertEquals,
assertEqualsRequest,
assertThrows,
describe,
it,
} from "./dev_deps.ts";

const input = "http://localhost:8000";
const document = `query{test}`;

describe("GraphQLRequest", () => {
describe("HTTP GET", () => {
it("should contain query string and accept header", async () => {
const request = new GraphQLRequest(input, document, { method: "GET" });

await assertEqualsRequest(
request,
new Request(`http://localhost:8000/?query=query%7Btest%7D`, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
},
}),
);
});

it("should contain variables in query string", async () => {
const request = new GraphQLRequest(input, document, {
method: "GET",
variables: {
a: "1",
},
});

await assertEqualsRequest(
request,
new Request(
`http://localhost:8000/?query=query%7Btest%7D&variables=%7B%22a%22%3A%221%22%7D`,
{
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
},
},
),
);
});

it("should contain operationName in query string", async () => {
const request = new GraphQLRequest(input, document, {
method: "GET",
operationName: "NameQuery",
});

await assertEqualsRequest(
request,
new Request(
`http://localhost:8000/?query=query%7Btest%7D&operationName=NameQuery`,
{
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
},
},
),
);
});

it("should contain extensions in query string", async () => {
const request = new GraphQLRequest(input, document, {
method: "GET",
extensions: { a: "0" },
});

await assertEqualsRequest(
request,
new Request(
`http://localhost:8000/?query=query%7Btest%7D&extensions=%7B%22a%22%3A%220%22%7D`,
{
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
},
},
),
);
});

it("should override header", async () => {
const request = new GraphQLRequest(input, document, {
method: "GET",
headers: { "x-custom": "test" },
});

await assertEqualsRequest(
request,
new Request(`http://localhost:8000/?query=query%7Btest%7D`, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
"x-custom": "test",
},
}),
);
});
});

describe("HTTP POST", () => {
it("should contain query in body and equal header", async () => {
const request = new GraphQLRequest(input, document);

await assertEqualsRequest(
request,
new Request(input, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
"content-type": "application/json;charset=UTF-8",
},
body: `{"query":"query{test}"}`,
method: "POST",
}),
);
});

it("should contain query and variables in body", async () => {
const request = new GraphQLRequest(input, document, {
variables: { a: "0" },
});

await assertEqualsRequest(
request,
new Request(input, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
"content-type": "application/json;charset=UTF-8",
},
body: `{"query":"query{test}","variables":{"a":"0"}}`,
method: "POST",
}),
);
});

it("should contain query and operationName in body", async () => {
const request = new GraphQLRequest(input, document, {
operationName: "Query",
});

await assertEqualsRequest(
request,
new Request(input, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
"content-type": "application/json;charset=UTF-8",
},
body: `{"query":"query{test}","operationName":"Query"}`,
method: "POST",
}),
);
});

it("should contain query and operationName in body", async () => {
const request = new GraphQLRequest(input, document, {
extensions: { a: "a", b: undefined },
});

await assertEqualsRequest(
request,
new Request(input, {
headers: {
accept:
"application/graphql-response+json;charset=UTF-8,application/json;charset=UTF-8",
"content-type": "application/json;charset=UTF-8",
},
body: `{"query":"query{test}","extensions":{"a":"a"}}`,
method: "POST",
}),
);
});
});

it("should throw error when the input is invalid url", () => {
assertThrows(() => new GraphQLRequest("", ""), TypeError, "Invalid URL");
});

it("should throw error when the header name is invalid", () => {
assertThrows(
() => new GraphQLRequest(input, "", { headers: { "?": "" } }),
TypeError,
"Header name is not valid.",
);
});

it("should pass example", () => {
const query = `query {
person(personID: "1") {
name
}
}`;
const request = new GraphQLRequest(
"https://graphql.org/swapi-graphql",
query,
{
method: "GET",
},
);
assertEquals(
request.url,
"https://graphql.org/swapi-graphql?query=query+%7B%0A++++++person%28personID%3A+%221%22%29+%7B%0A++++++++name%0A++++++%7D%0A++++%7D",
);
});
});
26 changes: 26 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** GraphQL request options. */
export interface GraphQLRequestOptions {
/** The name of the Operation in the Document to execute. */
readonly operationName?: string;

/** Values for any Variables defined by the Operation. */
readonly variables?: Record<string, unknown>;

/** This entry is reserved for implementors to extend the protocol however they see fit. */
readonly extensions?: Record<string, unknown>;
}

/** GraphQL request parameters. */
export interface GraphQLRequestParams {
/** A Document containing GraphQL Operations and Fragments to execute. */
readonly query: string;
}

/** GraphQL-over-HTTP request parameters. */
export interface RequestParams extends GraphQLRequestParams {
/** retrieve resource. */
readonly input: string | URL;
}

/** GraphQL-over-HTTP request options. */
export interface RequestOptions extends GraphQLRequestOptions, RequestInit {}

0 comments on commit 8054e8e

Please sign in to comment.