diff --git a/src/codegen/codegenHelpers.ts b/src/codegen/codegenHelpers.ts index b6f82a7..0e4be51 100644 --- a/src/codegen/codegenHelpers.ts +++ b/src/codegen/codegenHelpers.ts @@ -1,9 +1,14 @@ -import { +import * as GraphQL from "graphql"; +import type { FragmentDefinitionNode, GraphQLSchema, OperationDefinitionNode, } from "graphql"; -import { NetlifyGraphConfig } from "../netlifyGraph"; +import { + NetlifyGraphConfig, + ParsedFragment, + ParsedFunction, +} from "../netlifyGraph"; /** * Keywords in both Javascript and TypeScript @@ -89,41 +94,45 @@ export type NamedExportedFile = { kind: "NamedExportedFile"; name: string[]; content: string; + language: string; + codeMirrorMode?: string; }; export type UnnamedExportedFile = { kind: "UnnamedExportedFile"; content: string; + language: string; + codeMirrorMode?: string; }; export type ExportedFile = NamedExportedFile | UnnamedExportedFile; export type ExporterResult = { exportedFiles: ExportedFile[]; - language: string; }; -export type FrameworkGenerator = (opts: { +export type GenerateHandlerFunction = (opts: { + GraphQL: typeof GraphQL; operationDataList: OperationData[]; netlifyGraphConfig: NetlifyGraphConfig; options: Record; schema: GraphQLSchema; }) => ExporterResult; +export type GenerateHandlerPreviewFunction = (opts: { + GraphQL: typeof GraphQL; + operationDataList: OperationData[]; + netlifyGraphConfig: NetlifyGraphConfig; + options: Record; + schema: GraphQLSchema; +}) => ExportedFile; + export type SnippetOption = { id: string; label: string; initial: boolean; }; -export type SnippetGeneratorWithMeta = { - language: string; - codeMirrorMode: string; - name: string; - options: SnippetOption[]; - generate: FrameworkGenerator; -}; - export type OperationDataList = { operationDefinitions: (OperationDefinitionNode | FragmentDefinitionNode)[]; fragmentDefinitions: FragmentDefinitionNode[]; @@ -140,3 +149,37 @@ export type OperationData = { operationDefinition: OperationDefinitionNode | FragmentDefinitionNode; fragmentDependencies: FragmentDefinitionNode[]; }; + +export type GenerateRuntimeFunction = (opts: { + GraphQL: typeof GraphQL; + operationDataList: OperationData[]; + netlifyGraphConfig: NetlifyGraphConfig; + options: Record; + schema: GraphQLSchema; + schemaId: string; + functionDefinitions: ParsedFunction[]; + fragments: ParsedFragment[]; +}) => NamedExportedFile[]; + +type CodeGeneratorSupportableDefinitionType = + | "query" + | "mutation" + | "subscription" + | "fragment"; + +export type CodeGenerator = { + generatePreview?: GenerateHandlerPreviewFunction; + generateHandler: GenerateHandlerFunction; + generateHandlerOptions?: { schemaSdl: string; inputTypename: string }; + supportedDefinitionTypes: CodeGeneratorSupportableDefinitionType[]; + name: string; + id: string; + version: string; +}; + +export type CodegenModule = { + id: string; + version: string; + generateRuntime: GenerateRuntimeFunction; + generators: CodeGenerator[]; +}; diff --git a/src/codegen/genericExporter.ts b/src/codegen/genericExporter.ts index 21c8ff4..1443879 100644 --- a/src/codegen/genericExporter.ts +++ b/src/codegen/genericExporter.ts @@ -10,7 +10,7 @@ import { ExportedFile, ExporterResult, munge, - SnippetGeneratorWithMeta, + CodeGenerator, UnnamedExportedFile, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; @@ -217,18 +217,26 @@ const addLeftWhitespace = (string, padding) => { const collapseExtraNewlines = (string) => string.replace(/\n{2,}/g, "\n\n"); -const snippetOptions = [ - { - id: "postHttpMethod", - label: "POST function", - initial: true, - }, - { - id: "useClientAuth", - label: "Use user's OAuth token", - initial: false, - }, -]; +const snippetOptions = { + inputTypename: "Options", + schemaSdl: ` + enum HttpMethod { + POST + GET + } + +input Options { + """ + Make call over POST + """ + postHttpMethod: HttpMethod + """ + Use user's OAuth token + """ + useClientAuth: Boolean! +} + `, +}; const operationFunctionName = (operationData) => { const { type } = operationData; @@ -449,6 +457,7 @@ const subscriptionHandler = ({ }): UnnamedExportedFile => { return { kind: "UnnamedExportedFile", + language: "javascript", content: `${imp( netlifyGraphConfig, "NetlifyGraph", @@ -522,12 +531,13 @@ const exp = (netlifyGraphConfig, name) => { }; // Snippet generation! -export const netlifyFunctionSnippet: SnippetGeneratorWithMeta = { - language: "JavaScript", - codeMirrorMode: "javascript", +export const netlifyFunctionSnippet: CodeGenerator = { name: "Netlify Function", - options: snippetOptions, - generate: (opts): ExporterResult => { + generateHandlerOptions: snippetOptions, + supportedDefinitionTypes: [], + id: "Netlify Functions", + version: "0.0.1", + generateHandler: (opts): ExporterResult => { const { netlifyGraphConfig, options } = opts; const operationDataList = opts.operationDataList.map( @@ -560,11 +570,11 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ if (!firstOperation) { return { - language: "javascript", exportedFiles: [ { kind: "UnnamedExportedFile", content: "// No operation found", + language: "javascript", }, ], }; @@ -581,7 +591,6 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ }); return { - language: "javascript", exportedFiles: [result], }; } @@ -657,13 +666,15 @@ ${clientSideCalls} const content = collapseExtraNewlines(snippet); return { - language: "javascript", exportedFiles: [ { kind: "UnnamedExportedFile", content: content, + language: "javascript", }, ], }; }, }; + +export const generators = [netlifyFunctionSnippet]; diff --git a/src/codegen/nextjsExporter.ts b/src/codegen/nextjsExporter.ts index 5720203..ab895a4 100644 --- a/src/codegen/nextjsExporter.ts +++ b/src/codegen/nextjsExporter.ts @@ -1,10 +1,8 @@ -import { - FragmentDefinitionNode, +import * as GraphQLPackage from "graphql"; +import type { GraphQLSchema, - Kind, OperationDefinitionNode, - parse, - print, + FragmentDefinitionNode, } from "graphql"; import { NetlifyGraphConfig } from "../netlifyGraph"; @@ -14,11 +12,12 @@ import { NamedExportedFile, OperationData, OperationDataList, - SnippetGeneratorWithMeta, + CodeGenerator, UnnamedExportedFile, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; import { formElComponent } from "../graphqlHelpers"; +import { GraphQL } from ".."; let operationNodesMemo = [null, null]; @@ -54,12 +53,14 @@ const formUpdateHandler = `const updateFormVariables = (setFormVariables, path, };`; const generatePage = (opts: { + GraphQL: typeof GraphQLPackage; netlifyGraphConfig: NetlifyGraphConfig; operationData: OperationData; schema: GraphQLSchema; route: string; }): NamedExportedFile => { const form = formElComponent({ + GraphQL, operationData: opts.operationData, schema: opts.schema, callFn: "submitForm()", @@ -70,6 +71,7 @@ const generatePage = (opts: { return { kind: "NamedExportedFile", + language: opts.netlifyGraphConfig.language, name: ["pages", `${opts.operationData.displayName}Form.${extension}`], content: `import Head from "next/head"; import React, { useState } from "react"; @@ -162,7 +164,8 @@ ${formUpdateHandler} }; }; -const getOperationNodes = (query) => { +const getOperationNodes = (GraphQL: typeof GraphQLPackage, query) => { + const { parse } = GraphQL; if (operationNodesMemo[0] === query && operationNodesMemo[1]) { return operationNodesMemo[1]; } @@ -304,10 +307,17 @@ const toposort = (graph) => { }; export const computeOperationDataList = ({ + GraphQL, query, variables, +}: { + GraphQL: typeof GraphQLPackage; + query: string; + variables: Record; }): OperationDataList => { - const operationDefinitions = getOperationNodes(query); + const { Kind, print } = GraphQL; + + const operationDefinitions = getOperationNodes(GraphQL, query); const fragmentDefinitions: FragmentDefinitionNode[] = []; @@ -365,18 +375,26 @@ const addLeftWhitespace = (string, padding) => { const collapseExtraNewlines = (string) => string.replace(/\n{2,}/g, "\n\n"); -const snippetOptions = [ - { - id: "postHttpMethod", - label: "POST function", - initial: true, - }, - { - id: "useClientAuth", - label: "Use user's OAuth token", - initial: false, - }, -]; +const snippetOptions = { + inputTypename: "Options", + schemaSdl: ` + enum HttpMethod { + POST + GET + } + +input Options { + """ + Make call over POST + """ + postHttpMethod: HttpMethod + """ + Use user's OAuth token + """ + useClientAuth: Boolean! +} + `, +}; const operationFunctionName = (operationData) => { const { type } = operationData; @@ -405,7 +423,9 @@ const operationFunctionName = (operationData) => { return fnName; }; -const coercerFor = (type, name) => { +const coercerFor = (GraphQL: typeof GraphQLPackage, type, name) => { + const { Kind, print } = GraphQL; + const typeName = print(type).replace(/\W+/gi, "").toLocaleLowerCase(); switch (typeName) { @@ -422,7 +442,13 @@ const coercerFor = (type, name) => { } }; -const asyncFetcherInvocation = (operationDataList, pluckerStyle) => { +const asyncFetcherInvocation = ( + GraphQL: typeof GraphQLPackage, + operationDataList, + pluckerStyle +) => { + const { print } = GraphQL; + const invocations = operationDataList .filter((operationData) => ["query", "mutation", "subscription"].includes(operationData.type) @@ -442,6 +468,7 @@ const asyncFetcherInvocation = (operationDataList, pluckerStyle) => { ?.map((def) => { const name = def.variable.name.value; const withCoercer = coercerFor( + GraphQL, def.type, `typeof req.query?.${name} === 'string' ? req.query?.${name} : req.query?.${name}[0]` ); @@ -602,6 +629,7 @@ const subscriptionHandler = ({ }): ExportedFile => { return { kind: "UnnamedExportedFile", + language: netlifyGraphConfig.language, content: `${ts( netlifyGraphConfig, 'import type { NextApiRequest, NextApiResponse } from "next";\n' @@ -720,12 +748,13 @@ const expDefault = (netlifyGraphConfig: NetlifyGraphConfig, name: string) => { }; // Snippet generation! -export const nextjsFunctionSnippet: SnippetGeneratorWithMeta = { - language: "JavaScript", - codeMirrorMode: "javascript", +export const nextjsFunctionSnippet: CodeGenerator = { name: "Next.js Function", - options: snippetOptions, - generate: (opts) => { + generateHandlerOptions: snippetOptions, + supportedDefinitionTypes: [], + id: "sgrove/next-js", + version: "0.0.1", + generateHandler: (opts) => { const { netlifyGraphConfig, options } = opts; const operationDataList = opts.operationDataList.map( @@ -758,11 +787,11 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ if (!firstOperation) { return { - language: "javascript", exportedFiles: [ { kind: "UnnamedExportedFile", content: "// No operation found", + language: "javascript", }, ], }; @@ -785,6 +814,7 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ } const fetcherInvocation = asyncFetcherInvocation( + opts.GraphQL, operationDataList, options.postHttpMethod === true ? "post" : "get" ); @@ -855,6 +885,7 @@ ${clientSideCalls} */`; const page: NamedExportedFile = generatePage({ + GraphQL: opts.GraphQL, netlifyGraphConfig, operationData: firstOperation, schema: opts.schema, @@ -864,6 +895,7 @@ ${clientSideCalls} const api: UnnamedExportedFile = { kind: "UnnamedExportedFile", content: collapseExtraNewlines(snippet), + language: netlifyGraphConfig.language, }; return { @@ -872,3 +904,5 @@ ${clientSideCalls} }; }, }; + +export const generators = [nextjsFunctionSnippet]; diff --git a/src/codegen/remixExporter.ts b/src/codegen/remixExporter.ts index 98701a0..48d1ffa 100644 --- a/src/codegen/remixExporter.ts +++ b/src/codegen/remixExporter.ts @@ -1,10 +1,8 @@ -import { - FragmentDefinitionNode, +import * as GraphQLPackage from "graphql"; +import type { GraphQLSchema, - Kind, OperationDefinitionNode, - parse, - print, + FragmentDefinitionNode, } from "graphql"; import { NetlifyGraphConfig } from "../netlifyGraph"; @@ -14,10 +12,11 @@ import { NamedExportedFile, OperationData, OperationDataList, - SnippetGeneratorWithMeta, + CodeGenerator, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; import { remixFormInput } from "../graphqlHelpers"; +import { GraphQL } from ".."; let operationNodesMemo = [null, null]; @@ -53,10 +52,13 @@ const formUpdateHandler = `const updateFormVariables = (setFormVariables, path, };`; export const formElComponent = ({ + GraphQL, operationData, schema, callFn, }: { + GraphQL: typeof GraphQLPackage; + operationData: OperationData; schema: GraphQLSchema; callFn: string; @@ -75,7 +77,7 @@ export const formElComponent = ({ const els = (operationData.operationDefinition.variableDefinitions || []).map( (def) => { - const genInput = remixFormInput(schema, def, []); + const genInput = remixFormInput(GraphQL, schema, def, []); const input = genInput || `UNABLE_TO_GENERATE_FORM_INPUT_FOR_GRAPHQL_TYPE(${def})`; @@ -98,12 +100,14 @@ export const formElComponent = ({ }; const generateRoute = (opts: { + GraphQL: typeof GraphQLPackage; netlifyGraphConfig: NetlifyGraphConfig; operationData: OperationData; schema: GraphQLSchema; route: string; }): NamedExportedFile => { const form = formElComponent({ + GraphQL: opts.GraphQL, operationData: opts.operationData, schema: opts.schema, callFn: "submitForm()", @@ -112,6 +116,7 @@ const generateRoute = (opts: { const { netlifyGraphConfig } = opts; const fetcherInvocation = asyncFetcherInvocation( + GraphQL, opts.netlifyGraphConfig, [opts.operationData], "get" @@ -119,6 +124,7 @@ const generateRoute = (opts: { return { kind: "NamedExportedFile", + language: opts.netlifyGraphConfig.language, name: [ "app", "routes", @@ -176,7 +182,9 @@ export default function handler() { }; }; -const getOperationNodes = (query) => { +const getOperationNodes = (GraphQL: typeof GraphQLPackage, query) => { + const { parse } = GraphQL; + if (operationNodesMemo[0] === query && operationNodesMemo[1]) { return operationNodesMemo[1]; } @@ -318,10 +326,17 @@ const toposort = (graph) => { }; export const computeOperationDataList = ({ + GraphQL, query, variables, +}: { + GraphQL: typeof GraphQLPackage; + query: string; + variables: Record; }): OperationDataList => { - const operationDefinitions = getOperationNodes(query); + const { Kind, print } = GraphQL; + + const operationDefinitions = getOperationNodes(GraphQL, query); const fragmentDefinitions: FragmentDefinitionNode[] = []; @@ -379,18 +394,26 @@ const addLeftWhitespace = (string, padding) => { const collapseExtraNewlines = (string) => string.replace(/\n{2,}/g, "\n\n"); -const snippetOptions = [ - { - id: "postHttpMethod", - label: "POST function", - initial: true, - }, - { - id: "useClientAuth", - label: "Use user's OAuth token", - initial: false, - }, -]; +const snippetOptions = { + inputTypename: "Options", + schemaSdl: ` + enum HttpMethod { + POST + GET + } + +input Options { + """ + Make call over POST + """ + postHttpMethod: HttpMethod + """ + Use user's OAuth token + """ + useClientAuth: Boolean! +} + `, +}; const operationFunctionName = (operationData) => { const { type } = operationData; @@ -419,7 +442,9 @@ const operationFunctionName = (operationData) => { return fnName; }; -const coercerFor = (type, name) => { +const coercerFor = (GraphQL: typeof GraphQLPackage, type, name) => { + const { print } = GraphQL; + const typeName = print(type).replace(/\W+/gi, "").toLocaleLowerCase(); switch (typeName) { @@ -437,10 +462,12 @@ const coercerFor = (type, name) => { }; const asyncFetcherInvocation = ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, operationDataList, pluckerStyle ) => { + const { print } = GraphQL; const invocations = operationDataList .filter((operationData) => ["query", "mutation", "subscription"].includes(operationData.type) @@ -460,6 +487,7 @@ const asyncFetcherInvocation = ( ?.map((def) => { const name = def.variable.name.value; const withCoercer = coercerFor( + GraphQL, def.type, `${munge(name)}FormValue` ); @@ -628,6 +656,7 @@ const subscriptionHandler = ({ }): ExportedFile => { return { kind: "NamedExportedFile", + language: netlifyGraphConfig.language, name: [ "app", "routes", @@ -715,12 +744,13 @@ const expDefault = (netlifyGraphConfig: NetlifyGraphConfig, name: string) => { }; // Snippet generation! -export const remixFunctionSnippet: SnippetGeneratorWithMeta = { - language: "JavaScript", - codeMirrorMode: "javascript", +export const remixFunctionSnippet: CodeGenerator = { name: "Remix Function", - options: snippetOptions, - generate: (opts) => { + generateHandlerOptions: snippetOptions, + supportedDefinitionTypes: [], + id: "sgrove/remix", + version: "0.0.1", + generateHandler: (opts) => { const { netlifyGraphConfig, options } = opts; const operationDataList = opts.operationDataList.map( @@ -753,11 +783,11 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ if (!firstOperation) { return { - language: "javascript", exportedFiles: [ { kind: "UnnamedExportedFile", content: "// No operation found", + language: "javascript", }, ], }; @@ -774,7 +804,6 @@ ${operationData.type} unnamed${capitalizeFirstLetter(operationData.type)}${ }); return { - language: netlifyGraphConfig.language, exportedFiles: [result], }; } @@ -819,6 +848,7 @@ ${clientSideCalls} */`; const route: NamedExportedFile = generateRoute({ + GraphQL: opts.GraphQL, netlifyGraphConfig: netlifyGraphConfig, operationData: firstOperation, schema: opts.schema, @@ -831,3 +861,5 @@ ${clientSideCalls} }; }, }; + +export const generators = [remixFunctionSnippet]; diff --git a/src/generatedOneGraphClient.js b/src/generatedOneGraphClient.js index dc9ed45..49ec7ed 100644 --- a/src/generatedOneGraphClient.js +++ b/src/generatedOneGraphClient.js @@ -1,10 +1,10 @@ /* eslint-disable */ // @ts-nocheck // GENERATED VIA NETLIFY AUTOMATED DEV TOOLS, EDIT WITH CAUTION! -const fetch = require('node-fetch') +const fetch = require("node-fetch"); const internalConsole = require("./internalConsole").internalConsole; -const netlifyGraphHost = process.env.NETLIFY_GRAPH_HOST || "graph.netlify.com" +const netlifyGraphHost = process.env.NETLIFY_GRAPH_HOST || "graph.netlify.com"; // Basic LRU cache implementation const makeLRUCache = (max) => { @@ -12,8 +12,8 @@ const makeLRUCache = (max) => { }; const oldestCacheKey = (lru) => { - return lru.keys().next().value -} + return lru.keys().next().value; +}; // Depend on Map keeping track of insertion order const getFromCache = (lru, key) => { @@ -64,7 +64,7 @@ const httpFetch = async (siteId, options) => { method: "POST", headers: headers, timeout: timeoutMs, - body: reqBody + body: reqBody, }; const url = "https://" + netlifyGraphHost + "/graphql?app_id=" + siteId; @@ -381,39 +381,63 @@ export const executeCreateCLISessionEventMutation = (variables, options) => { export const fetchCLISessionQuery = (variables, options) => { return fetchNetlifyGraph({ - query: `query CLISessionQuery($sessionId: String!, $first: Int!) { - oneGraph { - __typename - netlifyCliSession(id: $sessionId) { - appId - createdAt - id - cliHeartbeatIntervalMs - events(first: $first) { + query: `query CLISessionQuery($sessionId: String!, $first: Int!) + @netlify( + id: """ + 12b5bdea-9bab-4124-a731-5e697b155009 + """ + doc: """ + Fetch a single CLI session by its id + """ + ) { + oneGraph { __typename - createdAt - id - sessionId - ... on OneGraphNetlifyCliSessionLogEvent { - id - message - sessionId + netlifyCliSession(id: $sessionId) { + appId createdAt - } - ... on OneGraphNetlifyCliSessionTestEvent { id - createdAt - payload - sessionId + cliHeartbeatIntervalMs + events(first: $first) { + __typename + createdAt + id + sessionId + ... on OneGraphNetlifyCliSessionLogEvent { + id + message + sessionId + createdAt + } + ... on OneGraphNetlifyCliSessionTestEvent { + id + createdAt + payload + sessionId + } + } + lastEventAt + metadata + name + netlifyUserId + status + graphQLSchema { + createdAt + id + externalGraphQLSchemas { + nodes { + endpoint + id + service + serviceInfo { + friendlyServiceName + graphQLField + } + } + } + } } } - lastEventAt - metadata - name - netlifyUserId - } - } -}`, + }`, operationName: "CLISessionQuery", variables: variables, options: options, @@ -441,26 +465,31 @@ export const executeAckCLISessionEventMutation = (variables, options) => { export const fetchAppSchemaQuery = (variables, options) => { return fetchNetlifyGraph({ - query: `query AppSchemaQuery($appId: String!) { - oneGraph { - app(id: $appId) { - graphQLSchema { - appId - createdAt - id - services { - friendlyServiceName - logoUrl - graphQLField - slug - supportsCustomRedirectUri - supportsCustomServiceAuth - supportsOauthLogin + query: `query AppSchemaQuery($appId: String!) @netlify(id: """12b5bdea-9bab-4124-a731-5e697b155011""", doc: """Fetch the schema metadata for a site (enabled services, id, etc.)""") { + oneGraph { + app(id: $appId) { + graphQLSchema { + ...OneGraphGraphQLSchema + } } - updatedAt } } + + +fragment OneGraphGraphQLSchema on OneGraphGraphQLSchema @netlify(id: """0000bdea-9bab-4124-a731-5e697b150000""", doc: """Metadata for a GraphQL schema (enabled services, id, etc.)""") { + appId + createdAt + id + services { + friendlyServiceName + logoUrl + graphQLField + slug + supportsCustomRedirectUri + supportsCustomServiceAuth + supportsOauthLogin } + updatedAt }`, operationName: "AppSchemaQuery", variables: variables, diff --git a/src/graphql.test.ts b/src/graphql.test.ts index 3d02812..de7b6e3 100644 --- a/src/graphql.test.ts +++ b/src/graphql.test.ts @@ -1,5 +1,6 @@ import { writeFileSync, readFileSync } from "fs"; import { buildASTSchema, Kind, OperationDefinitionNode, parse } from "graphql"; +import * as GraphQL from "graphql"; import { normalizeOperationsDoc, typeScriptSignatureForOperationVariables, @@ -38,6 +39,7 @@ const test = () => { ) || []; const variableSignature = typeScriptSignatureForOperationVariables( + GraphQL, operationVariableNames, schema, operation @@ -45,7 +47,7 @@ const test = () => { console.log(`variableSignature:\n${variableSignature}`); - const result = normalizeOperationsDoc(sourceGraphQLFile); + const result = normalizeOperationsDoc(GraphQL, sourceGraphQLFile); if (!result) { throw new Error("result is undefined"); diff --git a/src/graphqlHelpers.ts b/src/graphqlHelpers.ts index 9c193a5..83719b7 100644 --- a/src/graphqlHelpers.ts +++ b/src/graphqlHelpers.ts @@ -1,42 +1,23 @@ -import { +import * as GraphQLPackage from "graphql"; +import type { + GraphQLSchema, + FragmentDefinitionNode, + OperationDefinitionNode, + VariableDefinitionNode, + GraphQLType, ArgumentNode, ASTVisitFn, DocumentNode, FragmentSpreadNode, - getNamedType, GraphQLInputField, GraphQLInterfaceType, GraphQLList, GraphQLObjectType, GraphQLScalarType, GraphQLUnionType, - isEnumType, - isInputObjectType, - isInterfaceType, - isListType, - isNonNullType, - isNullableType, - isObjectType, - isScalarType, - isWrappingType, - Kind, ObjectFieldNode, - parse, - parseType, - print, SelectionNode, SelectionSetNode, - typeFromAST, - TypeInfo, - visit, - visitWithTypeInfo, -} from "graphql"; -import { - GraphQLSchema, - FragmentDefinitionNode, - OperationDefinitionNode, - VariableDefinitionNode, - GraphQLType, } from "graphql"; import { Maybe } from "graphql/jsutils/Maybe"; @@ -103,9 +84,12 @@ const scalarMap: Record = { }; export function gatherAllReferencedTypes( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, query: OperationDefinitionNode ): Array { + const { getNamedType, TypeInfo, visit, visitWithTypeInfo } = GraphQL; + const types = new Set([]); const typeInfo = new TypeInfo(schema); visit( @@ -125,7 +109,11 @@ export function gatherAllReferencedTypes( return result; } -function unwrapOutputType(outputType: GraphQLType): GraphQLType { +function unwrapOutputType( + GraphQL: typeof GraphQLPackage, + outputType: GraphQLType +): GraphQLType { + const { isWrappingType } = GraphQL; let unwrappedType = outputType; while (isWrappingType(unwrappedType)) { unwrappedType = unwrappedType.ofType; @@ -134,8 +122,11 @@ function unwrapOutputType(outputType: GraphQLType): GraphQLType { } export function gatherVariableDefinitions( + GraphQL: typeof GraphQLPackage, definition: OperationDefinitionNode ): Array<[string, string]> { + const { print } = GraphQL; + const extract = (varDef: VariableDefinitionNode): [string, string] => [ varDef.variable.name.value, print(varDef.type), @@ -147,9 +138,20 @@ export function gatherVariableDefinitions( } export function typeScriptForGraphQLType( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, gqlType: GraphQLType ): string { + const { + getNamedType, + isEnumType, + isInputObjectType, + isListType, + isNonNullType, + isObjectType, + isWrappingType, + } = GraphQL; + let scalarMap = { String: "string", ID: "string", @@ -162,12 +164,12 @@ export function typeScriptForGraphQLType( }; if (isListType(gqlType)) { - let subType = typeScriptForGraphQLType(schema, gqlType.ofType); + let subType = typeScriptForGraphQLType(GraphQL, schema, gqlType.ofType); return `Array<${subType}>`; } else if (isObjectType(gqlType) || isInputObjectType(gqlType)) { let fields = Object.values(gqlType.getFields()).map((field) => { let nullable = !isNonNullType(field.type); - let type = typeScriptForGraphQLType(schema, field.type); + let type = typeScriptForGraphQLType(GraphQL, schema, field.type); const description = !!field.description ? `/** * ${field.description} @@ -184,7 +186,7 @@ export function typeScriptForGraphQLType( return "Record /* typeScriptForGraphQLType */"; } } else if (isWrappingType(gqlType)) { - return typeScriptForGraphQLType(schema, gqlType.ofType); + return typeScriptForGraphQLType(GraphQL, schema, gqlType.ofType); } else if (isEnumType(gqlType)) { let values = gqlType.getValues(); @@ -200,6 +202,7 @@ export function typeScriptForGraphQLType( } export const guessVariableDescriptions = ( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, operationDefinition: OperationDefinitionNode, variableNames: string[] @@ -210,6 +213,15 @@ export const guessVariableDescriptions = ( descriptions?: Set; } > => { + const { + getNamedType, + isInputObjectType, + Kind, + TypeInfo, + visit, + visitWithTypeInfo, + } = GraphQL; + const variableRecords: Record< string, { usageCount: number; descriptions?: Set } @@ -297,10 +309,13 @@ export const guessVariableDescriptions = ( }; export function typeScriptSignatureForOperationVariables( + GraphQL: typeof GraphQLPackage, variableNames: Array, schema: GraphQLSchema, operationDefinition: OperationDefinitionNode ) { + const { print, parseType, typeFromAST, isNonNullType } = GraphQL; + const helper: ( variableDefinition: VariableDefinitionNode ) => [string, VariableDefinitionNode] = ( @@ -324,6 +339,7 @@ export function typeScriptSignatureForOperationVariables( }); const variableUsageInfo = guessVariableDescriptions( + GraphQL, schema, operationDefinition, variableNames @@ -341,7 +357,7 @@ export function typeScriptSignatureForOperationVariables( let isRequired = isNonNullType(gqlType); - let tsType = typeScriptForGraphQLType(schema, gqlType); + let tsType = typeScriptForGraphQLType(GraphQL, schema, gqlType); return [varName, tsType, isRequired]; }) @@ -377,7 +393,9 @@ export function typeScriptSignatureForOperationVariables( return types === "" ? "null" : types; } -export function listCount(gqlType) { +export function listCount(GraphQL: typeof GraphQLPackage, gqlType) { + const { isListType, isWrappingType } = GraphQL; + let inspectedType = gqlType; let listCount = 0; @@ -437,10 +455,25 @@ const dummyOut: OutObject = { }; export function typeScriptDefinitionObjectForOperation( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, operationDefinition: OperationDefinitionNode | FragmentDefinitionNode, fragmentDefinitions: Record ): OutObject { + const { + getNamedType, + isEnumType, + isInterfaceType, + isListType, + isNonNullType, + isNullableType, + isObjectType, + isScalarType, + isWrappingType, + Kind, + typeFromAST, + } = GraphQL; + const objectHelper = ( type: GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType, selectionSet: SelectionSetNode @@ -742,7 +775,9 @@ const printObject = (obj: OutObject): string => { ` : ""; - return `${description}${fieldSelection.name}${fieldSelection.isNullable ? "?" : ""}: ${value};`; + return `${description}${fieldSelection.name}${ + fieldSelection.isNullable ? "?" : "" + }: ${value};`; }) .join("\n "); @@ -797,11 +832,13 @@ const printOut = (out: OutType): string => { }; export function typeScriptSignatureForOperation( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, operationDefinition: OperationDefinitionNode, fragmentDefinitions: Record ) { let typeMap = typeScriptDefinitionObjectForOperation( + GraphQL, schema, operationDefinition, fragmentDefinitions @@ -813,11 +850,25 @@ export function typeScriptSignatureForOperation( } export function typeScriptDefinitionObjectForFragment( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, fragmentDefinition: FragmentDefinitionNode, - fragmentDefinitions: Record, - shouldLog = true + fragmentDefinitions: Record ) { + const { + getNamedType, + isEnumType, + isInterfaceType, + isListType, + isNonNullType, + isNullableType, + isObjectType, + isScalarType, + isWrappingType, + Kind, + typeFromAST, + } = GraphQL; + const dummyOut: OutScalar = { kind: "scalar", type: "Record", @@ -940,7 +991,7 @@ export function typeScriptDefinitionObjectForFragment( kind: "selection_field", name: displayedName, type: scalar, - isNullable, + isNullable, description: field.description, }; } else if (isEnumType(namedType)) { @@ -1074,11 +1125,13 @@ export function typeScriptDefinitionObjectForFragment( } export function typeScriptSignatureForFragment( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, fragmentDefinition: FragmentDefinitionNode, fragmentDefinitions: Record ) { let typeMap = typeScriptDefinitionObjectForFragment( + GraphQL, schema, fragmentDefinition, fragmentDefinitions @@ -1097,12 +1150,16 @@ export function typeScriptTypeNameForOperation(name: string) { * Doesn't patch e.g. fragments */ export function patchSubscriptionWebhookField({ + GraphQL, schema, definition, }: { + GraphQL: typeof GraphQLPackage; schema: GraphQLSchema; definition: OperationDefinitionNode; }): OperationDefinitionNode { + const { Kind } = GraphQL; + if (definition.operation !== "subscription") { return definition; } @@ -1195,12 +1252,16 @@ export function patchSubscriptionWebhookField({ } export function patchSubscriptionWebhookSecretField({ + GraphQL, schema, definition, }: { + GraphQL: typeof GraphQLPackage; schema: GraphQLSchema; definition: OperationDefinitionNode; }): OperationDefinitionNode { + const { Kind } = GraphQL; + if (definition.operation !== "subscription") { return definition; } @@ -1302,7 +1363,21 @@ const addLeftWhitespace = (string, padding) => { .join("\n"); }; -export const formInput = (schema, def, path = []) => { +export const formInput = ( + GraphQL: typeof GraphQLPackage, + schema, + def, + path = [] +) => { + const { + getNamedType, + isEnumType, + isInputObjectType, + isListType, + isScalarType, + typeFromAST, + } = GraphQL; + const name = def.variable.name.value; function helper(path, type, subfield) { @@ -1427,7 +1502,21 @@ export const formInput = (schema, def, path = []) => { return `${formEl}`; }; -export const remixFormInput = (schema, def, path = []) => { +export const remixFormInput = ( + GraphQL: typeof GraphQLPackage, + schema, + def, + path = [] +) => { + const { + getNamedType, + isEnumType, + isInputObjectType, + isListType, + isScalarType, + typeFromAST, + } = GraphQL; + const name = def.variable.name.value; function helper(path, type, subfield) { @@ -1545,10 +1634,12 @@ export const remixFormInput = (schema, def, path = []) => { }; export const formElComponent = ({ + GraphQL, operationData, schema, callFn, }: { + GraphQL: typeof GraphQLPackage; operationData: OperationData; schema: GraphQLSchema; callFn: string; @@ -1567,7 +1658,7 @@ export const formElComponent = ({ const els = (operationData.operationDefinition.variableDefinitions || []).map( (def) => { - const genInput = formInput(schema, def, []); + const genInput = formInput(GraphQL, schema, def, []); const input = genInput || `UNABLE_TO_GENERATE_FORM_INPUT_FOR_GRAPHQL_TYPE(${def})`; @@ -1584,7 +1675,12 @@ export const formElComponent = ({ }; }; -export const normalizeOperationsDoc = (operationsDoc: string) => { +export const normalizeOperationsDoc = ( + GraphQL: typeof GraphQLPackage, + operationsDoc: string +) => { + const { Kind, parse, print, visit } = GraphQL; + const parsedOperations = parse(operationsDoc); const fragments: FragmentDefinitionNode[] = []; @@ -1642,7 +1738,12 @@ export const normalizeOperationsDoc = (operationsDoc: string) => { return fullDoc; }; -export const gatherHardcodedValues = (query: string) => { +export const gatherHardcodedValues = ( + GraphQL: typeof GraphQLPackage, + query: string +) => { + const { Kind, parse, visit } = GraphQL; + let parsedQuery; try { parsedQuery = parse(query); @@ -1695,11 +1796,18 @@ export const gatherHardcodedValues = (query: string) => { }; export const extractPersistableOperation = ( + GraphQL: typeof GraphQLPackage, doc: DocumentNode, operationDefinition: OperationDefinitionNode -): string | null => { +): { + fragmentDependencies: FragmentDefinitionNode[]; + persistableOperationString: string; +} | null => { + const { Kind, print, visit } = GraphQL; + // Visit the operationDefinition and find all fragments referenced, and include them all in a single printed document const fragments = new Set(); + const visitedFragmentNames = new Set(); const fragmentExtractor: ASTVisitFn = (node) => { const fragmentName = node.name.value; @@ -1711,6 +1819,10 @@ export const extractPersistableOperation = ( if (fragmentDefinition) { fragments.add(fragmentDefinition); + + visit(fragmentDefinition, { + FragmentSpread: { enter: fragmentExtractor }, + }); } else { console.warn( "Could not find fragment definition for referenced fragment: ", @@ -1743,5 +1855,8 @@ export const extractPersistableOperation = ( // Put the operation in the top to help a human looking at the doc identify the purpose quickly const fullDoc = [print(newOperation), ...fragmentStrings].join("\n\n"); - return fullDoc; + return { + fragmentDependencies: Array.from(fragments), + persistableOperationString: fullDoc, + }; }; diff --git a/src/legacy_to_multifile_migration.test.ts b/src/legacy_to_multifile_migration.test.ts index 7d21226..c9f00b6 100644 --- a/src/legacy_to_multifile_migration.test.ts +++ b/src/legacy_to_multifile_migration.test.ts @@ -1,6 +1,7 @@ import * as fs from "fs"; import { buildASTSchema, DocumentNode, Kind, parse } from "graphql"; import path = require("path/posix"); +import * as GraphQL from "graphql"; import { NetlifyGraph } from "./index"; @@ -77,7 +78,7 @@ const test = async () => { const legacyParsedDoc: DocumentNode = parse(legacySourceGraphQLFile); const { functions: legacyFunctions, fragments: legacyFragments } = - NetlifyGraph.extractFunctionsFromOperationDoc(legacyParsedDoc); + NetlifyGraph.extractFunctionsFromOperationDoc(GraphQL, legacyParsedDoc); console.log("Legacy operations file found, migrating..."); @@ -145,7 +146,7 @@ const test = async () => { }, emptyDocDefinitionNode); const { functions, fragments } = - NetlifyGraph.extractFunctionsFromOperationDoc(parsedDoc); + NetlifyGraph.extractFunctionsFromOperationDoc(GraphQL, parsedDoc); cleanDirectory(sourceGraphQLDirectory); diff --git a/src/netlifyGraph.ts b/src/netlifyGraph.ts index 2a76b29..d58dec2 100644 --- a/src/netlifyGraph.ts +++ b/src/netlifyGraph.ts @@ -12,6 +12,8 @@ import { print, } from "graphql"; +import * as GraphQLPackage from "graphql"; + import { internalConsole } from "./internalConsole"; import { extractPersistableOperation as extractPersistableOperationString, @@ -29,8 +31,13 @@ import { import { nextjsFunctionSnippet } from "./codegen/nextjsExporter"; import { remixFunctionSnippet } from "./codegen/remixExporter"; -import { ExportedFile, FrameworkGenerator } from "./codegen/codegenHelpers"; +import { + CodeGenerator, + ExportedFile, + GenerateHandlerFunction, +} from "./codegen/codegenHelpers"; import { executeCreatePersistedQueryMutation } from "./oneGraphClient"; +import { CodegenHelpers } from "."; export type State = { set: (key: string, value?: any) => any; @@ -698,7 +705,7 @@ const out = ( return value; }; -const exp = ( +const export_ = ( netlifyGraphConfig: NetlifyGraphConfig, envs: ("browser" | "node")[], name: string, @@ -715,7 +722,7 @@ const exp = ( return `export const ${name} = ${value}`; }; -const imp = ( +const import_ = ( netlifyGraphConfig: NetlifyGraphConfig, envs: ("browser" | "node")[], name: string, @@ -733,6 +740,7 @@ const imp = ( }; export const generateSubscriptionFunctionTypeDefinition = ( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, fn: ParsedFunction, fragments: Record @@ -743,6 +751,7 @@ export const generateSubscriptionFunctionTypeDefinition = ( }, {}); const parsingFunctionReturnSignature = typeScriptSignatureForOperation( + GraphQL, schema, fn.parsedOperation, fragmentDefinitions @@ -753,6 +762,7 @@ export const generateSubscriptionFunctionTypeDefinition = ( ); const variableSignature = typeScriptSignatureForOperationVariables( + GraphQL, variableNames, schema, fn.parsedOperation @@ -816,17 +826,20 @@ export function ${subscriptionParserName( // TODO: Handle fragments export const generateSubscriptionFunction = ( + GraphQL: typeof GraphQLPackage, schema: GraphQLSchema, fn: ParsedFunction, fragments: never[], netlifyGraphConfig: NetlifyGraphConfig ) => { const patchedWithWebhookUrl = patchSubscriptionWebhookField({ + GraphQL, schema, definition: fn.parsedOperation, }); const patched = patchSubscriptionWebhookSecretField({ + GraphQL, schema, definition: patchedWithWebhookUrl, }); @@ -887,6 +900,7 @@ const makeFunctionName = (kind: string, operationName: string) => { }; export const queryToFunctionDefinition = ( + GraphQL: typeof GraphQLPackage, fullSchema: GraphQLSchema, parsedDoc: DocumentNode, persistedQuery: ExtractedFunction, @@ -909,7 +923,9 @@ export const queryToFunctionDefinition = ( (def) => def.kind === Kind.FRAGMENT_DEFINITION ) as FragmentDefinitionNode[]; - const fragments = Object.values(enabledFragments).reduce( + const fragments: Record = Object.values( + enabledFragments + ).reduce( (acc, def) => ({ ...acc, [def.fragmentName]: def.parsedOperation }), {} ); @@ -927,6 +943,7 @@ export const queryToFunctionDefinition = ( } const returnSignature = typeScriptSignatureForOperation( + GraphQL, fullSchema, operation, fragments @@ -937,6 +954,7 @@ export const queryToFunctionDefinition = ( ); const variableSignature = typeScriptSignatureForOperationVariables( + GraphQL, variableNames, fullSchema, operation @@ -960,8 +978,14 @@ export const queryToFunctionDefinition = ( ), }; + const persistableOperationFacts = extractPersistableOperationString( + GraphQL, + parsedDoc, + operation + ) || { persistableOperationString: print(operation) }; + const persistableOperationString = - extractPersistableOperationString(parsedDoc, operation) || print(operation); + persistableOperationFacts.persistableOperationString; const cacheControl = pluckNetlifyCacheControlDirective(operation); const netlifyDirective = pluckNetlifyDirective(operation); @@ -989,6 +1013,7 @@ export const queryToFunctionDefinition = ( }; export const fragmentToParsedFragmentDefinition = ( + GraphQL: typeof GraphQLPackage, currentFragments: {}, fullSchema: GraphQLSchema, persistedQuery: ExtractedFragment @@ -1031,6 +1056,7 @@ export const fragmentToParsedFragmentDefinition = ( } const returnSignature = typeScriptSignatureForFragment( + GraphQL, fullSchema, operation, { ...currentFragments, ...fragments } @@ -1041,6 +1067,7 @@ export const fragmentToParsedFragmentDefinition = ( ); const variableSignature = typeScriptSignatureForOperationVariables( + GraphQL, variableNames, fullSchema, // @ts-ignore TODO: FIX THIS! @@ -1084,6 +1111,7 @@ export const fragmentToParsedFragmentDefinition = ( }; export const generateJavaScriptClient = ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, schema: GraphQLSchema, operationsDoc: string, @@ -1107,6 +1135,7 @@ export const generateJavaScriptClient = ( if (fn.kind === "subscription") { const fragments = []; return generateSubscriptionFunction( + GraphQL, schema, fn, fragments, @@ -1114,7 +1143,7 @@ export const generateJavaScriptClient = ( ); } - const dynamicFunction = `${exp( + const dynamicFunction = `${export_( netlifyGraphConfig, ["browser", "node"], fn.fnName, @@ -1138,7 +1167,7 @@ export const generateJavaScriptClient = ( )} `; - const staticFunction = `${exp( + const staticFunction = `${export_( netlifyGraphConfig, ["browser", "node"], fn.fnName, @@ -1207,7 +1236,7 @@ export const generateJavaScriptClient = ( .filter(Boolean) .join(",\n "); - const dummyHandler = exp( + const dummyHandler = export_( netlifyGraphConfig, ["node"], "handler", @@ -1225,12 +1254,12 @@ export const generateJavaScriptClient = ( const source = `/* eslint-disable */ // @ts-nocheck // GENERATED VIA NETLIFY AUTOMATED DEV TOOLS, EDIT WITH CAUTION! -${imp(netlifyGraphConfig, ["node"], "buffer", "buffer")} -${imp(netlifyGraphConfig, ["node"], "crypto", "crypto")} -${imp(netlifyGraphConfig, ["node"], "https", "https")} -${imp(netlifyGraphConfig, ["node"], "process", "process")} +${import_(netlifyGraphConfig, ["node"], "buffer", "buffer")} +${import_(netlifyGraphConfig, ["node"], "crypto", "crypto")} +${import_(netlifyGraphConfig, ["node"], "https", "https")} +${import_(netlifyGraphConfig, ["node"], "process", "process")} -${exp( +${export_( netlifyGraphConfig, ["node"], "verifySignature", @@ -1283,7 +1312,7 @@ ${exp( ${generatedNetlifyGraphDynamicClient(netlifyGraphConfig)} -${exp( +${export_( netlifyGraphConfig, ["node"], "verifyRequestSignature", @@ -1325,6 +1354,7 @@ ${dummyHandler}`; }; export const generateProductionJavaScriptClient = ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, schema: GraphQLSchema, operationsDoc: string, @@ -1339,6 +1369,7 @@ export const generateProductionJavaScriptClient = ( if (fn.kind === "subscription") { const fragments = []; return generateSubscriptionFunction( + GraphQL, schema, fn, fragments, @@ -1346,7 +1377,7 @@ export const generateProductionJavaScriptClient = ( ); } - const dynamicFunction = `${exp( + const dynamicFunction = `${export_( netlifyGraphConfig, ["browser", "node"], fn.fnName, @@ -1370,7 +1401,7 @@ export const generateProductionJavaScriptClient = ( )} `; - const staticFunction = `${exp( + const staticFunction = `${export_( netlifyGraphConfig, ["browser", "node"], fn.fnName, @@ -1441,7 +1472,7 @@ export const generateProductionJavaScriptClient = ( .filter(Boolean) .join(",\n "); - const dummyHandler = exp( + const dummyHandler = export_( netlifyGraphConfig, ["node"], "handler", @@ -1459,12 +1490,12 @@ export const generateProductionJavaScriptClient = ( const source = `/* eslint-disable */ // @ts-nocheck // GENERATED VIA NETLIFY AUTOMATED DEV TOOLS, EDIT WITH CAUTION! - ${imp(netlifyGraphConfig, ["node"], "buffer", "buffer")} - ${imp(netlifyGraphConfig, ["node"], "crypto", "crypto")} - ${imp(netlifyGraphConfig, ["node"], "https", "https")} - ${imp(netlifyGraphConfig, ["node"], "process", "process")} + ${import_(netlifyGraphConfig, ["node"], "buffer", "buffer")} + ${import_(netlifyGraphConfig, ["node"], "crypto", "crypto")} + ${import_(netlifyGraphConfig, ["node"], "https", "https")} + ${import_(netlifyGraphConfig, ["node"], "process", "process")} -${exp( +${export_( netlifyGraphConfig, ["node"], "verifySignature", @@ -1517,7 +1548,7 @@ ${exp( ${generatedNetlifyGraphPersistedClient(netlifyGraphConfig, schemaId)} -${exp( +${export_( netlifyGraphConfig, ["node"], "verifyRequestSignature", @@ -1580,6 +1611,7 @@ export type ${returnSignatureName} = ${fragment.returnSignature}; }; export const generateTypeScriptDefinitions = ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, schema: GraphQLSchema, enabledFunctions: ParsedFunction[], @@ -1606,6 +1638,7 @@ export const generateTypeScriptDefinitions = ( if (isSubscription) { return generateSubscriptionFunctionTypeDefinition( + GraphQL, schema, fn, enabledFragments @@ -1738,6 +1771,7 @@ export default functions; }; export const generateFunctionsSource = async ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, schema: GraphQLSchema, operationsDoc: string, @@ -1751,6 +1785,7 @@ export const generateFunctionsSource = async ( ).reduce( ({ fragmentDefinitions, fragmentNodes }, [fragmentName, fragment]) => { const parsed = fragmentToParsedFragmentDefinition( + GraphQL, fragmentNodes, schema, fragment @@ -1770,21 +1805,29 @@ export const generateFunctionsSource = async ( const functionDefinitions: ParsedFunction[] = Object.values(queries) .map((query) => - queryToFunctionDefinition(schema, parsedDoc, query, fragmentDefinitions) + queryToFunctionDefinition( + GraphQL, + schema, + parsedDoc, + query, + fragmentDefinitions + ) ) .filter(Boolean) .sort((a: ParsedFunction, b: ParsedFunction) => { return a.id.localeCompare(b.id); }) as ParsedFunction[]; - const clientSource = generateJavaScriptClient( + // @ts-expect-error + const clientSource = generateJavaScriptClient({ + GraphQL, netlifyGraphConfig, schema, - operationsDoc, - functionDefinitions - ); + functionDefinitions, + }); const typeDefinitionsSource = generateTypeScriptDefinitions( + GraphQL, netlifyGraphConfig, schema, functionDefinitions, @@ -1798,7 +1841,86 @@ export const generateFunctionsSource = async ( }; }; +export const generateRuntime = async ({ + GraphQL, + fragments, + generate, + netlifyGraphConfig, + operationsDoc, + operations, + schema, + schemaId, +}: { + GraphQL: typeof GraphQLPackage; + netlifyGraphConfig: NetlifyGraphConfig; + schema: GraphQLSchema; + operationsDoc: string; + operations: Record; + fragments: Record; + generate: CodegenHelpers.GenerateRuntimeFunction; + schemaId: string; +}) => { + const { + fragmentDefinitions, + }: { fragmentDefinitions: Record } = Object.entries( + fragments + ).reduce( + ({ fragmentDefinitions, fragmentNodes }, [fragmentName, fragment]) => { + const parsed = fragmentToParsedFragmentDefinition( + GraphQL, + fragmentNodes, + schema, + fragment + ); + return { + fragmentDefinitions: { + ...fragmentDefinitions, + [fragmentName]: parsed.fragment, + }, + fragmentNodes: { ...fragmentNodes, ...parsed.fragmentDefinitions }, + }; + }, + { fragmentNodes: {}, fragmentDefinitions: {} } + ); + + const parsedDoc = parse(operationsDoc, { noLocation: true }); + + const odl = computeOperationDataList({ + query: operationsDoc, + variables: [], + }); + + const functionDefinitions: ParsedFunction[] = Object.values(operations) + .map((query) => + queryToFunctionDefinition( + GraphQL, + schema, + parsedDoc, + query, + fragmentDefinitions + ) + ) + .filter(Boolean) + .sort((a: ParsedFunction, b: ParsedFunction) => { + return a.id.localeCompare(b.id); + }) as ParsedFunction[]; + + const runtime = generate({ + GraphQL, + netlifyGraphConfig, + schema, + functionDefinitions, + fragments: Object.values(fragmentDefinitions), + operationDataList: odl.operationDataList, + schemaId: schemaId, + options: {}, + }); + + return runtime; +}; + export const generatePersistedFunctionsSource = async ( + GraphQL: typeof GraphQLPackage, netlifyGraphConfig: NetlifyGraphConfig, netlifyJwt: string, siteId: string, @@ -1815,6 +1937,7 @@ export const generatePersistedFunctionsSource = async ( ).reduce( ({ fragmentDefinitions, fragmentNodes }, [fragmentName, fragment]) => { const parsed = fragmentToParsedFragmentDefinition( + GraphQL, fragmentNodes, schema, fragment @@ -1834,7 +1957,13 @@ export const generatePersistedFunctionsSource = async ( const functionDefinitions: ParsedFunction[] = Object.values(queries) .map((query) => - queryToFunctionDefinition(schema, parsedDoc, query, fragmentDefinitions) + queryToFunctionDefinition( + GraphQL, + schema, + parsedDoc, + query, + fragmentDefinitions + ) ) .filter(Boolean) as ParsedFunction[]; @@ -1896,6 +2025,7 @@ export const generatePersistedFunctionsSource = async ( } const clientSource = generateProductionJavaScriptClient( + GraphQL, netlifyGraphConfig, schema, operationsDoc, @@ -1904,6 +2034,7 @@ export const generatePersistedFunctionsSource = async ( ); const typeDefinitionsSource = generateTypeScriptDefinitions( + GraphQL, netlifyGraphConfig, schema, functionDefinitions, @@ -2097,6 +2228,7 @@ export const pluckNetlifyCacheControlDirective = ( * @returns {functions: Record, fragments: Record} */ export const extractFunctionsFromOperationDoc = ( + GraphQL: typeof GraphQLPackage, parsedDoc: DocumentNode ): { functions: Record; @@ -2156,10 +2288,11 @@ export const extractFunctionsFromOperationDoc = ( ? pluckNetlifyCacheControlDirective(next) : { cacheStrategy: undefined, fallbackOnError: false }; - const persistableOperationString = extractPersistableOperationString( + const { persistableOperationString } = extractPersistableOperationString( + GraphQL, parsedDoc, next - ); + ) || { persistableOperationString: null }; const operation: ExtractedFunction = { id: netlifyDirective.id, @@ -2184,24 +2317,26 @@ export const extractFunctionsFromOperationDoc = ( return { functions, fragments }; }; -const frameworkGeneratorMap: Record = { - "Next.js": nextjsFunctionSnippet.generate, - Remix: remixFunctionSnippet.generate, - default: genericNetlifyFunctionSnippet.generate, +const frameworkGeneratorMap: Record = { + "Next.js": nextjsFunctionSnippet.generateHandler, + Remix: remixFunctionSnippet.generateHandler, + default: genericNetlifyFunctionSnippet.generateHandler, }; -const defaultGenerator = genericNetlifyFunctionSnippet.generate; +const defaultGenerator = genericNetlifyFunctionSnippet.generateHandler; /** * Given a schema, GraphQL operations doc, a target operationId, and a Netlify Graph config, generates a set of handlers (and potentially components) for the correct framework. */ export const generateHandlerSource = ({ + GraphQL, handlerOptions, netlifyGraphConfig, operationId, operationsDoc, schema, }: { + GraphQL: typeof GraphQLPackage; handlerOptions: Record; netlifyGraphConfig: NetlifyGraphConfig; operationId: string; @@ -2214,7 +2349,7 @@ export const generateHandlerSource = ({ } | undefined => { const parsedDoc = parse(operationsDoc, { noLocation: true }); - const operations = extractFunctionsFromOperationDoc(parsedDoc); + const operations = extractFunctionsFromOperationDoc(GraphQL, parsedDoc); const functions = operations.functions; const fn = functions[operationId]; @@ -2236,6 +2371,7 @@ export const generateHandlerSource = ({ frameworkGeneratorMap[netlifyGraphConfig.framework] || defaultGenerator; const { exportedFiles } = generate({ + GraphQL, netlifyGraphConfig, operationDataList: odl.operationDataList, schema, @@ -2249,6 +2385,7 @@ export const generateHandlerSource = ({ * Given a schema, GraphQL operations doc, a target operationId, and a Netlify Graph config, generates a set of handlers (and potentially components) for the correct framework. */ export const generateCustomHandlerSource = ({ + GraphQL, handlerOptions, netlifyGraphConfig, operationId, @@ -2256,12 +2393,13 @@ export const generateCustomHandlerSource = ({ schema, generate, }: { + GraphQL: typeof GraphQLPackage; handlerOptions: Record; netlifyGraphConfig: NetlifyGraphConfig; operationId: string; operationsDoc: string; schema: GraphQLSchema; - generate: FrameworkGenerator; + generate: CodeGenerator["generateHandler"]; }): | { exportedFiles: ExportedFile[]; @@ -2269,7 +2407,7 @@ export const generateCustomHandlerSource = ({ } | undefined => { const parsedDoc = parse(operationsDoc, { noLocation: true }); - const operations = extractFunctionsFromOperationDoc(parsedDoc); + const operations = extractFunctionsFromOperationDoc(GraphQL, parsedDoc); const fn = operations.functions[operationId]; if (!fn) { @@ -2286,6 +2424,7 @@ export const generateCustomHandlerSource = ({ }); const { exportedFiles } = generate({ + GraphQL, netlifyGraphConfig, operationDataList: odl.operationDataList, schema, @@ -2294,3 +2433,64 @@ export const generateCustomHandlerSource = ({ return { exportedFiles, operation: fn.parsedOperation }; }; + +/** + * Given a schema, GraphQL operations doc, a target operationId, and a Netlify Graph config, generates a preview of the full handler's output + */ +export const generatePreview = ({ + GraphQL, + handlerOptions, + netlifyGraphConfig, + operationId, + operationsDoc, + schema, + generate, +}: { + GraphQL: typeof GraphQLPackage; + handlerOptions: Record; + netlifyGraphConfig: NetlifyGraphConfig; + operationId: string; + operationsDoc: string; + schema: GraphQLSchema; + generate: CodeGenerator["generatePreview"]; +}): + | { + exportedFile: ExportedFile; + operation: OperationDefinitionNode; + } + | undefined => { + if (!generate) { + return; + } + + const parsedDoc = parse(operationsDoc, { noLocation: true }); + const operations = extractFunctionsFromOperationDoc(GraphQL, parsedDoc); + const fn = operations.functions[operationId]; + + if (!fn) { + internalConsole.warn( + `Operation ${operationId} not found in graphql among: + [${Object.keys(operations).join(",\n ")}]` + ); + return; + } + + const odl = computeOperationDataList({ + query: fn.operationString, + variables: [], + }); + + const exportedFile = generate({ + GraphQL, + netlifyGraphConfig, + operationDataList: odl.operationDataList, + schema, + options: handlerOptions, + }); + + if (!exportedFile) { + return; + } + + return { exportedFile, operation: fn.parsedOperation }; +}; diff --git a/src/nextjs.test.ts b/src/nextjs.test.ts index a29d067..8eb7697 100644 --- a/src/nextjs.test.ts +++ b/src/nextjs.test.ts @@ -1,5 +1,6 @@ import { readFileSync } from "fs"; import { buildASTSchema, parse } from "graphql"; +import * as GraphQL from "graphql"; import { NetlifyGraph } from "./index"; @@ -36,9 +37,8 @@ const test = () => { runtimeTargetEnv: "node", }; - console.log("config: ", netlifyGraphConfig); - const result = NetlifyGraph.generateHandlerSource({ + GraphQL, handlerOptions: { postHttpMethod: true, useClientAuth: true, diff --git a/src/oneGraphClient.ts b/src/oneGraphClient.ts index 7b543f7..9ed0ac4 100644 --- a/src/oneGraphClient.ts +++ b/src/oneGraphClient.ts @@ -705,3 +705,9 @@ export const fetchListNetlifyEnabledServicesQuery: typeof GeneratedClient.fetchL */ export const executeCreateCLISessionEventMutation: typeof GeneratedClient.executeCreateCLISessionEventMutation = GeneratedClient.executeCreateCLISessionEventMutation; + +/** + * Fetch schema metadata for cli session + */ +export const fetchNetlifySessionSchemaQuery: typeof GeneratedClient.fetchFetchNetlifySessionSchemaQuery = + GeneratedClient.fetchFetchNetlifySessionSchemaQuery; diff --git a/src/remix.test.ts b/src/remix.test.ts index 8442ba4..5a1f785 100644 --- a/src/remix.test.ts +++ b/src/remix.test.ts @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync } from "fs"; import { buildASTSchema, parse } from "graphql"; +import * as GraphQL from "graphql"; import { NetlifyGraph } from "./index"; @@ -85,6 +86,7 @@ const test = () => { console.log("config: ", netlifyGraphConfig); const result = NetlifyGraph.generateHandlerSource({ + GraphQL, handlerOptions: { postHttpMethod: true, useClientAuth: true, diff --git a/src/runtime.test.ts b/src/runtime.test.ts index c0bf010..4d23888 100644 --- a/src/runtime.test.ts +++ b/src/runtime.test.ts @@ -1,5 +1,7 @@ import { writeFileSync, readFileSync } from "fs"; import { buildASTSchema, parse } from "graphql"; +import * as GraphQL from "graphql"; + import path = require("path/posix"); import { NetlifyGraph } from "./index"; @@ -16,7 +18,7 @@ const test = async () => { const parsedDoc = parse(sourceGraphQLFile); const { functions, fragments } = - NetlifyGraph.extractFunctionsFromOperationDoc(parsedDoc); + NetlifyGraph.extractFunctionsFromOperationDoc(GraphQL, parsedDoc); const netlifyGraphConfig: NetlifyGraph.NetlifyGraphConfig = { netlifyGraphPath: ["functions", "netlifyGraph"], @@ -48,6 +50,7 @@ const test = async () => { }; const result = NetlifyGraph.generateFunctionsSource( + GraphQL, netlifyGraphConfig, schema, sourceGraphQLFile, @@ -65,7 +68,7 @@ const test = async () => { const sourcePath = `/Users/s/code/gravity/gravity/netlify/functions/netlifyGraph/index.js`; - writeFileSync(sourcePath, clientSource); + writeFileSync(sourcePath, clientSource[0]); const typeDefinitionsSourcePath = `/Users/s/code/gravity/gravity/netlify/functions/netlifyGraph/index.d.ts`; writeFileSync(typeDefinitionsSourcePath, typeDefinitionsSource); diff --git a/src/sanity.test.ts b/src/sanity.test.ts index 960fd35..50475ff 100644 --- a/src/sanity.test.ts +++ b/src/sanity.test.ts @@ -1,5 +1,6 @@ import { writeFileSync, readFileSync } from "fs"; import { buildASTSchema, parse } from "graphql"; +import * as GraphQL from "graphql"; import path = require("path/posix"); import { NetlifyGraph } from "./index"; @@ -15,7 +16,10 @@ const test = () => { const schema = buildASTSchema(parse(schemaGraphQLFile)); const parsedDoc = parse(sourceGraphQLFile); - const functions = NetlifyGraph.extractFunctionsFromOperationDoc(parsedDoc); + const functions = NetlifyGraph.extractFunctionsFromOperationDoc( + GraphQL, + parsedDoc + ); const netlifyGraphConfig: NetlifyGraph.NetlifyGraphConfig = { netlifyGraphPath: ["..", "..", "lib", "netlifyGraph"], @@ -41,6 +45,7 @@ const test = () => { }; const result = NetlifyGraph.generateHandlerSource({ + GraphQL, handlerOptions: { postHttpMethod: true, useClientAuth: true,