@graphql-hive/core
Version:
217 lines (216 loc) • 9.13 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.createReporting = createReporting;
const graphql_1 = require("graphql");
const utils_1 = require("@graphql-tools/utils");
const version_js_1 = require("../version.js");
const http_client_js_1 = require("./http-client.js");
const utils_js_1 = require("./utils.js");
function createReporting(pluginOptions) {
var _a, _b, _c, _d;
if (!pluginOptions.reporting || pluginOptions.enabled === false) {
return {
async report() { },
async dispose() { },
};
}
const token = pluginOptions.token;
const selfHostingOptions = pluginOptions.selfHosting;
const reportingOptions = pluginOptions.reporting;
const logger = (0, utils_js_1.createHiveLogger)((_b = (_a = pluginOptions.agent) === null || _a === void 0 ? void 0 : _a.logger) !== null && _b !== void 0 ? _b : console, '[hive][reporting]');
(0, utils_js_1.logIf)(typeof reportingOptions.author !== 'string' || reportingOptions.author.length === 0, '[hive][reporting] author is missing', logger.error);
(0, utils_js_1.logIf)(typeof reportingOptions.commit !== 'string' || reportingOptions.commit.length === 0, '[hive][reporting] commit is missing', logger.error);
(0, utils_js_1.logIf)(typeof token !== 'string' || token.length === 0, '[hive][reporting] token is missing', logger.error);
const endpoint = (_d = (_c = selfHostingOptions === null || selfHostingOptions === void 0 ? void 0 : selfHostingOptions.graphqlEndpoint) !== null && _c !== void 0 ? _c : reportingOptions.endpoint) !== null && _d !== void 0 ? _d : 'https://app.graphql-hive.com/graphql';
return {
async report({ schema }) {
var _a, _b, _c;
logger.info(`Publish schema`);
try {
const response = await http_client_js_1.http.post(endpoint, JSON.stringify({
query,
operationName: 'schemaPublish',
variables: {
input: {
sdl: await printToSDL(schema),
author: reportingOptions.author,
commit: reportingOptions.commit,
service: (_a = reportingOptions.serviceName) !== null && _a !== void 0 ? _a : null,
url: (_b = reportingOptions.serviceUrl) !== null && _b !== void 0 ? _b : null,
force: true,
},
},
}), {
headers: {
'graphql-client-name': 'Hive Client',
'graphql-client-version': version_js_1.version,
authorization: `Bearer ${token}`,
'content-type': 'application/json',
},
logger,
});
if (response === null) {
throw new Error('Empty response');
}
const result = await response.json();
if (Array.isArray(result.errors)) {
throw new Error(result.errors.map(error => error.message).join('\n'));
}
const data = result.data.schemaPublish;
switch (data.__typename) {
case 'SchemaPublishSuccess': {
logger.info(`${(_c = data.successMessage) !== null && _c !== void 0 ? _c : 'Published schema'}`);
return;
}
case 'SchemaPublishMissingServiceError': {
throw new Error('Service name is not defined');
}
case 'SchemaPublishMissingUrlError': {
throw new Error('Service url is not defined');
}
case 'SchemaPublishError': {
logger.info(`Published schema (forced with ${data.errors.total} errors)`);
data.errors.nodes.slice(0, 5).forEach(error => {
logger.info(` - ${error.message}`);
});
return;
}
}
}
catch (error) {
logger.error(`Failed to report schema: ${error instanceof Error && 'message' in error ? error.message : error}`);
}
},
dispose() {
return Promise.resolve();
},
};
}
const query = (0, graphql_1.stripIgnoredCharacters)(/* GraphQL */ `
mutation schemaPublish($input: SchemaPublishInput!) {
schemaPublish(input: $input) {
__typename
... on SchemaPublishSuccess {
initial
valid
successMessage: message
}
... on SchemaPublishError {
valid
errors {
nodes {
message
}
total
}
}
... on SchemaPublishMissingServiceError {
missingServiceError: message
}
... on SchemaPublishMissingUrlError {
missingUrlError: message
}
}
}
`);
/**
* It's a bit tricky to detect if a schema is federated or not.
* For now, we just check if the schema has a _service that resolves to `_Service!` (as described in federation spec).
* This may lead to issues if the schema is not a federated schema but something made by the user (I don't think we will hit that issue soon).
*/
function isFederatedSchema(schema) {
const queryType = schema.getQueryType();
if (queryType) {
const fields = queryType.getFields();
if (fields._service && fields._service.type.toString() === `_Service!`) {
return true;
}
}
return false;
}
const federationV2 = {
scalars: new Set(['_Any', '_FieldSet']),
directives: new Set([
'key',
'requires',
'provides',
'external',
'shareable',
'extends',
'override',
'inaccessible',
'tag',
]),
types: new Set(['_Service']),
queryFields: new Set(['_service', '_entities']),
};
/**
* Extracts the SDL of a federated service from a GraphQLSchema object
* We do it to not send federated schema to the registry but only the original schema provided by user
*/
async function extractFederationServiceSDL(schema) {
const queryType = schema.getQueryType();
const serviceField = queryType.getFields()._service;
const resolved = await serviceField.resolve();
if (resolved.sdl.includes('_service')) {
// It seems that the schema is a federated (v2) schema.
// The _service field returns the SDL of the whole subgraph, not only the sdl provided by the user.
// We want to remove the federation specific types and directives from the SDL.
return (0, graphql_1.print)((0, graphql_1.visit)((0, graphql_1.parse)(resolved.sdl), {
ScalarTypeDefinition(node) {
if (federationV2.scalars.has(node.name.value)) {
return null;
}
return node;
},
DirectiveDefinition(node) {
if (federationV2.directives.has(node.name.value)) {
return null;
}
return node;
},
ObjectTypeDefinition(node) {
if (federationV2.types.has(node.name.value)) {
return null;
}
if (node.name.value === 'Query' && node.fields) {
return Object.assign(Object.assign({}, node), { fields: node.fields.filter(field => !federationV2.queryFields.has(field.name.value)) });
}
return node;
},
}));
}
return resolved.sdl;
}
function isSchemaOfCommonNames(schema) {
const queryType = schema.getQueryType();
if (queryType && queryType.name !== 'Query') {
return false;
}
const mutationType = schema.getMutationType();
if (mutationType && mutationType.name !== 'Mutation') {
return false;
}
const subscriptionType = schema.getSubscriptionType();
if (subscriptionType && subscriptionType.name !== 'Subscription') {
return false;
}
return true;
}
function printSchemaWithDirectives(schema) {
const doc = (0, utils_1.getDocumentNodeFromSchema)(schema);
if (schema.description == null && isSchemaOfCommonNames(schema)) {
// remove the schema definition if it's the default one
// We do it to avoid sending schema definition to the registry, which may be unwanted by federated services or something
return (0, graphql_1.print)({
kind: graphql_1.Kind.DOCUMENT,
definitions: doc.definitions.filter(def => def.kind !== graphql_1.Kind.SCHEMA_DEFINITION),
});
}
return (0, graphql_1.print)(doc);
}
async function printToSDL(schema) {
return (0, graphql_1.stripIgnoredCharacters)(isFederatedSchema(schema)
? await extractFederationServiceSDL(schema)
: printSchemaWithDirectives(schema));
}
;