From be3fe9950c872d9728da7dc4d1fc8c12ff350400 Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Sun, 31 Jul 2022 17:27:41 -0700 Subject: [PATCH 1/6] 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 962dbdb..52e5e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "netlify-onegraph-internal", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.3.1", + "version": "0.4.0", "license": "MIT", "dependencies": { "graphql": "16.0.0", diff --git a/package.json b/package.json index 4e9d1b2..aec2570 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netlify-onegraph-internal", - "version": "0.3.10", + "version": "0.4.0", "description": "Internal tools for use by Netlify", "main": "dist/index.js", "types": "dist/index.d.ts", From fdd0c82e2eea533b06de3338b4047e489c705d57 Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Tue, 2 Aug 2022 22:48:13 -0700 Subject: [PATCH 2/6] Interim commit for open-codegen --- src/codegen/codegenHelpers.ts | 67 +++++- src/codegen/genericExporter.ts | 53 +++-- src/codegen/nextjsExporter.ts | 90 ++++--- src/codegen/remixExporter.ts | 90 ++++--- src/generatedOneGraphClient.js | 127 ++++++---- src/graphql.test.ts | 4 +- src/graphqlHelpers.ts | 201 ++++++++++++---- src/legacy_to_multifile_migration.test.ts | 5 +- src/netlifyGraph.ts | 278 +++++++++++++++++++--- src/nextjs.test.ts | 4 +- src/oneGraphClient.ts | 6 + src/remix.test.ts | 2 + src/runtime.test.ts | 7 +- src/sanity.test.ts | 7 +- 14 files changed, 712 insertions(+), 229 deletions(-) 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, From 214efbfec1d54aff29fcf73674c10e904a31685b Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Thu, 4 Aug 2022 13:49:58 -0700 Subject: [PATCH 3/6] Add cli events and thread GraphQL --- package-lock.json | 17 +++--- package.json | 2 +- src/cliEventHelpers.ts | 100 +++++++++++++++++++++++++++++++++ src/codegen/codegenHelpers.ts | 27 ++++++++- src/generatedOneGraphClient.js | 2 + src/graphqlHelpers.ts | 52 +++++++++-------- src/netlifyGraph.ts | 2 +- 7 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 src/cliEventHelpers.ts diff --git a/package-lock.json b/package-lock.json index 52e5e8e..c5d5033 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,11 @@ "requires": true, "packages": { "": { + "name": "netlify-onegraph-internal", "version": "0.4.0", "license": "MIT", "dependencies": { - "graphql": "16.0.0", + "graphql": "16.5.0", "node-fetch": "^2.6.0", "uuid": "^8.3.2" }, @@ -25,11 +26,11 @@ "dev": true }, "node_modules/graphql": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.0.0.tgz", - "integrity": "sha512-n9NxoRfwnpYBZB/WJ7L166gyrShuZ8qYgVaX8oxVyELcJfAwkvwPt6WlYIl90WRlzqDjaNWvLmNOSnKs5llZWQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", + "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==", "engines": { - "node": "^12.22.0 || ^14.16.0 || >=16.0.0" + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/node-fetch": { @@ -112,9 +113,9 @@ "dev": true }, "graphql": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.0.0.tgz", - "integrity": "sha512-n9NxoRfwnpYBZB/WJ7L166gyrShuZ8qYgVaX8oxVyELcJfAwkvwPt6WlYIl90WRlzqDjaNWvLmNOSnKs5llZWQ==" + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.5.0.tgz", + "integrity": "sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==" }, "node-fetch": { "version": "2.6.7", diff --git a/package.json b/package.json index aec2570..4feffe9 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "author": "sgrove", "license": "MIT", "dependencies": { - "graphql": "16.0.0", + "graphql": "16.5.0", "node-fetch": "^2.6.0", "uuid": "^8.3.2" }, diff --git a/src/cliEventHelpers.ts b/src/cliEventHelpers.ts new file mode 100644 index 0000000..59adf7b --- /dev/null +++ b/src/cliEventHelpers.ts @@ -0,0 +1,100 @@ +export const OneGraphNetlifyCliSessionTestEventSdl = `type OneGraphNetlifyCliSessionTestEvent { + id: String! + sessionId: String! + createdAt: String! + payload: JSON! +}`; + +export type OneGraphNetlifyCliSessionTestEvent = { + __typename: "OneGraphNetlifyCliSessionTestEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: Record; +}; + +export const OneGraphNetlifyCliSessionGenerateHandlerEventSdl = `type OneGraphNetlifyCliSessionGenerateHandlerEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + cliSessionId: String! + operationId: String + codeGeneratorId: String! + options: JSON + } +}`; + +export type OneGraphNetlifyCliSessionGenerateHandlerEvent = { + __typename: "OneGraphNetlifyCliSessionGenerateHandlerEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + cliSessionId: string; + operationId: string; + codeGeneratorId: string; + options?: Record | null; + }; +}; + +export const OneGraphNetlifyCliSessionOpenFileEventSdl = `type OneGraphNetlifyCliSessionOpenFileEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + filePath: String! + } +}`; + +export type OneGraphNetlifyCliSessionOpenFileEvent = { + __typename: "OneGraphNetlifyCliSessionOpenFileEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + filePath: string; + }; +}; + +export const OneGraphNetlifyCliSessionPersistedLibraryUpdatedEventSdl = `type OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + docId: String! + } + }`; + +export type OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent = { + __typename: "OneGraphNetlifyCliSessionOpenFileEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + docId: string; + }; +}; + +export const OneGraphNetlifyCliSessionFileWrittenEventSdl = `type OneGraphNetlifyCliSessionFileWrittenEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + editor: String + filePath: String! + } + audience: String! +}`; + +export type OneGraphNetlifyCliSessionFileWrittenEvent = { + __typename: "OneGraphNetlifyCliSessionFileWrittenEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + editor: string | null; + filePath: string; + }; + audience: "UI" | "CLI"; +}; diff --git a/src/codegen/codegenHelpers.ts b/src/codegen/codegenHelpers.ts index 0e4be51..0d63eca 100644 --- a/src/codegen/codegenHelpers.ts +++ b/src/codegen/codegenHelpers.ts @@ -167,16 +167,41 @@ type CodeGeneratorSupportableDefinitionType = | "subscription" | "fragment"; +export type GenerateHandlerFunctionOptions = { + schemaSdl: string; + inputTypename: string; + defaultValue?: Record; +}; + +export type GenerateHandlerFunctionOptionsDeserialized = { + schema: GraphQL.GraphQLSchema; + inputTypename: string; + defaultValue?: Record; +}; + export type CodeGenerator = { generatePreview?: GenerateHandlerPreviewFunction; generateHandler: GenerateHandlerFunction; - generateHandlerOptions?: { schemaSdl: string; inputTypename: string }; + generateHandlerOptions?: GenerateHandlerFunctionOptions; supportedDefinitionTypes: CodeGeneratorSupportableDefinitionType[]; name: string; id: string; version: string; }; +export type CodeGeneratorMeta = { + id: string; + name: string; + options: GenerateHandlerFunctionOptions | null; + supportedDefinitionTypes: CodeGeneratorSupportableDefinitionType[]; +}; + +export type CodegenModuleMeta = { + id: string; + version: string; + generators: CodeGeneratorMeta[]; +}; + export type CodegenModule = { id: string; version: string; diff --git a/src/generatedOneGraphClient.js b/src/generatedOneGraphClient.js index 49ec7ed..6c9c004 100644 --- a/src/generatedOneGraphClient.js +++ b/src/generatedOneGraphClient.js @@ -121,6 +121,8 @@ const fetchNetlifyGraph = function fetchNetlifyGraph(input) { response.then((result) => { // Check response headers for a 304 Not Modified if (result.status === 304) { + // Drain the body so the connection will be closed + result.text(); // Return the cached result resolve(cachedResultValue); } else if (result.status === 200) { diff --git a/src/graphqlHelpers.ts b/src/graphqlHelpers.ts index 83719b7..5670658 100644 --- a/src/graphqlHelpers.ts +++ b/src/graphqlHelpers.ts @@ -409,7 +409,7 @@ export function listCount(GraphQL: typeof GraphQLPackage, gqlType) { totalCount = totalCount + 1; if (totalCount > 30) { - console.warn("Bailing on potential infinite recursion"); + internalConsole.warn("Bailing on potential infinite recursion"); return -99; } @@ -549,7 +549,7 @@ export function typeScriptDefinitionObjectForOperation( parentNamedType.getFields()[name]; if (!field) { - console.warn( + internalConsole.warn( "Could not find field", name, "in", @@ -621,7 +621,10 @@ export function typeScriptDefinitionObjectForOperation( } } } else { - console.warn("objectHelper got a non-field selection", selection); + internalConsole.warn( + "objectHelper got a non-field selection", + selection + ); } }); @@ -696,7 +699,7 @@ export function typeScriptDefinitionObjectForOperation( return outEnum; } else { - console.warn("Unrecognized type in operation", parentGqlType); + internalConsole.warn("Unrecognized type in operation", parentGqlType); } }; @@ -934,19 +937,6 @@ export function typeScriptDefinitionObjectForFragment( (isObjectType(parentNamedType) || isInterfaceType(parentNamedType)) && parentNamedType.getFields()[name]; - if (!field) { - console.warn( - "Could not find field", - name, - "on", - parentNamedType.name, - "among", - // @ts-ignore - Object.keys(parentNamedType.getFields()) - ); - return; - } - if (name.startsWith("__")) { return { kind: "object", @@ -962,6 +952,19 @@ export function typeScriptDefinitionObjectForFragment( }; } + if (!field) { + internalConsole.warn( + "Could not find field", + name, + "on", + parentNamedType.name, + "among", + // @ts-ignore + Object.keys(parentNamedType.getFields()) + ); + return; + } + let gqlType = field.type; let namedType = getNamedType(gqlType); const isNullable = isNullableType(gqlType); @@ -1024,7 +1027,10 @@ export function typeScriptDefinitionObjectForFragment( } } } else { - console.warn("objectHelper got a non-field selection", selection); + internalConsole.warn( + "objectHelper got a non-field selection", + selection + ); } }); @@ -1099,7 +1105,7 @@ export function typeScriptDefinitionObjectForFragment( return outEnum; } else { - console.warn("Unrecognized type in fragment", parentGqlType); + internalConsole.warn("Unrecognized type in fragment", parentGqlType); } }; @@ -1492,7 +1498,7 @@ export const formInput = ( const hydratedType = typeFromAST(schema, def.type); if (!hydratedType) { - console.warn("\tCould not hydrate type for ", def.type); + internalConsole.warn("\tCould not hydrate type for ", def.type); return null; } // const required = isNonNullType(hydratedType); @@ -1623,7 +1629,7 @@ export const remixFormInput = ( const hydratedType = typeFromAST(schema, def.type); if (!hydratedType) { - console.warn("\tCould not hydrate type for ", def.type); + internalConsole.warn("\tCould not hydrate type for ", def.type); return null; } // const required = isNonNullType(hydratedType); @@ -1790,7 +1796,7 @@ export const gatherHardcodedValues = ( return hardCodedValues; } catch (e) { - console.warn("Error parsing query", e); + internalConsole.warn("Error parsing query", e); return []; } }; @@ -1824,7 +1830,7 @@ export const extractPersistableOperation = ( FragmentSpread: { enter: fragmentExtractor }, }); } else { - console.warn( + internalConsole.warn( "Could not find fragment definition for referenced fragment: ", fragmentName ); diff --git a/src/netlifyGraph.ts b/src/netlifyGraph.ts index d58dec2..310ee67 100644 --- a/src/netlifyGraph.ts +++ b/src/netlifyGraph.ts @@ -2016,7 +2016,7 @@ export const generatePersistedFunctionsSource = async ( ...result, attemptedFunction: fn, }); - console.warn( + internalConsole.warn( "Failed to persist function", fn.operationName, result.errors From ec873891875f7db607cc0c14e8fd707856ff5c3f Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Thu, 4 Aug 2022 13:50:29 -0700 Subject: [PATCH 4/6] Export CLI event types --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index cfa2eac..fd9b2de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,5 +3,6 @@ export * as OneGraphClient from "./oneGraphClient"; export * as GraphQL from "graphql"; export * as InternalConsole from "./internalConsole"; export * as CodegenHelpers from "./codegen/codegenHelpers"; +export * as CliEventHelper from "./cliEventHelpers"; export * as GraphQLHelpers from "./graphqlHelpers"; export * as NetlifyGraphJsonConfig from "./netlifyGraphJsonConfig"; From c7cbb272484570c9d3980334751a065191094464 Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Sun, 7 Aug 2022 18:00:48 -0700 Subject: [PATCH 5/6] Update CLI events helper --- src/cliEventHelpers.ts | 110 ++++++++++++++++++++++++++++++++- src/codegen/codegenHelpers.ts | 14 ++--- src/codegen/genericExporter.ts | 5 +- src/codegen/nextjsExporter.ts | 4 +- src/codegen/remixExporter.ts | 4 +- src/netlifyGraph.ts | 6 +- src/oneGraphClient.ts | 12 ++-- 7 files changed, 132 insertions(+), 23 deletions(-) diff --git a/src/cliEventHelpers.ts b/src/cliEventHelpers.ts index 59adf7b..b5be717 100644 --- a/src/cliEventHelpers.ts +++ b/src/cliEventHelpers.ts @@ -1,3 +1,5 @@ +import { CodegenModuleMeta } from "./codegen/codegenHelpers"; + export const OneGraphNetlifyCliSessionTestEventSdl = `type OneGraphNetlifyCliSessionTestEvent { id: String! sessionId: String! @@ -20,7 +22,7 @@ export const OneGraphNetlifyCliSessionGenerateHandlerEventSdl = `type OneGraphNe payload: { cliSessionId: String! operationId: String - codeGeneratorId: String! + codegenId: String! options: JSON } }`; @@ -33,7 +35,7 @@ export type OneGraphNetlifyCliSessionGenerateHandlerEvent = { payload: { cliSessionId: string; operationId: string; - codeGeneratorId: string; + codegenId: string; options?: Record | null; }; }; @@ -63,16 +65,18 @@ export const OneGraphNetlifyCliSessionPersistedLibraryUpdatedEventSdl = `type On createdAt: String! payload: { docId: String! + schemaId: String! } }`; export type OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent = { - __typename: "OneGraphNetlifyCliSessionOpenFileEvent"; + __typename: "OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent"; id: string; sessionId: string; createdAt: string; payload: { docId: string; + schemaId: string; }; }; @@ -98,3 +102,103 @@ export type OneGraphNetlifyCliSessionFileWrittenEvent = { }; audience: "UI" | "CLI"; }; + +export const OneGraphNetlifyCliSessionCodegenHandlerFunctionOptionsSdl = `type OneGraphNetlifyCliSessionCodegenHandlerFunctionOptions { + schemaSdl: String! + inputTypename: String! + defaultValue: JSON +}`; + +export const OneGraphNetlifyCliSessionCodegenSupportedDefinitionTypesEnumSdl = `enum OneGraphNetlifyCliSessionCodegenSupportedDefinitionTypesEnum { + QUERY + MUTATION + SUBSCRIPTION + FRAGMENT +}`; + +export const OneGraphNetlifyCliSessionCodegenMetadataSdl = `type OneGraphNetlifyCliSessionCodegenMetadata { + id: String! + name: String! + operations: OneGraphNetlifyCliSessionCodegenHandlerFunctionOptions + supportedDefinitionTypes: [OneGraphNetlifyCliSessionCodegenSupportedDefinitionTypesEnum!]! +}`; + +export const OneGraphNetlifyCliSessionCodegenModuleMetadataSdl = `type OneGraphNetlifyCliSessionCodegenModuleMetadata { + id: String! + version: String! + generators: [OneGraphNetlifyCliSessionCodegenMetadata!]! +}`; + +export const OneGraphNetlifyCliSessionMetadataPublishEventSdl = `type OneGraphNetlifyCliSessionMetadataPublishEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + editor: String + siteRoot: String + siteRootFriendly: String + schemaId: String! + persistedDocId: String! + codegenModule: OneGraphNetlifyCliSessionCodegenModuleMetadata + } + audience: String! +}`; + +export type OneGraphNetlifyCliSessionMetadataPublishEvent = { + __typename: "OneGraphNetlifyCliSessionMetadataPublishEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + cliVersion: string; + editor: string | null; + siteRoot: string | null; + siteRootFriendly: string | null; + schemaId: string; + persistedDocId: string; + codegenModule: CodegenModuleMeta | null; + }; + audience: "UI"; +}; + +export const OneGraphNetlifyCliSessionMetadataRequestEventSdl = `type OneGraphNetlifyCliSessionMetadataRequestEvent { + id: String! + sessionId: String! + createdAt: String! + payload: { + minimumCliVersionExpected: String! + expectedAudience: String! + } + audience: String! +}`; + +export type OneGraphNetlifyCliSessionMetadataRequestEvent = { + __typename: "OneGraphNetlifyCliSessionMetadataRequestEvent"; + id: string; + sessionId: string; + createdAt: string; + payload: { + minimumCliVersionExpected: string; + expectedAudience: "UI"; + }; + audience: "CLI"; +}; + +export type CliEvent = + | OneGraphNetlifyCliSessionTestEvent + | OneGraphNetlifyCliSessionGenerateHandlerEvent + | OneGraphNetlifyCliSessionOpenFileEvent + | OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent + | OneGraphNetlifyCliSessionFileWrittenEvent + | OneGraphNetlifyCliSessionMetadataPublishEvent + | OneGraphNetlifyCliSessionMetadataRequestEvent; + +export type DetectedLocalCLISessionMetadata = { + gitBranch: string | null; + hostname: string | null; + username: string | null; + siteRoot: string | null; + cliVersion: string; + editor: string | null; + codegen: CodegenModuleMeta | null; +}; diff --git a/src/codegen/codegenHelpers.ts b/src/codegen/codegenHelpers.ts index 0d63eca..7ed4cfd 100644 --- a/src/codegen/codegenHelpers.ts +++ b/src/codegen/codegenHelpers.ts @@ -161,7 +161,7 @@ export type GenerateRuntimeFunction = (opts: { fragments: ParsedFragment[]; }) => NamedExportedFile[]; -type CodeGeneratorSupportableDefinitionType = +type CodegenSupportableDefinitionType = | "query" | "mutation" | "subscription" @@ -179,32 +179,32 @@ export type GenerateHandlerFunctionOptionsDeserialized = { defaultValue?: Record; }; -export type CodeGenerator = { +export type Codegen = { generatePreview?: GenerateHandlerPreviewFunction; generateHandler: GenerateHandlerFunction; generateHandlerOptions?: GenerateHandlerFunctionOptions; - supportedDefinitionTypes: CodeGeneratorSupportableDefinitionType[]; + supportedDefinitionTypes: CodegenSupportableDefinitionType[]; name: string; id: string; version: string; }; -export type CodeGeneratorMeta = { +export type CodegenMeta = { id: string; name: string; options: GenerateHandlerFunctionOptions | null; - supportedDefinitionTypes: CodeGeneratorSupportableDefinitionType[]; + supportedDefinitionTypes: CodegenSupportableDefinitionType[]; }; export type CodegenModuleMeta = { id: string; version: string; - generators: CodeGeneratorMeta[]; + generators: CodegenMeta[]; }; export type CodegenModule = { id: string; version: string; generateRuntime: GenerateRuntimeFunction; - generators: CodeGenerator[]; + generators: Codegen[]; }; diff --git a/src/codegen/genericExporter.ts b/src/codegen/genericExporter.ts index 1443879..49ae821 100644 --- a/src/codegen/genericExporter.ts +++ b/src/codegen/genericExporter.ts @@ -10,7 +10,8 @@ import { ExportedFile, ExporterResult, munge, - CodeGenerator, + GenerateHandlerFunction, + Codegen, UnnamedExportedFile, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; @@ -531,7 +532,7 @@ const exp = (netlifyGraphConfig, name) => { }; // Snippet generation! -export const netlifyFunctionSnippet: CodeGenerator = { +export const netlifyFunctionSnippet: Codegen = { name: "Netlify Function", generateHandlerOptions: snippetOptions, supportedDefinitionTypes: [], diff --git a/src/codegen/nextjsExporter.ts b/src/codegen/nextjsExporter.ts index ab895a4..0534059 100644 --- a/src/codegen/nextjsExporter.ts +++ b/src/codegen/nextjsExporter.ts @@ -12,7 +12,7 @@ import { NamedExportedFile, OperationData, OperationDataList, - CodeGenerator, + Codegen, UnnamedExportedFile, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; @@ -748,7 +748,7 @@ const expDefault = (netlifyGraphConfig: NetlifyGraphConfig, name: string) => { }; // Snippet generation! -export const nextjsFunctionSnippet: CodeGenerator = { +export const nextjsFunctionSnippet: Codegen = { name: "Next.js Function", generateHandlerOptions: snippetOptions, supportedDefinitionTypes: [], diff --git a/src/codegen/remixExporter.ts b/src/codegen/remixExporter.ts index 48d1ffa..525463c 100644 --- a/src/codegen/remixExporter.ts +++ b/src/codegen/remixExporter.ts @@ -12,7 +12,7 @@ import { NamedExportedFile, OperationData, OperationDataList, - CodeGenerator, + Codegen, } from "./codegenHelpers"; import { internalConsole } from "../internalConsole"; import { remixFormInput } from "../graphqlHelpers"; @@ -744,7 +744,7 @@ const expDefault = (netlifyGraphConfig: NetlifyGraphConfig, name: string) => { }; // Snippet generation! -export const remixFunctionSnippet: CodeGenerator = { +export const remixFunctionSnippet: Codegen = { name: "Remix Function", generateHandlerOptions: snippetOptions, supportedDefinitionTypes: [], diff --git a/src/netlifyGraph.ts b/src/netlifyGraph.ts index 609dad2..2499a0d 100644 --- a/src/netlifyGraph.ts +++ b/src/netlifyGraph.ts @@ -32,7 +32,7 @@ import { import { nextjsFunctionSnippet } from "./codegen/nextjsExporter"; import { remixFunctionSnippet } from "./codegen/remixExporter"; import { - CodeGenerator, + Codegen, ExportedFile, GenerateHandlerFunction, } from "./codegen/codegenHelpers"; @@ -2399,7 +2399,7 @@ export const generateCustomHandlerSource = ({ operationId: string; operationsDoc: string; schema: GraphQLSchema; - generate: CodeGenerator["generateHandler"]; + generate: Codegen["generateHandler"]; }): | { exportedFiles: ExportedFile[]; @@ -2452,7 +2452,7 @@ export const generatePreview = ({ operationId: string; operationsDoc: string; schema: GraphQLSchema; - generate: CodeGenerator["generatePreview"]; + generate: Codegen["generatePreview"]; }): | { exportedFile: ExportedFile; diff --git a/src/oneGraphClient.ts b/src/oneGraphClient.ts index 0d778ae..22c26c5 100644 --- a/src/oneGraphClient.ts +++ b/src/oneGraphClient.ts @@ -451,23 +451,27 @@ export const friendlyEventName = (event: OneGraphCliEvent): string => { } }; -export type OneGraphCliEventAudience = "ui" | "cli"; +export type OneGraphCliEventAudience = "UI" | "CLI"; /** * * @param {OneGraphCliEvent} event - * @returns {'ui' | 'cli'} Which audience the event is intended for + * @returns {OneGraphCliEventAudience} Which audience the event is intended for */ export const eventAudience = ( event: OneGraphCliEvent ): OneGraphCliEventAudience => { const { __typename, payload } = event; + if (event.audience || payload.audience) { + return event.audience || payload.audience; + } + switch (__typename) { case "OneGraphNetlifyCliSessionTestEvent": return eventAudience(payload); case "OneGraphNetlifyCliSessionFileWrittenEvent": - return "ui"; + return "UI"; default: { - return "cli"; + return "CLI"; } } }; From 87d454e5faf6f6ea804ad27fad6faca607d002ea Mon Sep 17 00:00:00 2001 From: Sean Grove Date: Mon, 8 Aug 2022 00:07:06 -0700 Subject: [PATCH 6/6] Rename session function to reflect it returns the full GraphQL schema metadata --- src/oneGraphClient.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/oneGraphClient.ts b/src/oneGraphClient.ts index 22c26c5..6a7924e 100644 --- a/src/oneGraphClient.ts +++ b/src/oneGraphClient.ts @@ -446,6 +446,9 @@ export const friendlyEventName = (event: OneGraphCliEvent): string => { case "OneGraphNetlifyCliSessionOpenFileEvent": return `Open file ${payload.filePath}`; default: { + if (__typename.startsWith("OneGraphNetlify")) { + return __typename.replace("OneGraphNetlify", ""); + } return `Unrecognized event (${__typename})`; } } @@ -600,9 +603,8 @@ export const fetchEnabledServicesForApp = async ( * Fetch a list of what services are enabled for the given session * @param {string} jwt The netlify jwt that is used for authentication * @param {string} sessionId The session ID to query against - * @returns */ -export const fetchEnabledServicesForSession = async ( +export const fetchGraphQLSchemaForSession = async ( jwt: string, siteId: string, sessionId: string @@ -617,8 +619,7 @@ export const fetchEnabledServicesForSession = async ( accessToken: jwt, } ); - return appSchemaResult.data?.oneGraph?.netlifyCliSession.graphQLSchema - ?.services; + return appSchemaResult.data?.oneGraph?.netlifyCliSession.graphQLSchema; }; export type MiniSession = {