netlify-cli 14.4.0 → 15.0.0

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,1392 +0,0 @@
1
- // @ts-check
2
- /* eslint-disable eslint-comments/disable-enable-pair */
3
- /* eslint-disable fp/no-loops */
4
- /* eslint-disable no-underscore-dangle */
5
- import crypto from 'crypto'
6
- import { readFileSync, writeFileSync } from 'fs'
7
- import os from 'os'
8
- import path from 'path'
9
- import process from 'process'
10
-
11
- import { listFrameworks } from '@netlify/framework-info'
12
- import gitRepoInfo from 'git-repo-info'
13
- import WSL from 'is-wsl'
14
- import { GraphQL, InternalConsole, NetlifyGraph, NetlifyGraphLockfile, OneGraphClient } from 'netlify-onegraph-internal'
15
-
16
- import { chalk, error, log, warn, watchDebounced } from '../../utils/command-helpers.mjs'
17
- import execa from '../../utils/execa.mjs'
18
- import getPackageJson from '../../utils/get-package-json.mjs'
19
-
20
- import {
21
- generateFunctionsFile,
22
- generateHandlerByOperationId,
23
- getCodegenFunctionById,
24
- getCodegenModule,
25
- normalizeOperationsDoc,
26
- readGraphQLOperationsSourceFile,
27
- setNetlifyTomlCodeGeneratorModule,
28
- writeGraphQLOperationsSourceFile,
29
- writeGraphQLSchemaFile,
30
- } from './cli-netlify-graph.mjs'
31
-
32
- const { parse } = GraphQL
33
- const { defaultExampleOperationsDoc, extractFunctionsFromOperationDoc } = NetlifyGraph
34
-
35
- const { version } = await getPackageJson()
36
-
37
- const internalConsole = {
38
- log,
39
- warn,
40
- error,
41
- debug: console.debug,
42
- }
43
-
44
- /** @type {string | null} */
45
- // eslint-disable-next-line import/no-mutable-exports
46
- let currentPersistedDocId = null
47
-
48
- /**
49
- * Keep track of which document hashes we've received from the server so we can ignore events from the filesystem based on them
50
- */
51
- const witnessedIncomingDocumentHashes = []
52
-
53
- InternalConsole.registerConsole(internalConsole)
54
-
55
- /**
56
- * Start polling for CLI events for a given session to process locally
57
- * @param {object} input
58
- * @param {string} input.appId The app to query against, typically the siteId
59
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
60
- * @param {object} input.config The parsed netlify.toml file
61
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
62
- * @param {function} input.onClose A function to call when the polling loop is closed
63
- * @param {function} input.onError A function to call when an error occurs
64
- * @param {function} input.onEvents A function to call when CLI events are received and need to be processed
65
- * @param {function} input.onSchemaIdChange A function to call when the CLI schemaId has changed
66
- * @param {string} input.sessionId The session id to monitor CLI events for
67
- * @param {import('../../utils/state-config.mjs').default} input.state A function to call to set/get the current state of the local Netlify project
68
- * @param {any} input.site The site object
69
- * @returns
70
- */
71
- export const monitorCLISessionEvents = (input) => {
72
- const { appId, config, netlifyGraphConfig, netlifyToken, onClose, onError, onEvents, site, state } = input
73
- const currentSessionId = input.sessionId
74
- // TODO (sg): Track changing schemaId for a session
75
-
76
- const frequency = 5000
77
- // 30 minutes
78
- const defaultHeartbeatFrequency = 30_000
79
- let shouldClose = false
80
- let nextMarkActiveHeartbeat = defaultHeartbeatFrequency
81
-
82
- const markActiveHelper = async () => {
83
- try {
84
- const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
85
- const fullSession = await OneGraphClient.fetchCliSession({
86
- jwt: graphJwt.jwt,
87
- appId,
88
- sessionId: currentSessionId,
89
- })
90
-
91
- const heartbeatIntervalms = fullSession.session.cliHeartbeatIntervalMs || defaultHeartbeatFrequency
92
- nextMarkActiveHeartbeat = heartbeatIntervalms
93
-
94
- const markCLISessionActiveResult = await OneGraphClient.executeMarkCliSessionActiveHeartbeat(
95
- graphJwt.jwt,
96
- site.id,
97
- currentSessionId,
98
- )
99
- if (markCLISessionActiveResult.errors && markCLISessionActiveResult.errors.length !== 0) {
100
- warn(`Failed to mark CLI session active: ${markCLISessionActiveResult.errors.join(', ')}`)
101
- }
102
- } catch {
103
- warn(`Unable to reach Netlify Graph servers in order to mark CLI session active`)
104
- }
105
- setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
106
- }
107
-
108
- setTimeout(markActiveHelper, nextMarkActiveHeartbeat)
109
-
110
- const enabledServiceWatcher = async (jwt, { appId: siteId, sessionId }) => {
111
- const enabledServices = state.get('oneGraphEnabledServices') || ['onegraph']
112
-
113
- try {
114
- const graphQLSchemaInfo = await OneGraphClient.fetchGraphQLSchemaForSession(jwt, siteId, input.sessionId)
115
- if (!graphQLSchemaInfo) {
116
- warn('Unable to fetch enabled services for site for code generation')
117
- return
118
- }
119
- const newEnabledServices = graphQLSchemaInfo.services.map((service) => service.graphQLField)
120
- const enabledServicesCompareKey = enabledServices.sort().join(',')
121
- const newEnabledServicesCompareKey = newEnabledServices.sort().join(',')
122
-
123
- if (enabledServicesCompareKey !== newEnabledServicesCompareKey) {
124
- log(
125
- `${chalk.magenta(
126
- 'Reloading',
127
- )} Netlify Graph schema..., ${enabledServicesCompareKey} => ${newEnabledServicesCompareKey}`,
128
- )
129
-
130
- const schemaId = graphQLSchemaInfo.id
131
-
132
- if (!schemaId) {
133
- warn(`Unable to read schemaId from Netlify Graph session, not regenerating code`)
134
- return
135
- }
136
-
137
- mergeLockfile({ siteRoot: site.root, schemaId })
138
-
139
- await refetchAndGenerateFromOneGraph({ config, netlifyGraphConfig, state, jwt, schemaId, siteId, sessionId })
140
- log(`${chalk.green('Reloaded')} Netlify Graph schema and regenerated functions`)
141
- }
142
- } catch {
143
- warn(`Unable to reach Netlify Graph servers in order to fetch enabled Graph services`)
144
- }
145
- }
146
-
147
- const close = () => {
148
- shouldClose = true
149
- }
150
-
151
- let handle
152
-
153
- const helper = async () => {
154
- try {
155
- if (shouldClose) {
156
- clearTimeout(handle)
157
- onClose && onClose()
158
- return
159
- }
160
-
161
- const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId: appId, nfToken: netlifyToken })
162
- const next = await OneGraphClient.fetchCliSessionEvents({ appId, jwt: graphJwt.jwt, sessionId: currentSessionId })
163
-
164
- if (next && next.errors) {
165
- next.errors.forEach((fetchEventError) => {
166
- onError(fetchEventError)
167
- })
168
- }
169
-
170
- const events = (next && next.events) || []
171
-
172
- if (events.length !== 0) {
173
- let ackIds = []
174
- try {
175
- ackIds = await onEvents(events)
176
- } catch (eventHandlerError) {
177
- warn(`Error handling event: ${eventHandlerError}`)
178
- } finally {
179
- await OneGraphClient.ackCLISessionEvents({
180
- appId,
181
- jwt: graphJwt.jwt,
182
- sessionId: currentSessionId,
183
- eventIds: ackIds,
184
- })
185
- }
186
- }
187
-
188
- await enabledServiceWatcher(graphJwt.jwt, { appId, sessionId: currentSessionId })
189
- } catch {
190
- warn(`Unable to reach Netlify Graph servers in order to sync Graph session`)
191
- }
192
-
193
- handle = setTimeout(helper, frequency)
194
- }
195
-
196
- // Fire immediately to start rather than waiting the initial `frequency`
197
- helper()
198
-
199
- return close
200
- }
201
-
202
- /**
203
- * Monitor the operations document for changes
204
- * @param {object} input
205
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
206
- * @param {() => void} input.onAdd A callback function to handle when the operations document is added
207
- * @param {() => void} input.onChange A callback function to handle when the operations document is changed
208
- * @param {() => void=} input.onUnlink A callback function to handle when the operations document is unlinked
209
- * @returns {Promise<any>}
210
- */
211
- const monitorOperationFile = async ({ netlifyGraphConfig, onAdd, onChange, onUnlink }) => {
212
- if (!netlifyGraphConfig.graphQLOperationsSourceFilename) {
213
- error('Please configure `graphQLOperationsSourceFilename` in your `netlify.toml` [graph] section')
214
- }
215
-
216
- const filePath = path.resolve(...(netlifyGraphConfig.graphQLOperationsSourceFilename || []))
217
- const newWatcher = await watchDebounced([filePath], {
218
- depth: 1,
219
- onAdd,
220
- onChange,
221
- onUnlink,
222
- })
223
-
224
- return newWatcher
225
- }
226
-
227
- /**
228
- * Fetch the schema for a site, and regenerate all of the downstream files
229
- * @param {object} input
230
- * @param {string} input.siteId The id of the site to query against
231
- * @param {string} input.jwt The Graph JWT
232
- * @param {string} input.sessionId The session ID for the current session
233
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
234
- * @param {import('../../utils/state-config.mjs').default} input.state A function to call to set/get the current state of the local Netlify project
235
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
236
- * @returns {Promise<Record<string, unknown> | undefined>}
237
- */
238
- const fetchCliSessionSchema = async (input) => {
239
- const { jwt, siteId } = input
240
-
241
- await OneGraphClient.ensureAppForSite(jwt, siteId)
242
-
243
- const schemaInfo = await OneGraphClient.fetchNetlifySessionSchemaQuery(
244
- { sessionId: input.sessionId },
245
- {
246
- accessToken: jwt,
247
- siteId,
248
- },
249
- )
250
-
251
- if (!schemaInfo) {
252
- warn('Unable to fetch schema for session')
253
- return
254
- }
255
-
256
- try {
257
- const schemaMetadata = schemaInfo.data.oneGraph.netlifyCliSession.graphQLSchema
258
- return schemaMetadata
259
- } catch {}
260
- }
261
-
262
- /**
263
- * Fetch the schema for a site, and regenerate all of the downstream files
264
- * @param {object} input
265
- * @param {string} input.siteId The id of the site to query against
266
- * @param {string} input.jwt The Graph JWT
267
- * @param {object} input.config The parsed netlify.toml file
268
- * @param {string} input.sessionId The session ID for the current session
269
- * @param {string} input.schemaId The schemaId for the current session
270
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
271
- * @param {import('../../utils/state-config.mjs').default} input.state A function to call to set/get the current state of the local Netlify project
272
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
273
- * @returns {Promise<void>}
274
- */
275
- export const refetchAndGenerateFromOneGraph = async (input) => {
276
- const { config, jwt, logger, netlifyGraphConfig, schemaId, siteId, state } = input
277
-
278
- await OneGraphClient.ensureAppForSite(jwt, siteId)
279
-
280
- const graphQLSchemaInfo = await OneGraphClient.fetchGraphQLSchemaForSession(jwt, siteId, input.sessionId)
281
- if (!graphQLSchemaInfo) {
282
- warn('Unable to fetch schema info for site for code generation')
283
- return
284
- }
285
-
286
- const enabledServices = graphQLSchemaInfo.services
287
- .map((service) => service.graphQLField)
288
- .sort((aString, bString) => aString.localeCompare(bString))
289
-
290
- const schema = await OneGraphClient.fetchOneGraphSchemaById({
291
- siteId,
292
- schemaId: graphQLSchemaInfo.id,
293
- accessToken: jwt,
294
- })
295
-
296
- if (!schema) {
297
- error('Unable to fetch schema from Netlify Graph')
298
- }
299
-
300
- let currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
301
- if (currentOperationsDoc.trim().length === 0) {
302
- currentOperationsDoc = defaultExampleOperationsDoc
303
- }
304
-
305
- const parsedDoc = parse(currentOperationsDoc)
306
- const { fragments, functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
307
-
308
- if (!schema) {
309
- warn('Unable to parse schema, please run graph:pull to update')
310
- return
311
- }
312
-
313
- await generateFunctionsFile({
314
- config,
315
- logger,
316
- netlifyGraphConfig,
317
- schema,
318
- operationsDoc: currentOperationsDoc,
319
- functions,
320
- fragments,
321
- schemaId,
322
- })
323
- writeGraphQLSchemaFile({ logger, netlifyGraphConfig, schema })
324
- state.set('oneGraphEnabledServices', enabledServices)
325
- }
326
-
327
- /**
328
- * Regenerate the function library based on the current operations document on disk
329
- * @param {object} input
330
- * @param {object} input.config The parsed netlify.toml file
331
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
332
- * @param {string} input.schemaId The GraphQL schemaId to use when generating code
333
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
334
- * @returns
335
- */
336
- const regenerateFunctionsFileFromOperationsFile = (input) => {
337
- const { config, netlifyGraphConfig, schema, schemaId } = input
338
-
339
- const appOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
340
-
341
- const hash = quickHash(appOperationsDoc)
342
-
343
- if (witnessedIncomingDocumentHashes.includes(hash)) {
344
- // We've already seen this document, so don't regenerate
345
- return
346
- }
347
-
348
- const parsedDoc = parse(appOperationsDoc, {
349
- noLocation: true,
350
- })
351
- const { fragments, functions } = extractFunctionsFromOperationDoc(GraphQL, parsedDoc)
352
- generateFunctionsFile({
353
- config,
354
- netlifyGraphConfig,
355
- schema,
356
- operationsDoc: appOperationsDoc,
357
- functions,
358
- fragments,
359
- schemaId,
360
- })
361
- }
362
-
363
- /**
364
- * Lockfile Operations
365
- */
366
-
367
- /**
368
- * Read the Netlify Graph lockfile from disk, if it exists
369
- * @param {object} input
370
- * @param {string} input.siteRoot The GraphQL schema to use when generating code
371
- * @return {NetlifyGraphLockfile.V0_format | undefined}
372
- */
373
- export const readLockfile = ({ siteRoot }) => {
374
- try {
375
- const buf = readFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName))
376
- return JSON.parse(buf.toString('utf8'))
377
- } catch {}
378
- }
379
-
380
- /**
381
- * Persist the Netlify Graph lockfile on disk
382
- * @param {object} input
383
- * @param {string} input.siteRoot The GraphQL schema to use when generating code
384
- * @param {NetlifyGraphLockfile.V0_format} input.lockfile
385
- */
386
- const writeLockfile = ({ lockfile, siteRoot }) => {
387
- writeFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName), JSON.stringify(lockfile, null, 2))
388
- }
389
-
390
- /**
391
- * Persist the Netlify Graph lockfile on disk
392
- * @param {object} input
393
- * @param {string} input.siteRoot The GraphQL schema to use when generating code
394
- * @param {string=} input.schemaId
395
- * @param {string=} input.operationsHash
396
- */
397
- const mergeLockfile = ({ operationsHash, schemaId, siteRoot }) => {
398
- const lockfile = readLockfile({ siteRoot })
399
- if (lockfile) {
400
- /** @type {NetlifyGraphLockfile.V0_format} */
401
- const newLockfile = {
402
- ...lockfile,
403
- locked: {
404
- ...lockfile.locked,
405
- },
406
- }
407
-
408
- if (operationsHash) {
409
- newLockfile.locked.operationsHash = operationsHash
410
- }
411
-
412
- if (schemaId) {
413
- newLockfile.locked.schemaId = schemaId
414
- }
415
-
416
- writeFileSync(path.join(siteRoot, NetlifyGraphLockfile.defaultLockFileName), JSON.stringify(newLockfile, null, 2))
417
- }
418
- }
419
-
420
- /**
421
- * Read the Netlify Graph schemaId from the lockfile on disk, if it exists
422
- * @param {object} input
423
- * @param {string} input.siteRoot The GraphQL schema to use when generating code
424
- * @return {string | undefined}
425
- */
426
- export const readSchemaIdFromLockfile = ({ siteRoot }) => {
427
- try {
428
- const lockfile = readLockfile({ siteRoot })
429
- return lockfile && lockfile.locked.schemaId
430
- } catch {}
431
- }
432
-
433
- /**
434
- * Compute a md5 hash of a string
435
- * @param {string} input String to compute a quick md5 hash for
436
- * @returns hex digest of the input string
437
- */
438
- const quickHash = (input) => {
439
- const hashSum = crypto.createHash('md5')
440
- hashSum.update(input)
441
- return hashSum.digest('hex')
442
- }
443
-
444
- /**
445
- * Fetch a persisted operations doc by its id, normalize it for Netlify Graph
446
- * and return its contents as a string
447
- * @param {object} input
448
- * @param {string} input.siteId The site id to query against
449
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
450
- * @param {string} input.docId The GraphQL operations document id to fetch
451
- * @param {object} input.config The parsed netlify.toml file
452
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
453
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
454
- * @param {string} input.schemaId The GraphQL schemaId to use when generating code
455
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
456
- * @returns {Promise<string | undefined>}
457
- */
458
- const fetchGraphQLOperationsLibraryFromPersistedDoc = async (input) => {
459
- try {
460
- const { docId, netlifyToken, siteId } = input
461
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
462
- const persistedDoc = await OneGraphClient.fetchPersistedQuery(jwt, siteId, docId)
463
- if (!persistedDoc) {
464
- warn(`No persisted doc found for: ${docId}`)
465
- return
466
- }
467
-
468
- // Sorts the operations stably, prepends the @netlify directive, etc.
469
- const operationsDocString = normalizeOperationsDoc(GraphQL, persistedDoc.query)
470
-
471
- currentPersistedDocId = docId
472
-
473
- return operationsDocString
474
- } catch {
475
- warn(`Unable to reach Netlify Graph servers in order to update Graph operations file`)
476
- }
477
- }
478
-
479
- /**
480
- * Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
481
- * @param {object} input
482
- * @param {object} input.config The parsed netlify.toml config file
483
- * @param {string} input.operationsDocString The contents of the GraphQL operations document
484
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
485
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
486
- * @param {string} input.schemaId The GraphQL schemaId to use when generating code
487
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
488
- * @returns
489
- */
490
- const updateGraphQLOperationsFileFromPersistedDoc = (input) => {
491
- const { config, logger, netlifyGraphConfig, operationsDocString, schema, schemaId } = input
492
-
493
- writeGraphQLOperationsSourceFile({ logger, netlifyGraphConfig, operationsDocString })
494
- regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
495
-
496
- const hash = quickHash(operationsDocString)
497
-
498
- const relevantHasLength = 10
499
-
500
- if (witnessedIncomingDocumentHashes.length > relevantHasLength) {
501
- witnessedIncomingDocumentHashes.shift()
502
- }
503
-
504
- witnessedIncomingDocumentHashes.push(hash)
505
- }
506
-
507
- /**
508
- * Fetch a persisted operations doc by its id, write it to the system, and regenerate the library
509
- * @param {object} input
510
- * @param {object} input.config The parsed netlify.toml config file
511
- * @param {string} input.siteId The site id to query against
512
- * @param {string} input.schemaId The schema ID to query against
513
- * @param {string} input.siteRoot Path to the root of the project
514
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
515
- * @param {string} input.docId The GraphQL operations document id to fetch
516
- * @param {(message: string) => void=} input.logger A function that if provided will be used to log messages
517
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
518
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
519
- * @returns {Promise<string | undefined>}
520
- */
521
- const handleOperationsLibraryPersistedEvent = async (input) => {
522
- const { schemaId, siteRoot } = input
523
- const operationsFileContents = await fetchGraphQLOperationsLibraryFromPersistedDoc(input)
524
-
525
- if (!operationsFileContents) {
526
- // `fetch` already warned
527
- return
528
- }
529
-
530
- const lockfile = NetlifyGraphLockfile.createLockfile({ operationsFileContents, schemaId })
531
- writeLockfile({ siteRoot, lockfile })
532
- updateGraphQLOperationsFileFromPersistedDoc({ ...input, operationsDocString: operationsFileContents })
533
- }
534
-
535
- /**
536
- *
537
- * @param {object} input
538
- * @param {any} input.site The site object
539
- * @param {import('netlify-onegraph-internal').CliEventHelper.CliEvent} input.event
540
- * @param {GraphQL.GraphQLSchema} input.schema The GraphQL schema to use when generating code
541
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
542
- * @param {object} input.config The parsed netlify.toml config file
543
- * @param {string} input.docId The GraphQL operations document id to fetch
544
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
545
- * @param {string} input.schemaId The schemaId for the current session
546
- * @param {string} input.sessionId The session ID to use for this CLI session (default: read from state)
547
- * @param {string} input.siteId The site id to query against
548
- * @param {string} input.siteRoot Path to the root of the project
549
- * @returns {Promise<void>}
550
- */
551
- export const handleCliSessionEvent = async ({
552
- config,
553
- docId,
554
- event,
555
- netlifyGraphConfig,
556
- netlifyToken,
557
- schema,
558
- schemaId,
559
- sessionId,
560
- site,
561
- siteId,
562
- siteRoot,
563
- }) => {
564
- switch (event.__typename) {
565
- case 'OneGraphNetlifyCliSessionTestEvent': {
566
- const { payload } = event
567
-
568
- await handleCliSessionEvent({
569
- config,
570
- docId,
571
- netlifyToken,
572
- // @ts-ignore
573
- event: payload,
574
- netlifyGraphConfig,
575
- schema,
576
- schemaId,
577
- sessionId,
578
- siteId,
579
- siteRoot,
580
- site,
581
- })
582
-
583
- break
584
- }
585
- case 'OneGraphNetlifyCliSessionGenerateHandlerEvent': {
586
- const { payload } = event
587
-
588
- if (!payload.operationId) {
589
- warn(`No operation id found in payload,
590
- ${JSON.stringify(payload, null, 2)}`)
591
- return
592
- }
593
-
594
- const codegenModule = await getCodegenModule({ config })
595
- if (!codegenModule) {
596
- error(
597
- `No Netlify Graph codegen module specified in netlify.toml under the [graph] header. Please specify 'codeGenerator' field and try again.`,
598
- )
599
- return
600
- }
601
-
602
- const codeGenerator = await getCodegenFunctionById({ config, id: payload.codegenId })
603
- if (!codeGenerator) {
604
- warn(
605
- `Unable to find Netlify Graph code generator with id "${payload.codegenId}" from ${JSON.stringify(
606
- payload,
607
- null,
608
- 2,
609
- )}`,
610
- )
611
- return
612
- }
613
-
614
- const files = await generateHandlerByOperationId({
615
- netlifyGraphConfig,
616
- schema,
617
- operationId: payload.operationId,
618
- handlerOptions: payload,
619
- generate: codeGenerator.generateHandler,
620
- })
621
-
622
- if (!files) {
623
- warn(`No files generated for operation id: ${payload.operationId}`)
624
- return
625
- }
626
-
627
- const editor = process.env.EDITOR || null
628
-
629
- /** @type {import('netlify-onegraph-internal').CliEventHelper.OneGraphNetlifyCliSessionFilesWrittenEvent} */
630
- const filesWrittenEvent = {
631
- id: crypto.randomUUID(),
632
- createdAt: new Date().toString(),
633
- __typename: 'OneGraphNetlifyCliSessionFilesWrittenEvent',
634
- sessionId,
635
- payload: {
636
- editor,
637
- // @ts-expect-error
638
- files: files.map((file) => ({
639
- name: file.name,
640
- filePath: file.filePath,
641
- })),
642
- },
643
- audience: 'UI',
644
- }
645
-
646
- try {
647
- const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
648
-
649
- await OneGraphClient.executeCreateCLISessionEventMutation(
650
- {
651
- sessionId,
652
- payload: filesWrittenEvent,
653
- },
654
- { accessToken: graphJwt.jwt },
655
- )
656
- } catch {
657
- warn(`Unable to reach Netlify Graph servers in order to notify handler files written to disk`)
658
- }
659
-
660
- break
661
- }
662
- case 'OneGraphNetlifyCliSessionOpenFileEvent': {
663
- if (!event.payload.filePath) {
664
- warn(`No filePath found in payload, ${JSON.stringify(event.payload, null, 2)}`)
665
- return
666
- }
667
-
668
- const editor = process.env.EDITOR || null
669
-
670
- if (editor) {
671
- log(`Opening ${editor} for ${event.payload.filePath}`)
672
- execa(editor, [event.payload.filePath], {
673
- preferLocal: true,
674
- // windowsHide needs to be false for child process to terminate properly on Windows
675
- windowsHide: false,
676
- })
677
- } else {
678
- warn('No $EDITOR set in env vars')
679
- }
680
- break
681
- }
682
- case 'OneGraphNetlifyCliSessionSetGraphCodegenModuleEvent': {
683
- setNetlifyTomlCodeGeneratorModule({ codegenModuleImportPath: event.payload.codegenModuleImportPath, siteRoot })
684
- break
685
- }
686
- case 'OneGraphNetlifyCliSessionMetadataRequestEvent': {
687
- const graphJwt = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
688
-
689
- const { minimumCliVersionExpected } = event.payload
690
-
691
- const cliIsOutOfDateForUI =
692
- version.localeCompare(minimumCliVersionExpected, undefined, { numeric: true, sensitivity: 'base' }) === -1
693
-
694
- if (cliIsOutOfDateForUI) {
695
- warn(
696
- `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.`,
697
- )
698
- }
699
-
700
- const input = { config, docId, jwt: graphJwt.jwt, schemaId, sessionId, siteRoot }
701
- await publishCliSessionMetadataPublishEvent(input)
702
- break
703
- }
704
- case 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent': {
705
- const { payload } = event
706
-
707
- if (!payload.schemaId || !payload.docId) {
708
- warn(`Malformed library update event, missing schemaId or docId in payload:
709
- ${JSON.stringify(event, null, 2)}`)
710
- break
711
- }
712
-
713
- await handleOperationsLibraryPersistedEvent({
714
- config,
715
- netlifyToken,
716
- docId: payload.docId,
717
- schemaId: payload.schemaId,
718
- netlifyGraphConfig,
719
- schema,
720
- siteId,
721
- siteRoot,
722
- })
723
-
724
- break
725
- }
726
- default: {
727
- warn(
728
- `Unrecognized event received, you may need to upgrade your CLI version: ${event.__typename}: ${JSON.stringify(
729
- event,
730
- null,
731
- 2,
732
- )}`,
733
- )
734
- }
735
- }
736
- }
737
-
738
- /**
739
- *
740
- * @param {object} input
741
- * @param {string} input.jwt The GraphJWT string
742
- * @param {string} input.oneGraphSessionId The id of the cli session to fetch the current metadata for
743
- * @param {object} input.siteId The site object that contains the root file path for the site
744
- */
745
- const getCLISession = async ({ jwt, oneGraphSessionId, siteId }) => {
746
- const input = {
747
- appId: siteId,
748
- sessionId: oneGraphSessionId,
749
- jwt,
750
- desiredEventCount: 1,
751
- }
752
- return await OneGraphClient.fetchCliSession(input)
753
- }
754
-
755
- /**
756
- *
757
- * @param {object} input
758
- * @param {string} input.jwt The GraphJWT string
759
- * @param {string} input.oneGraphSessionId The id of the cli session to fetch the current metadata for
760
- * @param {string} input.siteId The site object that contains the root file path for the site
761
- */
762
- const getCLISessionMetadata = async ({ jwt, oneGraphSessionId, siteId }) => {
763
- const result = await getCLISession({ jwt, oneGraphSessionId, siteId })
764
- if (!result) {
765
- warn(`Unable to fetch CLI session metadata`)
766
- }
767
- const { errors, session } = result
768
- return { metadata: session && session.metadata, errors }
769
- }
770
-
771
- /**
772
- * Look at the current project, filesystem, etc. and determine relevant metadata for a cli session
773
- * @param {object} input
774
- * @param {string} input.siteRoot The root file path for the site
775
- * @returns {Promise<import('netlify-onegraph-internal').CliEventHelper.DetectedLocalCLISessionMetadata>} Any locally detected facts that are relevant to include in the cli session metadata
776
- */
777
- const detectLocalCLISessionMetadata = async ({ siteRoot }) => {
778
- /** @type {string | null} */
779
- let framework = null
780
-
781
- try {
782
- const frameworks = await listFrameworks({ projectDir: siteRoot })
783
- framework = frameworks[0].id || null
784
- } catch {}
785
-
786
- const { branch } = gitRepoInfo()
787
- const hostname = os.hostname()
788
- const userInfo = os.userInfo({ encoding: 'utf-8' })
789
- const { username } = userInfo
790
- const cliVersion = version
791
-
792
- const platform = WSL ? 'wsl' : os.platform()
793
- const arch = os.arch() === 'ia32' ? 'x86' : os.arch()
794
-
795
- const editor = process.env.EDITOR || null
796
-
797
- const detectedMetadata = {
798
- gitBranch: branch,
799
- hostname,
800
- username,
801
- siteRoot,
802
- cliVersion,
803
- editor,
804
- platform,
805
- arch,
806
- nodeVersion: process.version,
807
- framework,
808
- codegen: null,
809
- }
810
-
811
- return detectedMetadata
812
- }
813
-
814
- /**
815
- *
816
- * @param {object} input
817
- * @param {string} input.jwt The GraphJWT string
818
- * @param {string} input.sessionId The id of the cli session to fetch the current metadata for
819
- * @param {string} input.siteRoot Path to the root of the project
820
- * @param {object} input.config The parsed netlify.toml config file
821
- * @param {string} input.docId The GraphQL operations document id to fetch
822
- * @param {string} input.schemaId The GraphQL schemaId to use when generating code
823
- */
824
- const publishCliSessionMetadataPublishEvent = async ({ config, docId, jwt, schemaId, sessionId, siteRoot }) => {
825
- const detectedMetadata = await detectLocalCLISessionMetadata({ siteRoot })
826
-
827
- /** @type {import('netlify-onegraph-internal').CodegenHelpers.CodegenModuleMeta | null} */
828
- let codegen = null
829
-
830
- const codegenModule = await getCodegenModule({ config })
831
-
832
- if (codegenModule) {
833
- codegen = {
834
- id: codegenModule.id,
835
- version: codegenModule.id,
836
- generators: codegenModule.generators.map((generator) => ({
837
- id: generator.id,
838
- name: generator.name,
839
- options: generator.generateHandlerOptions || null,
840
- supportedDefinitionTypes: generator.supportedDefinitionTypes,
841
- version: generator.version,
842
- })),
843
- }
844
- }
845
-
846
- /** @type {import('netlify-onegraph-internal').CliEventHelper.OneGraphNetlifyCliSessionMetadataPublishEvent} */
847
- const event = {
848
- __typename: 'OneGraphNetlifyCliSessionMetadataPublishEvent',
849
- audience: 'UI',
850
- createdAt: new Date().toString(),
851
- id: crypto.randomUUID(),
852
- sessionId,
853
- payload: {
854
- cliVersion: detectedMetadata.cliVersion,
855
- editor: detectedMetadata.editor,
856
- siteRoot: detectedMetadata.siteRoot,
857
- siteRootFriendly: detectedMetadata.siteRoot,
858
- persistedDocId: docId,
859
- schemaId,
860
- codegenModule: codegen,
861
- arch: detectedMetadata.arch,
862
- nodeVersion: detectedMetadata.nodeVersion,
863
- platform: detectedMetadata.platform,
864
- framework: detectedMetadata.framework,
865
- },
866
- }
867
-
868
- try {
869
- await OneGraphClient.executeCreateCLISessionEventMutation(
870
- {
871
- sessionId,
872
- payload: event,
873
- },
874
- { accessToken: jwt },
875
- )
876
- } catch {
877
- warn(`Unable to reach Netlify Graph servers in order to publish CLI session data for the Graph UI`)
878
- }
879
- }
880
-
881
- /**
882
- * Fetch the existing cli session metadata if it exists, and mutate it remotely with the passed in metadata
883
- * @param {object} input
884
- * @param {object} input.config The parsed netlify.toml file
885
- * @param {string} input.jwt The Graph JWT string
886
- * @param {string} input.oneGraphSessionId The id of the cli session to fetch the current metadata for
887
- * @param {string} input.siteId The site object that contains the root file path for the site
888
- * @param {string} input.siteRoot The root file path for the site
889
- * @param {object} input.newMetadata The metadata to merge into (with priority) the existing metadata
890
- * @returns {Promise<object>}
891
- */
892
- export const upsertMergeCLISessionMetadata = async ({ jwt, newMetadata, oneGraphSessionId, siteId, siteRoot }) => {
893
- const { errors, metadata } = await getCLISessionMetadata({ jwt, oneGraphSessionId, siteId })
894
- if (errors) {
895
- warn(`Error fetching cli session metadata: ${JSON.stringify(errors, null, 2)}`)
896
- }
897
-
898
- const detectedMetadata = await detectLocalCLISessionMetadata({ siteRoot })
899
-
900
- // @ts-ignore
901
- const finalMetadata = { ...metadata, ...detectedMetadata, ...newMetadata }
902
-
903
- const result = OneGraphClient.updateCLISessionMetadata(jwt, siteId, oneGraphSessionId, finalMetadata)
904
-
905
- return result
906
- }
907
-
908
- export const persistNewOperationsDocForSession = async ({
909
- config,
910
- netlifyGraphConfig,
911
- netlifyToken,
912
- oneGraphSessionId,
913
- operationsDoc,
914
- siteId,
915
- siteRoot,
916
- }) => {
917
- try {
918
- GraphQL.parse(operationsDoc)
919
- } catch (parseError) {
920
- // TODO: We should send a message to the web UI that the current GraphQL operations file can't be sync because it's invalid
921
- warn(
922
- `Unable to sync Graph operations file. Please ensure that your GraphQL operations file is valid GraphQL. Found error: ${JSON.stringify(
923
- parseError,
924
- null,
925
- 2,
926
- )}`,
927
- )
928
- return
929
- }
930
-
931
- const lockfile = readLockfile({ siteRoot })
932
-
933
- if (!lockfile) {
934
- warn(
935
- `can't find a lockfile for the project while running trying to persist operations for session. To pull a remote schema (and create a lockfile), run ${chalk.yellow(
936
- 'netlify graph:pull',
937
- )} `,
938
- )
939
- }
940
-
941
- // NOTE(anmonteiro): We still persist a new operations document because we
942
- // might be checking out someone else's branch whose session we don't have
943
- // access to.
944
-
945
- const { branch } = gitRepoInfo()
946
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
947
- const persistedResult = await OneGraphClient.executeCreatePersistedQueryMutation(
948
- {
949
- appId: siteId,
950
- description: 'Temporary snapshot of local queries',
951
- query: operationsDoc,
952
- tags: ['netlify-cli', `session:${oneGraphSessionId}`, `git-branch:${branch}`, `local-change`],
953
- },
954
- {
955
- accessToken: jwt,
956
- siteId,
957
- },
958
- )
959
-
960
- const persistedDoc =
961
- persistedResult.data &&
962
- persistedResult.data.oneGraph &&
963
- persistedResult.data.oneGraph.createPersistedQuery &&
964
- persistedResult.data.oneGraph.createPersistedQuery.persistedQuery
965
-
966
- if (!persistedDoc) {
967
- warn(`Failed to create persisted query for editing, ${JSON.stringify(persistedResult, null, 2)}`)
968
- }
969
-
970
- currentPersistedDocId = persistedDoc.id
971
-
972
- const newMetadata = { docId: persistedDoc.id }
973
- const result = await upsertMergeCLISessionMetadata({
974
- config,
975
- jwt,
976
- siteId,
977
- oneGraphSessionId,
978
- newMetadata,
979
- siteRoot,
980
- })
981
-
982
- if (!result || result.errors) {
983
- warn(
984
- `Unable to update session metadata with updated operations docId="${persistedDoc.id}": ${JSON.stringify(
985
- result && result.errors,
986
- null,
987
- 2,
988
- )}`,
989
- )
990
- } else if (lockfile != null) {
991
- // Now that we've persisted the document, lock it in the lockfile
992
- const currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
993
-
994
- /** @type {NetlifyGraphLockfile.V0_format} */
995
- const newLockfile = NetlifyGraphLockfile.createLockfile({
996
- schemaId: lockfile.locked.schemaId,
997
- operationsFileContents: currentOperationsDoc,
998
- })
999
- writeLockfile({ siteRoot, lockfile: newLockfile })
1000
- }
1001
- }
1002
-
1003
- export const createCLISession = async ({ metadata, netlifyToken, sessionName, siteId }) => {
1004
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
1005
- const result = OneGraphClient.createCLISession(jwt, siteId, sessionName, metadata)
1006
- return result
1007
- }
1008
-
1009
- /**
1010
- * Load the CLI session id from the local state
1011
- * @param {import('../../utils/state-config.mjs').default} state
1012
- * @returns
1013
- */
1014
- export const loadCLISession = (state) => state.get('oneGraphSessionId')
1015
-
1016
- /**
1017
- * Idemponentially save the CLI session id to the local state and start monitoring for CLI events, upstream schema changes, and local operation file changes
1018
- * @param {object} input
1019
- * @param {object} input.config
1020
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
1021
- * @param {string | undefined} input.oneGraphSessionId The session ID to use for this CLI session (default: read from state)
1022
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
1023
- * @param {import('../../utils/state-config.mjs').default} input.state A function to call to set/get the current state of the local Netlify project
1024
- * @param {any} input.site The site object
1025
- */
1026
- export const startOneGraphCLISession = async (input) => {
1027
- const { config, netlifyGraphConfig, netlifyToken, site, state } = input
1028
- const getJwt = async () => {
1029
- const accessToken = await OneGraphClient.getGraphJwtForSite({ siteId: site.id, nfToken: netlifyToken })
1030
- return accessToken.jwt
1031
- }
1032
-
1033
- OneGraphClient.ensureAppForSite(await getJwt(), site.id)
1034
-
1035
- const oneGraphSessionId = await ensureCLISession({
1036
- config,
1037
- netlifyGraphConfig,
1038
- metadata: {},
1039
- netlifyToken,
1040
- site,
1041
- state,
1042
- oneGraphSessionId: input.oneGraphSessionId,
1043
- })
1044
-
1045
- const syncUIHelper = async () => {
1046
- const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1047
-
1048
- if (!schemaId) {
1049
- warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1050
- return
1051
- }
1052
-
1053
- if (!currentPersistedDocId) {
1054
- warn('Unable to read current persisted Graph library doc id')
1055
- return
1056
- }
1057
-
1058
- const syncSessionMetadataInput = {
1059
- config,
1060
- docId: currentPersistedDocId,
1061
- jwt: await getJwt(),
1062
- schemaId,
1063
- sessionId: oneGraphSessionId,
1064
- siteRoot: site.root,
1065
- }
1066
- await publishCliSessionMetadataPublishEvent(syncSessionMetadataInput)
1067
- }
1068
-
1069
- await syncUIHelper()
1070
-
1071
- const enabledServices = []
1072
- const schema = await OneGraphClient.fetchOneGraphSchemaForServices(site.id, enabledServices)
1073
-
1074
- const opsFileWatcher = monitorOperationFile({
1075
- netlifyGraphConfig,
1076
- onChange: async (filePath) => {
1077
- log('NetlifyGraph operation file changed at', filePath, 'updating function library...')
1078
- if (!schema) {
1079
- warn('Unable to load schema, run graph:pull to update')
1080
- return
1081
- }
1082
-
1083
- const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1084
-
1085
- if (!schemaId) {
1086
- warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1087
- return
1088
- }
1089
-
1090
- regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
1091
- const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
1092
- await persistNewOperationsDocForSession({
1093
- config,
1094
- netlifyGraphConfig,
1095
- netlifyToken,
1096
- oneGraphSessionId,
1097
- operationsDoc: newOperationsDoc,
1098
- siteId: site.id,
1099
- siteRoot: site.root,
1100
- })
1101
- },
1102
- onAdd: async (filePath) => {
1103
- log('NetlifyGraph operation file created at', filePath, 'creating function library...')
1104
- if (!schema) {
1105
- warn('Unable to load schema, run graph:pull to update')
1106
- return
1107
- }
1108
-
1109
- const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1110
-
1111
- if (!schemaId) {
1112
- warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1113
- return
1114
- }
1115
-
1116
- regenerateFunctionsFileFromOperationsFile({ config, netlifyGraphConfig, schema, schemaId })
1117
- const newOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
1118
- await persistNewOperationsDocForSession({
1119
- config,
1120
- netlifyGraphConfig,
1121
- netlifyToken,
1122
- oneGraphSessionId,
1123
- operationsDoc: newOperationsDoc,
1124
- siteId: site.id,
1125
- siteRoot: site.root,
1126
- })
1127
- },
1128
- })
1129
-
1130
- const cliEventsCloseFn = monitorCLISessionEvents({
1131
- config,
1132
- appId: site.id,
1133
- netlifyToken,
1134
- netlifyGraphConfig,
1135
- sessionId: oneGraphSessionId,
1136
- site,
1137
- state,
1138
- onClose: () => {
1139
- log('CLI session closed, stopping monitor...')
1140
- },
1141
- onSchemaIdChange: (newSchemaId) => {
1142
- log('Netlify Graph schemaId changed:', newSchemaId)
1143
- },
1144
- onEvents: async (events) => {
1145
- const ackEventIds = []
1146
-
1147
- for (const event of events) {
1148
- try {
1149
- const audience = event.audience || OneGraphClient.eventAudience(event)
1150
- if (audience === 'CLI') {
1151
- ackEventIds.push(event.id)
1152
- const eventName = OneGraphClient.friendlyEventName(event)
1153
- log(`${chalk.magenta('Handling')} Netlify Graph: ${eventName}...`)
1154
- const schemaId = readSchemaIdFromLockfile({ siteRoot: site.root })
1155
-
1156
- if (!schemaId) {
1157
- warn('Unable to load schemaId from Netlify Graph lockfile, run graph:pull to update')
1158
- return
1159
- }
1160
-
1161
- if (!schema) {
1162
- warn('Unable to load schema from for Netlify Graph, run graph:pull to update')
1163
- return
1164
- }
1165
-
1166
- if (!currentPersistedDocId) {
1167
- warn('Unable to read current persisted Graph library doc id')
1168
- return
1169
- }
1170
-
1171
- await handleCliSessionEvent({
1172
- config,
1173
- docId: currentPersistedDocId,
1174
- netlifyToken,
1175
- event,
1176
- netlifyGraphConfig,
1177
- schema,
1178
- schemaId,
1179
- sessionId: oneGraphSessionId,
1180
- site,
1181
- siteId: site.id,
1182
- siteRoot: site.root,
1183
- })
1184
- log(`${chalk.green('Finished handling')} Netlify Graph: ${eventName}...`)
1185
- }
1186
- } catch (error_) {
1187
- warn(`Error processing individual Netlify Graph event, skipping:
1188
- ${JSON.stringify(error_, null, 2)}`)
1189
- ackEventIds.push(event.id)
1190
- }
1191
- }
1192
-
1193
- return ackEventIds
1194
- },
1195
- onError: (fetchEventError) => {
1196
- error(`Netlify Graph upstream error: ${fetchEventError}`)
1197
- },
1198
- })
1199
-
1200
- return async function unregisterWatchers() {
1201
- const watcher = await opsFileWatcher
1202
- watcher.close()
1203
- cliEventsCloseFn()
1204
- }
1205
- }
1206
-
1207
- /**
1208
- * Mark a session as inactive so it doesn't show up in any UI lists, and potentially becomes available to GC later
1209
- * @param {object} input
1210
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
1211
- * @param {string} input.siteId A function to call to set/get the current state of the local Netlify project
1212
- * @param {string} input.sessionId The session id to monitor CLI events for
1213
- */
1214
- export const markCliSessionInactive = async ({ netlifyToken, sessionId, siteId }) => {
1215
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
1216
- const result = await OneGraphClient.executeMarkCliSessionInactive(jwt, siteId, sessionId)
1217
- if (!result || result.errors) {
1218
- warn(`Unable to mark CLI session ${sessionId} inactive: ${JSON.stringify(result.errors, null, 2)}`)
1219
- }
1220
- }
1221
-
1222
- /**
1223
- * Generate a session name that can be identified as belonging to the current checkout
1224
- * @returns {string} The name of the session to create
1225
- */
1226
- export const generateSessionName = () => {
1227
- const userInfo = os.userInfo({ encoding: 'utf-8' })
1228
- const sessionName = `${userInfo.username}-${Date.now()}`
1229
- log(`Generated Netlify Graph session name: ${sessionName}`)
1230
- return sessionName
1231
- }
1232
-
1233
- /**
1234
- * Mark a session as inactive so it doesn't show up in any UI lists, and potentially becomes available to GC later
1235
- * @param {object} input
1236
- * @param {{metadata: {schemaId:string}; id: string; appId: string; name?: string}} input.session The current session
1237
- * @param {string} input.netlifyToken The (typically netlify) access token that is used for authentication, if any
1238
- * @param {NetlifyGraphLockfile.V0_format | undefined} input.lockfile A function to call to set/get the current state of the local Netlify project
1239
- */
1240
- const idempotentlyUpdateSessionSchemaIdFromLockfile = async (input) => {
1241
- const { lockfile, netlifyToken, session } = input
1242
- const sessionSchemaId = session.metadata && session.metadata.schemaId
1243
- const lockfileSchemaId = lockfile && lockfile.locked.schemaId
1244
-
1245
- if (lockfileSchemaId != null && sessionSchemaId !== lockfileSchemaId) {
1246
- // Local schema always wins, update the session metadata to reflect that
1247
- const siteId = session.appId
1248
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId, nfToken: netlifyToken })
1249
-
1250
- log(`Found new lockfile, overwriting session ${session.name || session.id}`)
1251
- return OneGraphClient.updateCLISessionMetadata(jwt, siteId, session.id, {
1252
- ...session.metadata,
1253
- schemaId: lockfileSchemaId,
1254
- })
1255
- }
1256
- }
1257
-
1258
- /**
1259
- * Ensures a cli session exists for the current checkout, or errors out if it doesn't and cannot create one.
1260
- * @param {object} input
1261
- * @param {object} input.config The parsed netlify.toml config file
1262
- * @param {NetlifyGraph.NetlifyGraphConfig} input.netlifyGraphConfig A standalone config object that contains all the information necessary for Netlify Graph to process events
1263
- * @param {object} input.metadata
1264
- * @param {string} input.netlifyToken
1265
- * @param {import('../../utils/state-config.mjs').default} input.state
1266
- * @param {string} [input.oneGraphSessionId]
1267
- * @param {any} input.site The site object
1268
- */
1269
- export const ensureCLISession = async (input) => {
1270
- const { config, metadata, netlifyGraphConfig, netlifyToken, site, state } = input
1271
- let oneGraphSessionId = input.oneGraphSessionId ? input.oneGraphSessionId : loadCLISession(state)
1272
- let parentCliSessionId = null
1273
- const { jwt } = await OneGraphClient.getGraphJwtForSite({ siteId: site.id, nfToken: netlifyToken })
1274
-
1275
- const lockfile = readLockfile({ siteRoot: site.root })
1276
-
1277
- // Validate that session still exists and we can access it
1278
- try {
1279
- if (oneGraphSessionId) {
1280
- const { errors, session } = await OneGraphClient.fetchCliSession({
1281
- appId: site.id,
1282
- jwt,
1283
- sessionId: oneGraphSessionId,
1284
- desiredEventCount: 0,
1285
- })
1286
-
1287
- if (errors) {
1288
- warn(`Unable to fetch cli session: ${JSON.stringify(errors, null, 2)}`)
1289
- log(`Creating new cli session`)
1290
- parentCliSessionId = oneGraphSessionId
1291
- oneGraphSessionId = null
1292
- }
1293
-
1294
- if (session && session.metadata && session.metadata.docId) {
1295
- currentPersistedDocId = session.metadata.docId
1296
- }
1297
-
1298
- // During the transition to lockfiles, write a lockfile if one isn't
1299
- // found. Later, only handling a 'OneGraphNetlifyCliSessionPersistedLibraryUpdatedEvent'
1300
- // will create or update the lockfile
1301
- // TODO(anmonteiro): remove this in the future?
1302
- if (lockfile == null && session.metadata.schemaId) {
1303
- const currentOperationsDoc = readGraphQLOperationsSourceFile(netlifyGraphConfig)
1304
- log(`Generating Netlify Graph lockfile at ${NetlifyGraphLockfile.defaultLockFileName}`)
1305
-
1306
- const newLockfile = NetlifyGraphLockfile.createLockfile({
1307
- schemaId: session.metadata.schemaId,
1308
- operationsFileContents: currentOperationsDoc,
1309
- })
1310
- writeLockfile({ siteRoot: site.root, lockfile: newLockfile })
1311
- }
1312
-
1313
- await idempotentlyUpdateSessionSchemaIdFromLockfile({ session, lockfile, netlifyToken })
1314
- }
1315
- } catch (fetchSessionError) {
1316
- warn(`Unable to fetch cli session events: ${JSON.stringify(fetchSessionError, null, 2)}`)
1317
- oneGraphSessionId = null
1318
- }
1319
-
1320
- if (oneGraphSessionId) {
1321
- await upsertMergeCLISessionMetadata({
1322
- jwt,
1323
- config,
1324
- newMetadata: {},
1325
- oneGraphSessionId,
1326
- siteId: site.id,
1327
- siteRoot: site.root,
1328
- })
1329
- } else {
1330
- // If we can't access the session in the state.json or it doesn't exist, create a new one
1331
- const sessionName = generateSessionName()
1332
- const detectedMetadata = await detectLocalCLISessionMetadata({
1333
- siteRoot: site.root,
1334
- })
1335
- const newSessionMetadata = parentCliSessionId ? { parentCliSessionId } : {}
1336
-
1337
- const sessionMetadata = {
1338
- ...detectedMetadata,
1339
- ...newSessionMetadata,
1340
- ...metadata,
1341
- }
1342
-
1343
- if (lockfile != null) {
1344
- log(`Creating new session "${sessionName}" from lockfile`)
1345
- sessionMetadata.schemaId = lockfile.locked.schemaId
1346
- }
1347
-
1348
- const oneGraphSession = await createCLISession({
1349
- netlifyToken,
1350
- siteId: site.id,
1351
- sessionName,
1352
- metadata: sessionMetadata,
1353
- })
1354
-
1355
- if (oneGraphSession) {
1356
- // @ts-expect-error
1357
- oneGraphSessionId = oneGraphSession.id
1358
- } else {
1359
- warn('Unable to load Netlify Graph session, please report this to Netlify support')
1360
- }
1361
- }
1362
-
1363
- if (!oneGraphSessionId) {
1364
- error('Unable to create or access Netlify Graph CLI session')
1365
- }
1366
-
1367
- state.set('oneGraphSessionId', oneGraphSessionId)
1368
- const { errors: markCLISessionActiveErrors } = await OneGraphClient.executeMarkCliSessionActiveHeartbeat(
1369
- jwt,
1370
- site.id,
1371
- oneGraphSessionId,
1372
- )
1373
-
1374
- if (markCLISessionActiveErrors) {
1375
- warn(`Unable to mark cli session active: ${JSON.stringify(markCLISessionActiveErrors, null, 2)}`)
1376
- }
1377
-
1378
- return oneGraphSessionId
1379
- }
1380
-
1381
- export const OneGraphCliClient = {
1382
- ackCLISessionEvents: OneGraphClient.ackCLISessionEvents,
1383
- executeCreatePersistedQueryMutation: OneGraphClient.executeCreatePersistedQueryMutation,
1384
- executeCreateApiTokenMutation: OneGraphClient.executeCreateApiTokenMutation,
1385
- fetchCliSessionEvents: OneGraphClient.fetchCliSessionEvents,
1386
- fetchCliSessionSchema,
1387
- ensureAppForSite: OneGraphClient.ensureAppForSite,
1388
- updateCLISessionMetadata: OneGraphClient.updateCLISessionMetadata,
1389
- getGraphJwtForSite: OneGraphClient.getGraphJwtForSite,
1390
- }
1391
-
1392
- export { currentPersistedDocId, extractFunctionsFromOperationDoc }