netlify-cli 10.15.0 → 10.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,24 +1,41 @@
1
1
  // @ts-check
2
2
  /* eslint-disable eslint-comments/disable-enable-pair */
3
3
  /* eslint-disable fp/no-loops */
4
+ /* eslint-disable no-underscore-dangle */
4
5
  const crypto = require('crypto')
5
6
  const { readFileSync, writeFileSync } = require('fs')
6
7
  const os = require('os')
7
8
  const path = require('path')
9
+ const process = require('process')
8
10
 
9
11
  const gitRepoInfo = require('git-repo-info')
10
- const { GraphQL, InternalConsole, OneGraphClient } = require('netlify-onegraph-internal')
11
- const { NetlifyGraph, NetlifyGraphLockfile } = require('netlify-onegraph-internal')
12
-
12
+ const WSL = require('is-wsl')
13
+ const {
14
+ // eslint-disable-next-line no-unused-vars
15
+ CliEventHelper,
16
+ // eslint-disable-next-line no-unused-vars
17
+ CodegenHelpers,
18
+ GraphQL,
19
+ InternalConsole,
20
+ NetlifyGraph,
21
+ NetlifyGraphLockfile,
22
+ OneGraphClient,
23
+ } = require('netlify-onegraph-internal')
24
+
25
+ const frameworkInfoPromise = import('@netlify/framework-info')
26
+
27
+ const { version } = require('../../../package.json')
13
28
  // eslint-disable-next-line no-unused-vars
14
29
  const { StateConfig, USER_AGENT, chalk, error, execa, log, warn, watchDebounced } = require('../../utils')
15
30
 
16
31
  const {
17
32
  generateFunctionsFile,
18
33
  generateHandlerByOperationId,
19
- loadNetlifyGraphConfig,
34
+ getCodegenFunctionById,
35
+ getCodegenModule,
20
36
  normalizeOperationsDoc,
21
37
  readGraphQLOperationsSourceFile,
38
+ setNetlifyTomlCodeGeneratorModule,
22
39
  writeGraphQLOperationsSourceFile,
23
40
  writeGraphQLSchemaFile,
24
41
  } = require('./cli-netlify-graph')
@@ -33,6 +50,9 @@ const internalConsole = {
33
50
  debug: console.debug,
34
51
  }
35
52
 
53
+ /** @type {string | null} */
54
+ let currentPersistedDocId = null
55
+
36
56
  /**
37
57
  * Keep track of which document hashes we've received from the server so we can ignore events from the filesystem based on them
38
58
  */
@@ -45,22 +65,25 @@ InternalConsole.registerConsole(internalConsole)
45
65
  * @param {object} input
46
66
  * @param {string} input.appId The app to query against, typically the siteId
47
67
  * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
68
+ * @param {object} input.config The parsed netlify.toml file
48
69
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
49
70
  * @param {function} input.onClose A function to call when the polling loop is closed
50
71
  * @param {function} input.onError A function to call when an error occurs
51
72
  * @param {function} input.onEvents A function to call when CLI events are received and need to be processed
73
+ * @param {function} input.onSchemaIdChange A function to call when the CLI schemaId has changed
52
74
  * @param {string} input.sessionId The session id to monitor CLI events for
53
75
  * @param {StateConfig} input.state A function to call to set/get the current state of the local Netlify project
54
76
  * @param {any} input.site The site object
55
77
  * @returns
56
78
  */
57
79
  const monitorCLISessionEvents = (input) => {
58
- const { appId, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, site, state } = input
80
+ const { appId, config, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, site, state } = input
59
81
  const currentSessionId = input.sessionId
82
+ // TODO (sg): Track changing schemaId for a session
60
83
 
61
84
  const frequency = 5000
62
85
  // 30 minutes
63
- const defaultHeartbeatFrequency = 1_800_000
86
+ const defaultHeartbeatFrequency = 30_000
64
87
  let shouldClose = false
65
88
  let nextMarkActiveHeartbeat = defaultHeartbeatFrequency
66
89
 
@@ -72,8 +95,10 @@ const monitorCLISessionEvents = (input) => {
72
95
  appId,
73
96
  sessionId: currentSessionId,
74
97
  })
98
+
75
99
  const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
76
100
  nextMarkActiveHeartbeat = heartbeatIntervalms
