From 41ece8d1b9ec9311593b29baf81ccc10ad7dfc25 Mon Sep 17 00:00:00 2001 From: guidohollander Date: Sat, 24 Jun 2023 20:53:30 +0200 Subject: [PATCH] switch, update, flyway support --- anglo-helper-config.json | 29 ++++ solutions.json => solutions copy 2.json | 4 +- src/AngloResource.ts | 74 +++++++- src/ConfigurationManager.ts | 36 ++++ src/ExternalComponentResource.ts | 106 +++++++++++- src/ImplementationInternalClass.ts | 20 --- src/ImplementationResource.ts | 107 +++++++++++- src/ImplementationResourceReader.ts | 22 +-- src/InternalComponentResource.ts | 65 ++++++- src/data/exclude.json | 8 - src/flyway.ts | 163 ++++++++++++++++++ src/index.ts | 27 ++- src/parser.ts | 8 + src/svn.ts | 215 +++++++++++++++++++----- src/types.ts | 65 +++++-- tsconfig.json | 2 +- 16 files changed, 839 insertions(+), 112 deletions(-) create mode 100644 anglo-helper-config.json rename solutions.json => solutions copy 2.json (98%) create mode 100644 src/ConfigurationManager.ts delete mode 100644 src/ImplementationInternalClass.ts delete mode 100644 src/data/exclude.json create mode 100644 src/flyway.ts diff --git a/anglo-helper-config.json b/anglo-helper-config.json new file mode 100644 index 0000000..9e7a5e5 --- /dev/null +++ b/anglo-helper-config.json @@ -0,0 +1,29 @@ +{ + "paths": { + "flywayPath": "c:/fw/flyway", + "componentMigrationsFolder": "_CONTINUOUS_DELIVERY/DATABASE_MIGRATIONS/", + "implementationMigrationsFolder": "_GENERAL/DATABASE_MIGRATIONS/" + }, + "exclusions": [ + ".metadata", + "TEST", + "com.bearingpoint.ird.anguilla.remoting_connector", + "console-1.0-dev.jar", + "migrations.bat", + "etc" + ], + "implementations": [ + { + "name": "MTS_ANGUILLA", + "functionalName": "MTS Anguilla", + "customer": "ANGUILLA ", + "customerCode": "aia_mts", + "path": "D:\\repo\\aia_mts_trunk\\", + "class": "MxS", + "baseUrl": "https://svn.hollanderconsulting.nl", + "url": "/svn/MTS_ANGUILLA/trunk", + "dbConnectionString": "jdbc:sqlserver://localhost:1433;databaseName=aia_mts;integratedSecurity=true;", + "selected": true + } + ] +} diff --git a/solutions.json b/solutions copy 2.json similarity index 98% rename from solutions.json rename to solutions copy 2.json index 727154d..82ba2c4 100644 --- a/solutions.json +++ b/solutions copy 2.json @@ -41,7 +41,7 @@ "class": "MxS", "baseUrl": "https://svn.bearingpointcaribbean.com", "url": "/svn/MTS_SKN/trunk", - "selected": true + "selected": false }, { "name": "ONLINE_SKN", @@ -75,7 +75,7 @@ "class": "MxS", "baseUrl": "https://svn.bearingpointcaribbean.com", "url": "/svn/MTS_GRENADA/trunk", - "selected": true + "selected": false }, { "name": "ONLINE_GD", diff --git a/src/AngloResource.ts b/src/AngloResource.ts index dff7509..7ab9ca5 100644 --- a/src/AngloResource.ts +++ b/src/AngloResource.ts @@ -1,24 +1,67 @@ import { DirType } from "./types.js"; import { ExternalComponentResource } from "./ExternalComponentResource.js"; +import { svnCheckout, svnInfo } from "./svn.js"; export class AngloResource { - protected _url = ""; protected _baseUrl = ""; protected _fullUrl = ""; protected _bareComponentUrl = ""; - protected _repository = ""; - protected _repositoryUrl = ""; protected _componentFolder?: string; - protected _implementationUrl = ""; + protected _componentName = ""; protected _dirType: DirType = DirType.trunk; + protected _implementationUrl = ""; + protected _isMissing?: boolean = undefined; + protected _workingCopyPath = ""; + protected _workingCopyResource?: AngloResource = undefined; + protected _repository = ""; + protected _repositoryUrl = ""; + protected _switchNeeded?: boolean = undefined; + protected _url = ""; protected _version?: string; - - protected _componentName = ""; + protected _checkoutNumberOfResults = 0; + protected _checkoutRevision = 0; constructor(baseUrl: string, url: string) { this.parseUrl(baseUrl, url); } + public async getWorkingCopyResource(): Promise { + if (this._workingCopyPath) { + svnInfo(this._workingCopyPath) + .then((svnResource) => { + const url = svnResource.fullUrl.replace(this._baseUrl, ""); + this._isMissing = svnResource.fullUrl === undefined; + if (!this._isMissing) { + this._workingCopyResource = new AngloResource(this._baseUrl, url); + this._switchNeeded = + this.dirTypeAndVersion() !== + this._workingCopyResource.dirTypeAndVersion(); + return this._workingCopyResource; + } + }) + .catch(async (error) => { + console.log("caught", error); + if (error.message === "NotAWorkingCopy") { + this._isMissing = true; + const checkoutResult = await svnCheckout( + this._baseUrl, + this._url, + this._workingCopyPath + ); + this._checkoutNumberOfResults = checkoutResult.numberOfUpdates; + this._checkoutRevision = checkoutResult.revision; + } else { + console.log("other error"); + throw error; + } + }); + } else { + this._isMissing = true; + // this._switchNeeded = true; + return undefined; + } + } + componentFolder(): string | undefined { return this._componentFolder; } @@ -41,6 +84,9 @@ export class AngloResource { implementationUrl(): string { return this._implementationUrl; } + private inspectVersion(): void { + // todo + } repositoryName(): string { return this._repository; } @@ -118,7 +164,6 @@ export class AngloResource { this._componentName = segments[trunkTagBranchIndex + 1]; } } - this._implementationUrl = segments .slice(0, trunkTagBranchIndex + (this._version === undefined ? 1 : 2)) .join("/"); @@ -140,4 +185,19 @@ export class AngloResource { return `${segments[0]}.${segments[1]}.${lastSegments.join("-")}`; } } + isMissingLocally(): boolean | undefined { + return this._isMissing; + } + localDiffersFromExternal(): boolean | undefined { + return this._switchNeeded; + } + // private getLocalPath(): string { + // if (this instanceof ExternalComponentResource) { + // return 'c:\\ext'; + // } else if (this instanceof ImplementationResource) { + // return 'c:\\imp'; + // // } else if (this instanceof InternalComponentResource) { + // // return 'c:\\int'; + // } else return ''; + // } } diff --git a/src/ConfigurationManager.ts b/src/ConfigurationManager.ts new file mode 100644 index 0000000..ec8b166 --- /dev/null +++ b/src/ConfigurationManager.ts @@ -0,0 +1,36 @@ +import * as fs from "fs"; +import { Settings } from "./types"; + +export class ConfigurationManager { + private static instance: ConfigurationManager; + private settings: Settings | null = null; + + private constructor() { + // private + } + + public static getInstance(): ConfigurationManager { + if (!ConfigurationManager.instance) { + ConfigurationManager.instance = new ConfigurationManager(); + } + return ConfigurationManager.instance; + } + + public loadSettings(filePath: string): void { + const fileData = fs.readFileSync(filePath, "utf-8"); + this.settings = JSON.parse(fileData) as Settings; + } + + public getSettings(): Settings { + if (!this.settings) { + throw new Error("Settings not loaded. Call loadSettings() first."); + } + return this.settings; + } +} + +export function getConfig(): Settings { + const configManager = ConfigurationManager.getInstance(); + configManager.loadSettings("./anglo-helper-config.json"); + return configManager.getSettings(); +} diff --git a/src/ExternalComponentResource.ts b/src/ExternalComponentResource.ts index b9ddc5c..afcc3a9 100644 --- a/src/ExternalComponentResource.ts +++ b/src/ExternalComponentResource.ts @@ -1,10 +1,26 @@ +import * as fs from "fs"; import { AngloVersionedResource } from "./AngloVersionedResource.js"; import { ImplementationResource } from "./ImplementationResource.js"; -import { ImplementationInternal } from "./ImplementationInternalClass.js"; +import { AngloResource } from "./AngloResource.js"; +import { getConfig } from "./ConfigurationManager.js"; +import { + svnPropGet, + svnSwitchExternal, + svnSwitchWorkingCopy, + svnUpdate, +} from "./svn.js"; +import { DirType } from "./types.js"; +import { flywayMigrate } from "./flyway.js"; +import { unifyPath } from "./parser.js"; + +const config = getConfig(); export class ExternalComponentResource extends AngloVersionedResource { public _parentSolutionImplementation: ImplementationResource; - private implementationInternal: ImplementationInternal; + private _updated = false; + private _switchedWorkingCopy = false; + private _flywayed = false; + private _dbScriptFolder = ""; constructor( baseUrl: string, @@ -13,12 +29,47 @@ export class ExternalComponentResource extends AngloVersionedResource { ) { super(baseUrl, url); this._parentSolutionImplementation = parentSolutionImplementation; - this.implementationInternal = new ImplementationInternal(); + this._workingCopyPath = + parentSolutionImplementation._implementationContext && this._componentName + ? `${parentSolutionImplementation._implementationContext.path}${this._componentName}` + : ""; + this._dbScriptFolder = unifyPath( + `${this._workingCopyPath}/${config.paths.componentMigrationsFolder}` + ); + this.getWorkingCopyResource(); } + dbScriptFolder() { + return this._dbScriptFolder; + } parentSolutionImplementation(): ImplementationResource { return this._parentSolutionImplementation; } + public async flyway(): Promise { + try { + if ( + this._workingCopyPath && + this._parentSolutionImplementation._implementationContext + ) { + if (fs.existsSync(this._dbScriptFolder)) { + //returns an array of flywayed + this._flywayed = await flywayMigrate( + this._componentName, + this._dbScriptFolder, + this._parentSolutionImplementation._implementationContext + .dbConnectionString + ); + return this._flywayed; + } else return false; + } else return false; + } catch (error) { + console.log("Other error", error); + throw error; + } + } + public flywayed(): boolean { + return this._flywayed; + } public async tag(): Promise { // todo return new ExternalComponentResource( @@ -27,7 +78,52 @@ export class ExternalComponentResource extends AngloVersionedResource { this._parentSolutionImplementation ); } - public async update(): Promise { - await this.implementationInternal.update(this); + public async switchWorkingCopy(): Promise { + try { + if (this._switchNeeded) { + this._switchedWorkingCopy = await svnSwitchWorkingCopy( + this._workingCopyPath, + this.baseUrl(), + this.url() + ); + return this._switchedWorkingCopy; + } else return false; + } catch (error) { + console.log("Other error", error); + throw error; + } + } + public switchedWorkingCopy(): boolean { + return this._switchedWorkingCopy; + } + public async switchExternal( + target: ExternalComponentResource + ): Promise { + await svnSwitchExternal( + this.parentSolutionImplementation().baseUrl(), + this.parentSolutionImplementation().url(), + this.componentName(), + this.implementationUrl(), + target.implementationUrl() + ); + return true; + } + public async update(): Promise { + try { + if (this._workingCopyPath && this.dirType() != DirType.tags) { + //returns an array of updated + this._updated = await svnUpdate(this._workingCopyPath); + return this._updated; + } else return false; + } catch (error) { + console.log("Other error", error); + throw error; + } + } + public updated(): boolean { + return this._updated; + } + workingCopy(): AngloResource | undefined { + return this._workingCopyResource; } } diff --git a/src/ImplementationInternalClass.ts b/src/ImplementationInternalClass.ts deleted file mode 100644 index 8a72812..0000000 --- a/src/ImplementationInternalClass.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { svnUpdate } from "./svn.js"; -import { ExternalComponentResource } from "./ExternalComponentResource"; -import { InternalComponentResource } from "./InternalComponentResource"; - -export class ImplementationInternal { - public async update( - resource: InternalComponentResource | ExternalComponentResource - ): Promise { - if ( - resource._parentSolutionImplementation._implementationContext && - resource.componentName() - ) { - await svnUpdate( - `${ - resource._parentSolutionImplementation._implementationContext.path - }${resource.componentName()}` - ); - } - } -} diff --git a/src/ImplementationResource.ts b/src/ImplementationResource.ts index 61553b1..61cb0ad 100644 --- a/src/ImplementationResource.ts +++ b/src/ImplementationResource.ts @@ -9,11 +9,13 @@ import { svnList, svnPropGet } from "./svn.js"; import { ExternalComponentResource } from "./ExternalComponentResource.js"; import { AngloVersionedResource } from "./AngloVersionedResource.js"; import { InternalComponentResource } from "./InternalComponentResource.js"; +import { AngloResource } from "./AngloResource.js"; +import { move } from "./parser.js"; export class ImplementationResource extends AngloVersionedResource { protected _externalsCollection: ExternalComponentResource[] = []; protected _internalsCollection: InternalComponentResource[] = []; - public _implementationContext?: TypeSolutionImplementation | undefined; + public _implementationContext: TypeSolutionImplementation | undefined; constructor( baseUrl: string, @@ -22,8 +24,17 @@ export class ImplementationResource extends AngloVersionedResource { ) { super(baseUrl, url); this._implementationContext = implementationContext; + this._workingCopyPath = implementationContext + ? implementationContext.path + : ""; + } + public async getCheckoutResults(): Promise { + let sum = 0; + for (const angloResource of AngloResource) { + sum += angloResource._checkoutNumberOfResults; + } + return sum; } - public async getExternals(): Promise { const propGetResult = await svnPropGet( "svn:externals", @@ -44,12 +55,12 @@ export class ImplementationResource extends AngloVersionedResource { (project) => !project.componentName().includes("FRONTEND") ); } + public async getInternals(): Promise { const svnListResult: InternalComponent[] = await svnList( this._baseUrl, this._url ); - svnListResult.forEach((internalComponent: InternalComponent) => { const thisInternal = new InternalComponentResource( this._baseUrl, @@ -59,9 +70,18 @@ export class ImplementationResource extends AngloVersionedResource { this._internalsCollection.push(thisInternal); }); + //move CD folder to last index position + const componentIndex = this._internalsCollection.findIndex( + (object) => object.componentName() === "_CONTINUOUS_DELIVERY" + ); + move( + this._internalsCollection as [], + componentIndex, + this._internalsCollection.length - 1 + ); + return this._internalsCollection; } - public async deploymentCheck(): Promise { if (!this._externalsCollection) { await this.getExternals(); @@ -79,4 +99,83 @@ export class ImplementationResource extends AngloVersionedResource { this._implementationContext ); } + public async suf( + doSwitch = false, + doUpdate = false, + doFlyway = false + ): Promise<{ + switched: AngloResource[]; + updated: AngloResource[]; + flyway: AngloResource[]; + }> { + // switch/update/flyway internals + const internalPromises = this._internalsCollection.map( + async (internalComponentResource: InternalComponentResource) => { + if (doUpdate) await internalComponentResource.update(); + if (doFlyway) await internalComponentResource.flyway(); + } + ); + // switch/update/flyway externals + const externalPromises = this._externalsCollection.map( + async (externalComponentResource: ExternalComponentResource) => { + if (doSwitch) await externalComponentResource.switchWorkingCopy(); + if (doUpdate) await externalComponentResource.update(); + if (doFlyway) await externalComponentResource.flyway(); + } + ); + // wait for all switch/update/flyway operations to complete + await Promise.all([...internalPromises, ...externalPromises]); + + // filter only switched ones + const switchedExternals = this._externalsCollection.filter( + (externalComponentResource) => { + return externalComponentResource.switchedWorkingCopy(); + } + ); + + // filter only updates ones + const updatedInternals = this._internalsCollection.filter( + (internalComponentResource) => { + return internalComponentResource.updated(); + } + ); + const updatedExternals = this._externalsCollection.filter( + (externalComponentResource) => { + return externalComponentResource.updated(); + } + ); + // combine internals and externals as AngloResources for return + const updatedAngloResources: AngloResource[] = []; + for (const internalComponentResource of updatedInternals) { + updatedAngloResources.push(internalComponentResource); + } + for (const externalComponentResource of updatedExternals) { + updatedAngloResources.push(externalComponentResource); + } + + // filter only flywayed ones + const flywayInternals = this._internalsCollection.filter( + (internalComponentResource) => { + return internalComponentResource.flywayed(); + } + ); + const flywayExternals = this._externalsCollection.filter( + (externalComponentResource) => { + return externalComponentResource.flywayed(); + } + ); + // combine internals and externals as AngloResources for return + const flywayAngloResources: AngloResource[] = []; + for (const internalComponentResource of flywayInternals) { + flywayAngloResources.push(internalComponentResource); + } + for (const externalComponentResource of flywayExternals) { + flywayAngloResources.push(externalComponentResource); + } + return { + switched: switchedExternals, + updated: updatedAngloResources, + flyway: flywayAngloResources, + }; + } } diff --git a/src/ImplementationResourceReader.ts b/src/ImplementationResourceReader.ts index d350667..4c16040 100644 --- a/src/ImplementationResourceReader.ts +++ b/src/ImplementationResourceReader.ts @@ -1,16 +1,16 @@ -import * as fs from "fs"; -import { DirType, TypeSolutionImplementation } from "./types.js"; +import { TypeSolutionImplementation } from "./types.js"; import { InternalComponentResource } from "./InternalComponentResource.js"; import { ImplementationResource } from "./ImplementationResource.js"; import { ExternalComponentResource } from "./ExternalComponentResource.js"; +import { getConfig } from "./ConfigurationManager.js"; export class ImplementationResourceReader { _solutionImplementationsInFile: TypeSolutionImplementation[]; _solutionImplementationCollection: ImplementationResource[] = []; constructor() { - const json = fs.readFileSync("solutions.json", "utf-8"); - this._solutionImplementationsInFile = JSON.parse(json).filter( + const config = getConfig(); + this._solutionImplementationsInFile = config.implementations.filter( (solutionImplementation: TypeSolutionImplementation) => { return solutionImplementation.selected; } @@ -50,17 +50,19 @@ export class ImplementationResourceReader { // ); externals.forEach( async (externalComponentResource: ExternalComponentResource) => { - externalComponentResource.update(); + // await externalComponentResource.getLocalWorkingCopyResource(); + // await externalComponentResource.switch(); + // await externalComponentResource.update(); } ); } if (autoLoadInternals) { const internals = await thisSolutionImplementation.getInternals(); - internals.forEach( - async (internalComponentResource: InternalComponentResource) => { - internalComponentResource.update(); - } - ); + // internals.forEach( + // async (internalComponentResource: InternalComponentResource) => { + // await internalComponentResource.update(); + // } + // ); } this._solutionImplementationCollection.push( thisSolutionImplementation diff --git a/src/InternalComponentResource.ts b/src/InternalComponentResource.ts index 863dee5..8b8f895 100644 --- a/src/InternalComponentResource.ts +++ b/src/InternalComponentResource.ts @@ -1,10 +1,19 @@ +import * as fs from "fs"; import { AngloResource } from "./AngloResource.js"; import { ImplementationResource } from "./ImplementationResource.js"; -import { ImplementationInternal } from "./ImplementationInternalClass.js"; +import { svnSwitchWorkingCopy, svnUpdate } from "./svn.js"; +import { DirType } from "./types.js"; +import { getConfig } from "./ConfigurationManager.js"; +import { flywayMigrate } from "./flyway.js"; +import { unifyPath } from "./parser.js"; + +const config = getConfig(); export class InternalComponentResource extends AngloResource { public _parentSolutionImplementation: ImplementationResource; - private sharedImplementationInternalClass: ImplementationInternal; + private _updated = false; + private _flywayed = false; + private _dbScriptFolder = ""; constructor( baseUrl: string, @@ -13,17 +22,61 @@ export class InternalComponentResource extends AngloResource { ) { super(baseUrl, url); this._parentSolutionImplementation = parentSolutionImplementation; - this.sharedImplementationInternalClass = new ImplementationInternal(); + this._workingCopyPath = + parentSolutionImplementation._implementationContext && this._componentName + ? `${parentSolutionImplementation._implementationContext.path}${this._componentName}` + : ""; + this._dbScriptFolder = unifyPath( + `${this._workingCopyPath}/${config.paths.implementationMigrationsFolder}` + ); + this.getWorkingCopyResource(); } public async checkSpecifics(): Promise { // todo return true; } - async update(): Promise { - await this.sharedImplementationInternalClass.update(this); + public async flyway(): Promise { + try { + if ( + this._workingCopyPath && + this._parentSolutionImplementation._implementationContext && + this._componentName === "_CONTINUOUS_DELIVERY" + ) { + if (fs.existsSync(this._dbScriptFolder)) { + //returns an array of flywayed + this._flywayed = await flywayMigrate( + "__MigrationsHistory", + this._dbScriptFolder, + this._parentSolutionImplementation._implementationContext + .dbConnectionString + ); + return this._flywayed; + } else return false; + } else return false; + } catch (error) { + console.log("Other error", error); + throw error; + } + } + public flywayed(): boolean { + return this._flywayed; + } + public updated(): boolean { + return this._updated; + } + public async update(): Promise { + try { + if (this._workingCopyPath && this.dirType() != DirType.tags) { + //returns an array of updated + this._updated = await svnUpdate(this._workingCopyPath); + return this._updated; + } else return false; + } catch (error) { + console.log("Other error", error); + throw error; + } } - parentSolutionImplementation(): ImplementationResource { return this._parentSolutionImplementation; } diff --git a/src/data/exclude.json b/src/data/exclude.json deleted file mode 100644 index 812286b..0000000 --- a/src/data/exclude.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - ".metadata", - "TEST", - "com.bearingpoint.ird.anguilla.remoting_connector", - "console-1.0-dev.jar", - "migrations.bat", - "etc" -] diff --git a/src/flyway.ts b/src/flyway.ts new file mode 100644 index 0000000..3ee682e --- /dev/null +++ b/src/flyway.ts @@ -0,0 +1,163 @@ +import { exec, ExecException } from "child_process"; +import { getConfig } from "./ConfigurationManager.js"; + +const config = getConfig(); +const flywayPath = config.paths.flywayPath; + +config.paths.implementationMigrationsFolder; + +function execShellCommand(cmd: string): Promise { + return new Promise((resolve, reject) => { + // console.log('cmd', cmd); + exec( + cmd, + { maxBuffer: 1024 * 500 }, + (error: ExecException | null, stdout: string, stderr: string) => { + if (error) { + // Handle known errors + if (error.message.includes("ERROR")) { + reject(error); + } else { + reject(error); + } + } + resolve(stdout || stderr); + } + ); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function flywayCmdWithResponse( + flywayCommand: string, + flywayArgs: string[] + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): Promise { + return new Promise((resolve, reject) => { + const execCommand = `"${flywayPath}" ${flywayCommand} ${flywayArgs.join( + " " + )}`; + execShellCommand(execCommand) + .then((flywayCmdResponse) => { + resolve(flywayCmdResponse); + }) + .catch((error) => { + reject(error); + }); + }); +} + +export async function flywayMigrate( + flywayTable: string, + componentScriptFolder: string, + dbConnectionString: string +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const flywayMigrateResponse: any = await flywayCmdWithResponse("migrate", [ + "-color=always", + `-locations="filesystem:${componentScriptFolder}"`, + '-schemas="migrations"', + `-table="${flywayTable}"`, + `-url=${dbConnectionString}`, + ]); + // migrate + if (!flywayMigrateResponse.includes("No migration necessary")) { + return true; + } else return false; + } catch (error) { + console.log(error); + throw error; + } +} +// async function perform(componentEntry) { +// // set default flyway action to migrate +// let flywayAction = 'migrate'; +// const dir = anglo.unifyPath(state.workingCopyFolder) + componentEntry.key; +// const dirWithQuotedProjectName = +// anglo.unifyPath(state.workingCopyFolder) + +// JSON.stringify(componentEntry.key); +// // override default when command line option flywayValidateOnly or flywayRepairOnly is set +// if (clargs.argv.flywayValidateOnly) flywayAction = 'validate'; // instead of migrate +// if (clargs.argv.flywayRepairOnly) flywayAction = 'repair'; // instead of migrate +// const flywayDatabaseTable = '__MigrationsHistory'; +// const flywayDatabaseSchema = 'migrations'; +// if ( +// componentEntry.componentContinuousDeliveryFolderFound || +// componentEntry.generalContinuousDeliveryFolderFound +// ) { +// // in case of flywayReplaceVariables: check for any uncommitted files beforehand, since all potential changes will be reverted when variables are replaced +// const uncommittedChanges = await checkUncommittedChanges(componentEntry); +// if (!uncommittedChanges) { +// let flywayTable; +// let FlywayDir; +// let FlywayDirWithQuotedProjectName; +// let suffix; +// let flywayLocations; +// if (componentEntry.generalContinuousDeliveryFolderFound) { +// flywayTable = JSON.stringify(flywayDatabaseTable); +// suffix = '/_GENERAL/DATABASE_MIGRATIONS/'; +// FlywayDir = `${dir}${suffix}`; +// FlywayDirWithQuotedProjectName = `${dirWithQuotedProjectName}/_GENERAL/DATABASE_MIGRATIONS/`; +// flywayLocations = JSON.stringify(`filesystem:${FlywayDir}`); +// } else { +// flywayTable = JSON.stringify(componentEntry.key); +// suffix = '/_CONTINUOUS_DELIVERY/DATABASE_MIGRATIONS/'; +// FlywayDir = `${dir}${suffix}`; +// FlywayDirWithQuotedProjectName = `${dirWithQuotedProjectName}/_CONTINUOUS_DELIVERY/DATABASE_MIGRATIONS/`; +// flywayLocations = JSON.stringify(`filesystem:${FlywayDir}`); +// } +// const flywayDatabaseConnectionString = `jdbc:sqlserver://${ +// state.profile.flywayDatabaseServer +// }:${state.profile.flywayDatabaseServerPort};databaseName=${ +// state.profile.flywayDatabaseName +// };integratedSecurity=${ +// state.profile.flywayDatabaseIntegratedSecurity ? 'true' : 'false' +// };`; +// let credentialsString = ''; +// if (!state.profile.flywayDatabaseIntegratedSecurity) { +// credentialsString = `-user=${state.profile.flywayDatabaseUsername} -password=${state.profile.flywayDatabasePassword}`; +// } +// const flywayCommand = `"${state.profile.flywayPath}flyway" ${flywayAction} -color=always -locations=${flywayLocations} -schemas=${flywayDatabaseSchema} -table=${flywayTable} -url=${flywayDatabaseConnectionString} ${credentialsString}`; + +// await updateVariablesInSqlFiles(componentEntry, FlywayDir); +// let flywayResult = await util.execShellCommand(flywayCommand); +// // flywayResult = flywayResult.replace(/^Database: .*\(Microsoft SQL Server [\d]+\.[\d]+\)/m, ''); +// // flywayResult = flywayResult.replace(/^Flyway Community Edition .*/m, ''); +// // flywayResult = flywayResult.replace(/^Current version of schema .*/m, ''); +// // flywayResult = flywayResult.trim(); +// if (flywayResult.includes('No migration necessary')) { +// consoleLog.logThisLine('[F]', 'gray'); +// } else { +// consoleLog.logNewLine('', 'white'); +// consoleLog.logNewLine( +// '---------------------------------------------------------------------------------------------', +// 'cyan' +// ); +// consoleLog.logNewLine(`${flywayCommand}`, 'cyan'); +// consoleLog.logNewLine( +// '---------------------------------------------------------------------------------------------', +// 'cyan' +// ); +// consoleLog.logNewLine('', 'white'); +// anglo.memorable( +// '[F]', +// state.arrFlywayUpdatedCollection, +// componentEntry, +// flywayResult, +// 'green' +// ); +// if (state.profile.verbose) { +// consoleLog.logNewLine('', 'white'); +// consoleLog.logNewLine('', 'white'); +// consoleLog.logNewLine(flywayResult, 'gray'); +// } +// } +// await revertChanges(FlywayDirWithQuotedProjectName); +// } else { +// consoleLog.logNewLine('[F] skipped - uncommitted changes found', 'red'); +// } +// } else { +// // flyway enabled, but no continuous delivery folder +// } +// } diff --git a/src/index.ts b/src/index.ts index e776e66..6893468 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,8 @@ import { ImplementationResourceReader } from "./ImplementationResourceReader.js" import { ImplementationResource } from "./ImplementationResource.js"; import { ExternalComponentResource } from "./ExternalComponentResource.js"; import { ExternalComponent } from "./types.js"; +import { ConfigurationManager } from "./ConfigurationManager.js"; + // import { svnPropGet, svnList, svnGetLatestTag, } from "./svn.js"; // const baseUrl = 'https://svn.bearingpointcaribbean.com'; @@ -20,6 +22,24 @@ async function deploymentCheck() { ); } +async function sufImplementations() { + const reader = new ImplementationResourceReader(); + const implementationResources = await reader.load(true, true); + + implementationResources.forEach( + async (implementationResource: ImplementationResource) => { + const implementationSufResults = await implementationResource.suf( + true, + true, + false + ); + console.debug("Switched:", implementationSufResults.switched); + console.debug("Updated:", implementationSufResults.updated); + console.debug("Flyway:", implementationSufResults.flyway); + } + ); + // console.debug(implementationResources); +} // async function start() { // svnList(baseUrl, url) // .then((listResult) => { @@ -98,4 +118,9 @@ async function deploymentCheck() { //start2(); //start3(); // start4(); -deploymentCheck(); +//deploymentCheck(); +//updateAllImplementations(); + +// Example usage: + +sufImplementations(); diff --git a/src/parser.ts b/src/parser.ts index f4ee54d..95dacfd 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -43,6 +43,14 @@ export function parseExternals( }); } +export function unifyPath(angloPath: string) { + return angloPath.toString().replaceAll("\\", "/"); +} + +export function move(array: [], from: number, to: number, on = 1) { + // eslint-disable-next-line no-sequences + return array.splice(to, 0, ...array.splice(from, on)), array; +} // splittedExternals.forEach((entry) => { // const tidied = anglo.tidyArrayContent(entry); // // for componentBaseFolder. If domain-specific, keep first 3, else keep first 4 parts diff --git a/src/svn.ts b/src/svn.ts index 1912001..fe05fc6 100644 --- a/src/svn.ts +++ b/src/svn.ts @@ -1,5 +1,4 @@ -import exclusions from "./data/exclude.json" assert { type: "json" }; -import { exec } from "child_process"; +import { exec, ExecException } from "child_process"; import { parseString } from "xml2js"; import { SVNList, @@ -7,48 +6,71 @@ import { SVNProperties, DirType, InternalComponent, + SvnResource, + SVNCheckoutResponse, + SVNCheckoutResult, } from "./types.js"; import { parseExternals } from "./parser.js"; +import { getConfig } from "./ConfigurationManager.js"; -function execShellCommand(cmd: string) { - return new Promise((resolve) => { - exec(cmd, { maxBuffer: 1024 * 500 }, (error, stdout, stderr) => { - if (error) { - console.warn(error); +function execShellCommand(cmd: string): Promise { + return new Promise((resolve, reject) => { + exec( + cmd, + { maxBuffer: 1024 * 500 }, + (error: ExecException | null, stdout: string, stderr: string) => { + if (error) { + // Handle known errors + if ( + error.message.includes("not a working copy") || + error.message.includes("does not exist") || + error.message.includes("None of the targets are working copies") + ) { + reject(new Error("NotAWorkingCopy")); + } else { + // Reject the promise with unknown errors + console.log("Unknown error occurred", error); + reject(new Error("Unknown error occurred")); + } + } + resolve(stdout || stderr); } - resolve(stdout || stderr); - }); + ); }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any async function svnCmdWithResponse( cmd: string, - baseUrl: string, - url: string, - returnRaw = false + baseUrl?: string, + url?: string, + localPath?: string, + returnXml = true // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise { return new Promise((resolve, reject) => { - const execCommand = `svn ${cmd} "${baseUrl}${url}" --xml`; + let execCommand = `svn ${cmd} `; + if (baseUrl && url) execCommand += `"${baseUrl}${url}" `; + if (localPath) execCommand += `"${localPath}" `; + if (returnXml) execCommand += `--xml`; execShellCommand(execCommand) .then((svnCmdResponse) => { - parseString( - svnCmdResponse as SVNList | SVNProperties, - (err, result) => { - if (err) { - reject(err); - return; - } - if (returnRaw) { - resolve(result); - } else { + if (returnXml) { + parseString( + svnCmdResponse as SVNList | SVNProperties, + (err, result) => { + if (err) { + reject(err); + return; + } const json = JSON.stringify(result); const svnCmdResponseJson = JSON.parse(json); resolve(svnCmdResponseJson); } - } - ); + ); + } else { + resolve(svnCmdResponse); + } }) .catch((error) => { reject(error); @@ -56,16 +78,49 @@ async function svnCmdWithResponse( }); } +export async function svnCheckout( + baseUrl: string, + url: string, + localPath: string +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const svnCheckoutResponse: any = await svnCmdWithResponse( + "checkout", + baseUrl, + url, + localPath, + false + ); + // checkout + if (svnCheckoutResponse.startsWith("A")) { + const updateRows = svnCheckoutResponse.split(/\r\n/); + const numberOfUpdates = updateRows.length - 3; + const revision = updateRows[updateRows.length - 2].match(/\d+/)?.[0]; + return { numberOfUpdates, revision }; + } else { + return { numberOfUpdates: 0, revision: 0 }; + } + } catch (error) { + console.log(error); + throw error; + } +} export async function svnList( baseUrl: string, url: string ): Promise { try { - const svnListResponse: InternalComponent[] = await svnCmdWithResponse( + const config = getConfig(); + const exclusions = config.exclusions; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const svnListResponse: any = await svnCmdWithResponse( "list", baseUrl, url, - false + undefined, + true ); type arrayOfSVNList = { @@ -87,6 +142,31 @@ export async function svnList( } } +export async function svnInfo( + // baseUrl: string, + // url: string, + localPath: string +): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const svnInfoResponse: any = await svnCmdWithResponse( + "info", + undefined, + undefined, + localPath, + true + ); + + const fullUrl = svnInfoResponse.info.entry[0].url[0]; + const revision = svnInfoResponse.info.entry[0].$.revision; + const returnObject: SvnResource = { fullUrl, revision }; + return returnObject; + } catch (error) { + console.log(error); + throw error; + } +} + async function svnCmdWithoutResponse( cmd: string, localPath: string @@ -96,7 +176,6 @@ async function svnCmdWithoutResponse( const execCommand = `svn ${cmd} "${localPath}"`; execShellCommand(execCommand) .then((svnCmdResponse) => { - console.log(`Performed ${execCommand}`); resolve(svnCmdResponse); }) .catch((error) => { @@ -105,16 +184,73 @@ async function svnCmdWithoutResponse( }); } -export async function svnUpdate(localPath: string): Promise { +export async function svnUpdate(localPath: string): Promise { try { await svnCmdWithoutResponse("cleanup", localPath); + const updateResponse = await svnCmdWithResponse( + "update", + undefined, + undefined, + localPath, + false + ); + return ( + !updateResponse.includes("At revision") || + updateResponse.includes("Updated to") + ); } catch (error) { - console.log(error); + console.log("svnUpdateFailed", error); + throw new Error("svnUpdateFailed"); + } +} +export async function svnSwitchWorkingCopy( + localPath: string, + targetBaseUrl: string, + targetUrl: string +): Promise { + try { + await svnCmdWithoutResponse("cleanup", localPath); + const switchResponse = await svnCmdWithResponse( + "switch", + targetBaseUrl, + targetUrl, + localPath, + false + ); + return ( + switchResponse.includes("At revision") || + switchResponse.includes("Updated to revision") + ); + } catch (error) { + console.log("svnSwitchFailed", error); + throw new Error("svnSwitchFailed"); } +} + +export async function svnSwitchExternal( + baseUrl: string, + url: string, + componentName: string, // in tortoise this is called Local path while editing svn:externals definitions + sourceUrl: string, + targetUrl: string +): Promise { try { - await svnCmdWithoutResponse("update", localPath); + // get fresh raw externals + const propGetResult = await svnPropGet("svn:externals", baseUrl, url); + console.log(propGetResult); + // find + // if found + // propGetResult.replace() + // svmuc back as externals + //else throw error + const switchResponse = ""; + return ( + !switchResponse.includes("At revision") || + switchResponse.includes("Updated to") + ); } catch (error) { - console.log(error); + console.log("svnSwitchFailed", error); + throw new Error("svnSwitchFailed"); } } @@ -127,6 +263,7 @@ export async function svnPropGet( `propget ${property}`, baseUrl, url, + undefined, true ); const rawExternals: string = @@ -139,7 +276,7 @@ export async function svnTagsBranchesList( baseUrl: string, url: string, // expects url that has tags and branches latestOnly = false -): Promise { +): Promise { try { url = url.replace(/trunk$/, ""); url = url.replace(/tags$/, ""); @@ -148,7 +285,9 @@ export async function svnTagsBranchesList( const svnListResponse: SVNList = await svnCmdWithResponse( "list", baseUrl, - url + url, + undefined, + true ); const regex = /^[0-9.]+$/; // Regular expression for semantic version format type arrayOfSVNList = { @@ -207,27 +346,27 @@ export async function svnTagsBranchesList( export async function svnGetLatestTag( baseUrl: string, url: string -): Promise { +): Promise { return svnTagsBranchesList(DirType.tags, baseUrl, url, true); } export async function svnGetLatestBranch( baseUrl: string, url: string -): Promise { +): Promise { return svnTagsBranchesList(DirType.branches, baseUrl, url, true); } export async function svnGetTagList( baseUrl: string, url: string -): Promise { +): Promise { return svnTagsBranchesList(DirType.tags, baseUrl, url, false); } export async function svnGetBranchesList( baseUrl: string, url: string -): Promise { +): Promise { return svnTagsBranchesList(DirType.branches, baseUrl, url, false); } diff --git a/src/types.ts b/src/types.ts index e891506..00dc2cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,15 @@ import { ExternalComponentResource } from "./ExternalComponentResource"; +export enum SVNCheckoutResult { + at_revision, + missing, +} + +export type SVNCheckoutResponse = { + numberOfUpdates: number; + revision: number; +}; + export type SVNProperties = { properties: { target: { @@ -39,6 +49,10 @@ export type SVNList = { }; }; +export type ErrorType = { + message: string; +}; + export type ExternalComponent = { original: string; key: string; @@ -65,16 +79,22 @@ export enum DirType { trunk = "trunk", } -export type TypeSolutionImplementation = { - name: string; - functionalName: string; - customer: string; - customerCode: string; - path: string; - class: string; - baseUrl: string; - url: string; - selected: boolean; +// export type TypeSolutionImplementation = { +// name: string; +// functionalName: string; +// customer: string; +// customerCode: string; +// path: string; +// class: string; +// baseUrl: string; +// url: string; +// dbConnectionString: string; +// selected: boolean; +// }; + +export type SvnResource = { + fullUrl: string; + revision: number; }; export type ApiExternalsResponse = { @@ -90,3 +110,28 @@ export type ApiExternalsResponse = { }; }; }; + +export interface TypeSolutionImplementation { + name: string; + functionalName: string; + customer: string; + customerCode: string; + path: string; + class: string; + baseUrl: string; + url: string; + dbConnectionString: string; + selected: boolean; +} + +export interface Paths { + flywayPath: string; + componentMigrationsFolder: string; + implementationMigrationsFolder: string; +} + +export interface Settings { + paths: Paths; + exclusions: string[]; + implementations: TypeSolutionImplementation[]; +} diff --git a/tsconfig.json b/tsconfig.json index 770a8f6..1880020 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,7 +39,7 @@ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ - "resolveJsonModule": true /* Enable importing .json files. */, + // "resolveJsonModule": true /* Enable importing .json files. */, // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */