[CORL-149] Persisted Queries (#2445)

* feat: enable persisted queries on the client

* fix: use `id` inside websocket message

* feat: initial server support for PQ

* feat: deeper server support

* feat: abstracted persisted query replacing logic
This commit is contained in:
Vinh
2019-08-15 21:03:32 +00:00
committed by Wyatt Johnson
parent 635e740fc0
commit 43b6a2cdcd
30 changed files with 1268 additions and 465 deletions
@@ -0,0 +1,2 @@
export * from "./loader";
export * from "./mapper";
@@ -0,0 +1,56 @@
import fs from "fs-extra";
import { parse } from "graphql";
import path from "path";
import { version } from "coral-common/version";
import { getOperationMetadata } from "coral-server/graph/common/extensions/helpers";
import { PersistedQuery } from "coral-server/models/queries";
export function loadPersistedQueries() {
// Load the files in the persisted queries folder.
const dir = path.join(__dirname, "__generated__");
const files = fs.readdirSync(dir);
// Load each of the persisted queries.
const queries: PersistedQuery[] = [];
for (const filePath of files) {
// Parse the bundle name from the file.
const bundle = filePath.split(".")[0];
// Load the queries from this file.
const fullFilePath = path.join(dir, filePath);
const persistedQueries: Record<string, string> = fs.readJSONSync(
fullFilePath
);
// Go over each of the persisted queries and collect the ID and query to
// merge in.
for (const id in persistedQueries) {
if (!persistedQueries.hasOwnProperty(id)) {
continue;
}
// Grab the actual query out of the file.
const query = persistedQueries[id];
// Parse the file to extract the GraphQL Operation Name.
const { operation, operationName } = getOperationMetadata(parse(query));
if (!operation || !operationName) {
throw new Error(
`operation in ${fullFilePath} with ID ${id} does not have valid operation metadata`
);
}
queries.push({
id,
operation,
operationName,
query,
bundle,
version,
});
}
}
return queries;
}
@@ -0,0 +1,41 @@
import { PersistedQueryNotFound } from "coral-server/errors";
import { PersistedQuery } from "coral-server/models/queries";
interface Payload {
id?: string;
query?: string;
}
interface Cache {
get(id: string): Promise<PersistedQuery | null>;
}
/**
* getPersistedQuery will try to get the persisted query referenced by the
* payload and return it if one exists. If a persisted query is referenced, but
* non is available, it will throw an error.
*
* @param cache the cache to pull the query from
* @param payload the payload that references the query that should be read
*/
export async function getPersistedQuery(
cache: Cache,
payload?: Readonly<Payload>
) {
if (
!payload ||
!payload.id ||
// Persisted queries can either have a query set to `PERSISTED_QUERY` or is
// empty.
!(payload.query === "PERSISTED_QUERY" || payload.query === "")
) {
return null;
}
const query = await cache.get(payload.id);
if (!query) {
throw new PersistedQueryNotFound(payload.id);
}
return query;
}
@@ -9,6 +9,7 @@ import http, { IncomingMessage } from "http";
import {
ConnectionContext,
ExecutionParams,
OperationMessage,
OperationMessagePayload,
SubscriptionServer,
} from "subscriptions-transport-ws";
@@ -25,6 +26,7 @@ import {
CoralError,
InternalError,
LiveUpdatesDisabled,
RawQueryNotAuthorized,
TenantNotFoundError,
} from "coral-server/errors";
import {
@@ -33,6 +35,8 @@ import {
logQuery,
} from "coral-server/graph/common/extensions";
import { getOperationMetadata } from "coral-server/graph/common/extensions/helpers";
import { getPersistedQuery } from "coral-server/graph/tenant/persisted";
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
import logger from "coral-server/logger";
import { userIsStaff } from "coral-server/models/user/helpers";
import { extractTokenFromRequest } from "coral-server/services/jwt";
@@ -205,13 +209,41 @@ export function formatResponse({ metrics }: FormatResponseOptions) {
};
}
export type OnOperationOptions = FormatResponseOptions;
export type OnOperationOptions = FormatResponseOptions &
Pick<AppOptions, "persistedQueryCache" | "persistedQueriesRequired">;
export function onOperation(options: OnOperationOptions) {
return (message: any, params: ExecutionParams<TenantContext>) => {
return async (
message: OperationMessage,
params: ExecutionParams<TenantContext>
) => {
// Attach the response formatter.
params.formatResponse = formatResponse(options);
// Handle the payload if it is a persisted query.
const query = await getPersistedQuery(
options.persistedQueryCache,
message.payload
);
if (!query) {
// Check to see if this is from an ADMIN token which is allowed to run
// un-persisted queries.
if (
options.persistedQueriesRequired &&
(!params.context.user ||
params.context.user.role !== GQLUSER_ROLE.ADMIN)
) {
throw new RawQueryNotAuthorized(
params.context.tenant.id,
params.context.user ? params.context.user.id : null
);
}
} else {
// The query was found for this operation, replace the query with the one
// provided.
params.query = query.query;
}
return params;
};
}