Skip to content

Commit

Permalink
fix(graph): Harden Graph calls (#4879)
Browse files Browse the repository at this point in the history
* fix: harden Graph calls and provide warnings

* fix: bump internal package to avoid lru issue

* fix: update snapshots

* fix: format

* Update src/lib/one-graph/cli-client.js

Co-authored-by: Antonio Nuno Monteiro <anmonteiro@gmail.com>

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Antonio Nuno Monteiro <anmonteiro@gmail.com>
  • Loading branch information
3 people authored Aug 1, 2022
1 parent a04bc10 commit 20fa2b2
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 120 deletions.
14 changes: 7 additions & 7 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@
"multiparty": "^4.2.1",
"netlify": "^12.0.0",
"netlify-headers-parser": "^6.0.2",
"netlify-onegraph-internal": "0.3.10",
"netlify-onegraph-internal": "0.4.0",
"netlify-redirect-parser": "^13.0.5",
"netlify-redirector": "^0.2.1",
"node-fetch": "^2.6.0",
Expand Down
230 changes: 134 additions & 96 deletions src/lib/one-graph/cli-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@ const {

const { parse } = GraphQL
const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
const {
ensureAppForSite,
executeCreatePersistedQueryMutation,
executeMarkCliSessionActiveHeartbeat,
executeMarkCliSessionInactive,
updateCLISessionMetadata,
} = OneGraphClient

const internalConsole = {
log,
Expand Down Expand Up @@ -71,18 +64,25 @@ const monitorCLISessionEvents = (input) => {
let nextMarkActiveHeartbeat = defaultHeartbeatFrequency

const markActiveHelper = async () => {
const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
const fullSession = await OneGraphClient.fetchCliSession({ jwt: graphJwt.jwt, appId, sessionId: currentSessionId })
// @ts-ignore
const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
nextMarkActiveHeartbeat = heartbeatIntervalms
const markCLISessionActiveResult = await executeMarkCliSessionActiveHeartbeat(
graphJwt.jwt,
site.id,
currentSessionId,
)
if (markCLISessionActiveResult.errors && markCLISessionActiveResult.errors.length !== 0) {
warn(`Failed to mark CLI session active: ${markCLISessionActiveResult.errors.join(', ')}`)
try {
const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
const fullSession = await OneGraphClient.fetchCliSession({
jwt: graphJwt.jwt,
appId,
sessionId: currentSessionId,
})
const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
nextMarkActiveHeartbeat = heartbeatIntervalms
const markCLISessionActiveResult = await OneGraphClient.executeMarkCliSessionActiveHeartbeat(
graphJwt.jwt,
site.id,
currentSessionId,
)
if (markCLISessionActiveResult.errors && markCLISessionActiveResult.errors.length !== 0) {
warn(`Failed to mark CLI session active: ${markCLISessionActiveResult.errors.join(', ')}`)
}
} catch {
warn(`Unable to reach Netlify Graph servers in order to mark CLI session active`)
}
setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
}
Expand All @@ -92,23 +92,27 @@ const monitorCLISessionEvents = (input) => {
const enabledServiceWatcher = async (jwt, { appId: siteId, sessionId }) => {
const enabledServices = state.get('oneGraphEnabledServices') || ['onegraph']

const enabledServicesInfo = await OneGraphClient.fetchEnabledServicesForSession(jwt, siteId, sessionId)
if (!enabledServicesInfo) {
warn('Unable to fetch enabled services for site for code generation')
return
}
const newEnabledServices = enabledServicesInfo.map((service) => service.graphQLField)
const enabledServicesCompareKey = enabledServices.sort().join(',')
const newEnabledServicesCompareKey = newEnabledServices.sort().join(',')

if (enabledServicesCompareKey !== newEnabledServicesCompareKey) {
log(
`${chalk.magenta(
'Reloading',
)} Netlify Graph schema..., ${enabledServicesCompareKey} => ${newEnabledServicesCompareKey}`,
)
await refetchAndGenerateFromOneGraph({ netlifyGraphConfig, state, jwt, siteId, sessionId })
log(`${chalk.green('Reloaded')} Netlify Graph schema and regenerated functions`)
try {
const enabledServicesInfo = await OneGraphClient.fetchEnabledServicesForSession(jwt, siteId, sessionId)
if (!enabledServicesInfo) {
warn('Unable to fetch enabled services for site for code generation')
return
}
const newEnabledServices = enabledServicesInfo.map((service) => service.graphQLField)
const enabledServicesCompareKey = enabledServices.sort().join(',')
const newEnabledServicesCompareKey = newEnabledServices.sort().join(',')

if (enabledServicesCompareKey !== newEnabledServicesCompareKey) {
log(
`${chalk.magenta(
'Reloading',
)} Netlify Graph schema..., ${enabledServicesCompareKey} => ${newEnabledServicesCompareKey}`,
)
await refetchAndGenerateFromOneGraph({ netlifyGraphConfig, state, jwt, siteId, sessionId })
log(`${chalk.green('Reloaded')} Netlify Graph schema and regenerated functions`)
}
} catch {
warn(`Unable to reach Netlify Graph servers in order to fetch enabled Graph services`)
}
}

Expand All @@ -119,39 +123,43 @@ const monitorCLISessionEvents = (input) => {
let handle

const helper = async () => {
if (shouldClose) {
clearTimeout(handle)
onClose && onClose()
}

const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
const next = await OneGraphClient.fetchCliSessionEvents({ appId, jwt: graphJwt.jwt, sessionId: currentSessionId })
try {
if (shouldClose) {
clearTimeout(handle)
onClose && onClose()
}

if (next && next.errors) {
next.errors.forEach((fetchEventError) => {
onError(fetchEventError)
})
}
const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
const next = await OneGraphClient.fetchCliSessionEvents({ appId, jwt: graphJwt.jwt, sessionId: currentSessionId })

const events = (next && next.events) || []

if (events.length !== 0) {
let ackIds = []
try {
ackIds = await onEvents(events)
} catch (eventHandlerError) {
warn(`Error handling event: ${eventHandlerError}`)
} finally {
await OneGraphClient.ackCLISessionEvents({
appId,
jwt: graphJwt.jwt,
sessionId: currentSessionId,
eventIds: ackIds,
if (next && next.errors) {
next.errors.forEach((fetchEventError) => {
onError(fetchEventError)
})
}
}

await enabledServiceWatcher(graphJwt.jwt, { appId, sessionId: currentSessionId })
const events = (next && next.events) || []

if (events.length !== 0) {
let ackIds = []
try {
ackIds = await onEvents(events)
} catch (eventHandlerError) {
warn(`Error handling event: ${eventHandlerError}`)
} finally {
await OneGraphClient.ackCLISessionEvents({
appId,
jwt: graphJwt.jwt,
sessionId: currentSessionId,
eventIds: ackIds,
})
}
}

await enabledServiceWatcher(graphJwt.jwt, { appId, sessionId: currentSessionId })
} catch {
warn(`Unable to reach Netlify Graph servers in order to sync Graph session`)
}

handle = setTimeout(helper, frequency)
}
Expand All @@ -172,7 +180,11 @@ const monitorCLISessionEvents = (input) => {
* @returns {Promise<any>}
*/
const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnlink }) => {
const filePath = path.resolve(...netlifyGraphConfig.graphQLOperationsSourceFilename)
if (!netlifyGraphConfig.graphQLOperationsSourceFilename) {
error('Please configure `graphQLOperationsSourceFilename` in your `netlify.toml` [graph] section')
}

const filePath = path.resolve(...(netlifyGraphConfig.graphQLOperationsSourceFilename || []))
const newWatcher = await watchDebounced([filePath], {
depth: 1,
onAdd,
Expand Down Expand Up @@ -211,6 +223,10 @@ const refetchAndGenerateFromOneGraph = async (input) => {

const schema = await OneGraphClient.fetchOneGraphSchemaForServices(siteId, enabledServices)

if (!schema) {
error('Unable to fetch schema from Netlify Graph')
}

let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
if (currentOperationsDoc.trim().length === 0) {
currentOperationsDoc = defaultExampleOperationsDoc
Expand Down Expand Up @@ -280,29 +296,33 @@ const quickHash = (input) => {
* @returns
*/
const updateGraphQLOperationsFileFromPersistedDoc = async (input) => {
const { docId, logger, netlifyGraphConfig, netlifyToken, schema, siteId } = input
const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
const persistedDoc = await OneGraphClient.fetchPersistedQuery(jwt, siteId, docId)
if (!persistedDoc) {
warn(`No persisted doc found for: ${docId}`)
return
}
try {
const { docId, logger, netlifyGraphConfig, netlifyToken, schema, siteId } = input
const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
const persistedDoc = await OneGraphClient.fetchPersistedQuery(jwt, siteId, docId)
if (!persistedDoc) {
warn(`No persisted doc found for: ${docId}`)
return
}

// Sorts the operations stably, prepends the @netlify directive, etc.
const operationsDocString = normalizeOperationsDoc(persistedDoc.query)
// Sorts the operations stably, prepends the @netlify directive, etc.
const operationsDocString = normalizeOperationsDoc(persistedDoc.query)

writeGraphQLOperationsSourceFile({ logger, netlifyGraphConfig, operationsDocString })
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
writeGraphQLOperationsSourceFile({ logger, netlifyGraphConfig, operationsDocString })
regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })

const hash = quickHash(operationsDocString)
const hash = quickHash(operationsDocString)

const relevantHasLength = 10
const relevantHasLength = 10

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

witnessedIncomingDocumentHashes.push(hash)
witnessedIncomingDocumentHashes.push(hash)
} catch {
warn(`Unable to reach Netlify Graph servers in order to update Graph operations file`)
}
}

const handleCliSessionEvent = async ({
Expand Down Expand Up @@ -357,15 +377,19 @@ const handleCliSessionEvent = async ({
},
}

const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })

await OneGraphClient.executeCreateCLISessionEventMutation(
{
sessionId,
payload: fileWrittenEvent,
},
{ accesToken: graphJwt.jwt },
)
try {
const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })

await OneGraphClient.executeCreateCLISessionEventMutation(
{
sessionId,
payload: fileWrittenEvent,
},
{ accessToken: graphJwt.jwt },
)
} catch {
warn(`Unable to reach Netlify Graph servers in order to notify handler files written to disk`)
}
}
break
}
Expand Down Expand Up @@ -500,9 +524,23 @@ const persistNewOperationsDocForSession = async ({
siteId,
siteRoot,
}) => {
try {
GraphQL.parse(operationsDoc)
} catch (parseError) {
// TODO: We should send a message to the web UI that the current GraphQL operations file can't be sync because it's invalid
warn(
`Unable to sync Graph operations file. Please ensure that your GraphQL operations file is valid GraphQL. Found error: ${JSON.stringify(
parseError,
null,
2,
)}`,
)
return
}

const { branch } = gitRepoInfo()
const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
const persistedResult = await executeCreatePersistedQueryMutation(
const persistedResult = await OneGraphClient.executeCreatePersistedQueryMutation(
{
appId: siteId,
description: 'Temporary snapshot of local queries',
Expand Down Expand Up @@ -663,7 +701,7 @@ const startOneGraphCLISession = async (input) => {
*/
const markCliSessionInactive = async ({ netlifyToken, sessionId, siteId }) => {
const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
const result = await executeMarkCliSessionInactive(jwt, siteId, sessionId)
const result = await OneGraphClient.executeMarkCliSessionInactive(jwt, siteId, sessionId)
if (result.errors) {
warn(`Unable to mark CLI session ${sessionId} inactive: ${JSON.stringify(result.errors, null, 2)}`)
}
Expand Down Expand Up @@ -733,7 +771,7 @@ const ensureCLISession = async (input) => {
}

state.set('oneGraphSessionId', oneGraphSessionId)
const { errors: markCLISessionActiveErrors } = await executeMarkCliSessionActiveHeartbeat(
const { errors: markCLISessionActiveErrors } = await OneGraphClient.executeMarkCliSessionActiveHeartbeat(
jwt,
site.id,
oneGraphSessionId,
Expand All @@ -751,8 +789,8 @@ const OneGraphCliClient = {
executeCreatePersistedQueryMutation: OneGraphClient.executeCreatePersistedQueryMutation,
executeCreateApiTokenMutation: OneGraphClient.executeCreateApiTokenMutation,
fetchCliSessionEvents: OneGraphClient.fetchCliSessionEvents,
ensureAppForSite,
updateCLISessionMetadata,
ensureAppForSite: OneGraphClient.ensureAppForSite,
updateCLISessionMetadata: OneGraphClient.updateCLISessionMetadata,
getGraphJwtForSite: OneGraphClient.getGraphJwtForSite,
}

Expand Down
Loading

1 comment on commit 20fa2b2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 224 MB

Please sign in to comment.