Skip to content

Commit

Permalink
GraphQL Hive Plugin & Transform (#4854)
Browse files Browse the repository at this point in the history
* GraphQL Hive Plugin

* yarn.lock

* Fix version

* Fix typings

* Update
  • Loading branch information
ardatan authored Nov 25, 2022
1 parent 6a96456 commit 0d97714
Show file tree
Hide file tree
Showing 14 changed files with 745 additions and 22 deletions.
8 changes: 8 additions & 0 deletions .changeset/bright-fireants-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphql-mesh/config': patch
'@graphql-mesh/types': patch
'@graphql-mesh/plugin-hive': patch
'@graphql-mesh/transform-hive': patch
---

New GraphQL Hive plugin
8 changes: 6 additions & 2 deletions packages/config/src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ export async function processConfig(
baseDir,
cache,
pubsub,
importFn
importFn,
logger,
});`);
}

Expand All @@ -233,6 +234,7 @@ export async function processConfig(
cache,
pubsub,
importFn,
logger,
});
})
),
Expand Down Expand Up @@ -275,7 +277,8 @@ export async function processConfig(
baseDir,
cache,
pubsub,
importFn
importFn,
logger,
})`);
}
return new TransformLibrary({
Expand All @@ -285,6 +288,7 @@ export async function processConfig(
cache,
pubsub,
importFn,
logger,
});
}) || []
),
Expand Down
44 changes: 44 additions & 0 deletions packages/plugins/hive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@graphql-mesh/plugin-hive",
"version": "0.0.0",
"repository": {
"type": "git",
"url": "Urigo/graphql-mesh",
"directory": "packages/plugins/hive"
},
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.mjs",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./*": {
"require": "./dist/*.js",
"import": "./dist/*.mjs"
}
},
"typings": "dist/index.d.ts",
"peerDependencies": {
"graphql": "*"
},
"dependencies": {
"@graphql-hive/client": "0.21.1",
"@graphql-mesh/cross-helpers": "0.2.10",
"@graphql-mesh/string-interpolation": "0.3.3",
"@graphql-mesh/types": "0.86.0",
"tslib": "^2.4.0"
},
"devDependencies": {
"@types/http-cache-semantics": "4.0.1"
},
"publishConfig": {
"access": "public",
"directory": "dist"
},
"sideEffects": false,
"typescript": {
"definition": "dist/index.d.ts"
}
}
81 changes: 81 additions & 0 deletions packages/plugins/hive/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createHive, HivePluginOptions, useHive } from '@graphql-hive/client';
import { MeshPlugin, MeshPluginOptions, YamlConfig } from '@graphql-mesh/types';
import { process } from '@graphql-mesh/cross-helpers';
import { stringInterpolator } from '@graphql-mesh/string-interpolation';

