feat(plugins): onInstall hook (#236)
This commit is contained in:
parent
62a6006c64
commit
bac64560e7
|
@ -11,6 +11,7 @@
|
|||
"test:ci": "jest -i --coverage",
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w",
|
||||
"dev:test": "jest --watch",
|
||||
"dev:examples": "yarn -s link-examples && tsc -w",
|
||||
"lint": "tslint -p tsconfig.json",
|
||||
"clean": "rm -rf dist",
|
||||
|
|
|
@ -128,9 +128,19 @@ import {
|
|||
import { DynamicInputMethodDef, DynamicOutputMethodDef } from "./dynamicMethod";
|
||||
import { DynamicOutputPropertyDef } from "./dynamicProperty";
|
||||
import { decorateType } from "./definitions/decorateType";
|
||||
import * as Plugins from "./plugins";
|
||||
|
||||
export type Maybe<T> = T | null;
|
||||
|
||||
export type NexusAcceptedTypeDef =
|
||||
| AllNexusNamedTypeDefs
|
||||
| NexusExtendInputTypeDef<string>
|
||||
| NexusExtendTypeDef<string>
|
||||
| GraphQLNamedType
|
||||
| DynamicInputMethodDef<string>
|
||||
| DynamicOutputMethodDef<string>
|
||||
| DynamicOutputPropertyDef<string>;
|
||||
|
||||
type NexusShapedOutput = {
|
||||
name: string;
|
||||
definition: (t: ObjectDefinitionBlock<string>) => void;
|
||||
|
@ -173,6 +183,7 @@ export const UNKNOWN_TYPE_SCALAR = decorateType(
|
|||
);
|
||||
|
||||
export interface BuilderConfig {
|
||||
plugins?: Plugins.PluginDef[];
|
||||
/**
|
||||
* Generated artifact settings. Set to false to disable all.
|
||||
* Set to true to enable all and use default paths. Leave
|
||||
|
@ -396,6 +407,12 @@ export type DynamicOutputProperties = Record<
|
|||
* circular references at this step, while fields will guard for it during lazy evaluation.
|
||||
*/
|
||||
export class SchemaBuilder {
|
||||
/**
|
||||
* Used to track all _GraphQL_ types that have been added to the builder.
|
||||
* This supports hasType method which permits asking the question "Will
|
||||
* the GraphQL schema have _this_ type (name)".
|
||||
*/
|
||||
protected allTypeDefs: Record<string, NexusAcceptedTypeDef> = {};
|
||||
/**
|
||||
* Used to check for circular references.
|
||||
*/
|
||||
|
@ -481,25 +498,17 @@ export class SchemaBuilder {
|
|||
return this.config;
|
||||
}
|
||||
|
||||
hasType = (typeName: string): boolean => {
|
||||
return Boolean(this.allTypeDefs[typeName]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add type takes a Nexus type, or a GraphQL type and pulls
|
||||
* it into an internal "type registry". It also does an initial pass
|
||||
* on any types that are referenced on the "types" field and pulls
|
||||
* those in too, so you can define types anonymously, without
|
||||
* exporting them.
|
||||
*
|
||||
* @param typeDef
|
||||
* Add type takes a Nexus type, or a GraphQL type and pulls it into an
|
||||
* internal "type registry". It also does an initial pass on any types that
|
||||
* are referenced on the "types" field and pulls those in too, so you can
|
||||
* define types anonymously, without exporting them.
|
||||
*/
|
||||
addType(
|
||||
typeDef:
|
||||
| AllNexusNamedTypeDefs
|
||||
| NexusExtendInputTypeDef<string>
|
||||
| NexusExtendTypeDef<string>
|
||||
| GraphQLNamedType
|
||||
| DynamicInputMethodDef<string>
|
||||
| DynamicOutputMethodDef<string>
|
||||
| DynamicOutputPropertyDef<string>
|
||||
) {
|
||||
addType = (typeDef: NexusAcceptedTypeDef) => {
|
||||
if (isNexusDynamicInputMethod(typeDef)) {
|
||||
this.dynamicInputFields[typeDef.name] = typeDef;
|
||||
return;
|
||||
|
@ -540,6 +549,10 @@ export class SchemaBuilder {
|
|||
throw extendError(typeDef.name);
|
||||
}
|
||||
|
||||
if (!this.allTypeDefs[typeDef.name]) {
|
||||
this.allTypeDefs[typeDef.name] = typeDef;
|
||||
}
|
||||
|
||||
if (isNexusScalarTypeDef(typeDef) && typeDef.value.asNexusMethod) {
|
||||
this.dynamicInputFields[typeDef.value.asNexusMethod] = typeDef.name;
|
||||
this.dynamicOutputFields[typeDef.value.asNexusMethod] = typeDef.name;
|
||||
|
@ -579,7 +592,7 @@ export class SchemaBuilder {
|
|||
if (isNexusInterfaceTypeDef(typeDef)) {
|
||||
this.typesToWalk.push({ type: "interface", value: typeDef.value });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walkTypes() {
|
||||
let obj;
|
||||
|
@ -1459,7 +1472,14 @@ export function buildTypesInternal<
|
|||
schemaBuilder?: SchemaBuilder
|
||||
): BuildTypes<TypeMapDefs> {
|
||||
const builder = schemaBuilder || new SchemaBuilder(config);
|
||||
const plugins = config.plugins || [];
|
||||
const pluginControllers = plugins.map((plugin) =>
|
||||
Plugins.initialize(builder, plugin)
|
||||
);
|
||||
addTypes(builder, types);
|
||||
pluginControllers.forEach((pluginController) =>
|
||||
pluginController.triggerOnInstall()
|
||||
);
|
||||
return builder.getFinalTypeMap();
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ export enum NexusTypes {
|
|||
DynamicInput = "DynamicInput",
|
||||
DynamicOutputMethod = "DynamicOutputMethod",
|
||||
DynamicOutputProperty = "DynamicOutputProperty",
|
||||
Plugin = "Plugin",
|
||||
}
|
||||
|
||||
export interface DeprecationInfo {
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
// All of the Public API definitions
|
||||
export {
|
||||
createPlugin,
|
||||
PluginConfig,
|
||||
PluginBuilderLens,
|
||||
PluginOnInstallHandler,
|
||||
} from "./plugins";
|
||||
export { buildTypes, makeSchema } from "./builder";
|
||||
export {
|
||||
arg,
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
import { SchemaBuilder, NexusAcceptedTypeDef } from "./builder";
|
||||
import { withNexusSymbol, NexusTypes } from "./definitions/_types";
|
||||
import { venn } from "./utils";
|
||||
|
||||
/**
|
||||
* A read-only builder api exposed to plugins in the onInstall hook which
|
||||
* proxies very limited functionality into the internal Nexus Builder.
|
||||
*/
|
||||
export type PluginBuilderLens = {
|
||||
hasType: (typeName: string) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the Neuxs Plugin interface that allows users to extend Nexus at
|
||||
* particular extension points. Plugins are just functions that receive hooks
|
||||
* which can then be registered upon with callbacks.
|
||||
*/
|
||||
export type PluginConfig = {
|
||||
name: string;
|
||||
onInstall?: PluginOnInstallHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* The plugin callback to execute when onInstall lifecycle event occurs.
|
||||
* OnInstall event occurs before type walking which means inline types are not
|
||||
* visible at this point yet. `builderLens.hasType` will only return true
|
||||
* for types the user has defined top level in their app, and any types added by
|
||||
* upstream plugins.
|
||||
*/
|
||||
export type PluginOnInstallHandler = (
|
||||
builder: PluginBuilderLens
|
||||
) => { types: NexusAcceptedTypeDef[] };
|
||||
|
||||
/**
|
||||
* The processed version of a plugin config. This lower level version has
|
||||
* defaults provided for optionals etc.
|
||||
*/
|
||||
export type InternalPluginConfig = Required<PluginConfig>;
|
||||
|
||||
/**
|
||||
* A plugin defines configuration which can document additional metadata options
|
||||
* for a type definition. This metadata can be used to decorate the "resolve" function
|
||||
* to provide custom functionality, such as logging, error handling, additional type
|
||||
* validation.
|
||||
*
|
||||
* You can specify options which can be defined on the schema,
|
||||
* the type or the plugin. The config from each of these will be
|
||||
* passed in during schema construction time, and used to augment the field as necessary.
|
||||
*
|
||||
* You can either return a function, with the new defintion of a resolver implementation,
|
||||
* or you can return an "enter" / "leave" pairing which will wrap the pre-execution of the
|
||||
* resolver and the "result" of the resolver, respectively.
|
||||
*/
|
||||
export function createPlugin(config: PluginConfig): PluginDef {
|
||||
validatePluginConfig(config);
|
||||
const internalConfig = { ...configDefaults, ...config };
|
||||
return new PluginDef(internalConfig);
|
||||
}
|
||||
|
||||
const configDefaults = {
|
||||
onInstall: () => ({ types: [] }),
|
||||
};
|
||||
|
||||
/**
|
||||
* A definition for a plugin. Should be passed to the `plugins: []` option
|
||||
* on makeSchema. Refer to `createPlugin` factory for full doc.
|
||||
*/
|
||||
export class PluginDef {
|
||||
constructor(readonly config: InternalPluginConfig) {}
|
||||
}
|
||||
withNexusSymbol(PluginDef, NexusTypes.Plugin);
|
||||
|
||||
/**
|
||||
* The interface used to drive plugin execution via lifecycle event triggering.
|
||||
*/
|
||||
type PluginController = {
|
||||
triggerOnInstall: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* This will gather the hook handlers (aka. callbacks, event handlers) a the
|
||||
* plugin has registered for and return a controller to trigger said hooks,
|
||||
* thus controlling execution of the plugins' hook handlers.
|
||||
*/
|
||||
export function initialize(
|
||||
builder: SchemaBuilder,
|
||||
plugin: PluginDef
|
||||
): PluginController {
|
||||
const state = {
|
||||
onInstallTriggered: false,
|
||||
};
|
||||
|
||||
const builderLens: PluginBuilderLens = {
|
||||
hasType: builder.hasType,
|
||||
};
|
||||
|
||||
return {
|
||||
triggerOnInstall: () => {
|
||||
// Enforce the invariant that a lifecycle hook will only ever be called once.
|
||||
if (state.onInstallTriggered) {
|
||||
throw new Error(
|
||||
"Multiple triggers of onInstall hook detected. This should never happen. This is an internal error."
|
||||
);
|
||||
} else {
|
||||
state.onInstallTriggered = true;
|
||||
}
|
||||
|
||||
// By doing addType on the types returned by a plugin right after it has
|
||||
// done so we make it possible for downstream plugins to see types added
|
||||
// by upstream plugins.
|
||||
let hookResult: ReturnType<PluginOnInstallHandler>;
|
||||
try {
|
||||
hookResult = plugin.config.onInstall(builderLens);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Plugin ${plugin.config.name} failed on "onInstall" hook:\n\n${error.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
validateOnInstallHookResult(plugin, hookResult);
|
||||
hookResult.types.forEach(builder.addType);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the configuration given by a plugin is valid.
|
||||
*/
|
||||
function validatePluginConfig(plugin: PluginConfig): void {
|
||||
const validRequiredProps = ["name"];
|
||||
const validOptionalProps = ["onInstall"];
|
||||
const validProps = [...validRequiredProps, ...validOptionalProps];
|
||||
const givenProps = Object.keys(plugin);
|
||||
|
||||
const printProps = (props: Iterable<string>): string => {
|
||||
return [...props].join(", ");
|
||||
};
|
||||
|
||||
const [missingRequiredProps, ,] = venn(validRequiredProps, givenProps);
|
||||
if (missingRequiredProps.size > 0) {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.name}" is missing required properties: ${printProps(
|
||||
missingRequiredProps
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const nameType = typeof plugin.name;
|
||||
if (nameType !== "string") {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.name}" is giving an invalid value for property name: expected "string" type, got ${nameType} type`
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.name === "") {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.name}" is giving an invalid value for property name: empty string`
|
||||
);
|
||||
}
|
||||
|
||||
const [, , invalidGivenProps] = venn(validProps, givenProps);
|
||||
if (invalidGivenProps.size > 0) {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.name}" is giving unexpected properties: ${printProps(
|
||||
invalidGivenProps
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (plugin.onInstall) {
|
||||
const onInstallType = typeof plugin.onInstall;
|
||||
if (onInstallType !== "function") {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.name}" is giving an invalid value for onInstall hook: expected "function" type, got ${onInstallType} type`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the data returned from a plugin from the `onInstall` hook is valid.
|
||||
*/
|
||||
function validateOnInstallHookResult(
|
||||
plugin: PluginDef,
|
||||
hookResult: ReturnType<PluginOnInstallHandler>
|
||||
): void {
|
||||
if (
|
||||
hookResult === null ||
|
||||
typeof hookResult !== "object" ||
|
||||
!Array.isArray(hookResult.types)
|
||||
) {
|
||||
throw new Error(
|
||||
`Plugin "${plugin.config.name}" returned invalid data for "onInstall" hook:\n\nexpected structure:\n\n { types: NexusAcceptedTypeDef[] }\n\ngot:\n\n ${hookResult}`
|
||||
);
|
||||
}
|
||||
// TODO we should validate that the array members all fall under NexusAcceptedTypeDef
|
||||
}
|
34
src/utils.ts
34
src/utils.ts
|
@ -236,3 +236,37 @@ export function relativePathTo(
|
|||
}
|
||||
return path.join(relative, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the venn diagram between two iterables based on reference equality
|
||||
* checks. The returned tripple contains items thusly:
|
||||
*
|
||||
* * items only in arg 1 --> first tripple slot
|
||||
* * items in args 1 & 2 --> second tripple slot
|
||||
* * items only in arg 2 --> third tripple slot
|
||||
*/
|
||||
export function venn<T>(
|
||||
xs: Iterable<T>,
|
||||
ys: Iterable<T>
|
||||
): [Set<T>, Set<T>, Set<T>] {
|
||||
const lefts: Set<T> = new Set(xs);
|
||||
const boths: Set<T> = new Set();
|
||||
const rights: Set<T> = new Set(ys);
|
||||
|
||||
for (const l of lefts) {
|
||||
if (rights.has(l)) {
|
||||
boths.add(l);
|
||||
lefts.delete(l);
|
||||
rights.delete(l);
|
||||
}
|
||||
}
|
||||
for (const r of rights) {
|
||||
if (lefts.has(r)) {
|
||||
boths.add(r);
|
||||
lefts.delete(r);
|
||||
rights.delete(r);
|
||||
}
|
||||
}
|
||||
|
||||
return [lefts, boths, rights];
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ export const testSchema = (name: string) => {
|
|||
|
||||
expect([appFilePath]).toTypeCheck({
|
||||
sourceMap: false,
|
||||
downlevelIteration: true,
|
||||
noEmitOnError: true,
|
||||
esModuleInterop: true,
|
||||
strict: true,
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
import {
|
||||
createPlugin,
|
||||
makeSchema,
|
||||
PluginConfig,
|
||||
queryType,
|
||||
objectType,
|
||||
PluginOnInstallHandler,
|
||||
} from "../src";
|
||||
import { printSchema } from "graphql";
|
||||
import {
|
||||
NexusAcceptedTypeDef,
|
||||
buildTypes,
|
||||
inputObjectType,
|
||||
extendType,
|
||||
buildTypesInternal,
|
||||
} from "../src/core";
|
||||
|
||||
const fooObject = objectType({
|
||||
name: "foo",
|
||||
definition(t) {
|
||||
t.string("bar");
|
||||
},
|
||||
});
|
||||
|
||||
const queryField = extendType({
|
||||
type: "Query",
|
||||
definition(t) {
|
||||
t.string("something");
|
||||
},
|
||||
});
|
||||
|
||||
describe("runtime config validation", () => {
|
||||
const whenGiven = (config: any) => () => createPlugin(config);
|
||||
|
||||
it("checks name present", () => {
|
||||
expect(whenGiven({})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"undefined\\" is missing required properties: name"`
|
||||
);
|
||||
});
|
||||
|
||||
it("checks name is string", () => {
|
||||
expect(whenGiven({ name: 1 })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"1\\" is giving an invalid value for property name: expected \\"string\\" type, got number type"`
|
||||
);
|
||||
});
|
||||
|
||||
it("checks name is not empty", () => {
|
||||
expect(whenGiven({ name: "" })).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"\\" is giving an invalid value for property name: empty string"`
|
||||
);
|
||||
});
|
||||
|
||||
it("checks onInstall is a function if defined", () => {
|
||||
expect(
|
||||
whenGiven({ name: "x", onInstall: "foo" })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"x\\" is giving an invalid value for onInstall hook: expected \\"function\\" type, got string type"`
|
||||
);
|
||||
|
||||
expect(
|
||||
whenGiven({ name: "x", onInstall: {} })
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Plugin \\"x\\" is giving an invalid value for onInstall hook: expected \\"function\\" type, got object type"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime onInstall hook handler", () => {
|
||||
const whenGiven = (onInstall: any) => () =>
|
||||
makeSchema({
|
||||
types: [],
|
||||
plugins: [createPlugin({ name: "x", onInstall })],
|
||||
});
|
||||
|
||||
it("validates return value against shallow schema", () => {
|
||||
expect(whenGiven(() => null)).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Plugin \\"x\\" returned invalid data for \\"onInstall\\" hook:
|
||||
|
||||
expected structure:
|
||||
|
||||
{ types: NexusAcceptedTypeDef[] }
|
||||
|
||||
got:
|
||||
|
||||
null"
|
||||
`);
|
||||
|
||||
expect(whenGiven(() => ({ types: null })))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Plugin \\"x\\" returned invalid data for \\"onInstall\\" hook:
|
||||
|
||||
expected structure:
|
||||
|
||||
{ types: NexusAcceptedTypeDef[] }
|
||||
|
||||
got:
|
||||
|
||||
[object Object]"
|
||||
`);
|
||||
|
||||
expect(whenGiven(() => ({}))).toThrowErrorMatchingInlineSnapshot(`
|
||||
"Plugin \\"x\\" returned invalid data for \\"onInstall\\" hook:
|
||||
|
||||
expected structure:
|
||||
|
||||
{ types: NexusAcceptedTypeDef[] }
|
||||
|
||||
got:
|
||||
|
||||
[object Object]"
|
||||
`);
|
||||
});
|
||||
|
||||
it("gracefully handles thrown errors", () => {
|
||||
expect(
|
||||
whenGiven(() => {
|
||||
throw new Error("plugin failed somehow oops");
|
||||
})
|
||||
).toThrow(
|
||||
/Plugin x failed on "onInstall" hook:\n\nError: plugin failed somehow oops\n at.*/
|
||||
);
|
||||
});
|
||||
|
||||
it("does not validate types array members yet", () => {
|
||||
expect(
|
||||
whenGiven(() => ({ types: [null, 1, "bad"] }))
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Cannot read property 'name' of null"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a plugin may", () => {
|
||||
const whenGiven = (pluginConfig: PluginConfig) => () =>
|
||||
makeSchema({
|
||||
types: [],
|
||||
plugins: [createPlugin(pluginConfig)],
|
||||
});
|
||||
|
||||
it("do nothing", () => {
|
||||
expect(whenGiven({ name: "x" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("onInstall plugins", () => {
|
||||
const whenGiven = ({
|
||||
onInstall,
|
||||
plugin,
|
||||
appTypes,
|
||||
}: {
|
||||
onInstall?: PluginOnInstallHandler;
|
||||
plugin?: Omit<PluginConfig, "name">;
|
||||
appTypes?: NexusAcceptedTypeDef[];
|
||||
}) => {
|
||||
const xPluginConfig = plugin || { onInstall };
|
||||
|
||||
return printSchema(
|
||||
makeSchema({
|
||||
types: appTypes || [],
|
||||
plugins: [createPlugin({ name: "x", ...xPluginConfig })],
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
it("is an optional hook", () => {
|
||||
expect(whenGiven({ plugin: {} }));
|
||||
});
|
||||
|
||||
it("may return an empty array of types", () => {
|
||||
expect(whenGiven({ onInstall: () => ({ types: [] }) }));
|
||||
});
|
||||
|
||||
it("may contribute types", () => {
|
||||
expect(
|
||||
whenGiven({
|
||||
onInstall: () => ({
|
||||
types: [queryField],
|
||||
}),
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"type Query {
|
||||
something: String!
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("has access to top-level types", () => {
|
||||
expect(
|
||||
whenGiven({
|
||||
onInstall: (builder) => ({
|
||||
types: builder.hasType("foo") ? [] : [queryField],
|
||||
}),
|
||||
appTypes: [fooObject],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"type foo {
|
||||
bar: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
ok: Boolean!
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("does not see fallback ok-query", () => {
|
||||
expect(
|
||||
whenGiven({
|
||||
onInstall(builder) {
|
||||
return {
|
||||
types: builder.hasType("Query") ? [queryField] : [],
|
||||
};
|
||||
},
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"type Query {
|
||||
ok: Boolean!
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("does not have access to inline types", () => {
|
||||
expect(
|
||||
whenGiven({
|
||||
onInstall: (builder) => ({
|
||||
types: builder.hasType("Inline") ? [queryField] : [],
|
||||
}),
|
||||
appTypes: [
|
||||
queryType({
|
||||
definition(t) {
|
||||
t.string("bar", {
|
||||
args: {
|
||||
inline: inputObjectType({
|
||||
name: "Inline",
|
||||
definition(t2) {
|
||||
t2.string("hidden");
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
"input Inline {
|
||||
hidden: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
bar(inline: Inline): String!
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"downlevelIteration": true,
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
|
|
Loading…
Reference in New Issue