@graphql-hive/apollo
Version:
GraphQL Hive + Apollo Server
367 lines (366 loc) • 17.3 kB
JavaScript
import { GraphQLError } from 'graphql';
import { autoDisposeSymbol, createCDNArtifactFetcher, createHive as createHiveClient, isHiveClient, joinUrl, } from '@graphql-hive/core';
import { Logger } from '@graphql-hive/logger';
import { version } from './version.js';
export { atLeastOnceSampler, createSchemaFetcher, createServicesFetcher, createSupergraphSDLFetcher, } from '@graphql-hive/core';
export function createSupergraphManager(args) {
var _a, _b, _c, _d;
const logger = (_a = args.logger) !== null && _a !== void 0 ? _a : new Logger({ level: false });
const pollIntervalInMs = (_b = args.pollIntervalInMs) !== null && _b !== void 0 ? _b : 30000;
let endpoints = Array.isArray(args.endpoint) ? args.endpoint : [args.endpoint];
const endpoint = endpoints.map(endpoint => endpoint.endsWith('/supergraph') ? endpoint : joinUrl(endpoint, 'supergraph'));
const artifactsFetcher = createCDNArtifactFetcher({
endpoint: endpoint,
accessKey: args.key,
client: {
name: (_c = args.name) !== null && _c !== void 0 ? _c : '@graphql-hive/apollo',
version: (_d = args.version) !== null && _d !== void 0 ? _d : version,
},
logger,
fetch: args.fetchImplementation,
circuitBreaker: args.circuitBreaker,
});
let timer = null;
return {
async initialize(hooks) {
const initialResult = await artifactsFetcher.fetch();
function poll() {
timer = setTimeout(async () => {
var _a;
try {
const result = await artifactsFetcher.fetch();
if (result.contents) {
(_a = hooks.update) === null || _a === void 0 ? void 0 : _a.call(hooks, result.contents);
}
}
catch (error) {
logger.error({ error }, `Failed to update supergraph.`);
}
poll();
}, pollIntervalInMs);
}
poll();
return {
supergraphSdl: initialResult.contents,
cleanup: async () => {
if (timer) {
clearTimeout(timer);
}
artifactsFetcher.dispose();
},
};
},
};
}
function addRequestWithHeaders(context, http) {
if (!!http && !('request' in context)) {
context.request = {
headers: http.headers,
};
}
return context;
}
function getLoggerFromContext(ctx) {
if (ctx === null || ctx === void 0 ? void 0 : ctx.logger) {
return new Logger({
level: 'debug',
writers: [
{
write(level, attrs, msg) {
const payload = attrs ? Object.assign({ msg }, attrs) : msg;
switch (level) {
case 'trace':
case 'debug':
ctx.logger.debug(payload);
break;
case 'info':
ctx.logger.info(payload);
break;
case 'warn':
ctx.logger.warn(payload);
break;
case 'error':
ctx.logger.error(payload);
break;
}
},
},
],
});
}
return undefined;
}
function getPersistedDocumentsCacheFromContext(ctx) {
if (ctx === null || ctx === void 0 ? void 0 : ctx.cache) {
return {
async get(key) {
const value = await ctx.cache.get(key);
return value != null ? value : null;
},
set(key, value, options) {
return ctx.cache.set(key, value, options);
},
};
}
return undefined;
}
export function createHive(clientOrOptions, ctx) {
var _a;
const persistedDocumentsCache = getPersistedDocumentsCacheFromContext(ctx);
// Only use context logger if user didn't provide their own
const contextLogger = !clientOrOptions.logger && !((_a = clientOrOptions.agent) === null || _a === void 0 ? void 0 : _a.logger)
? getLoggerFromContext(ctx)
: undefined;
return createHiveClient(Object.assign(Object.assign({ logger: contextLogger }, clientOrOptions), { agent: Object.assign({ name: 'hive-client-apollo', version }, clientOrOptions.agent), persistedDocuments: clientOrOptions.persistedDocuments
? Object.assign(Object.assign({}, clientOrOptions.persistedDocuments), { layer2Cache: (() => {
var _a;
const userL2Config = (_a = clientOrOptions.persistedDocuments) === null || _a === void 0 ? void 0 : _a.layer2Cache;
if (persistedDocumentsCache) {
return Object.assign({ cache: persistedDocumentsCache }, (userL2Config || {}));
}
return userL2Config;
})() }) : undefined }));
}
export function useHive(clientOrOptions) {
let hive;
return {
requestDidStart(context) {
var _a;
// `overallCachePolicy` does not exist in v0
const isLegacyV0 = !('overallCachePolicy' in context);
// `context` does not exist in v4, it is `contextValue` instead
const isLegacyV3 = 'context' in context;
let doc;
let didResolveSource = false;
const complete = hive.collectUsage();
const args = {
schema: context.schema,
get document() {
return doc;
},
operationName: context.operationName,
contextValue: addRequestWithHeaders(isLegacyV3 ? context.context : context.contextValue, (_a = context.request) === null || _a === void 0 ? void 0 : _a.http),
variableValues: context.request.variables,
};
if (isLegacyV0) {
return {
didResolveSource() {
didResolveSource = true;
},
willSendResponse(ctx) {
if (!didResolveSource) {
void complete(args, {
action: 'abort',
reason: 'Did not resolve source',
logging: false,
});
return;
}
doc = ctx.document;
void complete(args, ctx.response);
},
};
}
let didFailValidation = false;
if (isLegacyV3) {
return Promise.resolve({
didResolveSource() {
didResolveSource = true;
},
async validationDidStart() {
return function onErrors(errors) {
if (errors === null || errors === void 0 ? void 0 : errors.length) {
didFailValidation = true;
}
};
},
async willSendResponse(ctx) {
if (didFailValidation) {
void complete(args, {
action: 'abort',
reason: 'Validation failed',
logging: false,
});
return;
}
if (!didResolveSource) {
void complete(args, {
action: 'abort',
reason: 'Did not resolve source',
logging: false,
});
return;
}
if (!ctx.document) {
const details = ctx.operationName ? `operationName: ${ctx.operationName}` : '';
void complete(args, {
action: 'abort',
reason: 'Document is not available' + (details ? ` (${details})` : ''),
logging: true,
});
return;
}
doc = ctx.document;
void complete(args, ctx.response);
},
});
}
return (async () => {
var _a;
let persistedDocumentError = null;
let persistedDocumentHash;
if (hive.persistedDocuments) {
if (((_a = context.request.http) === null || _a === void 0 ? void 0 : _a.body) &&
typeof context.request.http.body === 'object' &&
'documentId' in context.request.http.body &&
typeof context.request.http.body.documentId === 'string') {
persistedDocumentHash = context.request.http.body.documentId;
try {
// Pass waitUntil from context if available for serverless environments
const contextValue = isLegacyV3
? context.context
: context.contextValue;
const document = await hive.persistedDocuments.resolve(context.request.http.body.documentId, { waitUntil: contextValue === null || contextValue === void 0 ? void 0 : contextValue.waitUntil });
if (document) {
context.request.query = document;
}
else {
// Document not found - this is also a client error (400)
context.request.query = '{__typename}';
persistedDocumentError = new GraphQLError('Persisted document not found.', {
extensions: {
code: 'PERSISTED_DOCUMENT_NOT_FOUND',
http: {
status: 400,
},
},
});
}
}
catch (error) {
// Check if this is a client error (400-range status) - preserve the error
if ((error === null || error === void 0 ? void 0 : error.status) >= 400 && (error === null || error === void 0 ? void 0 : error.status) < 500) {
context.request.query = '{__typename}';
// Use the original error's extensions if available, otherwise create basic ones
const extensions = Object.assign(Object.assign({}, error === null || error === void 0 ? void 0 : error.extensions), { code: (error === null || error === void 0 ? void 0 : error.code) || 'CLIENT_ERROR', http: {
status: (error === null || error === void 0 ? void 0 : error.status) || 400,
} });
persistedDocumentError = new GraphQLError(error.message || 'Client error.', {
extensions,
});
}
else {
// Re-throw server errors (500+ status) - these should remain as server errors
throw error;
}
}
}
else if (false ===
(await hive.persistedDocuments.allowArbitraryDocuments({
headers: {
get(name) {
var _a, _b, _c;
return (_c = (_b = (_a = context.request.http) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.get(name)) !== null && _c !== void 0 ? _c : null;
},
},
}))) {
context.request.query = '{__typename}';
persistedDocumentError = new GraphQLError('No persisted document provided.', {
extensions: {
code: 'PERSISTED_DOCUMENT_REQUIRED',
http: {
status: 400,
},
},
});
}
}
// v4
return {
didResolveSource() {
didResolveSource = true;
},
async validationDidStart() {
return function onErrors(errors) {
if (errors === null || errors === void 0 ? void 0 : errors.length) {
didFailValidation = true;
}
};
},
didResolveOperation() {
if (persistedDocumentError) {
throw persistedDocumentError;
}
},
async willSendResponse(ctx) {
if (didFailValidation) {
void complete(args, {
action: 'abort',
reason: 'Validation failed',
logging: false,
}, persistedDocumentHash);
return;
}
if (!didResolveSource) {
void complete(args, {
action: 'abort',
reason: 'Did not resolve source',
logging: false,
}, persistedDocumentHash);
return;
}
if (!ctx.document) {
const details = ctx.operationName ? `operationName: ${ctx.operationName}` : '';
void complete(args, {
action: 'abort',
reason: 'Document is not available' + (details ? ` (${details})` : ''),
logging: true,
}, persistedDocumentHash);
return;
}
doc = ctx.document;
if (ctx.response.body.kind === 'incremental') {
void complete(args, {
action: 'abort',
reason: '@defer and @stream is not supported by Hive',
logging: true,
}, persistedDocumentHash);
}
else {
void complete(args, ctx.response.body.singleResult, persistedDocumentHash);
}
},
};
})();
},
serverWillStart(ctx) {
hive = isHiveClient(clientOrOptions) ? clientOrOptions : createHive(clientOrOptions, ctx);
void hive.info();
// `engine` does not exist in v3
const isLegacyV0 = 'engine' in ctx;
hive.reportSchema({ schema: ctx.schema });
if (isLegacyV0) {
return {
async serverWillStop() {
if (hive === null || hive === void 0 ? void 0 : hive[autoDisposeSymbol]) {
await hive.dispose();
}
},
};
}
// Works on v3 and v4
return Promise.resolve({
async serverWillStop() {
if (hive === null || hive === void 0 ? void 0 : hive[autoDisposeSymbol]) {
await hive.dispose();
}
},
schemaDidLoadOrUpdate(schemaContext) {
if (ctx.schema !== schemaContext.apiSchema) {
hive.reportSchema({ schema: schemaContext.apiSchema });
}
},
});
},
};
}