export default function useMeshHive(pluginOptions: MeshPluginOptions<YamlConfig.HivePlugin>): MeshPlugin<{}> {
const token = stringInterpolator.parse(pluginOptions.token, {
env: process.env,
});
if (!token) {
return {};
}

let usage: HivePluginOptions['usage'];
if (pluginOptions.usage) {
usage = {
max: pluginOptions.usage.max,
ttl: pluginOptions.usage.ttl,
exclude: pluginOptions.usage.exclude,
sampleRate: pluginOptions.usage.sampleRate,
processVariables: pluginOptions.usage.processVariables,
};
if (pluginOptions.usage?.clientInfo) {
usage.clientInfo = function (context) {
return {
name: stringInterpolator.parse(pluginOptions.usage.clientInfo.name, {
context,
env: process.env,
}),
version: stringInterpolator.parse(pluginOptions.usage.clientInfo.version, {
context,
env: process.env,
}),
};
};
}
}
let reporting: HivePluginOptions['reporting'];
if (pluginOptions.reporting) {
reporting = {
author: stringInterpolator.parse(pluginOptions.reporting.author, { env: process.env }),
commit: stringInterpolator.parse(pluginOptions.reporting.commit, { env: process.env }),
serviceName: stringInterpolator.parse(pluginOptions.reporting.serviceName, {
env: process.env,
}),
serviceUrl: stringInterpolator.parse(pluginOptions.reporting.serviceUrl, {
env: process.env,
}),
};
}
let agent: HivePluginOptions['agent'];
if (pluginOptions.agent) {
agent = {
timeout: pluginOptions.agent.timeout,
maxRetries: pluginOptions.agent.maxRetries,
minTimeout: pluginOptions.agent.minTimeout,
sendInterval: pluginOptions.agent.sendInterval,
maxSize: pluginOptions.agent.maxSize,
logger: pluginOptions.logger,
};
}
const hiveClient = createHive({
enabled: true,
debug: !!process.env.DEBUG,
token,
agent,
usage,
reporting,
});
const id = pluginOptions.pubsub.subscribe('destroy', () => {
hiveClient
.dispose()
.catch(e => pluginOptions.logger.error(`Hive client failed to dispose`, e))
.finally(() => pluginOptions.pubsub.unsubscribe(id));
});
return {
onPluginInit({ addPlugin }) {
addPlugin(useHive(hiveClient));
},
};
}
106 changes: 106 additions & 0 deletions packages/plugins/hive/yaml-config.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
extend type Plugin {
hive: HivePlugin
}

type HivePlugin @md {
"""
Access Token
"""
token: String!
"""
Agent Options
"""
agent: HiveAgentOptions
"""
Collects schema usage based on operations
"""
usage: HiveUsageOptions
"""
Schema reporting
"""
reporting: HiveReportingOptions
}

type HiveAgentOptions {
"""
30s by default
"""
timeout: Int
"""
5 by default
"""
maxRetries: Int
"""
200 by default
"""
minTimeout: Int
"""
Send reports in interval (defaults to 10_000ms)
"""
sendInterval: Int
"""
Max number of traces to send at once (defaults to 25)
"""
maxSize: Int
}

type HiveUsageOptions {
"""
Extract client info from GraphQL Context
"""
clientInfo: HiveClientInfo
"""
Hive uses LRU cache to store info about operations.
This option represents the maximum size of the cache.
Default: 1000
"""
max: Int
"""
Hive uses LRU cache to store info about operations.
This option represents the maximum age of an unused operation in the cache.
Default: no ttl
"""
ttl: Int
"""
A list of operations (by name) to be ignored by Hive.
"""
exclude: [String]
"""
Sample rate to determine sampling.
0.0 = 0% chance of being sent
1.0 = 100% chance of being sent
Default: 1.0
"""
sampleRate: Float
"""
(Experimental) Enables collecting Input fields usage based on the variables passed to the operation.
Default: false
"""
processVariables: Boolean
}

type HiveClientInfo {
"""
Extract client name
Example: "{context.headers['x-client-name']}"
"""
name: String
"""
Extract client version
Example: "{context.headers['x-client-version']}"
"""
version: String
}

