@coalescesoftware/coa 1.0.157 → 1.0.158

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.
package/src/Package.ts DELETED
@@ -1,1017 +0,0 @@
1
- /* eslint-disable multiline-comment-style */
2
- import * as Shared from "@coalescesoftware/shared"
3
- import fs from "fs"
4
-
5
- const os = require("os");
6
-
7
- /**
8
- * coa package uses the same config file that should be setup to leverage coa.
9
- *
10
- * project - the user's ws data
11
- * package - the targetted repo being applied to the project
12
- * dependency - any package installed to a project/another package
13
- *
14
- * coa add works at a high level in these steps:
15
- * 1 - collects ws data from a targetted repo (local or remote)
16
- * 2 - namespaces that ws data w/package ID
17
- * 3 - adds to firestore packages collection the info for the package being installed (dependency information) w/status: 'adding'
18
- * 4 - updates project ws data on firestore with namespaced ws data from package
19
- * 5 - updates firestore package dependency information w/status: 'added' and a timestamp
20
- * 6 - in case of error, will clean up any added package information including dependency info from project ws data on FS
21
- *
22
- * coa remove works similarly
23
- */
24
-
25
- export const localInstallKeyword = "file:"
26
- const PackagesLogger = Shared.Logging.GetLogger(Shared.Logging.LoggingArea.CLI)
27
-
28
- type TPackageIdentifier = string // the ID of the package (currently comes from project name assigned in the git settings)
29
- type TPackageVersion = string // for now version is a URL for the git repo
30
-
31
- type TPackageGitCommitInfo = {
32
- commit: string
33
- }
34
-
35
- type TPackageFileInfo = {
36
- filePath: TPackageVersion
37
- }
38
-
39
- export type TVersionInfo = TPackageGitCommitInfo | TPackageFileInfo
40
-
41
- interface IEntityPackageInformation { // added to entity to track versioning under entity.packageInfo
42
- id: TPackageIdentifier,
43
- version: TPackageVersion,
44
- }
45
-
46
- interface IPackageInformation { // added to workspace as own entity in workspaceData.packages
47
- id: TPackageIdentifier,
48
- status: EPackageStatus,
49
- addedBy: string, // fbTeam userID
50
- createdAt: firebase.default.firestore.Timestamp | null, // should be timestamped by firstore
51
- version: TPackageVersion,
52
- versionInfo: TVersionInfo,
53
- manifest?: Shared.Workspaces.AllWorkspaceData // added on success of firestore flush - all entities that have been added by the package
54
- }
55
-
56
- // now a collection in AllWorkspaceData
57
- export interface IPackages {
58
- [packageID: string]: IPackageInformation
59
- }
60
-
61
- enum EPackageStatus {
62
- adding = "adding",
63
- added = "added",
64
- removing = "removing",
65
- error = "error",
66
- }
67
-
68
- export interface IPackageContext {
69
- firestore
70
- teamID: string
71
- environmentID: number
72
- loggerContext: Shared.Logging.LogContext
73
- timestamp // a firestore timestamp function called on firestore at time of successful install
74
- }
75
-
76
- type TNamespacedEntityData =
77
- Shared.Runner.IBSSteps |
78
- Shared.State.IWorkspaces |
79
- Shared.FolderOperations.FolderData |
80
- Shared.JobOperations.IJobsData |
81
- Shared.MacroOperations.IMacrosData |
82
- Shared.StepTypes.IAllStepTypesParsed |
83
- Shared.MappingOperations.TPhysicalLocations
84
-
85
- interface INamespacerDictionary {
86
- [entity: string]: {
87
- add: (packageID: TPackageIdentifier, packageURL: string, ...args: any) => TNamespacedEntityData
88
- remove: (packageID: TPackageIdentifier, ...args: any) => TNamespacedEntityData
89
- }
90
- }
91
-
92
- enum EVersionType {
93
- file = "file",
94
- url = "url"
95
- }
96
-
97
- ////////////////////////////////////////////////////////////////////
98
- //////////////////// reusable namespacing helper functions
99
- ////////////////////////////////////////////////////////////////////
100
-
101
- /**
102
- * Recieves a string or a number that will be namespaced (NAMESPACEVALUE::STRINGorNUMBER)
103
- * @param value
104
- * @param namespace
105
- * @returns
106
- */
107
- const UpdateValueWithNamespace = (value: string | number, namespace: TPackageIdentifier): string => {
108
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, typeof namespace === "string", `Namespace was not a string, but was type ${typeof namespace}`)
109
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, !!namespace.trim(), `Namespace cannot be an empty string`)
110
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, typeof value === "string" || typeof value === "number", `Value was not a string, but was type ${typeof value}`)
111
- const stringValue = String(value)
112
-
113
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, stringValue.indexOf("::") === -1, `value to namespace contained invalid character ":"`)
114
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, namespace.indexOf("::") === -1, `namespace contained invalid character ":"`)
115
-
116
- if (stringValue.trim() === "") return stringValue //empty string should not be namespaced but returned as is
117
-
118
- return `${namespace}::${value}`
119
- }
120
-
121
- /**
122
- * Updates originalObject field values (fieldsToUpdate) using fieldValueUpdateCB and returns original value for non selected fields
123
- *
124
- * @param entity object with field values to update
125
- * @param entityFieldsToUpdate an array of fields to update, or null to update all fields
126
- * @param namespacerCB CB to call on each field to update
127
- * @returns entity with field values that have been namespaced or updated with CB
128
- */
129
- const EntityNamespacer = (
130
- entity: any,
131
- entityFieldsToUpdate: (string | number)[] | null,
132
- namespacerCB: (fieldValueToUpdate) => any
133
- ): any => {
134
- const updatedEntity = { ...entity }
135
-
136
- if (!entityFieldsToUpdate) {
137
- // update all
138
- Object.keys(entity).forEach(field => {
139
- updatedEntity[field] = namespacerCB(updatedEntity[field])
140
- })
141
- } else {
142
- Object.keys(entity).forEach(key => {
143
- // if key included in keypath array, update, otherwise return original value
144
- if (entityFieldsToUpdate.includes(key)) {
145
- updatedEntity[key] = namespacerCB(updatedEntity[key])
146
- } else {
147
- updatedEntity[key] = entity[key]
148
- }
149
- })
150
- }
151
-
152
- return updatedEntity
153
- }
154
-
155
- ////////////////////////////////////////////////////////////////////
156
- ///////////////// Parent namespacing functions
157
- ////////////////////////////////////////////////////////////////////
158
- /**
159
- * internal functions that will add or remove namespaced package data
160
- * to/from a project's ws data and return the modified workspace data
161
- */
162
-
163
- /**
164
- * function adds namespaced package data to workspace data and return the modified workspace data
165
- *
166
- * @param allWorkspaceData
167
- * @param packageInfo
168
- * @param packageContext
169
- * @returns AllWorkspaceData with added namespaced data
170
- */
171
- const GetNamespacedWorkspaceData = (
172
- allWorkspaceData: Shared.Workspaces.AllWorkspaceData,
173
- packageInfo: IPackageInformation,
174
- packageContext: IPackageContext
175
- ): Shared.Workspaces.AllWorkspaceData => {
176
- const { id, version } = packageInfo
177
- const namespacedWorkspaceData = {}
178
-
179
- Object.keys(allWorkspaceData).forEach(wsKey => {
180
- try {
181
- if (WorkspaceEntityNamespaceFunctionLookup[wsKey]) {
182
- PackagesLogger.info(`Preparing workspace data for import: ${wsKey}`)
183
- namespacedWorkspaceData[wsKey] = WorkspaceEntityNamespaceFunctionLookup[wsKey].add(id, version, allWorkspaceData[wsKey])
184
- }
185
- } catch (error) {
186
- const message = `error when preparing workspace data for import: ${wsKey}`
187
-
188
- if (error instanceof Error) {
189
- PackagesLogger.errorContext(packageContext.loggerContext, message, error.message, error.name, error.stack, error)
190
- } else {
191
- PackagesLogger.errorContext(packageContext.loggerContext, message, error)
192
- }
193
-
194
- throw error
195
- }
196
- })
197
-
198
- return namespacedWorkspaceData as Shared.Workspaces.AllWorkspaceData
199
- }
200
-
201
- /**
202
- * will remove namespaced package workspace data from allWorkspaceData and return the updated object
203
- *
204
- * @param allWorkspaceData
205
- * @param packageInfo
206
- * @param packageContext
207
- * @returns AllWorkspaceData with added namespaced data
208
- */
209
- const RemoveNamespacedWorkspaceData = (
210
- allWorkspaceData: Shared.Workspaces.AllWorkspaceData,
211
- packageInfo: IPackageInformation,
212
- packageContext: IPackageContext
213
- ): Shared.Workspaces.AllWorkspaceData => {
214
- const prunedWorkspaceData = {}
215
- const { id } = packageInfo
216
-
217
- Object.keys(allWorkspaceData).forEach(wsKey => {
218
- try {
219
- // if included in entities that should be imported, then add with namespace
220
- if (WorkspaceEntityNamespaceFunctionLookup[wsKey]) {
221
- PackagesLogger.info(`Pruning workspace data: ${wsKey}`)
222
- prunedWorkspaceData[wsKey] = WorkspaceEntityNamespaceFunctionLookup[wsKey].remove(id, allWorkspaceData[wsKey])
223
- } else {
224
- // else just add back the original data
225
- prunedWorkspaceData[wsKey] = allWorkspaceData[wsKey]
226
- }
227
- } catch (error) {
228
- const message = `error when pruning workspace data for: ${wsKey}`
229
-
230
- if (error instanceof Error) {
231
- PackagesLogger.errorContext(packageContext.loggerContext, message, error.message, error.name, error.stack, error)
232
- } else {
233
- PackagesLogger.errorContext(packageContext.loggerContext, message, error)
234
- }
235
-
236
- throw error
237
- }
238
- })
239
-
240
- return prunedWorkspaceData as Shared.Workspaces.AllWorkspaceData
241
- }
242
-
243
- /**
244
- * Takes a parentEntityObject (ie 'stepTypes') that contains nested entity objects (ie 'stepType-1') that will be namespaced
245
- * a) all nested entity object keys will be namespaced
246
- * b) all nested entity object field values (fieldsToUpdate) will be namespaced
247
- * c) (optional) additional work can be done on the newly namespaced nested entity object using the nestedObjectUpdateCB
248
- *
249
- * @param parentEntityObject object containing nested entity objects which will be iterated through and namespaced
250
- * @param packageID used as new namespace
251
- * @param fieldsToUpdate array of fields whose values will be namespaced for each nested entity object, or null to update all fields
252
- * @param nestedObjectUpdateCB (optional) any additional work that should be done to namespaced nested entity object
253
- * @param entity the entity (singular) being updated, ie. "step type" or "folder", etc. used for info logger
254
- * @returns
255
- */
256
- const NamespaceEntity = (
257
- parentEntityObject: any,
258
- packageID: TPackageIdentifier,
259
- packageVersion: TPackageVersion,
260
- fieldsToUpdate: string[] | null,
261
- nestedObjectUpdateCB: ((nestedObject: any, key: string) => void) | null,
262
- entity: string
263
- ) => {
264
- const namespacedDataObject = {}
265
-
266
- Object.keys(parentEntityObject).forEach(key => {
267
- const isPackageImportedObject = !!Shared.Common.getValueSafe(parentEntityObject[key], [ "packageInfo", "id" ], null)
268
-
269
- if (isPackageImportedObject) return // do not namespace already namespaced data
270
-
271
- const namespacedKey = UpdateValueWithNamespace(key, packageID)
272
- PackagesLogger.info(`Adding ${entity}: ${key} as ${namespacedKey}`)
273
-
274
- namespacedDataObject[namespacedKey] = {
275
- ...EntityNamespacer(
276
- parentEntityObject[key],
277
- fieldsToUpdate,
278
- (fieldValueToUpdate: string | number) => UpdateValueWithNamespace(fieldValueToUpdate, packageID)
279
- )
280
- }
281
-
282
- // do additional work if needed on newly namespaced nested object
283
- if (nestedObjectUpdateCB) {
284
- nestedObjectUpdateCB(namespacedDataObject[namespacedKey], key)
285
- }
286
-
287
- // add package information to top level of entity
288
- namespacedDataObject[namespacedKey]["packageInfo"] = BuildPackageInfoForEntity(packageID, packageVersion)
289
- })
290
-
291
- return namespacedDataObject
292
- }
293
-
294
- ////////////////////////////////////////////////////////////////////
295
- ///////////////// Namespacing function dictionary
296
- ////////////////////////////////////////////////////////////////////
297
-
298
- // to start packages will support import/removal of macros and steptypes
299
- const WorkspaceEntityNamespaceFunctionLookup: INamespacerDictionary = {
300
- macros: {
301
- add: (
302
- packageID: TPackageIdentifier,
303
- packageVersion: TPackageVersion,
304
- macros: Shared.MacroOperations.IMacrosData
305
- ): Shared.MacroOperations.IMacrosData => MacrosNamespacer(packageID, macros, packageVersion),
306
- remove: (
307
- packageID: TPackageIdentifier,
308
- macros: Shared.MacroOperations.IMacrosData
309
- ): Shared.MacroOperations.IMacrosData => MacrosNamespaceRemover(packageID, macros)
310
- },
311
- stepTypes: {
312
- add: (
313
- packageID: TPackageIdentifier,
314
- packageVersion: TPackageVersion,
315
- stepTypes: Shared.StepTypes.IAllStepTypesParsed
316
- ): Shared.StepTypes.IAllStepTypesParsed => StepTypesNamespacer(packageID, stepTypes, packageVersion),
317
- remove: (
318
- packageID: TPackageIdentifier,
319
- stepTypes: Shared.StepTypes.IAllStepTypesParsed
320
- ): Shared.StepTypes.IAllStepTypesParsed => StepTypesNamespaceRemover(packageID, stepTypes)
321
- },
322
- }
323
-
324
- ////////////////////////////////////////////////////////////////////
325
- ///////////////// Workspace Data Namespacing functions by entity (adding)
326
- ////////////////////////////////////////////////////////////////////
327
-
328
- const MacrosNamespacer = (packageID: TPackageIdentifier, macros: Shared.MacroOperations.IMacrosData, packageVersion: TPackageVersion): Shared.MacroOperations.IMacrosData => {
329
- return NamespaceEntity(macros, packageID, packageVersion, [ "id" ], null, "macro")
330
- }
331
-
332
- const StepTypesNamespacer = (packageID: TPackageIdentifier, stepTypes: Shared.StepTypes.IAllStepTypesParsed, packageVersion: TPackageVersion): Shared.StepTypes.IAllStepTypesParsed => {
333
- const filteredStepTypes = Object.keys(stepTypes)
334
- .filter(stepTypeID => !Shared.StepTypes.isDefaultStepType(stepTypeID)) // remove default steptypes
335
- .reduce((acc, id) => { // turn back into object
336
- acc[id] = stepTypes[id]
337
-
338
- return acc
339
- }, {})
340
-
341
- const updateStepTypeCB = (namespacedStepTypeData: Shared.StepTypes.TStepTypeParsed) => {
342
- namespacedStepTypeData.metadata["defaultStorageLocation"] = null
343
- }
344
-
345
- return NamespaceEntity(filteredStepTypes, packageID, packageVersion, [ "id" ], updateStepTypeCB, "stepType")
346
- }
347
-
348
- ////////////////////////////////////////////////////////////////////
349
- ///////////////// Workspace Data Namespace Remover functions by entity (remove)
350
- ////////////////////////////////////////////////////////////////////
351
-
352
- const EntityRemover = (packageID: TPackageIdentifier, parentEntityObject: any, entityName: string) => {
353
- const prunedEntityObject: any = {}
354
-
355
- Object.keys(parentEntityObject)
356
- .filter(entityKey => {
357
- const entityPackageInfo: IEntityPackageInformation = Shared.Common.getValueSafe(parentEntityObject[entityKey], [ "packageInfo" ], null)
358
-
359
- // if no info, then not imported, if info doesn't match, then not imported from this package
360
- if (!entityPackageInfo || (!!entityPackageInfo && entityPackageInfo.id !== packageID)) {
361
- return entityKey
362
- } else {
363
- PackagesLogger.info(`Removing ${entityName}: ${entityKey}`)
364
- }
365
- })
366
- .forEach(prunedEntityKey => prunedEntityObject[prunedEntityKey] = parentEntityObject[prunedEntityKey])
367
-
368
- return prunedEntityObject
369
- }
370
-
371
- const MacrosNamespaceRemover = (packageID: TPackageIdentifier, macros: Shared.MacroOperations.IMacrosData): Shared.MacroOperations.IMacrosData => {
372
- return EntityRemover(packageID, macros, "macro")
373
- }
374
-
375
- const StepTypesNamespaceRemover = (packageID: TPackageIdentifier, stepTypes: Shared.StepTypes.IAllStepTypesParsed): Shared.StepTypes.IAllStepTypesParsed => {
376
- return EntityRemover(packageID, stepTypes, "step type")
377
- }
378
-
379
- ////////////////////////////////////////////////////////////////////
380
- ///////////////// Package/Dependency getters/builders
381
- ////////////////////////////////////////////////////////////////////
382
-
383
- const BuildPackageInfoForEntity = (packageID: TPackageIdentifier, version: TPackageVersion): IEntityPackageInformation => {
384
- return {
385
- id: packageID,
386
- version
387
- }
388
- }
389
-
390
- const InitializePackageDependencyInfoForPackage = (
391
- id: TPackageIdentifier,
392
- versionInfo: TVersionInfo,
393
- version: TPackageVersion,
394
- userID: string
395
- ): IPackageInformation => {
396
- return {
397
- id,
398
- version,
399
- versionInfo,
400
- status: EPackageStatus.adding,
401
- createdAt: null, //timestamped on FS
402
- addedBy: userID
403
- }
404
- }
405
-
406
- ////////////////////////////////////////////////////////////////////
407
- ///////////////// Workspace Data Collection functions
408
- ////////////////////////////////////////////////////////////////////
409
-
410
- const GetRepoNameFromURL = (repoURL: string) => {
411
- try {
412
- const nameIndex = repoURL.lastIndexOf("/") + 1
413
- const gitIndex = repoURL.lastIndexOf(".git")
414
- return repoURL.substring(nameIndex, gitIndex)
415
- } catch (error) {
416
- PackagesLogger.error(`Error when reading package dependency URL: ${repoURL}`, error)
417
- throw error
418
- }
419
- }
420
-
421
- const GetAllWorkspaceDataAndCommitHashFromRemoteRepository = (location: TPackageVersion): Promise<{ workspaceData: Shared.Workspaces.AllWorkspaceData, hashFromRemote: string | null }> => {
422
- return new Promise((resolve, reject) => {
423
- // ensure a unique temp folder name
424
- const tempRepoFolder = `COA_TEMP_${GetRepoNameFromURL(location)}_${Math.random()}`
425
- const tempFolderPath = os.tmpdir()
426
- let commitHashToReturn: string
427
-
428
- let commitOrBranch: string | null = null
429
- let locationToUse: string = location
430
-
431
- const indexOfHash = location.indexOf("#")
432
- // if hash indicator, then grab commit and remove #info from URL
433
- if (indexOfHash > -1) {
434
- commitOrBranch = location.slice(indexOfHash + 1)
435
- locationToUse = location.substring(0, indexOfHash)
436
- }
437
-
438
- const toExecute = commitOrBranch ? `&& git checkout ${commitOrBranch} && git log -n 1 ${commitOrBranch}` : `&& git log -n 1`
439
-
440
- PackagesLogger.info(`Cloning Coalesce package from repo URL: ${locationToUse}`)
441
-
442
- return Shared.Common.executeCommand(`cd ${tempFolderPath} && git clone ${locationToUse} ${tempRepoFolder} && ls && cd ${tempRepoFolder} && ls ${toExecute}`)
443
- .then(res => {
444
- const fileSystemSettings: Shared.GitOperations.Filesystem = { fs, dir: `${tempFolderPath}/${tempRepoFolder}` }
445
- const getCommitFromGitLogRegex = new RegExp(/commit ([\s\S]*?)\n/)
446
- const regexResult = getCommitFromGitLogRegex.exec(res[0])
447
-
448
- if (!regexResult) {
449
- const message = `No commit was found for ${commitOrBranch}`
450
- throw new Error(message)
451
- }
452
-
453
- commitHashToReturn = regexResult[1]
454
-
455
- return GetAllWorkspaceDataFromFileSystem(fileSystemSettings)
456
- })
457
- .then((workspaceData) => {
458
- const additionalLogInfo = commitOrBranch ? ` at commit or branch: "${commitOrBranch}"` : ""
459
- PackagesLogger.info(`Reading Coalesce package data from repo "${GetRepoNameFromURL(locationToUse)}"${additionalLogInfo}...`)
460
-
461
- resolve({
462
- workspaceData,
463
- hashFromRemote: commitHashToReturn
464
- })
465
- })
466
- .catch(error => {
467
- PackagesLogger.error(`Error while collecting workspace data for repo ${GetRepoNameFromURL(locationToUse)}`, error)
468
- reject(error)
469
- })
470
- })
471
- }
472
-
473
- const GetAllWorkspaceDataFromFileSystem = (fileSystemSettings: Shared.GitOperations.Filesystem) => {
474
- return Shared.Workspaces.GetAllWorkspaceDataFromGitCommit(fileSystemSettings, null, null)
475
- }
476
-
477
- ////////////////////////////////////////////////////////////////////
478
- ///////////////// Other helpers
479
- ////////////////////////////////////////////////////////////////////
480
-
481
- /**
482
- * this function will remove any ws data associated with packageInfo from projectWorkspaceData
483
- * before reinstalling to ensure up-to-date package information
484
- *
485
- * @param packageInfo
486
- * @param packageWorkspaceData
487
- * @param projectWorkspaceData
488
- * @param packageContext
489
- * @returns {
490
- * desiredState: what FS should look like
491
- * stateToReplace: what FS looks like now
492
- * }
493
- */
494
- const GetNamespacedDataAndDesiredStateForFSFlush = (
495
- packageInfo: IPackageInformation,
496
- packageWorkspaceData: Shared.Workspaces.AllWorkspaceData,
497
- projectWorkspaceData: Shared.Workspaces.AllWorkspaceData,
498
- packageContext: IPackageContext
499
- ): { desiredState: Shared.Workspaces.AllWorkspaceData, namespacedData: Shared.Workspaces.AllWorkspaceData } => {
500
- if (!packageInfo.id) {
501
- const message = `PackageID was ${packageInfo.id} when it should be a string`
502
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, false, message)
503
- throw new Error(message)
504
- }
505
-
506
- PackagesLogger.info(`Cleaning up any stale package data in your project...`)
507
-
508
- // remove entities that were installed by current package
509
- const prunedProjectWorkspaceData = RemoveNamespacedWorkspaceData(projectWorkspaceData, packageInfo, packageContext)
510
- // get namespaced data for current package
511
- const namespacedDataFromPackage = GetNamespacedWorkspaceData(packageWorkspaceData, packageInfo, packageContext)
512
- // add namespaced data to project data
513
- const projectWorkspaceDataWithUpdates = CombineWorkspaceDataByEntities(namespacedDataFromPackage, prunedProjectWorkspaceData)
514
-
515
- return {
516
- desiredState: projectWorkspaceDataWithUpdates,
517
- namespacedData: namespacedDataFromPackage
518
- }
519
- }
520
-
521
- // get a package context object utilized by much of the functionality in this file
522
- export const InitializePackageContext = (
523
- authInfo: Shared.ConnectionOperations.ITeamInfoAndFirebase,
524
- environmentID: number
525
- ): IPackageContext => {
526
- return {
527
- firestore: authInfo.firebase.firestore(),
528
- timestamp: Shared.Firebase.serverTimestamp,
529
- teamID: authInfo.teamInfo.fbTeamID,
530
- environmentID,
531
- loggerContext: { orgID: authInfo.teamInfo.fbTeamID, userID: authInfo.teamInfo.fbUserID }
532
- }
533
- }
534
-
535
- const CombineWorkspaceDataByEntities = (
536
- namespacedWorkspaceData: Shared.Workspaces.AllWorkspaceData,
537
- projectWorkspaceData: Shared.Workspaces.AllWorkspaceData
538
- ): Shared.Workspaces.AllWorkspaceData => {
539
- let updatedProjectWorkspaceData = {}
540
-
541
- Object.keys(projectWorkspaceData).forEach(entityKey => {
542
- updatedProjectWorkspaceData[entityKey] = projectWorkspaceData[entityKey]
543
- })
544
-
545
- Object.keys(namespacedWorkspaceData).forEach(entityKey => {
546
- updatedProjectWorkspaceData[entityKey] = { ...updatedProjectWorkspaceData[entityKey], ...namespacedWorkspaceData[entityKey] }
547
- })
548
-
549
- return updatedProjectWorkspaceData as Shared.Workspaces.AllWorkspaceData
550
- }
551
-
552
- const InitializeVersionInfo = (commitOrBranch: string | null, repoLocation: string): TVersionInfo => {
553
- if (!!commitOrBranch) {
554
- return {
555
- commit: commitOrBranch,
556
- }
557
- } else {
558
- return {
559
- filePath: repoLocation,
560
- }
561
- }
562
- }
563
-
564
- const BuildManifest = (workspaceData: Shared.Workspaces.AllWorkspaceData): Shared.Workspaces.AllWorkspaceData => {
565
- const manifest = {}
566
-
567
- const getEntityNameFromPackageManifest = (entityType: string, entityID: string) => {
568
- return Shared.Common.getValueSafe(workspaceData, [ entityType, entityID, "name" ], entityID);
569
- }
570
-
571
- Object.keys(workspaceData).forEach(entityType => {
572
- const entityImportedData = Object.keys(workspaceData[entityType])
573
-
574
- manifest[entityType] = {}
575
-
576
- entityImportedData.forEach(namedEntityKey => {
577
- manifest[entityType][namedEntityKey] = {
578
- id: namedEntityKey,
579
- name: getEntityNameFromPackageManifest(entityType, namedEntityKey)
580
- }
581
-
582
- })
583
- })
584
-
585
- return manifest as Shared.Workspaces.AllWorkspaceData
586
- }
587
-
588
- /**
589
- * Returns a boolean telling whether the package being installed is already installed - note, does not differentiate by commit or branch, but detects by package name
590
- * @param packages
591
- * @param version
592
- */
593
- export const IsPackageAlreadyInstalled = (installedPackages: IPackages, packageID: string) => {
594
- return packageID! in installedPackages
595
- }
596
-
597
- ////////////////////////////////////////////////////
598
- ///////////// Firestore read/write funcs
599
- ////////////////////////////////////////////////////
600
-
601
- const GetPackageDocRefAdmin = (
602
- packageInfo: IPackageInformation,
603
- packageContext: IPackageContext,
604
- ) => {
605
- const { environmentID, teamID, firestore } = packageContext
606
-
607
- return Shared.CommonOperations.getOrgConfigDocRefAdmin(firestore, teamID).collection("workspaces").doc(environmentID.toString()).collection("packages").doc(packageInfo.id)
608
- }
609
-
610
- const UpdatePackageInformationFS = (
611
- packageInfo: IPackageInformation,
612
- status: EPackageStatus,
613
- packageContext: IPackageContext,
614
- setTimestamp: boolean
615
- ) => {
616
- // example: setting timestamp once package has completed install
617
- const createdAt = setTimestamp ? packageContext.timestamp : packageInfo.createdAt
618
- const update = Object.assign({}, { ...packageInfo, status, createdAt }) as IPackageInformation
619
-
620
- return GetPackageDocRefAdmin(packageInfo, packageContext).set(update)
621
- .then(() => {
622
- return { status, id: packageInfo.id }
623
- })
624
- }
625
-
626
- const RemovePackageInformationFS = (
627
- packageInfo: IPackageInformation,
628
- packageContext: IPackageContext
629
- ) => {
630
- const { environmentID, teamID, firestore } = packageContext
631
-
632
- return Shared.CommonOperations.getOrgConfigDocRefAdmin(firestore, teamID).collection("workspaces").doc(environmentID.toString()).collection("packages").doc(packageInfo.id).delete()
633
- }
634
-
635
- const FlushFirestoreWorkspaceWithAllWorkspaceDataForCLIPackages = (
636
- firestore: firebase.default.firestore.Firestore,
637
- teamID: string,
638
- workspaceIDForTargetFlush: number,
639
- desiredWorkspaceDataState: Shared.Workspaces.AllWorkspaceData,
640
- workspaceDataToReplace: Shared.Workspaces.AllWorkspaceData
641
- ) => {
642
- // we do not want to compare packages when installing a package
643
- delete desiredWorkspaceDataState.packages
644
- delete workspaceDataToReplace.packages
645
-
646
- return Shared.Workspaces.FlushFirestoreWorkspaceWithAllWorkspaceData(
647
- firestore,
648
- teamID,
649
- workspaceIDForTargetFlush,
650
- desiredWorkspaceDataState,
651
- workspaceDataToReplace
652
- )
653
- }
654
-
655
- export const GetPackageListMessageCLI = (packageContext: IPackageContext) => {
656
- const { environmentID, teamID, firestore } = packageContext
657
-
658
- return Shared.Workspaces.getAllWorkspaceDataFromFirebase(firestore, teamID, environmentID.toString())
659
- .then(wsData => {
660
- const packages = Shared.Common.getValueSafe(wsData, [ "packages" ], null)
661
-
662
- if (packages && Object.keys(packages).length) {
663
- const listForCLI = Object.keys(packages).map(packageID => `${packageID} \n`)
664
- listForCLI.unshift(`\nPackages on workspace ${environmentID}: \n`)
665
- return listForCLI.join("")
666
- } else {
667
- return `No packages added to workspace ${environmentID}`
668
- }
669
- })
670
- }
671
-
672
- //////////////////////////////////////////////////////////
673
- ///////////// install/remove kickoff functions and classes
674
- //////////////////////////////////////////////////////////
675
-
676
- export class PackageProvider {
677
- id: string | null
678
- version: TPackageVersion
679
- loggerContext: Shared.Logging.LogContext
680
- versionInfo: TVersionInfo | null
681
- workspaceData: Shared.Workspaces.AllWorkspaceData | null
682
- commitHash: string | null
683
-
684
- constructor(dirOrURL: string, loggerContext: Shared.Logging.LogContext) {
685
- this.version = dirOrURL
686
- this.loggerContext = loggerContext
687
- // these vars will be null until getWorkspaceDataFS is called
688
- this.commitHash = null
689
- this.id = null
690
- this.workspaceData = null
691
- this.versionInfo = null
692
-
693
- // init logic
694
- if (!(typeof this.version === "string")) {
695
- const message = `Location should be a string, but is of type ${typeof this.version}`
696
- PackagesLogger.errorContext(this.loggerContext, message)
697
-
698
- throw new Error(`Invalid Package package location: ${message}`)
699
- }
700
- }
701
-
702
- _getVersionType(): EVersionType {
703
- const fileKeywordIndex = this.version.indexOf(localInstallKeyword)
704
- const httpIndex = this.version.indexOf("http")
705
-
706
- if (fileKeywordIndex === 0) {
707
- return EVersionType.file
708
- } else if (httpIndex === 0) {
709
- return EVersionType.url
710
- } else {
711
- const message = `Error with URL or filepath: "${this.version}" - filepath missing prefix "filepath:" or URL missing "http://" or "https://"`
712
- PackagesLogger.errorContext(this.loggerContext, message)
713
- throw new Error(message)
714
- }
715
- }
716
-
717
- _isProjectNameValid(name: string): boolean {
718
- let isValid = true
719
- const indexOfNamespace = name.indexOf("::")
720
-
721
- // projectName should not be only whitespace
722
- if (indexOfNamespace > -1) {
723
- isValid = false
724
- }
725
-
726
- return isValid
727
- }
728
-
729
- getWorkspaceDataFS(): Promise<Shared.Workspaces.AllWorkspaceData> {
730
- const versionType = this._getVersionType()
731
- let wsDataFunc
732
-
733
- switch (versionType) {
734
- case EVersionType.file: {
735
- const dirWithoutKeyword = this.version.substring(localInstallKeyword.length)
736
- const fsSettings: Shared.GitOperations.Filesystem = { fs, dir: dirWithoutKeyword }
737
-
738
- this.commitHash = null
739
- wsDataFunc = GetAllWorkspaceDataFromFileSystem(fsSettings)
740
-
741
- break
742
- }
743
- case EVersionType.url: {
744
- wsDataFunc = GetAllWorkspaceDataAndCommitHashFromRemoteRepository(this.version)
745
- .then((response) => {
746
- const { workspaceData, hashFromRemote } = response
747
-
748
- this.commitHash = hashFromRemote
749
-
750
- return workspaceData
751
- })
752
- break
753
- }
754
- default: {
755
- const message = `versionType was invariant: ${versionType}`
756
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, false, message)
757
- throw new Error(message)
758
- }
759
- }
760
-
761
- return wsDataFunc
762
- .then((workspaceData: Shared.Workspaces.AllWorkspaceData) => {
763
- const projects = workspaceData.projects
764
-
765
- if (!projects || !Object.keys(projects)) {
766
- PackagesLogger.errorContext(this.loggerContext, `Invalid Package: Missing projects, cannot generate package ID`)
767
- throw new Error(`Invalid Package: Missing projects, cannot generate package ID; Package owner must add a project name in Coalesce project settings and commit`)
768
- }
769
-
770
- // ensure package ID to be used as namespace, which is gathered from the default project name as of Lhotse 4.0
771
- const projectName = Shared.ProjectOperations.GetDefaultProjectName(projects!)
772
-
773
- if (!projectName) {
774
- PackagesLogger.errorContext(this.loggerContext, `Invalid Package: Missing project name`)
775
- throw new Error(`Invalid Package: Missing package ID; Package owner must add a project name in Coalesce project settings and commit`)
776
- }
777
-
778
- const defaultProject = projects![Shared.ProjectOperations.defaultProjectID] // currently only one default project
779
- const isDefaultProjectValid = Shared.ProjectOperations.ValidateProjectAsRunType(defaultProject)
780
-
781
- if (!isDefaultProjectValid.success) {
782
- const errorMessage = `Invalid Package: projects failed Run Type validation - projects["1"]: ${defaultProject}`
783
- PackagesLogger.errorContext(this.loggerContext, errorMessage, isDefaultProjectValid.errorString)
784
-
785
- throw new Error(errorMessage)
786
- }
787
-
788
- // validate project name
789
- if (!this._isProjectNameValid(projectName)) {
790
- PackagesLogger.errorContext(this.loggerContext, `Invalid Package: Project name contains reserved namespacing characters "::"`)
791
-
792
- throw new Error(`${projectName} contains "::" and is not a valid project name`)
793
- }
794
-
795
- this.id = projectName.toUpperCase() // should be uppercase
796
- this.workspaceData = workspaceData
797
- this.versionInfo = InitializeVersionInfo(this.commitHash, this.version)
798
-
799
- return this.workspaceData
800
- })
801
- }
802
- }
803
-
804
- export const InstallPackage = (
805
- packageProvider: PackageProvider,
806
- packageContext: IPackageContext
807
- ): Promise<void> => {
808
- const { firestore, teamID, environmentID, loggerContext } = packageContext
809
-
810
- return new Promise((resolve, reject) => {
811
- let projectWorkspaceData: Shared.Workspaces.AllWorkspaceData
812
- let packageDependencyInfo: IPackageInformation
813
- let manifest: Shared.Workspaces.AllWorkspaceData
814
-
815
- PackagesLogger.infoContext(loggerContext, `Gathering your project workspace data for environment: ${environmentID}`)
816
-
817
- return Shared.Workspaces.getAllWorkspaceDataFromFirebase(firestore, teamID, environmentID.toString())
818
- .then((projectWorkspaceDataFromFirestore) => {
819
- projectWorkspaceData = projectWorkspaceDataFromFirestore
820
-
821
- return packageProvider.getWorkspaceDataFS()
822
- })
823
- .then(() => {
824
- const { id, versionInfo } = packageProvider // at this point these vars have been defined
825
-
826
- if (!versionInfo) {
827
- const message = `versionInfo was ${versionInfo}`
828
- Shared.Common.assert(Shared.Logging.LoggingArea.Packages, false, message)
829
- throw new Error(message)
830
- }
831
-
832
- PackagesLogger.infoContext(loggerContext, `Installing package using namespace "${id!}::"`)
833
-
834
- packageDependencyInfo = InitializePackageDependencyInfoForPackage(id!, versionInfo, packageProvider.version, loggerContext.userID! || "unknown")
835
-
836
- return UpdatePackageInformationFS(packageDependencyInfo, EPackageStatus.adding, packageContext, false)
837
- })
838
- .then((packageInfoFS) => {
839
- const { status, id } = packageInfoFS
840
-
841
- PackagesLogger.infoContext(loggerContext, `${status} ${id}...building firestore updates payload...`)
842
- const { desiredState, namespacedData } = GetNamespacedDataAndDesiredStateForFSFlush(packageDependencyInfo, packageProvider.workspaceData!, projectWorkspaceData, packageContext)
843
-
844
- manifest = BuildManifest(namespacedData)
845
-
846
- PackagesLogger.infoContext(loggerContext, "Applying updates to firestore...")
847
- return FlushFirestoreWorkspaceWithAllWorkspaceDataForCLIPackages(
848
- firestore,
849
- teamID,
850
- environmentID,
851
- desiredState,
852
- projectWorkspaceData
853
- )
854
- })
855
- .then(() => {
856
- return UpdatePackageInformationFS({ ...packageDependencyInfo, manifest }, EPackageStatus.added, packageContext, true)
857
- })
858
- .then((packageInfoFS) => {
859
- const { status, id } = packageInfoFS
860
- PackagesLogger.infoContext(loggerContext, `${status} ${id}...update successful!`)
861
- resolve()
862
- })
863
- .catch(error => {
864
- if (!!packageDependencyInfo) {
865
- return UpdatePackageInformationFS(packageDependencyInfo, EPackageStatus.error, packageContext, false)
866
- .then((packageInfo) => {
867
- const { status, id } = packageInfo
868
- PackagesLogger.errorContext(loggerContext, `Update failed for ${id}. Package ${id} status: ${status}, removing package information from project: `, error)
869
- PackagesLogger.errorContext(loggerContext, error)
870
-
871
- return UninstallPackage(id, packageContext)
872
- })
873
- .then(() => {
874
- const message = `All data from package ${packageDependencyInfo.id} removed from project after error during installation.`
875
- PackagesLogger.infoContext(loggerContext, message)
876
- resolve()
877
- })
878
- .catch(error => reject(error))
879
- } else {
880
- PackagesLogger.errorContext(loggerContext, `Update failed when installing package from ${packageProvider.version}`, error)
881
- reject(error)
882
- }
883
- })
884
- })
885
- }
886
-
887
- export const UninstallPackage = (
888
- packageID: TPackageIdentifier,
889
- packageContext: IPackageContext
890
- ): Promise<void> => {
891
- const { firestore, teamID, environmentID, loggerContext } = packageContext
892
-
893
- return new Promise((resolve, reject) => {
894
- let projectWorkspaceData: Shared.Workspaces.AllWorkspaceData
895
- let prunedWorkspaceData: Shared.Workspaces.AllWorkspaceData
896
-
897
- PackagesLogger.infoContext(loggerContext, `Gathering your project workspace data for environment: ${environmentID}`)
898
-
899
- return Shared.Workspaces.getAllWorkspaceDataFromFirebase(firestore, teamID, environmentID.toString())
900
- .then((projectWorkspaceDataFromFirestore) => {
901
- projectWorkspaceData = projectWorkspaceDataFromFirestore
902
- const allProjectDependencies: IPackages = Shared.Common.getValueSafe(projectWorkspaceDataFromFirestore, [ "packages" ], {})
903
- const thisDependencyInfo = allProjectDependencies[packageID]
904
-
905
- if (!thisDependencyInfo) {
906
- const depKeys = Object.keys(allProjectDependencies)
907
- reject(new Error(`Environment ${environmentID} has ${depKeys.length} packages installed: ${depKeys}. Package "${packageID}" is not installed to this environment.`))
908
- return
909
- }
910
-
911
- PackagesLogger.infoContext(loggerContext, `Removing package workspace data from your project`)
912
- prunedWorkspaceData = RemoveNamespacedWorkspaceData(projectWorkspaceData, thisDependencyInfo, packageContext)
913
-
914
- return UpdatePackageInformationFS(thisDependencyInfo, EPackageStatus.removing, packageContext, false)
915
- .then(() => {
916
- PackagesLogger.infoContext(loggerContext, `Applying updates to firestore...`)
917
-
918
- return FlushFirestoreWorkspaceWithAllWorkspaceDataForCLIPackages(
919
- firestore,
920
- teamID,
921
- environmentID,
922
- prunedWorkspaceData,
923
- projectWorkspaceData
924
- )
925
- })
926
- .then(() => {
927
- return RemovePackageInformationFS(thisDependencyInfo, packageContext)
928
- })
929
- .then(() => {
930
- PackagesLogger.infoContext(loggerContext, `Removed ${packageID} workspace data from your project`)
931
- resolve()
932
- })
933
- .catch((error) => {
934
- if (!!thisDependencyInfo) {
935
- return UpdatePackageInformationFS(thisDependencyInfo, EPackageStatus.error, packageContext, false)
936
- .then(() => {
937
- PackagesLogger.errorContext(loggerContext, `Encountered a problem when removing ${thisDependencyInfo.id}, removal aborted. Please try again.`)
938
- reject(error)
939
- })
940
- .catch(error => reject(error))
941
- } else {
942
- PackagesLogger.errorContext(loggerContext, `Update failed when removing package`, error)
943
- reject(error)
944
- }
945
- })
946
- })
947
- })
948
- }
949
-
950
- ////////////////////////////////////////////////////
951
- ///////////// helper Jest test funcs
952
- ////////////////////////////////////////////////////
953
-
954
- export const NamespaceWorkspaceData_Testing = (
955
- allWorkspaceData: Shared.Workspaces.AllWorkspaceData,
956
- packageInfo: IPackageInformation,
957
- ): Shared.Workspaces.AllWorkspaceData => {
958
- const { id, version } = packageInfo
959
- const namespacedWorkspaceData = {}
960
-
961
- Object.keys(allWorkspaceData).forEach(wsKey => {
962
- try {
963
- if (WorkspaceEntityNamespaceFunctionLookup[wsKey]) {
964
- PackagesLogger.info(`JEST TEST: Preparing workspace data for import: ${wsKey}`)
965
- namespacedWorkspaceData[wsKey] = WorkspaceEntityNamespaceFunctionLookup[wsKey].add(id, version, allWorkspaceData[wsKey])
966
- }
967
- } catch (error) {
968
- const message = `JEST TEST: error when preparing workspace data for import: ${wsKey}`
969
-
970
- if (error instanceof Error) {
971
- PackagesLogger.error(message, error.message, error.name, error.stack, error)
972
- } else {
973
- PackagesLogger.error(message, error)
974
- }
975
-
976
- throw error
977
- }
978
- })
979
-
980
- return namespacedWorkspaceData as Shared.Workspaces.AllWorkspaceData
981
- }
982
-
983
- // will remove namespaced package workspace data from allWorkspaceData and return the updated object
984
- export const RemoveNamespacedWorkspaceData_Testing = (
985
- allWorkspaceData: Shared.Workspaces.AllWorkspaceData,
986
- packageInfo: IPackageInformation
987
- ): Shared.Workspaces.AllWorkspaceData => {
988
- const prunedWorkspaceData = {}
989
- const { id } = packageInfo
990
-
991
- Object.keys(allWorkspaceData).forEach(wsKey => {
992
- try {
993
- if (WorkspaceEntityNamespaceFunctionLookup[wsKey]) {
994
- PackagesLogger.info(`JEST TEST: Pruning workspace data: ${wsKey}`)
995
- prunedWorkspaceData[wsKey] = WorkspaceEntityNamespaceFunctionLookup[wsKey].remove(id, allWorkspaceData[wsKey])
996
- } else {
997
- prunedWorkspaceData[wsKey] = allWorkspaceData[wsKey]
998
- }
999
- } catch (error) {
1000
- const message = `JEST TEST: error when pruning workspace data for: ${wsKey}`
1001
-
1002
- if (error instanceof Error) {
1003
- PackagesLogger.error(message, error.message, error.name, error.stack, error)
1004
- } else {
1005
- PackagesLogger.error(message, error)
1006
- }
1007
-
1008
- throw error
1009
- }
1010
- })
1011
-
1012
- return prunedWorkspaceData as Shared.Workspaces.AllWorkspaceData
1013
- }
1014
-
1015
- export const GetPackageSupportedEntityTypes = (): string[] => {
1016
- return Object.keys(WorkspaceEntityNamespaceFunctionLookup)
1017
- }