Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Show edit graph link on dev startup, send operations library edits to UI, and run prettier (if available) on code output #4183

Merged
merged 3 commits into from
Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 34 additions & 4 deletions src/commands/dev/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ const stripAnsiCc = require('strip-ansi-control-characters')
const waitPort = require('wait-port')

const { startFunctionsServer } = require('../../lib/functions/server')
const { OneGraphCliClient, startOneGraphCLISession } = require('../../lib/one-graph/cli-client')
const { getNetlifyGraphConfig } = require('../../lib/one-graph/cli-netlify-graph')
const {
OneGraphCliClient,
loadCLISession,
persistNewOperationsDocForSession,
startOneGraphCLISession,
} = require('../../lib/one-graph/cli-client')
const {
defaultExampleOperationsDoc,
getGraphEditUrlBySiteId,
getNetlifyGraphConfig,
readGraphQLOperationsSourceFile,
} = require('../../lib/one-graph/cli-netlify-graph')
const {
NETLIFYDEV,
NETLIFYDEVERR,
Expand Down Expand Up @@ -349,9 +359,29 @@ const dev = async (options, command) => {
await OneGraphCliClient.ensureAppForSite(netlifyToken, site.id)
const netlifyGraphConfig = await getNetlifyGraphConfig({ command, options, settings })

log(`Starting Netlify Graph session, to edit your library run \`netlify graph:edit\` in another tab`)
let graphqlDocument = readGraphQLOperationsSourceFile(netlifyGraphConfig)

if (!graphqlDocument || graphqlDocument.trim().length === 0) {
graphqlDocument = defaultExampleOperationsDoc
}

await startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })

// Should be created by startOneGraphCLISession
const oneGraphSessionId = loadCLISession(state)

startOneGraphCLISession({ netlifyGraphConfig, netlifyToken, site, state })
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: graphqlDocument,
siteId: site.id,
})

const graphEditUrl = getGraphEditUrlBySiteId({ siteId: site.id, oneGraphSessionId })

log(
`Starting Netlify Graph session, to edit your library visit ${graphEditUrl} or run \`netlify graph:edit\` in another tab`,
)
}

printBanner({ url })
Expand Down
16 changes: 3 additions & 13 deletions src/commands/graph/graph-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const gitRepoInfo = require('git-repo-info')
const { OneGraphCliClient, generateSessionName, loadCLISession } = require('../../lib/one-graph/cli-client')
const {
defaultExampleOperationsDoc,
getGraphEditUrlBySiteName,
getGraphEditUrlBySiteId,
getNetlifyGraphConfig,
readGraphQLOperationsSourceFile,
} = require('../../lib/one-graph/cli-netlify-graph')
Expand All @@ -19,7 +19,7 @@ const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessi
* @returns
*/
const graphEdit = async (options, command) => {
const { api, site, siteInfo, state } = command.netlify
const { site, state } = command.netlify
const siteId = site.id

if (!site.id) {
Expand Down Expand Up @@ -60,17 +60,7 @@ const graphEdit = async (options, command) => {

await updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, { docId: persistedDoc.id })

let siteName = siteInfo.name

if (!siteName) {
const siteData = await api.getSite({ siteId })
siteName = siteData.name
if (!siteName) {
error(`No site name found for siteId ${siteId}`)
}
}

const graphEditUrl = getGraphEditUrlBySiteName({ siteName, oneGraphSessionId })
const graphEditUrl = getGraphEditUrlBySiteId({ siteId, oneGraphSessionId })

await openBrowser({ url: graphEditUrl })
}
Expand Down
143 changes: 131 additions & 12 deletions src/lib/one-graph/cli-client.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable fp/no-loops */
const crypto = require('crypto')
const os = require('os')
const path = require('path')

const gitRepoInfo = require('git-repo-info')
const { GraphQL, InternalConsole, OneGraphClient } = require('netlify-onegraph-internal')
const { NetlifyGraph } = require('netlify-onegraph-internal')

const { chalk, error, log, warn } = require('../../utils')

const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient
const { watchDebounced } = require('../functions/watcher')

const {
generateFunctionsFile,
Expand All @@ -19,6 +21,7 @@ const {

const { parse } = GraphQL
const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
const { createCLISession, createPersistedQuery, ensureAppForSite, updateCLISessionMetadata } = OneGraphClient

const internalConsole = {
log,
Expand All @@ -27,6 +30,9 @@ const internalConsole = {
debug: console.debug,
}

const witnessedIncomingDocumentHashes = []

// Keep track of which document hashes we've received from the server so we can ignore events from the filesystem based on them
InternalConsole.registerConsole(internalConsole)

/**
Expand Down Expand Up @@ -108,6 +114,26 @@ const monitorCLISessionEvents = (input) => {
return close
}

/**
* Monitor the operations document for changes
* @param {object} input
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @param {function} input.onAdd A callback function to handle when the operations document is added
* @param {function} input.onChange A callback function to handle when the operations document is changed
* @param {function} input.onUnlink A callback function to handle when the operations document is unlinked
* @returns {Promise<watcher>}
*/
const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnlink }) => {
const filePath = path.resolve(...netlifyGraphConfig.graphQLOperationsSourceFilename)
const newWatcher = await watchDebounced([filePath], {
onAdd,
onChange,
onUnlink,
})

return newWatcher
}

/**
* Fetch the schema for a site, and regenerate all of the downstream files
* @param {object} input
Expand Down Expand Up @@ -146,7 +172,44 @@ const refetchAndGenerateFromOneGraph = async (input) => {
}

/**
*
* Regenerate the function library based on the current operations document on disk
* @param {object} input
* @param {string} input.schema The GraphQL schema to use when generating code
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @returns
*/
const regenerateFunctionsFileFromOperationsFile = (input) => {
const { netlifyGraphConfig, schema } = input

const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)

const hash = quickHash(appOperationsDoc)

if (witnessedIncomingDocumentHashes.includes(hash)) {
// We've already seen this document, so don't regenerate
return
}

const parsedDoc = parse(appOperationsDoc, {
noLocation: true,
})
const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
}

/**
* Compute a md5 hash of a string
* @param {string} input String to compute a quick md5 hash for
* @returns hex digest of the input string
*/
const quickHash = (input) => {
const hashSum = crypto.createHash('md5')
hashSum.update(input)
return hashSum.digest('hex')
}

/**
* Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
* @param {object} input
* @param {string} input.siteId The site id to query against
* @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
Expand All @@ -155,7 +218,7 @@ const refetchAndGenerateFromOneGraph = async (input) => {
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
* @returns
*/
const updateGraphQLOperationsFile = async (input) => {
const updateGraphQLOperationsFileFromPersistedDoc = async (input) => {
const { docId, netlifyGraphConfig, netlifyToken, schema, siteId } = input
const persistedDoc = await OneGraphClient.fetchPersistedQuery(netlifyToken, siteId, docId)
if (!persistedDoc) {
Expand All @@ -166,12 +229,17 @@ const updateGraphQLOperationsFile = async (input) => {
const doc = persistedDoc.query

writeGraphQLOperationsSourceFile(netlifyGraphConfig, doc)
const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
const parsedDoc = parse(appOperationsDoc, {
noLocation: true,
})
const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })

const hash = quickHash(doc)

const relevantHasLength = 10

if (witnessedIncomingDocumentHashes.length > relevantHasLength) {
witnessedIncomingDocumentHashes.shift()
}

witnessedIncomingDocumentHashes.push(hash)
}

const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken, schema, siteId }) => {
Expand All @@ -184,7 +252,13 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
await generateHandler(netlifyGraphConfig, schema, payload.operationId, payload)
break
case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent':
await updateGraphQLOperationsFile({ netlifyToken, docId: payload.docId, netlifyGraphConfig, schema, siteId })
await updateGraphQLOperationsFileFromPersistedDoc({
netlifyToken,
docId: payload.docId,
netlifyGraphConfig,
schema,
siteId,
})
break
default: {
warn(`Unrecognized event received, you may need to upgrade your CLI version`, __typename, payload)
Expand All @@ -193,6 +267,24 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
}
}

