rivet-graphql
Advanced tools
Comparing version 0.5.0 to 0.6.0-canary-20230308183240
# rivet-graphql | ||
## 0.6.0-canary-20230308183240 | ||
### Minor Changes | ||
- [#34](https://github.com/hashicorp/rivet-graphql/pull/34) [`060c5c5`](https://github.com/hashicorp/rivet-graphql/commit/060c5c541be010722bf7cd8a81af464c5b9b9c20) Thanks [@dstaley](https://github.com/dstaley)! - Add support for TypedDocumentNode | ||
## 0.5.0 | ||
@@ -4,0 +10,0 @@ |
@@ -1,26 +0,28 @@ | ||
declare function _exports(url: string, options: import("graphql-request/dist/types.dom").RequestInit & { | ||
/** | ||
* Copyright (c) HashiCorp, Inc. | ||
* SPDX-License-Identifier: MPL-2.0 | ||
*/ | ||
import { GraphQLClient, type Variables } from 'graphql-request'; | ||
import type { DocumentNode } from 'graphql'; | ||
import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; | ||
interface FragmentSpec { | ||
fragment?: string | DocumentNode; | ||
dependencies?: { | ||
fragmentSpec?: FragmentSpec; | ||
}[]; | ||
requiredVariables?: Record<string, unknown>; | ||
} | ||
declare const _default: (url: string, options: ConstructorParameters<typeof GraphQLClient>[1] & { | ||
timeout?: number; | ||
retryCount?: number; | ||
}): { | ||
<T = any>({ query, dependencies, variables }: { | ||
query: string | DocumentNode; | ||
}) => { | ||
<T, V extends Variables = Variables>({ query, dependencies, variables, }: { | ||
query: string | DocumentNode | TypedDocumentNode<T, V>; | ||
dependencies?: { | ||
fragmentSpec?: FragmentSpec; | ||
}[]; | ||
variables?: Record<string, any>; | ||
}): Promise<T>; | ||
variables?: Record<string, unknown>; | ||
}): Promise<V>; | ||
client: GraphQLClient; | ||
}; | ||
export = _exports; | ||
export type GQLRequestInit = import("graphql-request/dist/types.dom").RequestInit; | ||
export type DocumentNode = import("graphql/language/ast").DocumentNode; | ||
export type exports = import("graphql-request").GraphQLClient; | ||
export type FragmentSpec = { | ||
fragment?: string | DocumentNode; | ||
dependencies?: { | ||
fragmentSpec?: FragmentSpec; | ||
}[]; | ||
requiredVariables?: Record<string, any>; | ||
}; | ||
import { GraphQLClient } from "graphql-request"; | ||
//# sourceMappingURL=index.d.ts.map | ||
export = _default; |
330
index.js
@@ -0,1 +1,2 @@ | ||
"use strict"; | ||
/** | ||
@@ -5,81 +6,20 @@ * Copyright (c) HashiCorp, Inc. | ||
*/ | ||
//@ts-check | ||
let { GraphQLClient } = require('graphql-request') | ||
const { parse, parseType } = require('graphql/language/parser') | ||
const { print } = require('graphql/language/printer') | ||
/** @typedef { import("graphql-request/dist/types.dom").RequestInit} GQLRequestInit */ | ||
/** @typedef { import("graphql/language/ast").DocumentNode} DocumentNode */ | ||
/** @typedef { import("graphql-request").GraphQLClient } */ | ||
const graphql_request_1 = require("graphql-request"); | ||
const parser_1 = require("graphql/language/parser"); | ||
const printer_1 = require("graphql/language/printer"); | ||
/** | ||
* @typedef {Object} FragmentSpec | ||
* @property {string | DocumentNode} [fragment] | ||
* @property {{ fragmentSpec?: FragmentSpec }[]} [dependencies] | ||
* @property {Record<string, any>} [requiredVariables] | ||
*/ | ||
/** | ||
* | ||
* @param {string} url | ||
* @param {GQLRequestInit & { timeout?: number, retryCount?: number }} options | ||
*/ | ||
module.exports = function Rivet(url, options) { | ||
if (!options.timeout) options.timeout = 30000 | ||
const retryCount = options.retryCount || 0 | ||
delete options.retryCount | ||
const client = new GraphQLClient(url, options) | ||
if (retryCount) { | ||
client.request = requestWithRetry.bind( | ||
null, | ||
retryCount, | ||
client.request.bind(client) | ||
) | ||
} | ||
/** | ||
* | ||
* @template [T=any] | ||
* @param {Object} params | ||
* @param {string | DocumentNode} params.query | ||
* @param {{ fragmentSpec?: FragmentSpec }[]} [params.dependencies] | ||
* @param {Record<string, any>} [params.variables] | ||
* @returns {Promise<T>} | ||
*/ | ||
function fetch({ query, dependencies = [], variables }) { | ||
if (!query) throw fetchMissingQueryError() | ||
const _dependencies = processDependencies(dependencies) | ||
const _query = processVariables(dependencies, variables, query) | ||
return client.request( | ||
`${_query}\n${[..._dependencies].join('\n')}`, | ||
variables | ||
) | ||
} | ||
fetch.client = client | ||
return fetch | ||
} | ||
/** | ||
* | ||
* @param {{ fragmentSpec?: FragmentSpec }[]} dependencies | ||
*/ | ||
function extractFragmentSpecs(dependencies) { | ||
// throw an error if dependencies isn't an array | ||
if (!Array.isArray(dependencies)) throw dependenciesTypeError(dependencies) | ||
// filter out any dependencies that don't have a fragment spec | ||
return dependencies | ||
.filter((d) => d.fragmentSpec) | ||
.map((d) => { | ||
return Object.assign({}, d.fragmentSpec, { __original: d }) | ||
}) | ||
// throw an error if dependencies isn't an array | ||
if (!Array.isArray(dependencies)) | ||
throw dependenciesTypeError(dependencies); | ||
// filter out any dependencies that don't have a fragment spec | ||
return dependencies | ||
.filter((d) => d.fragmentSpec) | ||
.map((d) => { | ||
return Object.assign({}, d.fragmentSpec, { __original: d }); | ||
}); | ||
} | ||
// Go through component dependencies and extract all of the fragments that we need | ||
@@ -93,23 +33,18 @@ // to make the query. This is a recursive function to account for deep nested deps. | ||
function processDependencies(_dependencies) { | ||
const dependencies = extractFragmentSpecs(_dependencies) | ||
return dependencies.reduce((acc, component) => { | ||
// Add the main fragment if one is provided | ||
if (component.fragment) { | ||
acc.push( | ||
typeof component.fragment === 'string' | ||
? component.fragment | ||
: print(component.fragment) | ||
) | ||
} | ||
// Recursively iterate through dependencies and collect all fragments | ||
if (component.dependencies) { | ||
acc.push(...processDependencies(component.dependencies)) | ||
} | ||
// Dedupe the array before returning | ||
return [...new Set(acc)] | ||
}, []) | ||
const dependencies = extractFragmentSpecs(_dependencies); | ||
return dependencies.reduce((acc, component) => { | ||
// Add the main fragment if one is provided | ||
if (component.fragment) { | ||
acc.push(typeof component.fragment === 'string' | ||
? component.fragment | ||
: (0, printer_1.print)(component.fragment)); | ||
} | ||
// Recursively iterate through dependencies and collect all fragments | ||
if (component.dependencies) { | ||
acc.push(...processDependencies(component.dependencies)); | ||
} | ||
// Dedupe the array before returning | ||
return [...new Set(acc)]; | ||
}, []); | ||
} | ||
// Go through components and variables and ensure that the user has provided values | ||
@@ -126,41 +61,35 @@ // for all variables that components need. Then dynamically inject variables that | ||
function processVariables(dependencies, variables, query) { | ||
// First, we loop through dependencies to extract the variables they define | ||
// Along the way we throw clear errors if there are any variable mismatched | ||
const vars = _findVariables(dependencies, variables) | ||
// If there are no variables, we can return | ||
if (!Object.keys(vars).length) { | ||
return typeof query === 'string' ? query : print(query) | ||
} | ||
// Otherwise, inject those variables into the query's params. | ||
// First we parse the query into an AST | ||
const ast = typeof query === 'string' ? parse(query) : query | ||
// See function definition below for details | ||
if (ast.definitions.length > 1) throw multipleQueriesError() | ||
// Then we loop through the variables and create AST nodes for them | ||
Object.entries(vars).map(([_name, _type]) => { | ||
const variable = { | ||
kind: 'Variable', | ||
name: { kind: 'Name', value: _name }, | ||
// First, we loop through dependencies to extract the variables they define | ||
// Along the way we throw clear errors if there are any variable mismatched | ||
const vars = _findVariables(dependencies, variables); | ||
// If there are no variables, we can return | ||
if (!Object.keys(vars).length) { | ||
return typeof query === 'string' ? query : (0, printer_1.print)(query); | ||
} | ||
const type = parseType(_type) | ||
// Add the AST nodes to the variable definitions at the top of the query. | ||
// Worth noting it only does this for the first query defined in the file, | ||
// but we throw if there is more than one anyway. | ||
//@ts-ignore | ||
ast.definitions[0].variableDefinitions.push({ | ||
kind: 'VariableDefinition', | ||
variable, | ||
type, | ||
}) | ||
}) | ||
// Finally we stringify the modified AST back into a graphql string | ||
return print(ast) | ||
// Otherwise, inject those variables into the query's params. | ||
// First we parse the query into an AST | ||
const ast = typeof query === 'string' ? (0, parser_1.parse)(query) : query; | ||
// See function definition below for details | ||
if (ast.definitions.length > 1) | ||
throw multipleQueriesError(); | ||
// Then we loop through the variables and create AST nodes for them | ||
Object.entries(vars).map(([_name, _type]) => { | ||
const variable = { | ||
kind: 'Variable', | ||
name: { kind: 'Name', value: _name }, | ||
}; | ||
const type = (0, parser_1.parseType)(_type); | ||
// Add the AST nodes to the variable definitions at the top of the query. | ||
// Worth noting it only does this for the first query defined in the file, | ||
// but we throw if there is more than one anyway. | ||
//@ts-ignore | ||
ast.definitions[0].variableDefinitions.push({ | ||
kind: 'VariableDefinition', | ||
variable, | ||
type, | ||
}); | ||
}); | ||
// Finally we stringify the modified AST back into a graphql string | ||
return (0, printer_1.print)(ast); | ||
} | ||
// Internal function, recursively extracts "variables" arguments from a set of components | ||
@@ -175,78 +104,62 @@ // and its deep nested dependencies. | ||
function _findVariables(_dependencies, variables) { | ||
const dependencies = extractFragmentSpecs(_dependencies) | ||
return dependencies.reduce((acc, component) => { | ||
if (component.requiredVariables) { | ||
// If no variables are passed to fetch but dependencies define variables, error | ||
if (!variables) throw variableMismatchError(component) | ||
Object.entries(component.requiredVariables).map(([k, v]) => { | ||
// If variables are present but the one we need is missing, error | ||
if (!variables[k]) throw variableMismatchError(component, k) | ||
// Otherwise, add the variable to our list | ||
acc[k] = v | ||
}) | ||
} | ||
// If the component has dependencies, we recurse to get an object containing | ||
// any dependency variables, then add to the object. We naturally dedupe since | ||
// this is an object, so we just add all. | ||
if (component.dependencies) { | ||
Object.entries(_findVariables(component.dependencies, variables)).map( | ||
([k, v]) => { | ||
acc[k] = v | ||
const dependencies = extractFragmentSpecs(_dependencies); | ||
return dependencies.reduce((acc, component) => { | ||
if (component.requiredVariables) { | ||
// If no variables are passed to fetch but dependencies define variables, error | ||
if (!variables) | ||
throw variableMismatchError(component, null); | ||
Object.entries(component.requiredVariables).map(([k, v]) => { | ||
// If variables are present but the one we need is missing, error | ||
if (!variables[k]) | ||
throw variableMismatchError(component, k); | ||
// Otherwise, add the variable to our list | ||
acc[k] = v; | ||
}); | ||
} | ||
) | ||
} | ||
return acc | ||
}, {}) | ||
// If the component has dependencies, we recurse to get an object containing | ||
// any dependency variables, then add to the object. We naturally dedupe since | ||
// this is an object, so we just add all. | ||
if (component.dependencies) { | ||
Object.entries(_findVariables(component.dependencies, variables)).map(([k, v]) => { | ||
acc[k] = v; | ||
}); | ||
} | ||
return acc; | ||
}, {}); | ||
} | ||
// Super clear error messages when component dependencies ask for variables that | ||
// are not provided in the fetch query. | ||
function variableMismatchError(component, specificVar) { | ||
//@ts-ignore | ||
const fragmentName = parse(component.fragment).definitions[0].name.value | ||
const fragmentVars = Object.keys(component.requiredVariables).map( | ||
(v) => `"${v}"` | ||
) | ||
return new Error( | ||
`The fragment "${fragmentName}" requires ${ | ||
specificVar | ||
//@ts-ignore | ||
const fragmentName = (0, parser_1.parse)(component.fragment).definitions[0].name.value; | ||
const fragmentVars = Object.keys(component.requiredVariables).map((v) => `"${v}"`); | ||
return new Error(`The fragment "${fragmentName}" requires ${specificVar | ||
? `the variable "${specificVar}"` | ||
: `variables ${fragmentVars.join(', ')}` | ||
}, but it is not provided. Make sure you are passing "variables" as an argument to "fetch", and that it defines ${ | ||
specificVar ? `"${specificVar}"` : fragmentVars.join(', ') | ||
}.` | ||
) | ||
: `variables ${fragmentVars.join(', ')}`}, but it is not provided. Make sure you are passing "variables" as an argument to "fetch", and that it defines ${specificVar ? `"${specificVar}"` : fragmentVars.join(', ')}.`); | ||
} | ||
// request with retries if the query fails | ||
async function requestWithRetry(retryCount, originalRequest, ...args) { | ||
const uuid = _createUUID() | ||
const maxRetries = retryCount | ||
for (let retry = 1; retry <= maxRetries; retry++) { | ||
try { | ||
return await originalRequest(...args) | ||
} catch (err) { | ||
console.log(`[${uuid}] Failed retry #${retry}, retrying...`) | ||
const isLastAttempt = retry === maxRetries | ||
if (isLastAttempt) { | ||
console.error(`[${uuid}] Failed all retries, throwing!`) | ||
throw err | ||
} | ||
const uuid = _createUUID(); | ||
const maxRetries = retryCount; | ||
for (let retry = 1; retry <= maxRetries; retry++) { | ||
try { | ||
return await originalRequest(...args); | ||
} | ||
catch (err) { | ||
console.log(`[${uuid}] Failed retry #${retry}, retrying...`); | ||
const isLastAttempt = retry === maxRetries; | ||
if (isLastAttempt) { | ||
console.error(`[${uuid}] Failed all retries, throwing!`); | ||
throw err; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// used to identify a retried request | ||
function _createUUID() { | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | ||
var r = (Math.random() * 16) | 0, | ||
v = c == 'x' ? r : (r & 0x3) | 0x8 | ||
return v.toString(16) | ||
}) | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | ||
var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; | ||
return v.toString(16); | ||
}); | ||
} | ||
// We error if there were multiple queries, since graphql errors both | ||
@@ -257,17 +170,28 @@ // if a variable is present but not used, or not present and used. In theory | ||
function multipleQueriesError() { | ||
return new Error( | ||
'You have defined multiple queries in one request and are also using variables. At the moment, we do not support the use of variables with multiple queries. Please either consolidate to one query per request, or make a PR to to add this functionalty.' | ||
) | ||
return new Error('You have defined multiple queries in one request and are also using variables. At the moment, we do not support the use of variables with multiple queries. Please either consolidate to one query per request, or make a PR to to add this functionalty.'); | ||
} | ||
function fetchMissingQueryError() { | ||
return new Error('The "query" parameter is required') | ||
return new Error('The "query" parameter is required'); | ||
} | ||
function dependenciesTypeError(dependencies) { | ||
return new Error( | ||
`The "dependencies" argument must be an array, the following dependency argument is not valid: ${JSON.stringify( | ||
dependencies | ||
)}` | ||
) | ||
return new Error(`The "dependencies" argument must be an array, the following dependency argument is not valid: ${JSON.stringify(dependencies)}`); | ||
} | ||
module.exports = function Rivet(url, options) { | ||
if (!options.timeout) | ||
options.timeout = 30000; | ||
const retryCount = options.retryCount || 0; | ||
delete options.retryCount; | ||
const client = new graphql_request_1.GraphQLClient(url, options); | ||
if (retryCount) { | ||
client.request = requestWithRetry.bind(null, retryCount, client.request.bind(client)); | ||
} | ||
function fetch({ query, dependencies = [], variables, }) { | ||
if (!query) | ||
throw fetchMissingQueryError(); | ||
const _dependencies = processDependencies(dependencies); | ||
const _query = processVariables(dependencies, variables, query); | ||
return client.request(`${_query}\n${[..._dependencies].join('\n')}`, variables); | ||
} | ||
fetch.client = client; | ||
return fetch; | ||
}; |
{ | ||
"name": "rivet-graphql", | ||
"description": "a relay-like graphql data loading system for nextjs", | ||
"version": "0.5.0", | ||
"version": "0.6.0-canary-20230308183240", | ||
"author": "Jeff Escalante", | ||
@@ -10,4 +10,5 @@ "bugs": { | ||
"dependencies": { | ||
"graphql": "^15.3.0", | ||
"graphql-request": "^3.0.0" | ||
"@graphql-typed-document-node/core": "^3.1.2", | ||
"graphql": "^16.6.0", | ||
"graphql-request": "^5.2.0" | ||
}, | ||
@@ -19,3 +20,3 @@ "devDependencies": { | ||
"rewire": "^5.0.0", | ||
"typescript": "^4.6.2" | ||
"typescript": "^4.9.5" | ||
}, | ||
@@ -39,5 +40,5 @@ "homepage": "https://github.com/hashicorp/rivet-graphql#readme", | ||
"release:canary": "npm run generate:types && changeset publish --tag canary", | ||
"test": "jest", | ||
"test": "npm run generate:types && jest", | ||
"generate:types": "tsc -p ." | ||
} | ||
} |
{ | ||
"compilerOptions": { | ||
"target": "es2020", | ||
"lib": ["es2020"], | ||
"module": "commonjs", | ||
"allowJs": true, | ||
"declaration": true, | ||
"emitDeclarationOnly": true, | ||
"declarationMap": true, | ||
"downlevelIteration": true, | ||
"skipLibCheck": true | ||
}, | ||
"include": ["index.js"] | ||
"include": ["index.ts"] | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
45694
479
3
3
+ Added@graphql-typed-document-node/core@3.2.0(transitive)
+ Addedgraphql@16.9.0(transitive)
+ Addedgraphql-request@5.2.0(transitive)
- Removedgraphql@15.9.0(transitive)
- Removedgraphql-request@3.7.0(transitive)
Updatedgraphql@^16.6.0
Updatedgraphql-request@^5.2.0