type HiveReportingOptions {
"""
Author of current version of the schema
"""
author: String!
"""
Commit SHA hash (or any identifier) related to the schema version
"""
commit: String!
serviceName: String
serviceUrl: String
}
28 changes: 20 additions & 8 deletions packages/runtime/src/in-context-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
} from '@graphql-mesh/types';
import { parseWithCache } from '@graphql-mesh/utils';
import { BatchDelegateOptions, batchDelegateToSchema } from '@graphql-tools/batch-delegate';
import { SubschemaConfig, StitchingInfo, IDelegateToSchemaOptions, delegateToSchema } from '@graphql-tools/delegate';
import {
SubschemaConfig,
StitchingInfo,
IDelegateToSchemaOptions,
delegateToSchema,
} from '@graphql-tools/delegate';
import { isDocumentNode, memoize1 } from '@graphql-tools/utils';
import { WrapQuery } from '@graphql-tools/wrap';
import {
Expand All @@ -29,7 +34,7 @@ export async function getInContextSDK(
unifiedSchema: GraphQLSchema,
rawSources: RawSourceOutput[],
logger: Logger,
onDelegateHooks: OnDelegateHook<any>[]
onDelegateHooks: OnDelegateHook<any>[],
) {
const inContextSDK: Record<string, any> = {};
const sourceMap = unifiedSchema.extensions.sourceMap as Map<RawSourceOutput, GraphQLSchema>;
Expand Down Expand Up @@ -69,7 +74,9 @@ export async function getInContextSDK(
const rootTypeFieldMap = rootType.getFields();
for (const fieldName in rootTypeFieldMap) {
const rootTypeField = rootTypeFieldMap[fieldName];
const inContextSdkLogger = rawSourceLogger.child(`InContextSDK.${rootType.name}.${fieldName}`);
const inContextSdkLogger = rawSourceLogger.child(
`InContextSDK.${rootType.name}.${fieldName}`,
);
const namedReturnType = getNamedType(rootTypeField.type);
const shouldHaveSelectionSet = !isLeafType(namedReturnType);
rawSourceContext[rootType.name][fieldName] = async ({
Expand Down Expand Up @@ -98,6 +105,7 @@ export async function getInContextSDK(
},
},
variableValues: {},
cacheControl: {} as any,
},
selectionSet,
key,
Expand Down Expand Up @@ -140,7 +148,7 @@ export async function getInContextSDK(
if (selectionCount === 0) {
if (!selectionSet) {
throw new Error(
`You have to provide 'selectionSet' for context.${rawSource.name}.${rootType.name}.${fieldName}`
`You have to provide 'selectionSet' for context.${rawSource.name}.${rootType.name}.${fieldName}`,
);
}
commonDelegateOptions.info = {
Expand Down Expand Up @@ -209,7 +217,11 @@ export async function getInContextSDK(
if (selectionSet) {
const selectionSetFactory = normalizeSelectionSetParamOrFactory(selectionSet);
const path = [fieldName];
const wrapQueryTransform = new WrapQuery(path, selectionSetFactory, valuesFromResults || identical);
const wrapQueryTransform = new WrapQuery(
path,
selectionSetFactory,
valuesFromResults || identical,
);
regularDelegateOptions.transforms = [wrapQueryTransform as any];
}
const onDelegateHookDones: OnDelegateHookDone[] = [];
Expand Down Expand Up @@ -246,7 +258,7 @@ export async function getInContextSDK(

function getSelectionSetFromDocumentNode(documentNode: DocumentNode): SelectionSetNode {
const operationDefinition = documentNode.definitions.find(
definition => definition.kind === Kind.OPERATION_DEFINITION
definition => definition.kind === Kind.OPERATION_DEFINITION,
) as OperationDefinitionNode;
if (!operationDefinition) {
throw new Error('DocumentNode must contain an OperationDefinitionNode');
Expand All @@ -266,7 +278,7 @@ function normalizeSelectionSetParam(selectionSetParam: SelectionSetParam): Selec
}

const normalizeSelectionSetParamFactory = memoize1(function normalizeSelectionSetParamFactory(
selectionSetParamFactory: (subtree: SelectionSetNode) => SelectionSetParam
selectionSetParamFactory: (subtree: SelectionSetNode) => SelectionSetParam,
) {
const memoizedSelectionSetFactory = memoize1(selectionSetParamFactory);
return function selectionSetFactory(subtree: SelectionSetNode) {
Expand All @@ -276,7 +288,7 @@ const normalizeSelectionSetParamFactory = memoize1(function normalizeSelectionSe
});

function normalizeSelectionSetParamOrFactory(
selectionSetParamOrFactory: SelectionSetParamOrFactory
selectionSetParamOrFactory: SelectionSetParamOrFactory,
): (subtree: SelectionSetNode) => SelectionSetNode {
if (typeof selectionSetParamOrFactory === 'function') {
return normalizeSelectionSetParamFactory(selectionSetParamOrFactory);
Expand Down
Loading

0 comments on commit 0d97714

Please sign in to comment.