const persistNewOperationsDocForSession = async ({ netlifyToken, oneGraphSessionId, operationsDoc, siteId }) => {
const { branch } = gitRepoInfo()

const payload = {
appId: siteId,
description: 'Temporary snapshot of local queries',
document: operationsDoc,
tags: ['netlify-cli', `session:${oneGraphSessionId}`, `git-branch:${branch}`, `local-change`],
}
const persistedDoc = await createPersistedQuery(netlifyToken, payload)
const newMetadata = await { docId: persistedDoc.id }
const result = await OneGraphClient.updateCLISessionMetadata(netlifyToken, siteId, oneGraphSessionId, newMetadata)

if (result.errors) {
warn('Unable to update session metadata with updated operations doc', result.errors)
}
}

/**
* Load the CLI session id from the local state
* @param {state} state
Expand All @@ -201,7 +293,7 @@ const handleCliSessionEvent = async ({ event, netlifyGraphConfig, netlifyToken,
const loadCLISession = (state) => state.get('oneGraphSessionId')

/**
* Idemponentially save the CLI session id to the local state and start monitoring for CLI events and upstream schema changes
* Idemponentially save the CLI session id to the local state and start monitoring for CLI events, upstream schema changes, and local operation file changes
* @param {object} input
* @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
* @param {NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
Expand All @@ -223,6 +315,32 @@ const startOneGraphCLISession = async (input) => {
const enabledServices = []
const schema = await OneGraphClient.fetchOneGraphSchema(site.id, enabledServices)

monitorOperationFile({
netlifyGraphConfig,
onChange: async (filePath) => {
log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: newOperationsDoc,
siteId: site.id,
})
},
onAdd: async (filePath) => {
log('NetlifyGraph operation file created at', filePath, 'creating function library...')
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
await persistNewOperationsDocForSession({
netlifyToken,
oneGraphSessionId,
operationsDoc: newOperationsDoc,
siteId: site.id,
})
},
})

monitorCLISessionEvents({
appId: site.id,
netlifyToken,
Expand Down Expand Up @@ -273,6 +391,7 @@ module.exports = {
generateSessionName,
loadCLISession,
monitorCLISessionEvents,
persistNewOperationsDocForSession,
refetchAndGenerateFromOneGraph,
startOneGraphCLISession,
}
Loading