101
+
77
102
  const markCLISessionActiveResult = await OneGraphClient.executeMarkCliSessionActiveHeartbeat(
78
103
  graphJwt.jwt,
79
104
  site.id,
@@ -94,12 +119,12 @@ const monitorCLISessionEvents = (input) => {
94
119
  const enabledServices = state.get('oneGraphEnabledServices') || ['onegraph']
95
120
 
96
121
  try {
97
- const enabledServicesInfo = await OneGraphClient.fetchEnabledServicesForSession(jwt, siteId, sessionId)
98
- if (!enabledServicesInfo) {
122
+ const graphQLSchemaInfo = await OneGraphClient.fetchGraphQLSchemaForSession(jwt, siteId, input.sessionId)
123
+ if (!graphQLSchemaInfo) {
99
124
  warn('Unable to fetch enabled services for site for code generation')
100
125
  return
101
126
  }
102
- const newEnabledServices = enabledServicesInfo.map((service) => service.graphQLField)
127
+ const newEnabledServices = graphQLSchemaInfo.services.map((service) => service.graphQLField)
103
128
  const enabledServicesCompareKey = enabledServices.sort().join(',')
104
129
  const newEnabledServicesCompareKey = newEnabledServices.sort().join(',')
105
130
 
@@ -109,7 +134,17 @@ const monitorCLISessionEvents = (input) => {
109
134
  'Reloading',
110
135
  )} Netlify Graph schema..., ${enabledServicesCompareKey} => ${newEnabledServicesCompareKey}`,
111
136
  )
112
- await refetchAndGenerateFromOneGraph({ netlifyGraphConfig, state, jwt, siteId, sessionId })
137
+
138
+ const schemaId = graphQLSchemaInfo.id
139
+
140
+ if (!schemaId) {
141
+ warn(`Unable to read schemaId from Netlify Graph session, not regenerating code`)
142
+ return
143
+ }
144
+
145
+ mergeLockfile({ siteRoot: site.root, schemaId })
146
+
147
+ await refetchAndGenerateFromOneGraph({ config, netlifyGraphConfig, state, jwt, schemaId, siteId, sessionId })
113
148
  log(`${chalk.green('Reloaded')} Netlify Graph schema and regenerated functions`)
114
149
  }
115
150
  } catch {
@@ -128,6 +163,7 @@ const monitorCLISessionEvents = (input) => {
128
163
  if (shouldClose) {
129
164
  clearTimeout(handle)
130
165
  onClose && onClose()
166
+ return
131
167
  }
132
168
 
133
169
  const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
@@ -205,24 +241,65 @@ const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnl
205
241
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
206
242
  * @param {StateConfig} input.state A function to call to set/get the current state of the local Netlify project
207
243
  * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
244
+ * @returns {Promise<Record<string, unknown> | undefined>}
245
+ */
246
+ const fetchCliSessionSchema = async (input) => {
247
+ const { jwt, siteId } = input
248
+
249
+ await OneGraphClient.ensureAppForSite(jwt, siteId)
250
+
251
+ const schemaInfo = await OneGraphClient.fetchNetlifySessionSchemaQuery(
252
+ { sessionId: input.sessionId },
253
+ {
254
+ accessToken: jwt,
255
+ siteId,
256
+ },
257
+ )
258
+
259
+ if (!schemaInfo) {
260
+ warn('Unable to fetch schema for session')
261
+ return
262
+ }
263
+
264
+ try {
265
+ const schemaMetadata = schemaInfo.data.oneGraph.netlifyCliSession.graphQLSchema
266
+ return schemaMetadata
267
+ } catch {}
268
+ }
269
+
270
+ /**
271
+ * Fetch the schema for a site, and regenerate all of the downstream files
272
+ * @param {object} input
273
+ * @param {string} input.siteId The id of the site to query against
274
+ * @param {string} input.jwt The Graph JWT
275
+ * @param {object} input.config The parsed netlify.toml file
276
+ * @param {string} input.sessionId The session ID for the current session
277
+ * @param {string} input.schemaId The schemaId for the current session
278
+ * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
279
+ * @param {StateConfig} input.state A function to call to set/get the current state of the local Netlify project
280
+ * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
208
281
  * @returns {Promise<void>}
209
282
  */
210
283
  const refetchAndGenerateFromOneGraph = async (input) => {
211
- const { jwt, logger, netlifyGraphConfig, siteId, state } = input
284
+ const { config, jwt, logger, netlifyGraphConfig, schemaId, siteId, state } = input
212
285
 
213
286
  await OneGraphClient.ensureAppForSite(jwt, siteId)
214
287
 
215
- const enabledServicesInfo = await OneGraphClient.fetchEnabledServicesForSession(jwt, siteId, input.sessionId)
216
- if (!enabledServicesInfo) {
217
- warn('Unable to fetch enabled services for site for code generation')
288
+ const graphQLSchemaInfo = await OneGraphClient.fetchGraphQLSchemaForSession(jwt, siteId, input.sessionId)
289
+ if (!graphQLSchemaInfo) {
290
+ warn('Unable to fetch schema info for site for code generation')
218
291
  return
219
292
  }
220
293
 
221
- const enabledServices = enabledServicesInfo
294
+ const enabledServices = graphQLSchemaInfo.services
222
295
  .map((service) => service.graphQLField)
223
296
  .sort((aString, bString) => aString.localeCompare(bString))
224
297
 
225
- const schema = await OneGraphClient.fetchOneGraphSchemaForServices(siteId, enabledServices)
298
+ const schema = await OneGraphClient.fetchOneGraphSchemaById({
299
+ siteId,
300
+ schemaId: graphQLSchemaInfo.id,
301
+ accessToken: jwt,
302
+ })
226
303
 
227
304
  if (!schema) {
228
305
  error('Unable to fetch schema from Netlify Graph')
@@ -234,15 +311,22 @@ const refetchAndGenerateFromOneGraph = async (input) => {
234
311
  }
235
312
 
236
313
  const parsedDoc = parse(currentOperationsDoc)
237
- const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
314
+ const { fragments, functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
315
+
316
+ if (!schema) {
317
+ warn('Unable to parse schema, please run graph:pull to update')
318
+ return
319
+ }
238
320
 
239
321
  generateFunctionsFile({
322
+ config,
240
323
  logger,
241
324
  netlifyGraphConfig,
242
325
  schema,
243
326
  operationsDoc: currentOperationsDoc,
244
327
  functions,
245
328
  fragments,
329
+ schemaId,
246
330
  })
247
331
  writeGraphQLSchemaFile({ logger, netlifyGraphConfig, schema })
248
332
  state.set('oneGraphEnabledServices', enabledServices)
@@ -251,12 +335,14 @@ const refetchAndGenerateFromOneGraph = async (input) => {
251
335
  /**
252
336
  * Regenerate the function library based on the current operations document on disk
253
337
  * @param {object} input
338
+ * @param {object} input.config The parsed netlify.toml file
254
339
  * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
340
+ * @param {string} input.schemaId The GraphQL schemaId to use when generating code
255
341
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
256
342
  * @returns
257
343
  */
258
344
  const regenerateFunctionsFileFromOperationsFile = (input) => {
259
- const { netlifyGraphConfig, schema } = input
345
+ const { config, netlifyGraphConfig, schema, schemaId } = input
260
346
 
261
347
  const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
262
348
 
@@ -270,14 +356,35 @@ const regenerateFunctionsFileFromOperationsFile = (input) => {
270
356
  const parsedDoc = parse(appOperationsDoc, {
271
357
  noLocation: true,
272
358
  })
273
- const { fragments, functions } = extractFunctionsFromOperationDoc(parsedDoc)
274
- generateFunctionsFile({ netlifyGraphConfig, schema, operationsDoc: appOperationsDoc, functions, fragments })
359
+ const { fragments, functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
360
+ generateFunctionsFile({
361
+ config,
362
+ netlifyGraphConfig,
363
+ schema,
364
+ operationsDoc: appOperationsDoc,
365
+ functions,
366
+ fragments,
367
+ schemaId,
368
+ })
275
369
  }
276
370
 
277
371
  /**
278
372
  * Lockfile Operations
279
373
  */
280
374
 
375
+ /**
376
+ * Read the Netlify Graph lockfile from disk, if it exists
377
+ * @param {object} input
378
+ * @param {string} input.siteRoot The GraphQL schema to use when generating code
379
+ * @return {NetlifyGraphLockfile.V0_format | undefined}
380
+ */
381
+ const readLockfile = ({ siteRoot }) => {
382
+ try {
383
+ const buf = readFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName))
384
+ return JSON.parse(buf.toString('utf8'))
385
+ } catch {}
386
+ }
387
+
281
388
  /**
282
389
  * Persist the Netlify Graph lockfile on disk
283
390
  * @param {object} input
@@ -289,15 +396,45 @@ const writeLockfile = ({ lockfile, siteRoot }) => {
289
396
  }
290
397
 
291
398
  /**
292
- * Read the Netlify Graph lockfile from disk, if it exists
399
+ * Persist the Netlify Graph lockfile on disk
293
400
  * @param {object} input
294
401
  * @param {string} input.siteRoot The GraphQL schema to use when generating code
295
- * @return {NetlifyGraphLockfile.V0_format | undefined}
402
+ * @param {string=} input.schemaId
403
+ * @param {string=} input.operationsHash
296
404
  */
297
- const readLockfile = ({ siteRoot }) => {
405
+ const mergeLockfile = ({ operationsHash, schemaId, siteRoot }) => {
406
+ const lockfile = readLockfile({ siteRoot })
407
+ if (lockfile) {
408
+ /** @type {NetlifyGraphLockfile.V0_format} */
409
+ const newLockfile = {
410
+ ...lockfile,
411
+ locked: {
412
+ ...lockfile.locked,
413
+ },
414
+ }
415
+
416
+ if (operationsHash) {
417
+ newLockfile.locked.operationsHash = operationsHash
418
+ }
419
+
420
+ if (schemaId) {
421
+ newLockfile.locked.schemaId = schemaId
422
+ }
423
+
424
+ writeFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName), JSON.stringify(newLockfile, null, 2))
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Read the Netlify Graph schemaId from the lockfile on disk, if it exists
430
+ * @param {object} input
431
+ * @param {string} input.siteRoot The GraphQL schema to use when generating code
432
+ * @return {string | undefined}
433
+ */
434
+ const readSchemaIdFromLockfile = ({ siteRoot }) => {
298
435
  try {
299
- const buf = readFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName))
300
- return JSON.parse(buf.toString('utf8'))
436
+ const lockfile = readLockfile({ siteRoot })
437
+ return lockfile && lockfile.locked.schemaId
301
438
  } catch {}
302
439
  }
303
440
 
@@ -319,8 +456,10 @@ const quickHash = (input) => {
319
456
  * @param {string} input.siteId The site id to query against
320
457
  * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
321
458
  * @param {string} input.docId The GraphQL operations document id to fetch
459
+ * @param {object} input.config The parsed netlify.toml file
322
460
  * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
323
461
  * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
462
+ * @param {string} input.schemaId The GraphQL schemaId to use when generating code
324
463
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
325
464
  * @returns {Promise<string | undefined>}
326
465
  */
@@ -335,7 +474,9 @@ const fetchGraphQLOperationsLibraryFromPersistedDoc = async (input) => {
335
474
  }
336
475
 
337
476
  // Sorts the operations stably, prepends the @netlify directive, etc.
338
- const operationsDocString = normalizeOperationsDoc(persistedDoc.query)
477
+ const operationsDocString = normalizeOperationsDoc(GraphQL, persistedDoc.query)
478
+
479
+ currentPersistedDocId = docId
339
480
 
340
481
  return operationsDocString
341
482
  } catch {
@@ -346,17 +487,19 @@ const fetchGraphQLOperationsLibraryFromPersistedDoc = async (input) => {
346
487
  /**
347
488
  * Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
348
489
  * @param {object} input
490
+ * @param {object} input.config The parsed netlify.toml config file
349
491
  * @param {string} input.operationsDocString The contents of the GraphQL operations document
350
492
  * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
351
493
  * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
494
+ * @param {string} input.schemaId The GraphQL schemaId to use when generating code
352
495
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
353
496
  * @returns
354
497
  */
355
498
  const updateGraphQLOperationsFileFromPersistedDoc = (input) => {
356
- const { logger, netlifyGraphConfig, operationsDocString, schema } = input
499
+ const { config, logger, netlifyGraphConfig, operationsDocString, schema, schemaId } = input
357
500
 
358
501
  writeGraphQLOperationsSourceFile({ logger, netlifyGraphConfig, operationsDocString })
359
- regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
502
+ regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
360
503
 
361
504
  const hash = quickHash(operationsDocString)
362
505
 
@@ -372,6 +515,7 @@ const updateGraphQLOperationsFileFromPersistedDoc = (input) => {
372
515
  /**
373
516
  * Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
374
517
  * @param {object} input
518
+ * @param {object} input.config The parsed netlify.toml config file
375
519
  * @param {string} input.siteId The site id to query against
376
520
  * @param {string} input.schemaId The schema ID to query against
377
521
  * @param {string} input.siteRoot Path to the root of the project
@@ -396,94 +540,186 @@ const handleOperationsLibraryPersistedEvent = async (input) => {
396
540
  updateGraphQLOperationsFileFromPersistedDoc({ ...input, operationsDocString: operationsFileContents })
397
541
  }
398
542
 
543
+ /**
544
+ *
545
+ * @param {object} input
546
+ * @param {any} input.site The site object
547
+ * @param {CliEventHelper.CliEvent} input.event
548
+ * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
549
+ * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
550
+ * @param {object} input.config The parsed netlify.toml config file
551
+ * @param {string} input.docId The GraphQL operations document id to fetch
552
+ * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
553
+ * @param {string} input.schemaId The schemaId for the current session
554
+ * @param {string} input.sessionId The session ID to use for this CLI session (default: read from state)
555
+ * @param {string} input.siteId The site id to query against
556
+ * @param {string} input.siteRoot Path to the root of the project
557
+ * @returns {Promise<void>}
558
+ */
399
559
  const handleCliSessionEvent = async ({
560
+ config,
561
+ docId,
400
562
  event,
401
563
  netlifyGraphConfig,
402
564
  netlifyToken,
403
565
  schema,
566
+ schemaId,
404
567
  sessionId,
568
+ site,
405
569
  siteId,
406
570
  siteRoot,
407
571
  }) => {
408
- const { __typename, payload } = await event
409
- switch (__typename) {
410
- case 'OneGraphNetlifyCliSessionTestEvent':
572
+ switch (event.__typename) {
573
+ case 'OneGraphNetlifyCliSessionTestEvent': {
574
+ const { payload } = event
575
+
411
576
  await handleCliSessionEvent({
577
+ config,
578
+ docId,
412
579
  netlifyToken,
580
+ // @ts-ignore
413
581
  event: payload,
414
582
  netlifyGraphConfig,
415
583
  schema,
584
+ schemaId,
416
585
  sessionId,
417
586
  siteId,
418
587
  siteRoot,
588
+ site,
419
589
  })
590
+
420
591
  break
592
+ }
421
593
  case 'OneGraphNetlifyCliSessionGenerateHandlerEvent': {
422
- if (!payload.operationId && !payload.operation.id) {
594
+ const { payload } = event
595
+
596
+ if (!payload.operationId) {
423
597
  warn(`No operation id found in payload,
424
- ${JSON.stringify(payload, null, 2)}`)
598
+ ${JSON.stringify(payload, null, 2)}`)
425
599
  return
426
600
  }
601
+
602
+ const codegenModule = await getCodegenModule({ config })
603
+ if (!codegenModule) {
604
+ error(
605
+ `No Netlify Graph codegen module specified in netlify.toml under the [graph] header. Please specify 'codeGenerator' field and try again.`,
606
+ )
607
+ return
608
+ }
609
+
610
+ const codeGenerator = await getCodegenFunctionById({ config, id: payload.codegenId })
611
+ if (!codeGenerator) {
612
+ warn(
613
+ `Unable to find Netlify Graph code generator with id "${payload.codegenId}" from ${JSON.stringify(
614
+ payload,
615
+ null,
616
+ 2,
617
+ )}`,
618
+ )
619
+ return
620
+ }
621
+
427
622
  const files = generateHandlerByOperationId({
428
623
  netlifyGraphConfig,
429
624
  schema,
430
- operationId: payload.operationId || payload.operation.id,
625
+ operationId: payload.operationId,
431
626
  handlerOptions: payload,
627
+ generate: codeGenerator.generateHandler,
432
628
  })
433
629
 
434
630
  if (!files) {
435
- warn(`No files generated for operation id: ${payload.operationId || payload.operation.id}`)
631
+ warn(`No files generated for operation id: ${payload.operationId}`)
436
632
  return
437
633
  }
438
634
 
439
- const config = loadNetlifyGraphConfig(siteRoot)
440
- for (const file of files) {
441
- const fileWrittenEvent = {
442
- __typename: 'OneGraphNetlifyCliSessionFileWrittenEvent',
443
- cliSessionId: sessionId,
444
- payload: {
445
- editor: config.editor,
446
- filepath: file.filePath,
447
- audience: 'ui',
448
- },
449
- }
635
+ const editor = process.env.EDITOR || null
450
636
 
451
- try {
452
- const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
453
-
454
- await OneGraphClient.executeCreateCLISessionEventMutation(
455
- {
456
- sessionId,
457
- payload: fileWrittenEvent,
458
- },
459
- { accessToken: graphJwt.jwt },
460
- )
461
- } catch {
462
- warn(`Unable to reach Netlify Graph servers in order to notify handler files written to disk`)
463
- }
637
+ /** @type {CliEventHelper.OneGraphNetlifyCliSessionFilesWrittenEvent} */
638
+ const filesWrittenEvent = {
639
+ id: crypto.randomUUID(),
640
+ createdAt: new Date().toString(),
641
+ __typename: 'OneGraphNetlifyCliSessionFilesWrittenEvent',
642
+ sessionId,
643
+ payload: {
644
+ editor,
645
+ // @ts-expect-error
646
+ files: files.map((file) => ({
647
+ name: file.name,
648
+ filePath: file.filePath,
649
+ })),
650
+ },
651
+ audience: 'UI',
464
652
  }
653
+
654
+ try {
655
+ const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
656
+
657
+ await OneGraphClient.executeCreateCLISessionEventMutation(
658
+ {
659
+ sessionId,
660
+ payload: filesWrittenEvent,
661
+ },
662
+ { accessToken: graphJwt.jwt },
663
+ )
664
+ } catch {
665
+ warn(`Unable to reach Netlify Graph servers in order to notify handler files written to disk`)
666
+ }
667
+
465
668
  break
466
669
  }
467
670
  case 'OneGraphNetlifyCliSessionOpenFileEvent': {
468
- if (!payload.filePath) {
469
- warn(`No filePath found in payload, ${JSON.stringify(payload, null, 2)}`)
671
+ if (!event.payload.filePath) {
672
+ warn(`No filePath found in payload, ${JSON.stringify(event.payload, null, 2)}`)
470
673
  return
471
674
  }
472
- const config = loadNetlifyGraphConfig(siteRoot)
473
- if (config.editor) {
474
- log(`Opening ${config.editor} for ${payload.filePath}`)
475
- execa(config.editor, [payload.filePath], {
675
+
676
+ const editor = process.env.EDITOR || null
677
+
678
+ if (editor) {
679
+ log(`Opening ${editor} for ${event.payload.filePath}`)
680
+ execa(editor, [event.payload.filePath], {
476
681
  preferLocal: true,
477
682
  // windowsHide needs to be false for child process to terminate properly on Windows
478
683
  windowsHide: false,
479
684
  })
480
685
  } else {
481
- warn('No editor found in config')
686
+ warn('No $EDITOR set in env vars')
482
687
  }
483
688
  break
484
689
  }
485
- case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent':
690
+ case 'OneGraphNetlifyCliSessionSetGraphCodegenModuleEvent': {
691
+ setNetlifyTomlCodeGeneratorModule({ codegenModuleImportPath: event.payload.codegenModuleImportPath, siteRoot })
692
+ break
693
+ }
694
+ case 'OneGraphNetlifyCliSessionMetadataRequestEvent': {
695
+ const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
696
+
697
+ const { minimumCliVersionExpected } = event.payload
698
+
699
+ const cliIsOutOfDateForUI =
700
+ version.localeCompare(minimumCliVersionExpected, undefined, { numeric: true, sensitivity: 'base' }) === -1
701
+
702
+ if (cliIsOutOfDateForUI) {
703
+ warn(
704
+ `The Netlify Graph UI expects the netlify-cli to be at least at version "${minimumCliVersionExpected}", but you're running ${version}. You may need to upgrade for a stable experience.`,
705
+ )
706
+ }
707
+
708
+ const input = { config, docId, jwt: graphJwt.jwt, schemaId, sessionId, siteRoot }
709
+ await publishCliSessionMetadataPublishEvent(input)
710
+ break
711
+ }
712
+ case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent': {
713
+ const { payload } = event
714
+
715
+ if (!payload.schemaId || !payload.docId) {
716
+ warn(`Malformed library update event, missing schemaId or docId in payload:
717
+ ${JSON.stringify(event, null, 2)}`)
718
+ break
719
+ }
720
+
486
721
  await handleOperationsLibraryPersistedEvent({
722
+ config,
487
723
  netlifyToken,
488
724
  docId: payload.docId,
489
725
  schemaId: payload.schemaId,
@@ -492,16 +728,17 @@ const handleCliSessionEvent = async ({
492
728
  siteId,
493
729
  siteRoot,
494
730
  })
731
+
495
732
  break
733
+ }
496
734
  default: {
497
735
  warn(
498
- `Unrecognized event received, you may need to upgrade your CLI version: ${__typename}: ${JSON.stringify(
499
- payload,
736
+ `Unrecognized event received, you may need to upgrade your CLI version: ${event.__typename}: ${JSON.stringify(
737
+ event,
500
738
  null,
501
739
  2,
502
740
  )}`,
503
741
  )
504
- break
505
742
  }
506
743
  }
507
744
  }
@@ -531,7 +768,11 @@ const getCLISession = async ({ jwt, oneGraphSessionId, siteId }) => {
531
768
  * @param {string} input.siteId The site object that contains the root file path for the site
532
769
  */
533
770
  const getCLISessionMetadata = async ({ jwt, oneGraphSessionId, siteId }) => {
534
- const { errors, session } = await getCLISession({ jwt, oneGraphSessionId, siteId })
771
+ const result = await getCLISession({ jwt, oneGraphSessionId, siteId })
772
+ if (!result) {
773
+ warn(`Unable to fetch CLI session metadata`)
774
+ }
775
+ const { errors, session } = result
535
776
  return { metadata: session && session.metadata, errors }
536
777
  }
537
778
 
@@ -539,18 +780,50 @@ const getCLISessionMetadata = async ({ jwt, oneGraphSessionId, siteId }) => {
539
780
  * Look at the current project, filesystem, etc. and determine relevant metadata for a cli session
540
781
  * @param {object} input
541
782
  * @param {string} input.siteRoot The root file path for the site
542
- * @returns {Record<string, any>} Any locally detected facts that are relevant to include in the cli session metadata
783
+ * @param {object} input.config The parsed netlify.toml config file
784
+ * @returns {Promise<CliEventHelper.DetectedLocalCLISessionMetadata>} Any locally detected facts that are relevant to include in the cli session metadata
543
785
  */
544
- const detectLocalCLISessionMetadata = ({ siteRoot }) => {
786
+ const detectLocalCLISessionMetadata = async ({ config, siteRoot }) => {
787
+ // @ts-ignore
788
+ const { listFrameworks } = await frameworkInfoPromise
789
+
790
+ /** @type {string | null} */
791
+ let framework = null
792
+
793
+ try {
794
+ const frameworks = await listFrameworks({ projectDir: siteRoot })
795
+ framework = frameworks[0].id || null
796
+ } catch {}
797
+
545
798
  const { branch } = gitRepoInfo()
546
799
  const hostname = os.hostname()
547
800
  const userInfo = os.userInfo({ encoding: 'utf-8' })
548
801
  const { username } = userInfo
549
- const cliVersion = USER_AGENT
802
+ const cliVersion = version
803
+
804
+ const platform = WSL ? 'wsl' : os.platform()
805
+ const arch = os.arch() === 'ia32' ? 'x86' : os.arch()
806
+
807
+ const editor = process.env.EDITOR || null
550
808
 
551
- const config = loadNetlifyGraphConfig(siteRoot)
809
+ /** @type {CodegenHelpers.CodegenModuleMeta | null} */
810
+ let codegen = null
552
811
 
553
- const { editor } = config
812
+ const codegenModule = await getCodegenModule({ config })
813
+
814
+ if (codegenModule) {
815
+ codegen = {
816
+ id: codegenModule.id,
817
+ version: codegenModule.id,
818
+ generators: codegenModule.generators.map((generator) => ({
819
+ id: generator.id,
820
+ name: generator.name,
821
+ options: generator.generateHandlerOptions || null,
822
+ supportedDefinitionTypes: generator.supportedDefinitionTypes,
823
+ version: generator.version,
824
+ })),
825
+ }
826
+ }
554
827
 
555
828
  const detectedMetadata = {
556
829
  gitBranch: branch,
@@ -559,15 +832,68 @@ const detectLocalCLISessionMetadata = ({ siteRoot }) => {
559
832
  siteRoot,
560
833
  cliVersion,
561
834
  editor,
835
+ platform,
836
+ arch,
837
+ nodeVersion: process.version,
838
+ codegen,
839
+ framework,
562
840
  }
563
841
 
564
842
  return detectedMetadata
565
843
  }
566
844
 
845
+ /**
846
+ *
847
+ * @param {object} input
848
+ * @param {string} input.jwt The GraphJWT string
849
+ * @param {string} input.sessionId The id of the cli session to fetch the current metadata for
850
+ * @param {object} input.config The parsed netlify.toml config file
851
+ * @param {string} input.siteRoot Path to the root of the project
852
+ * @param {string} input.docId The GraphQL operations document id to fetch
853
+ * @param {string} input.schemaId The GraphQL schemaId to use when generating code
854
+ */
855
+ const publishCliSessionMetadataPublishEvent = async ({ config, docId, jwt, schemaId, sessionId, siteRoot }) => {
856
+ const detectedMetadata = await detectLocalCLISessionMetadata({ config, siteRoot })
857
+
858
+ /** @type {CliEventHelper.OneGraphNetlifyCliSessionMetadataPublishEvent} */
859
+ const event = {
860
+ __typename: 'OneGraphNetlifyCliSessionMetadataPublishEvent',
861
+ audience: 'UI',
862
+ createdAt: new Date().toString(),
863
+ id: crypto.randomUUID(),
864
+ sessionId,
865
+ payload: {
866
+ cliVersion: detectedMetadata.cliVersion,
867
+ editor: detectedMetadata.editor,
868
+ siteRoot: detectedMetadata.siteRoot,
869
+ siteRootFriendly: detectedMetadata.siteRoot,
870
+ persistedDocId: docId,
871
+ schemaId,
872
+ codegenModule: detectedMetadata.codegen,
873
+ arch: detectedMetadata.arch,
874
+ nodeVersion: detectedMetadata.nodeVersion,
875
+ platform: detectedMetadata.platform,
876
+ framework: detectedMetadata.framework,
877
+ },
878
+ }
879
+
880
+ try {
881
+ await OneGraphClient.executeCreateCLISessionEventMutation(
882
+ {
883
+ sessionId,
884
+ payload: event,
885
+ },
886
+ { accessToken: jwt },
887
+ )
888
+ } catch {
889
+ warn(`Unable to reach Netlify Graph servers in order to publish CLI session data for the Graph UI`)
890
+ }
891
+ }
892
+
567
893
  /**
568
894
  * Fetch the existing cli session metadata if it exists, and mutate it remotely with the passed in metadata
569
895
  * @param {object} input
570
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig The (typically netlify) access token that is used for authentication, if any
896
+ * @param {object} input.config The parsed netlify.toml file
571
897
  * @param {string} input.jwt The Graph JWT string
572
898
  * @param {string} input.oneGraphSessionId The id of the cli session to fetch the current metadata for
573
899
  * @param {string} input.siteId The site object that contains the root file path for the site
@@ -575,21 +901,24 @@ const detectLocalCLISessionMetadata = ({ siteRoot }) => {
575
901
  * @param {object} input.newMetadata The metadata to merge into (with priority) the existing metadata
576
902
  * @returns {Promise<object>}
577
903
  */
578
- const upsertMergeCLISessionMetadata = async ({ jwt, newMetadata, oneGraphSessionId, siteId, siteRoot }) => {
904
+ const upsertMergeCLISessionMetadata = async ({ config, jwt, newMetadata, oneGraphSessionId, siteId, siteRoot }) => {
579
905
  const { errors, metadata } = await getCLISessionMetadata({ jwt, oneGraphSessionId, siteId })
580
906
  if (errors) {
581
907
  warn(`Error fetching cli session metadata: ${JSON.stringify(errors, null, 2)}`)
582
908
  }
583
909
 
584
- const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot })
910
+ const detectedMetadata = await detectLocalCLISessionMetadata({ config, siteRoot })
585
911
 
586
912
  // @ts-ignore
587
913
  const finalMetadata = { ...metadata, ...detectedMetadata, ...newMetadata }
588
914
 
589
- return OneGraphClient.updateCLISessionMetadata(jwt, siteId, oneGraphSessionId, finalMetadata)
915
+ const result = OneGraphClient.updateCLISessionMetadata(jwt, siteId, oneGraphSessionId, finalMetadata)
916
+
917
+ return result
590
918
  }
591
919
 
592
920
  const persistNewOperationsDocForSession = async ({
921
+ config,
593
922
  netlifyGraphConfig,
594
923
  netlifyToken,
595
924
  oneGraphSessionId,
@@ -650,9 +979,11 @@ const persistNewOperationsDocForSession = async ({
650
979
  warn(`Failed to create persisted query for editing, ${JSON.stringify(persistedResult, null, 2)}`)
651
980
  }
652
981
 
982
+ currentPersistedDocId = persistedDoc.id
983
+
653
984
  const newMetadata = { docId: persistedDoc.id }
654
985
  const result = await upsertMergeCLISessionMetadata({
655
- netlifyGraphConfig,
986
+ config,
656
987
  jwt,
657
988
  siteId,
658
989
  oneGraphSessionId,
@@ -660,12 +991,13 @@ const persistNewOperationsDocForSession = async ({
660
991
  siteRoot,
661
992
  })
662
993
 
663
- if (result.errors) {
994
+ if (!result || result.errors) {
664
995
  warn(`Unable to update session metadata with updated operations doc ${JSON.stringify(result.errors, null, 2)}`)
665
996
  } else if (lockfile != null) {
666
997
  // Now that we've persisted the document, lock it in the lockfile
667
998
  const currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
668
999
 
1000
+ /** @type {NetlifyGraphLockfile.V0_format} */
669
1001
  const newLockfile = NetlifyGraphLockfile.createLockfile({
670
1002
  schemaId: lockfile.locked.schemaId,
671
1003
  operationsFileContents: currentOperationsDoc,
@@ -690,6 +1022,7 @@ const loadCLISession = (state) => state.get('oneGraphSessionId')
690
1022
  /**
691
1023
  * Idemponentially save the CLI session id to the local state and start monitoring for CLI events, upstream schema changes, and local operation file changes
692
1024
  * @param {object} input
1025
+ * @param {object} input.config
693
1026
  * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
694
1027
  * @param {string | undefined} input.oneGraphSessionId The session ID to use for this CLI session (default: read from state)
695
1028
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
@@ -697,19 +1030,50 @@ const loadCLISession = (state) => state.get('oneGraphSessionId')
697
1030
  * @param {any} input.site The site object
698
1031
  */
699
1032
  const startOneGraphCLISession = async (input) => {
700
- const { netlifyGraphConfig, netlifyToken, site, state } = input
701
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId: site.id, nfToken: netlifyToken })
702
- OneGraphClient.ensureAppForSite(jwt, site.id)
1033
+ const { config, netlifyGraphConfig, netlifyToken, site, state } = input
1034
+ const getJwt = async () => {
1035
+ const accessToken = await OneGraphClient.getGraphJwtForSite({ siteId: site.id, nfToken: netlifyToken })
1036
+ return accessToken.jwt
1037
+ }
1038
+
1039
+ OneGraphClient.ensureAppForSite(await getJwt(), site.id)
703
1040
 
704
1041
  const oneGraphSessionId = await ensureCLISession({
1042
+ config,
1043
+ netlifyGraphConfig,
705
1044
  metadata: {},
706
1045
  netlifyToken,
707
1046
  site,
708
1047
  state,
709
1048
  oneGraphSessionId: input.oneGraphSessionId,
710
- netlifyGraphConfig,
711
1049
  })
712
1050
 
1051
+ const syncUIHelper = async () => {
1052
+ const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1053
+
1054
+ if (!schemaId) {
1055
+ warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1056
+ return
1057
+ }
1058
+
1059
+ if (!currentPersistedDocId) {
1060
+ warn('Unable to read current persisted Graph library doc id')
1061
+ return
1062
+ }
1063
+
1064
+ const syncSessionMetadataInput = {
1065
+ config,
1066
+ docId: currentPersistedDocId,
1067
+ jwt: await getJwt(),
1068
+ schemaId,
1069
+ sessionId: oneGraphSessionId,
1070
+ siteRoot: site.root,
1071
+ }
1072
+ await publishCliSessionMetadataPublishEvent(syncSessionMetadataInput)
1073
+ }
1074
+
1075
+ await syncUIHelper()
1076
+
713
1077
  const enabledServices = []
714
1078
  const schema = await OneGraphClient.fetchOneGraphSchemaForServices(site.id, enabledServices)
715
1079
 
@@ -717,9 +1081,22 @@ const startOneGraphCLISession = async (input) => {
717
1081
  netlifyGraphConfig,
718
1082
  onChange: async (filePath) => {
719
1083
  log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
720
- regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
1084
+ if (!schema) {
1085
+ warn('Unable to load schema, run graph:pull to update')
1086
+ return
1087
+ }
1088
+
1089
+ const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1090
+
1091
+ if (!schemaId) {
1092
+ warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1093
+ return
1094
+ }
1095
+
1096
+ regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
721
1097
  const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
722
1098
  await persistNewOperationsDocForSession({
1099
+ config,
723
1100
  netlifyGraphConfig,
724
1101
  netlifyToken,
725
1102
  oneGraphSessionId,
@@ -730,9 +1107,22 @@ const startOneGraphCLISession = async (input) => {
730
1107
  },
731
1108
  onAdd: async (filePath) => {
732
1109
  log('NetlifyGraph operation file created at', filePath, 'creating function library...')
733
- regenerateFunctionsFileFromOperationsFile({ netlifyGraphConfig, schema })
1110
+ if (!schema) {
1111
+ warn('Unable to load schema, run graph:pull to update')
1112
+ return
1113
+ }
1114
+
1115
+ const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1116
+
1117
+ if (!schemaId) {
1118
+ warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1119
+ return
1120
+ }
1121
+
1122
+ regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
734
1123
  const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
735
1124
  await persistNewOperationsDocForSession({
1125
+ config,
736
1126
  netlifyGraphConfig,
737
1127
  netlifyToken,
738
1128
  oneGraphSessionId,
@@ -744,6 +1134,7 @@ const startOneGraphCLISession = async (input) => {
744
1134
  })
745
1135
 
746
1136
  const cliEventsCloseFn = monitorCLISessionEvents({
1137
+ config,
747
1138
  appId: site.id,
748
1139
  netlifyToken,
749
1140
  netlifyGraphConfig,
@@ -751,26 +1142,56 @@ const startOneGraphCLISession = async (input) => {
751
1142
  site,
752
1143
  state,
753
1144
  onClose: () => {
754
- log('CLI session closed, stopping monitoring...')
1145
+ log('CLI session closed, stopping monitor...')
1146
+ },
1147
+ onSchemaIdChange: (newSchemaId) => {
1148
+ log('Netlify Graph schemaId changed:', newSchemaId)
755
1149
  },
756
1150
  onEvents: async (events) => {
757
1151
  const ackEventIds = []
758
1152
 
759
1153
  for (const event of events) {
760
- const audience = OneGraphClient.eventAudience(event)
761
- if (audience === 'cli') {
762
- const eventName = OneGraphClient.friendlyEventName(event)
763
- log(`${chalk.magenta('Handling')} Netlify Graph: ${eventName}...`)
764
- await handleCliSessionEvent({
765
- netlifyToken,
766
- event,
767
- netlifyGraphConfig,
768
- schema,
769
- sessionId: oneGraphSessionId,
770
- siteId: site.id,
771
- siteRoot: site.root,
772
- })
773
- log(`${chalk.green('Finished handling')} Netlify Graph: ${eventName}...`)
1154
+ try {
1155
+ const audience = event.audience || OneGraphClient.eventAudience(event)
1156
+ if (audience === 'CLI') {
1157
+ ackEventIds.push(event.id)
1158
+ const eventName = OneGraphClient.friendlyEventName(event)
1159
+ log(`${chalk.magenta('Handling')} Netlify Graph: ${eventName}...`)
1160
+ const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1161
+
1162
+ if (!schemaId) {
1163
+ warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1164
+ return
1165
+ }
1166
+
1167
+ if (!schema) {
1168
+ warn('Unable to load schema from for Netlify Graph, run graph:pull to update')
1169
+ return
1170
+ }
1171
+
1172
+ if (!currentPersistedDocId) {
1173
+ warn('Unable to read current persisted Graph library doc id')
1174
+ return
1175
+ }
1176
+
1177
+ await handleCliSessionEvent({
1178
+ config,
1179
+ docId: currentPersistedDocId,
1180
+ netlifyToken,
1181
+ event,
1182
+ netlifyGraphConfig,
1183
+ schema,
1184
+ schemaId,
1185
+ sessionId: oneGraphSessionId,
1186
+ site,
1187
+ siteId: site.id,
1188
+ siteRoot: site.root,
1189
+ })
1190
+ log(`${chalk.green('Finished handling')} Netlify Graph: ${eventName}...`)
1191
+ }
1192
+ } catch (error_) {
1193
+ warn(`Error processing individual Netlify Graph event, skipping:
1194
+ ${JSON.stringify(error_, null, 2)}`)
774
1195
  ackEventIds.push(event.id)
775
1196
  }
776
1197
  }
@@ -799,7 +1220,7 @@ const startOneGraphCLISession = async (input) => {
799
1220
  const markCliSessionInactive = async ({ netlifyToken, sessionId, siteId }) => {
800
1221
  const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
801
1222
  const result = await OneGraphClient.executeMarkCliSessionInactive(jwt, siteId, sessionId)
802
- if (result.errors) {
1223
+ if (!result || result.errors) {
803
1224
  warn(`Unable to mark CLI session ${sessionId} inactive: ${JSON.stringify(result.errors, null, 2)}`)
804
1225
  }
805
1226
  }
@@ -843,6 +1264,7 @@ const idempotentlyUpdateSessionSchemaIdFromLockfile = async (input) => {
843
1264
  /**
844
1265
  * Ensures a cli session exists for the current checkout, or errors out if it doesn't and cannot create one.
845
1266
  * @param {object} input
1267
+ * @param {object} input.config The parsed netlify.toml config file
846
1268
  * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
847
1269
  * @param {object} input.metadata
848
1270
  * @param {string} input.netlifyToken
@@ -851,7 +1273,7 @@ const idempotentlyUpdateSessionSchemaIdFromLockfile = async (input) => {
851
1273
  * @param {any} input.site The site object
852
1274
  */
853
1275
  const ensureCLISession = async (input) => {
854
- const { metadata, netlifyGraphConfig, netlifyToken, site, state } = input
1276
+ const { config, metadata, netlifyGraphConfig, netlifyToken, site, state } = input
855
1277
  let oneGraphSessionId = input.oneGraphSessionId ? input.oneGraphSessionId : loadCLISession(state)
856
1278
  let parentCliSessionId = null
857
1279
  const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId: site.id, nfToken: netlifyToken })
@@ -867,6 +1289,7 @@ const ensureCLISession = async (input) => {
867
1289
  sessionId: oneGraphSessionId,
868
1290
  desiredEventCount: 0,
869
1291
  })
1292
+
870
1293
  if (errors) {
871
1294
  warn(`Unable to fetch cli session: ${JSON.stringify(errors, null, 2)}`)
872
1295
  log(`Creating new cli session`)
@@ -874,6 +1297,10 @@ const ensureCLISession = async (input) => {
874
1297
  oneGraphSessionId = null
875
1298
  }
876
1299
 
1300
+ if (session && session.metadata && session.metadata.docId) {
1301
+ currentPersistedDocId = session.metadata.docId
1302
+ }
1303
+
877
1304
  // During the transition to lockfiles, write a lockfile if one isn't
878
1305
  // found. Later, only handling a 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent'
879
1306
  // will create or update the lockfile
@@ -896,10 +1323,22 @@ const ensureCLISession = async (input) => {
896
1323
  oneGraphSessionId = null
897
1324
  }
898
1325
 
899
- if (!oneGraphSessionId) {
1326
+ if (oneGraphSessionId) {
1327
+ await upsertMergeCLISessionMetadata({
1328
+ jwt,
1329
+ config,
1330
+ newMetadata: {},
1331
+ oneGraphSessionId,
1332
+ siteId: site.id,
1333
+ siteRoot: site.root,
1334
+ })
1335
+ } else {
900
1336
  // If we can't access the session in the state.json or it doesn't exist, create a new one
901
1337
  const sessionName = generateSessionName()
902
- const detectedMetadata = detectLocalCLISessionMetadata({ siteRoot: site.root })
1338
+ const detectedMetadata = await detectLocalCLISessionMetadata({
1339
+ config,
1340
+ siteRoot: site.root,
1341
+ })
903
1342
  const newSessionMetadata = parentCliSessionId ? { parentCliSessionId } : {}
904
1343
 
905
1344
  const sessionMetadata = {
@@ -920,7 +1359,12 @@ const ensureCLISession = async (input) => {
920
1359
  metadata: sessionMetadata,
921
1360
  })
922
1361
 
923
- oneGraphSessionId = oneGraphSession.id
1362
+ if (oneGraphSession) {
1363
+ // @ts-expect-error
1364
+ oneGraphSessionId = oneGraphSession.id
1365
+ } else {
1366
+ warn('Unable to load Netlify Graph session, please report this to Netlify support')
1367
+ }
924
1368
  }
925
1369
 
926
1370
  if (!oneGraphSessionId) {
@@ -946,6 +1390,7 @@ const OneGraphCliClient = {
946
1390
  executeCreatePersistedQueryMutation: OneGraphClient.executeCreatePersistedQueryMutation,
947
1391
  executeCreateApiTokenMutation: OneGraphClient.executeCreateApiTokenMutation,
948
1392
  fetchCliSessionEvents: OneGraphClient.fetchCliSessionEvents,
1393
+ fetchCliSessionSchema,
949
1394
  ensureAppForSite: OneGraphClient.ensureAppForSite,
950
1395
  updateCLISessionMetadata: OneGraphClient.updateCLISessionMetadata,
951
1396
  getGraphJwtForSite: OneGraphClient.getGraphJwtForSite,
@@ -954,6 +1399,7 @@ const OneGraphCliClient = {
954
1399
  module.exports = {
955
1400
  OneGraphCliClient,
956
1401
  createCLISession,
1402
+ currentPersistedDocId,
957
1403
  ensureCLISession,
958
1404
  extractFunctionsFromOperationDoc,
959
1405
  handleCliSessionEvent,
@@ -962,6 +1408,7 @@ module.exports = {
962
1408
  markCliSessionInactive,
963
1409
  monitorCLISessionEvents,
964
1410
  persistNewOperationsDocForSession,
1411
+ readSchemaIdFromLockfile,
965
1412
  refetchAndGenerateFromOneGraph,
966
1413
  startOneGraphCLISession,
967
1414
  upsertMergeCLISessionMetadata,