mirror of
https://github.com/wassname/talk.git
synced 2026-07-30 12:40:41 +08:00
[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:
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user