diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6a3e68d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/.DS_Store \ No newline at end of file diff --git a/.github/workflows/bump-module-version.yml b/.github/workflows/bump-module-version.yml new file mode 100644 index 0000000..5532a56 --- /dev/null +++ b/.github/workflows/bump-module-version.yml @@ -0,0 +1,30 @@ +name: versionbump + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + release: + types: + - released + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Bump version + run: | + git config --global user.name 'ProjectBot' + git config --global user.email 'bot@users.noreply.github.com' + VERSION=$(sed -n '0,/.*\(.*\)<\/Version>.*/s//\1/p' module.xml) + VERSION=`echo $VERSION | awk -F. '/[0-9]+\./{$NF++;print}' OFS=.` + sed -i "0,/\(.*\)<\/Version>/s//$VERSION<\/Version>/" module.xml + git add module.xml + git commit -m 'auto bump version' + git push diff --git a/.github/workflows/objectscript-quality.yml b/.github/workflows/objectscript-quality.yml new file mode 100644 index 0000000..4bda31c --- /dev/null +++ b/.github/workflows/objectscript-quality.yml @@ -0,0 +1,12 @@ +name: objectscriptquality +on: push + +jobs: + linux: + name: Linux build + runs-on: ubuntu-latest + + steps: + - name: Execute ObjectScript Quality Analysis + run: wget https://raw.githubusercontent.com/litesolutions/objectscriptquality-jenkins-integration/master/iris-community-hook.sh && sh ./iris-community-hook.sh + diff --git a/.github/workflows/runtests.yml b/.github/workflows/runtests.yml new file mode 100644 index 0000000..7464209 --- /dev/null +++ b/.github/workflows/runtests.yml @@ -0,0 +1,28 @@ +name: unittest + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + release: + types: + - released + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build and Test + uses: docker/build-push-action@v2 + with: + context: . + push: false + load: true + tags: ${{ github.repository }}:${{ github.sha }} + build-args: TESTS=1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5dbf95a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +ARG IMAGE=intersystemsdc/irishealth-community:2020.3.0.200.0-zpm +ARG IMAGE=intersystemsdc/iris-community:2020.4.0.547.0-zpm +ARG IMAGE=containers.intersystems.com/intersystems/iris:2021.1.0.215.0 +ARG IMAGE=intersystemsdc/iris-community +FROM $IMAGE + +WORKDIR /home/irisowner/irisdev/ + +## install git +## USER root +##RUN apt update && apt-get -y install git +##USER ${ISC_PACKAGE_MGRUSER} + +ARG TESTS=0 +ARG MODULE="isc-dev" +ARG NAMESPACE="USER" + +RUN --mount=type=bind,src=.,dst=. \ + #pip3 install -r requirements.txt && \ + iris start IRIS && \ + iris session IRIS < iris.script && \ + ([ $TESTS -eq 0 ] || iris session iris -U $NAMESPACE "##class(%ZPM.PackageManager).Shell(\"test $MODULE -v -only\",1,1)") && \ + iris stop IRIS quietly diff --git a/README.md b/README.md index 43f2811..12ebc9a 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,88 @@ -# cache-udl -Export/Import sources in UDL format for [ISC Caché 2016.2](http://www.intersystems.com/our-products/cache/cache-overview/) +# ISC-DEV + +[![Gitter](https://img.shields.io/badge/chat-on%20telegram-blue.svg)](https://t.me/joinchat/FoZ4MxGETT0_VsVglVl0DA) +[![Quality Gate Status](https://community.objectscriptquality.com/api/project_badges/measure?project=intersystems_iris_community%2Fisc-dev&metric=alert_status)](https://community.objectscriptquality.com/dashboard?id=intersystems_iris_community%2Fisc-dev) + +Export/Import source code (classes, macro, routines) and DeepSee artefacts(pivots, dashboards, termlists, pivot variables, shared measures) from and to InterSystems Data Platform products(Caché, Ensemble, IRIS). Support versions from 2016.2 # Installation Download code and run ``` -set dir="/dir/cache-udl -do $System.OBJ.ImportDir(dir,"*.xml;*.cls;*.mac;*.int;*.inc;*.dfi","ck",,1) +set dir="/your_download_dir/isc-dev +do $System.OBJ.ImportDir(dir,"*.xml;*.cls;*.mac;*.int;*.inc;*.dfi","cuk",,1) ``` or import the [release](https://github.com/intersystems-ru/cache-udl/releases) to the namespace. -Map sc package to %All namespace to make it visible in any namespace. +Map dev package to %All namespace to make it visible in any namespace. + +## Docker + +To install using Docker. Follow this instructions: + +Open terminal and clone the repo into any local directory + +``` +$ git clone https://github.com/intersystems-community/isc-dev.git +``` + +Open the terminal in this directory and run: + +``` +$ docker-compose build +``` + +Run the IRIS container with your project: +``` +$ docker-compose up -d +``` # Usage ## Setup working directory ( optional ) ``` -NS> w ##class(sc.code).workdir("/path/to/your/working/directory/") +NS> w ##class(dev.code).workdir("/path/to/your/working/directory/") ``` + +## Import: +``` +NS> d ##class(dev.code).import() +``` + ## Export to working directory: ``` -NS> d ##class(sc.code).export() +NS> d ##class(dev.code).export() ``` -## Import: + +#### Method to export all cubes, source classes, dashboards, pivots and globals from the namespace available to the current user: ``` -NS> d ##class(sc.code).import() +NS> d ##class(dev.code).exportBI() ``` +## Рossible continuous export of files to the working directory +Now the Classes/Rutines/DeepSee files will be automatically exported to the working directory after saving or compiling. + +How to enable this feature: + +- Go to Management Portal -> System Administration -> Configuration -> Additional Settings -> Source Control +- Select the target namespace and set the `dev.FileManExtension` class as the main one and save it +- Try to create a new class in the studio and save/compile it +- Enjoy! + ## Compile, Release and Patch: Introduce isc.json file in the source root directory with settings for the code mask, for the name of the project and for get the patch form local git or GitHub. e.g. ``` -"git": 0 - files diff from local git +"git": 0 - files diff from local git (default) "git": 1 - files diff from GitHub +use below params in case of "git" : 1 +"owner": - name of the github e.g. intersystems-community +"repository": - name of the repo e.g. dc-analytics + "user": - user and password for private github repo + "password": ``` + ``` isc.json "compileList": "Classes*.INC,classes*.CLS,*.DFI", @@ -47,25 +95,47 @@ isc.json ``` Run init method to initialize project settings: ``` -NS> d ##class(sc.code).init() +NS> d ##class(dev.code).init() ``` -Then run release to export all the classes in comileList into one "myproject.xml" release file. It will export it into the default for current Namespace directory. +Then run release to export all the classes in compileList into one "myproject.xml" release file. It will export it into the default for current Namespace directory. ``` -NS> d ##class(sc.code).release() +NS> d ##class(dev.code).release() ``` Or compile it whenever you want to compile all the proejct related resources. ``` -NS> d ##class(sc.code).compile() +NS> d ##class(dev.code).compile() ``` -Get last changes from github or local git. Run patch to export the classes in comileList into one "patch.xml" patch file. It will export it into the default for current Namespace directory or you can choose where export. By default, makes a patch from the last commit if you do not specify `commitFrom` and `commitTo` e.g. +Get last changes from github or local git. Run patch to export the classes in compileList into one "patch.xml" patch file. It will export it into the default for current Namespace directory or you can choose where export. By default, makes a patch from the last commit if you do not specify `commitFrom` and `commitTo` e.g. ``` NS> s filename = "c:\patch.xml" NS> s commitFrom = 1 NS> s commitTo = 5 -NS> d ##class(sc.code).patch(filename,commitFrom,commitTo) +NS> d ##class(dev.code).patch(filename,commitFrom,commitTo) ``` +## Known issues +Be careful with import termlists, pivot variables and shared measures. In current implementation imported artefacts replace those you have in the target namespace. It happens because the utility uses standard global import for globals in XML with $System.OBJ.Import which kills the global first and imports the new one. + +#### If after using the export command, git treats unaltered files as modified, the problem may be in the following: +- When moving sources from one OS to another (f.e. from win to mac), the end of the line character was not taken into account. To fix this, you need to specify git to make the end of line character look the same. + + solution: ```git config --global core.autocrlf input``` +- File access rights have been changed. In this case, you will need to specify that you do not want to track these changes. + + solution: ```git config core.filemode false``` + +## Development + +To update the module in ZPM do the following: +USER> zpm +zpm: USER>load /opt +zpm: USER>repo -n registry -user USER -pass PASSWORD +1) Filesystem +2) Local Cache +3) Remote Repository +Which sort of repository do you wish to configure? 3 +zpm: USER>module-action isc-dev publish diff --git a/cls/sc/code.cls b/cls/sc/code.cls deleted file mode 100644 index fe7ec92..0000000 --- a/cls/sc/code.cls +++ /dev/null @@ -1,447 +0,0 @@ -/// Export different types of modules in different subfolders in UDL (plain) format -/// test.dfi -> /dfi/test.dfi -/// testpkg.test.cls -> /cls/testpkg/test.cls -Class sc.code [ Abstract ] -{ - -/// export all available code -ClassMethod export(generated = 0, system = 0, percent = 0, mapped = 0, mask = "", dfi = 0) -{ - - #define export(%code, %file) s sc = $system.OBJ.ExportUDL(%code, %file,"/diffexport") ##continue - w:sc "Ok" d:'sc $system.OBJ.DisplayError(sc) - - #define isGenerated(%code) ##class(%RoutineMgr).IsGenerated( %code ) - #define isPercented(%code) ("%" = $e(%code)) - #define isMapped(%code) ##class(%RoutineMgr).IsMapped( %code ) - #define log w !, code, " -> ", $piece(filename,..workdir(),2), " " - - #define mkdir(%filename) ##continue - s path = ##class(%File).GetDirectory( %filename ) ##continue - if '##class(%File).DirectoryExists( path ) { ##continue - s sc = ##class(%File).CreateDirectoryChain( path ) ##continue - w !, "mkdir ", path, " ", sc ##continue - } - - w "#; Exporting to ", ..workdir(),! - #; classes - s rs = ##class(%ResultSet).%New("%Dictionary.ClassDefinition:Summary") - if rs.Execute() { - - while rs.%Next(){ - - s code = rs.Name _ ".cls", isSystem = rs.System - if ( 'system && isSystem ) continue - if ( 'generated && $$$isGenerated( code ) ) continue - if ( 'percent && $$$isPercented( code ) ) continue - if ( 'mapped && $$$isMapped( code ) ) continue - if ( '$find( code, mask ) ) continue - - s filename = ..filename( code ) - $$$mkdir( filename ) - $$$log - $$$export( code, filename ) - - } s rs="" - } - - #; routines - s rs = ##class(%ResultSet).%New("%Routine:RoutineList") - if rs.Execute() { - - while rs.%Next() { - - s code = rs.Name - w code,! - if ( 'generated && $$$isGenerated( code ) ) continue - if ( 'percent && $$$isPercented( code ) ) continue - if ( 'mapped && $$$isMapped( code ) ) continue - if ( '$find( code, mask ) ) continue - - s filename = ..filename( code ) - $$$mkdir( filename ) - $$$log - $$$export( code, filename ) - - } s rs="" - } - - - #; dfi - do ..fixDashIntoName() - #define export(%code,%file) s sc = ##class(%DeepSee.UserLibrary.Utils).%Export( %code, %file, 0 ) - - s sql = "Select id, fullName as Name, documentName From %DeepSee_UserLibrary.FolderItem" - s rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) - while rs.%Next() { - set code = rs.Name - set filename = ..filename( code_".dfi" ) - set documentName = rs.documentName - - if ( '$find($zcvt(documentName,"l"), mask ) ) continue - if ($L(code,"$TRASH")>1) continue - $$$mkdir( filename ) - - if dfi{ - $$$log - $$$export( code, filename) - } - elseif ('dfi){ - set documentName = $replace(documentName,"/","-") - set filename = $extract(filename,1,*-4) - do ..fixXMLLine(documentName,filename_".xml") - } - - } s rs="" - - w !,!, "#; Exported to ", ..workdir() - - Q 1 -} - -/// import all from workdir -ClassMethod import(filemask = "*.xml;*.cls;*.mac;*.int;*.inc;*.dfi", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status -{ - #define push(%dir) s dirs( $i( dirs ) ) = %dir - #define next(%i,%dir) s %i=$o( dirs( "" ), 1, %dir ) k:%i'="" dirs(%i) - #define isDirectory(%type) ( %type = "D" ) - #define log w !, $piece(filename,..workdir(),2), " " w:sc "Ok" d:'sc $system.OBJ.DisplayError(sc) - - s sc = 1, dirs = "", dir = ..workdir() $$$push(dir) - if verbose w "#; Importing from ", dir,! - d ..setIgnore(.fm) - for { $$$next(i,dir) Q:i="" Q:dir="" - - s rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) - - while rs.%Next() { - s filename = rs.Name - - if $$$isDirectory(rs.Type) { - if recurse $$$push(filename) - continue - } - // check file filter - if ..inFilter(.fm,filename) continue - - s ext = $zcvt($p( filename, ".", * ),"l") - - if (ext = "dfi") { - s sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .dsloaded ) - - } else { - // load classes only - s sc = $system.OBJ.Load( filename, "k-d", .err, .loaded) - } - - if verbose $$$log - - } - // compile all the loaded classes to obey dependencies - s sc=$system.OBJ.CompileList( .loaded, "cukbr-d/multicompile",.err ) - - } - - if verbose { - - if $D(err) { - s i=$Order(err("")) - while i'="" { - w !,err(i) - s i=$O(err(i)) - } - } - - w !,!,"#; Imported from ", ..workdir() - } - Q sc -} - -ClassMethod patch(filename = "", commitFrom = "", commitTo = "") As %Status -{ - s gln = ..gln() - s git = @gln@("git") - - set:commitTo="" commitTo=1 - if (git = 0) { - set:(commitFrom="")||(commitFrom=1) commitFrom=0 - set sc = ##class(sc.diff.gitLocal).buildDiff(..workdir(), "HEAD~"_commitTo, "HEAD~"_commitFrom, .items) - return:$$$ISERR(sc) sc - }elseif(git = 1) { - set owner = @gln@("owner") - set repository = @gln@("repository") - set user = @gln@("user") - set password = @gln@("password") - set:commitFrom="" commitFrom=1 - - set sc = ##class(sc.diff.gitHub).Get(.items, owner, repository, user, password, commitFrom, commitTo) - return:$$$ISERR(sc) sc - } - - if (filename="") { - set filename=$G(@gln@("projectName"),"patch") - set filename=filename_$p($zdt($h,8)," ")_$tr($p($p($zdt($h)," ",2),":",1,2),":")_".xml" - } - zw filename - set sc = $system.OBJ.Export(.items,filename) - - return sc -} - -ClassMethod fixXMLLine(documentName, filename) As %Status -{ - set stream = ##class(%Stream.FileBinary).%New() - do stream.LinkToFile(filename) - do $system.OBJ.ExportToStream(documentName, .stream) - - set matcher=##class(%Regex.Matcher).%New("") - set matcher.Text = stream.Read(stream.Size) - set data = matcher.ReplaceFirst("") - - do stream.Clear() - do stream.Write(data) - return stream.%Save() -} - -ClassMethod inFilter(ByRef filtermask, filename) As %Boolean -{ - s result=0 - for - { - if '$d(filtermask($I(i))) quit - set filter=$piece(filtermask(i),"*") - if $length(filename,filter)>1 set result=1 - } - - quit result -} - -/// get the filtermask for the repository. -/// looks for .gitignore file and applies all the lines to filters -ClassMethod setIgnore(ByRef filtermask) -{ - - // working with .gitignore file - // gitignore consists of masks of filtering files - s filename=..workdir()_"/.gitignore" - if '##class(%File).Exists(filename) quit - s file=##class(%File).%New(filename) - s sc=file.Open("R") - if 'sc - { - if verbose d $System.OBJ.DisplayError(sc) return - } - - While 'file.AtEnd - { - s filtermask($I(filtermask))=file.ReadLine() - } - quit -} - -/// get or set working directory for export/import source -ClassMethod workdir(workdir) -{ - s gln = ..gln() s:$d(workdir) @gln = workdir - ///zu(12) namespace directory by default - #define nsdir $zu(12,"") - Q $g(@gln, $$$nsdir) -} - -/// gl[obal] n[ame] - storage for settings -ClassMethod gln() [ CodeMode = expression, Private ] -{ -"^"_$classname() -} - -ClassMethod init(confile = "isc.json") -{ - set stream=##class(%Stream.FileCharacter).%New() - set sc=stream.LinkToFile(..workdir()_"/"_confile) - s conf={}.%FromJSON(stream.Read($$$MaxCacheInt)) - - s gln=..gln() - - s @gln@("compileList")=conf.compileList - s @gln@("projectName")=conf.projectName - s @gln@("owner")=conf.owner - s @gln@("repository")=conf.repository - s @gln@("user")=conf.user - s @gln@("password")=conf.password - s @gln@("git")=conf.git - - w "compileList="_conf.compileList,! - w "projectName="_conf.projectName,! - w "owner="_conf.owner,! - w "repository="_conf.repository,! - w "user="_conf.user,! - w "password="_conf.password,! - w "git="_conf.git,! - - quit $$$OK -} - -/// export release file for list and project settings -ClassMethod release() -{ - s gln=..gln() - s list=$G(@gln@("compileList")) - if list="" w "Nothing to release. Run init method first" quit - - s release=$G(@gln@("projectName"),"release") - s release=release_$p($zdt($h,8)," ")_$tr($p($p($zdt($h)," ",2),":",1,2),":")_".xml" - D $System.OBJ.Export(list,release) - w "All objects with mask "_list_" has been exported to "_release -} - -ClassMethod compile() -{ - s gln=..gln() - s list=$G(@gln@("compileList")) - if list="" w "Nothing to compile. Run init method first" quit - d $System.OBJ.compile(list) -} - -/// test.dfi -> /dfi/test.dfi -/// test.cls -> /cls/test.cls -/// testpkg.test.cls -> /cls/testpkg/test.cls -/// etc -ClassMethod filename(code) -{ - #define log(%dir,%sc) w !, "mkdir ", %dir, " ", sc - - s wd = ..workdir() - - if '##class(%File).DirectoryExists( wd ) { - s sc = ##class(%File).CreateDirectoryChain( wd ) - $$$log(wd,sc) - } - - s ext = $p( code, ".", * ), ext = $zcvt( ext, "l" ) - #; for each type - different directory - - s:ext'="" wd = ##class(%File).NormalizeDirectory( ext, wd ) - - #; directory must exist before any call (%File).NormalizeFilename( , wd) - if '##class(%File).DirectoryExists( wd ) { - s sc = ##class(%File).CreateDirectoryChain( wd ) - $$$log(wd,sc) - } - - s filename = ##class(%File).NormalizeFilename( code, wd ) - #; lowercase file extension - s $p(filename,".",*)=ext - //B:code="DPRep.Rest.JSON.cls" "L" - #; for *.cls Package.Subpackage.ClassName.cls -> Folder/Subfolder/ClassName.cls - if ext ="cls" { - s dirs = $piece( code, ".",1, *-2 ), dirs = $translate( dirs, ".", "/" ) - s relpath = dirs _ "/" _ $piece( code, ".", *-1, * ) ; - s filename = ##class(%File).NormalizeFilename( relpath, wd ) - } - Q filename -} - -/// import from workdir all files with ts newer than code ts in db -ClassMethod importUpdated(filemask = "*.*", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status -{ - #define push(%dir) s dirs( $i( dirs ) ) = %dir - #define next(%i,%dir) s %i=$o( dirs( "" ), 1, %dir ) k:%i'="" dirs(%i) - #define isDirectory(%type) ( %type = "D" ) - #define log w !, $piece(filename,..workdir(),2)," " w:sc "Ok" d:'sc $system.OBJ.DisplayError( sc ) - - s sc = 1, dirs = "", dir = ..workdir() $$$push(dir) - if verbose w "#; Importing updated from ", dir,! - d ..setIgnore(.fm) - for { $$$next(i,dir) Q:i="" Q:dir="" - - s rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) - - while rs.%Next() { - s filename = rs.Name - - if $$$isDirectory( rs.Type ) { - if ( recurse ) $$$push(filename) ;push directory - continue - } - - // check file filter - if ..inFilter(.fm,filename) continue - - s filets = rs.DateModified - s codename = ..codename( filename, .ext ) - s codets = ..codets( codename, ext ) - - #; w !, filename, $c(9), "(", filets, ")" - #; w !, codename, $c(9), "(", codets, ")" - - if ( filets '] codets ) continue - - if (ext = "dfi"){ - s sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .dsloaded ) - } else { - - #; drop existing code before import ( purge DateModified ) - #; s:codets'="" sc = ##class(%RoutineMgr).Delete( codename ) - k err s sc = $system.OBJ.Load( filename, qspec, .err, .loaded) - - } - - if verbose $$$log - } - } - w:verbose !,"#; Imported updated from ", ..workdir() - Q sc -} - -/// presumable codename -ClassMethod codename(filename, ByRef ext = "") -{ - s ext = $p( filename, ".", * ), ext = $zcvt( ext, "l" ) - s path = ##class(%File).NormalizeDirectory( ext, ..workdir() ) - s codename = $p( filename, path, 2 ) - if ext = "dfi" { - s fullname = $tr( codename, "\", "/" ) ; return fullname for dfi in $$$IsWINDOWS - Q $p( fullname, ".", 1, *-1 ) ;remove extension - } - if (ext ="cls")!(ext="int")!(ext="inc")!(ext="mac") s codename=$tr(codename,"\/","..") - Q codename -} - -ClassMethod codets(codename, ext) -{ - s ts = "" - if ext'="dfi" { - s ts = ##class(%RoutineMgr).TS( codename ) - } else { - s sql="Select timeModified From %DeepSee_UserLibrary.FolderItem Where fullname = ?" - s rs = ##class(%SQL.Statement).%ExecDirect( , sql, codename ) - if rs.%Next() { - s utcts = rs.timeModified - s utch = $zdth( utcts, 3, , 3 ) ;utc internal format - s loch = $zdth( utch, -3 ) ; utc to local timezone - s ts = $zdt( loch, 3, ,0 ) ; local timestamp*/ - } - } - Q $p( ts, "." ) ;remove ms -} - -// fix "-" into dfi files - -ClassMethod fixDashIntoName() -{ - s sql = "Select id, fullName as Name, documentName From %DeepSee_UserLibrary.FolderItem" - s rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) - while rs.%Next() { - if $find(rs.Name, "-"){ - w rs.Name," -> " - set item = ##class(%DeepSee.UserLibrary.FolderItem).%OpenId(rs.id) - set item.name = $replace(item.name,"-"," ") - w item.name,! - do item.%Save() - } - } - k rs -} - -} - diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1948f69 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.6' +services: + iris: + build: + context: . + dockerfile: Dockerfile + restart: always + command: --check-caps false + ports: + - 1972 + - 52773 + - 53773 + volumes: + - ./:/home/irisowner/irisdev diff --git a/iris.script b/iris.script new file mode 100755 index 0000000..642cbfd --- /dev/null +++ b/iris.script @@ -0,0 +1,11 @@ + // Unexpire passwords to simplify dev mode. Comment these two lines for Production use + zn "%SYS" + Do ##class(Security.Users).UnExpireUserPasswords("*") + + + // load all the code of the project as a ZPM package + zn "USER" + do ##class(dev.code).workdir("/home/irisowner/irisdev/src") + + zpm "load /home/irisowner/irisdev/ -v" + halt diff --git a/isc.json b/isc.json new file mode 100644 index 0000000..99f8aa5 --- /dev/null +++ b/isc.json @@ -0,0 +1,5 @@ +{ + "compileList": "dev*.CLS", + "projectName": "dev", + "git": 0 +} \ No newline at end of file diff --git a/module.xml b/module.xml new file mode 100644 index 0000000..45da2f8 --- /dev/null +++ b/module.xml @@ -0,0 +1,13 @@ + + + + + isc-dev + 1.4.17 + Export/Import InterSystems Data Platform development artefacts + src + module + + + + diff --git a/src/dev/FileManExtension.cls b/src/dev/FileManExtension.cls new file mode 100755 index 0000000..482e3f7 --- /dev/null +++ b/src/dev/FileManExtension.cls @@ -0,0 +1,170 @@ +Class dev.FileManExtension Extends %Studio.Extension.Base +{ + +/// Called before the item is saved to the database it is passed +/// a reference to the current temporary storage of this item so that it +/// can be modified before the save completes. If you quit with an error +/// value then it will abort the save. +Method OnBeforeSave(InternalName As %String, Location As %String = "", Object As %RegisteredObject = {$$$NULLOREF}) As %Status +{ + Quit $$$OK +} + +/// This is called after the item has been saved to the database. +/// It may be passed a reference to the object representing the item +/// just saved. It can be use to export this documement to an external form for example. +Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NULLOREF}) As %Status +{ + set ignorList = $lb("dev.code.CLS", + "dev.deepsee.CLS", + "dev.FileManExtension.CLS", + "dev.diff.gitHub.CLS", + "dev.diff.gitLocal.CLS", + "dev.diff.utils.CLS") + quit:$lf(ignorList, InternalName)=1 $$$OK + + set classAndRoutinesExts = $lb("cls","mac","int","inc") + + set ext = $zcvt($p(InternalName,".",*), "l") + + if $lf(classAndRoutinesExts, ext) + { + do ..exportClassAndRoutine($p(InternalName, ".", 1, *-1)_"."_ext) + } + elseif ext = "dfi" + { + set fileName = ##class(dev.code).filename($replace(InternalName,"-","/")) + set fileName = $extract(fileName,1,*-4)_".xml" + + do ##class(dev.code).mkdir(fileName) + do ##class(dev.code).fixXMLLine(InternalName, fileName) + } + + Quit $$$OK +} + +ClassMethod exportClassAndRoutine(className) As %Status +{ + #define isGenerated(%code) ##class(%RoutineMgr).IsGenerated( %code ) + #define isPercented(%code) ("%" = $e(%code)) + #define isMapped(%code) ##class(%RoutineMgr).IsMapped( %code ) + + if ($$$isGenerated(className)) quit $$$OK + if ($$$isPercented(className)) quit $$$OK + if ($$$isMapped(className)) quit $$$OK + + set fileName = ##class(dev.code).filename(className) + do ##class(dev.code).mkdir(fileName) + set sc = $system.OBJ.ExportUDL(className, fileName,"/diffexport") + + return $$$OK +} + +/// This is called if you compile a class and the compilation updates the class storage. +/// It is called after the storage has been updated so you can determine how to deal with this +/// change in the class. The Location is the global reference to the class definition that was changed. +Method OnAfterStorage(InternalName As %String, Location As %String = "") As %Status +{ + Quit $$$OK +} + +/// Called before the compile of the item is done. +/// It is passed a qstruct which can be inspected and modified. +Method OnBeforeCompile(InternalName As %String, ByRef qstruct As %String) As %Status +{ + Quit $$$OK +} + +/// Called after the compile of the item is done. +Method OnAfterCompile(InternalName As %String) As %Status +{ + Quit $$$OK +} + +/// Called in the class compiler after we have resolved the list of classes to be compiled but +/// before we have started the compile itself. +/// List is a subscripted array of items that will be compiled. +Method OnBeforeClassCompile(List As %String, Level As %Integer, ByRef qstruct As %String) As %Status +{ + Quit $$$OK +} + +/// Called in the class compiler after we have compiled all the classes before we exit. +/// List is a subscripted array of items that were compiled. Status is the current compile return +/// error %Status, so the callback can check if there was any error in compiling the list of classes. +Method OnAfterClassCompile(List As %String, Level As %Integer, ByRef qstruct As %String, Status As %Status) As %Status +{ + Quit $$$OK +} + +/// Called in the activated source control class before starting a compile after we have resolved the list of classes +/// but before we have started the compile itself. Note that this is a class method and it does not require the source +/// control class to be instantiated in order for it to be called. By default it will call the OnBeforeClassCompile +/// if there is a source control class instantiated, but you can override this to do whatever you require. +/// Returning an error here will abort the compile. +/// List is a subscripted array of items that will be compiled. +ClassMethod OnBeforeAllClassCompile(List As %String, Level As %Integer, ByRef qstruct As %String) As %Status +{ + Set sc=$$$OK + If $isobject($get(%SourceControl)) { + Set tmp=%SourceControl New %SourceControl + Set sc=tmp.OnBeforeClassCompile(.List,Level,.qstruct) + } + Quit sc +} + +/// Called in the activated source control class after we have compiled all the classes but before we exit. +/// Note that this is a class method and it does not require the source +/// control class to be instantiated in order for it to be called. By default it will call the OnAfterClassCompile +/// if there is a source control class instantiated, but you can override this to do whatever you require. +/// List is a subscripted array of items that were compiled. Status is the current compile return +/// error %Status, so the callback can check if there was any error in compiling the list of classes. +ClassMethod OnAfterAllClassCompile(List As %String, Level As %Integer, ByRef qstruct As %String, Status As %Status) As %Status +{ + Set sc=$$$OK + If $isobject($get(%SourceControl)) { + Set tmp=%SourceControl New %SourceControl + Set sc=tmp.OnAfterClassCompile(.List,Level,.qstruct,.Status) + } + Quit sc +} + +/// Called before an item is deleted. Returning an error code prevents the delete from occurring. +Method OnBeforeDelete(InternalName As %String) As %Status +{ + Quit $$$OK +} + +/// Called after an item is deleted. +Method OnAfterDelete(InternalName As %String) As %Status +{ + set ext = $zcvt($p(InternalName,".",*), "l") + + if ext = "dfi" + { + set fileName = ##class(dev.code).filename($replace(InternalName,"-","/")) + set fileName = $extract(fileName,1,*-4)_".xml" + } + else + { + set fileName = ##class(dev.code).filename(InternalName) + } + + do ##class(%File).Delete(fileName) + + set dir = ##class(%File).ParentDirectoryName(fileName) + + while dir '= ##class(dev.code).workdir() + { + set rs = ##class(%File).FileSetFunc(dir, , , 1) + quit:rs.%Next()=1 + + do ##class(%File).RemoveDirectory(dir) + set dir = ##class(%File).ParentDirectoryName(dir) + } + + Quit $$$OK +} + +} + diff --git a/src/dev/bi.cls b/src/dev/bi.cls new file mode 100644 index 0000000..be485cb --- /dev/null +++ b/src/dev/bi.cls @@ -0,0 +1,326 @@ +Class dev.bi +{ + +/// do ##class(dev.bi).exportCubes() +ClassMethod exportCubes() As %Status +{ + + set sc = ##class(%DeepSee.Utils).%GetCubeList(.listOfCubes,"cubes") + + set key = $order(listOfCubes("")) + + while (key'="") + { + set cubeClassName = ##class(%DeepSee.Utils).%GetCubeClass(key) + set cubeSourceClassName = ..getSourceClass(key) + + do ..exportUDL(cubeClassName_".cls") + do ..exportUDL(cubeSourceClassName_".cls") + + set key = $order(listOfCubes(key)) + } + + return $$$OK +} + +/// do ##class(dev.bi).exportKPIs() +ClassMethod exportKPIs() As %Status +{ + + set sc = ##class(%DeepSee.Utils).%GetKPIList(.listOfKPIs) + + set key = $order(listOfKPIs("")) + + while (key'="") + { + set kpiClassName = ##class(%DeepSee.Utils).%GetKPIClass(key) + + do ..exportUDL(kpiClassName_".cls") + set key = $order(listOfKPIs(key)) + } + + return $$$OK +} + +/// do ##class(dev.bi).exportDashboards() +ClassMethod exportDashboards() As %Status +{ + do ..fixDashIntoName() + + set sc = ##class(%DeepSee.Utils).%GetDashboardList(.listOfDashboards) + + set key = $order(listOfDashboards("")) + + while (key'="") + { + do ..exportDFIinXML(key_".DFI") + set key = $order(listOfDashboards(key)) + } + + return $$$OK +} + +/// do ##class(dev.bi).exportPivots() +ClassMethod exportPivots() As %Status +{ + do ..fixDashIntoName() + + set sc = ##class(%DeepSee.Utils).%GetPivotList(.listOfPivots) + + set key = $order(listOfPivots("")) + + while (key'="") + { + do ..exportDFIinXML(key_".DFI") + set key = $order(listOfPivots(key)) + } + + return $$$OK +} + +/// do ##class(dev.bi).checkAllPivots() +ClassMethod checkAllPivots(stopOnError As %Boolean = 0) As %Status +{ + set countOfPivots = 0 + set countOfErrors = 0 + + set sc = ##class(%DeepSee.Utils).%GetPivotList(.listOfPivots) + + set key = $order(listOfPivots("")) + + while (key'="") + { + set sc = ..checkPivot(key) + + set countOfPivots = countOfPivots + 1 + set:$$$ISERR(sc) countOfErrors = countOfErrors + 1 + quit:$$$ISERR(sc)&&stopOnError + set key = $order(listOfPivots(key)) + } + + w !,"Pivots tested: "_countOfPivots,! + w "Errors: "_countOfErrors + + if countOfErrors { + set sc=$$$ERROR($$$GeneralError, "Found "_countOfErrors_" errors in pivots in "_$namespace_" namespace") + } + return sc +} + +/// do ##class(dev.bi).checkPivot() +ClassMethod checkPivot(pivotFullName, debug = 1) As %Status +{ + do ##class(%DeepSee.Utils).%GetResultSetFromPivot(pivotFullName, .sc,1, .pParms) + w:debug "Pivot: "_pivotFullName,! + w:debug "Status: "_$system.Status.DisplayError(sc),! + return sc +} + +/// do ##class(dev.bi).checkDashboards() +ClassMethod checkDashboards(stopOnError As %Boolean = 0) As %Status +{ + set countOfDashboards = 0 + set countOfPivots = 0 + set countOfPitotErrors = 0 + set countOfControlErrors = 0 + set scStop = 0 + + set sc = ##class(%DeepSee.Utils).%GetDashboardList(.listOfDashboards) + + set key = $order(listOfDashboards("")) + + while (key'="") + { + do ..checkDashboard(key, .countOfPivots, .countOfPitotErrors, .countOfControlErrors, .scStop) + + set countOfDashboards = countOfDashboards + 1 + quit:scStop&&stopOnError + set key = $order(listOfDashboards(key)) + } + + w !,"Dashboards tested: "_countOfDashboards,! + w "Pivots tested: "_countOfPivots,! + w "Pivots errors: "_countOfPitotErrors,! + w "Filters errors: "_countOfControlErrors,! + + if countOfPitotErrors { + set sc=$$$ERROR($$$GeneralError, "Found "_countOfPitotErrors_" errors in pivots in "_$namespace_" namespace") + } + if countOfControlErrors { + set sc=$SYSTEM.Status.AppendStatus(sc,$$$ERROR($$$GeneralError, "Found "_countOfControlErrors_" errors in filters in "_$namespace_" namespace")) + } + return sc +} + +/// do ##class(dev.bi).checkDashboard("Basic Dashboard Demo.dashboard") +ClassMethod checkDashboard(dashboardName, ByRef countOfPivots = 0, ByRef countOfPitotErrors = 0, ByRef countOfControlErrors = 0, ByRef scStop = 0) As %Status +{ + Set dashboard = ##class(%DeepSee.Dashboard.Utils).%OpenDashboard(dashboardName,.tSC) + w !,"Dashboard name: "_dashboardName,! + ;set wDashName = 1 + for i=1:1:dashboard.widgets.Count() + { + set widget = dashboard.widgets.GetAt(i) + set pivotFullName= widget.dataSource + w "Widget name: "_widget.name,! + + if $$$LOWER($E(pivotFullName,*-3,*))=".kpi" { + w "The datasource is KPI, tests are not supported for now skip",! + continue + } + + if pivotFullName="" { + w "The datasource is empty, skip",! + continue + } + + set countOfPivots = countOfPivots + 1 + + set cubeName = ..getCubeNameByPivot(pivotFullName) + + #; w:wDashName !,"Dashboard name: "_dashboardName,! + #; set wDashName = 0 + + w "Cube name: "_cubeName,! + w "Pivot name: "_ pivotFullName,! + set pivotSC = ..checkPivot(pivotFullName,0) + + if $$$ISERR(pivotSC) + { + w "Pivot status: ",$system.Status.GetErrorText(pivotSC),!! + set countOfPitotErrors = countOfPitotErrors + 1 + set scStop = 1 + } + else + { + w "Pivot status: OK!",!! + } + + for j=1:1:widget.controls.Count() + { + set control = widget.controls.GetAt(j) + set action = control.action + set targetProperty = control.targetProperty + set label = control.label + + if (action = "setFilter") || (action = "applyFilter") + { + set controlSc = ##class(%DeepSee.Utils).%GetDimensionInfo(cubeName, targetProperty) + w "Filter name: "_label,! + + if $$$ISERR(controlSc) + { + set error = $system.Status.GetErrorText(controlSc) + w "Status: "_error,! + set countOfControlErrors = countOfControlErrors + 1 + set scStop = 1 + } + else + { + w "Status: OK!",! + } + } + } + } + return $$$OK +} + +ClassMethod getCubeNameByPivot(pivotFullName) As %String +{ + set mdx = ##class(%DeepSee.Utils).%GetMDXFromPivot(pivotFullName,,0) + + set cubeName = $p(mdx," FROM [", 2) + set cubeName = $p(cubeName,"]", 1) + + return cubeName +} + +ClassMethod getSourceClass(pCubeName As %String) As %String +{ + Quit $G(^DeepSee.Cubes("cubes",$$$UPPER(pCubeName),"sourceClass")) +} + +ClassMethod exportUDL(itemName As %String) As %Status +{ + set filename = ##class(dev.code).filename(itemName) + do ##class(dev.code).mkdir( filename ) + + write !, itemName, " -> ", $piece(filename,##class(dev.code).workdir(),2), " " + + set sc = $system.OBJ.ExportUDL(itemName, filename,"/diffexport") + write:sc "Ok" do:'sc $system.OBJ.DisplayError(sc) + + return sc +} + +ClassMethod exportDFIinXML(itemName As %String) As %Status +{ + set filename = ##class(dev.code).filename(itemName) + do ##class(dev.code).mkdir( filename ) + + write !, itemName, " -> ", $piece(filename,##class(dev.code).workdir(),2), " " + + set documentName = $replace(itemName,"/","-") + set filename = $extract(filename,1,*-4) + do ..exportXML(documentName,filename_".xml") + + return $$$OK +} + +/// do ##class(dev.bi).exportDeepSeeArtefacts() +ClassMethod exportDeepSeeArtefacts() As %Status +{ + set sc = $$$OK + set artefacts = $listbuild("DeepSee.Variables","DeepSee.TermList","DeepSee.CalcMbrs") + + for i=1:1:$listlength(artefacts) + { + set artefact = $list(artefacts,i) + continue:$data(@("^"_artefact))=0 + + set artefact = artefact_".GBL" + set filename = ##class(dev.code).filename(artefact) + do ##class(dev.code).mkdir(filename) + + //without .gbl in the file name + set filename = $e(filename,1, *-4) + + set sc = ..exportXML(artefact, filename_".xml") + } + + return sc +} + +ClassMethod fixDashIntoName() +{ + set sql = "Select id, fullName as Name, documentName From %DeepSee_UserLibrary.FolderItem" + set rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) + while rs.%Next() { + if $find(rs.Name, "-"){ + write rs.Name," -> " + set item = ##class(%DeepSee.UserLibrary.FolderItem).%OpenId(rs.id) + set item.name = $replace(item.name,"-"," ") + write item.name,! + do item.%Save() + } + } + kill rs +} + +/// do ##class(dev.bi).exportXML() +ClassMethod exportXML(documentName, filename) As %Status +{ + set stream = ##class(%Stream.FileBinary).%New() + do stream.LinkToFile(filename) + do $system.OBJ.ExportToStream(documentName, .stream) + + set matcher=##class(%Regex.Matcher).%New("") + set matcher.Text = stream.Read(stream.Size) + set data = matcher.ReplaceFirst("") + + do stream.Clear() + do stream.Write(data) + return stream.%Save() +} + +} diff --git a/src/dev/code.cls b/src/dev/code.cls new file mode 100755 index 0000000..a99613e --- /dev/null +++ b/src/dev/code.cls @@ -0,0 +1,599 @@ +/// Export different types of modules in different subfolders in UDL (plain) format +/// test.dfi -> /dfi/test.dfi +/// testpkg.test.cls -> /testpkg/test.cls +Class dev.code [ Abstract ] +{ + +Parameter version = "1.4.17"; + +/// export all available code +ClassMethod export(mask = "", generated = 0, system = 0, percent = 0, mapped = 0, dfi = 0) +{ + #define export(%code, %file) set sc = $system.OBJ.ExportUDL(%code, %file,"/diffexport") ##continue + write:sc "Ok" do:'sc $system.OBJ.DisplayError(sc) + + #define isGenerated(%code) ##class(%RoutineMgr).IsGenerated( %code ) + #define isPercented(%code) ("%" = $e(%code)) + #define isMapped(%code) ##class(%RoutineMgr).IsMapped( %code ) + #define log write !, code, " -> ", $piece(filename,..workdir(),2), " " + + write "#; Exporting to ", ..workdir(),! + #; classes + set rs = ##class(%ResultSet).%New("%Dictionary.ClassDefinition:Summary") + if rs.Execute() + { + while rs.%Next() + { + set code = rs.Name _ ".cls", isSystem = rs.System + if ( 'system && isSystem ) continue + if ( 'generated && $$$isGenerated( code ) ) continue + if ( 'percent && $$$isPercented( code ) ) continue + if ( 'mapped && $$$isMapped( code ) ) continue + if ( '..checkMasks( code, mask ) ) continue + + set filename = ..filename( code ) + do ..mkdir( filename ) + $$$log + $$$export( code, filename ) + } + set rs="" + } + + #; routines + set rs = ##class(%ResultSet).%New("%Routine:RoutineList") + if rs.Execute() + { + while rs.%Next() + { + set code = rs.Name + write code,! + if ( 'generated && $$$isGenerated( code ) ) continue + if ( 'percent && $$$isPercented( code ) ) continue + if ( 'mapped && $$$isMapped( code ) ) continue + if ( '..checkMasks( code, mask ) ) continue + + set filename = ..filename( code ) + do ..mkdir( filename ) + $$$log + $$$export( code, filename ) + + } + set rs="" + } + + #; dfi + do ..fixDashIntoName() + #define export(%code,%file) set sc = ##class(%DeepSee.UserLibrary.Utils).%Export( %code, %file, 0 ) + + set sql = "Select id, fullName as Name, documentName From %DeepSee_UserLibrary.FolderItem" + set rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) + while rs.%Next() + { + set code = rs.Name + set filename = ..filename( code_".dfi" ) + set documentName = rs.documentName + + if ($L(code,"$TRASH")>1) continue + + if ( 'generated && $$$isGenerated( code ) ) continue + if ( 'percent && $$$isPercented( code ) ) continue + if ( 'mapped && $$$isMapped( code ) ) continue + if ( '..checkMasks(documentName, mask) ) continue + + do ..mkdir( filename ) + + if dfi + { + $$$log + $$$export( code, filename) + } + elseif ('dfi) + { + set documentName = $replace(documentName,"/","-") + set filename = $extract(filename,1,*-4) + do ..fixXMLLine(documentName,filename_".xml") + } + + } + set rs="" + + #;DeepSee artefacts + do ..exportCubes() + do ##class(dev.bi).exportDeepSeeArtefacts() + write !,!, "#; Exported to ", ..workdir() + + return $$$OK +} + +/// Method to export all cubes, source cube classes, dashboards, +/// pivots and globals from the namespace available to the current user +/// do ##class(dev.code).exportBI() +ClassMethod exportBI() As %Status +{ + do ##class(dev.bi).exportCubes() + do ##class(dev.bi).exportKPIs() + do ##class(dev.bi).exportDashboards() + do ##class(dev.bi).exportPivots() + do ##class(dev.bi).exportDeepSeeArtefacts() + + return $$$OK +} + +/// Export Cube Classes +ClassMethod exportCubes() As %Status +{ + #define export(%code, %file) set sc = $system.OBJ.ExportUDL(%code, %file,"/diffexport") ##continue + write:sc "Ok" do:'sc $system.OBJ.DisplayError(sc) + #define isMapped(%code) ##class(%RoutineMgr).IsMapped( %code ) + #define isPercented(%code) ("%" = $e(%code)) + #define isCube(%code) $classmethod(%code,"%Extends","%DeepSee.CubeDefinition") + + #define log write !, code, " -> ", $piece(filename,..workdir(),2), " " + + Set sc = $$$OK + #; classes + set rs = ##class(%ResultSet).%New("%Dictionary.ClassDefinition:Summary") + if rs.Execute() + { + while rs.%Next() + { + set code = rs.Name _ ".cls", isSystem = rs.System + if isSystem continue + if $$$isPercented( code ) continue + if $$$isMapped( code ) continue + + if '($classmethod(rs.Name,"%Extends","%DeepSee.CubeDefinition")) continue + set filename = ..filename( code ) + do ..mkdir( filename ) + $$$log + $$$export( code, filename ) + } + set rs="" + } + + + Return sc +} + +/// import all from workdir +ClassMethod import(filemask = "*.xml;*.cls;*.mac;*.int;*.inc;*.dfi", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status +{ + #define push(%dir) set dirs( $i( dirs ) ) = %dir + #define next(%i,%dir) set %i=$o( dirs( "" ), 1, %dir ) kill:%i'="" dirs(%i) + #define isDirectory(%type) ( %type = "D" ) + #define log write !, $piece(filename,..workdir(),2), " " write:sc "Ok" do:'sc $system.OBJ.DisplayError(sc) + + set sc = 1, dirs = "", dir = ..workdir() $$$push(dir) + if verbose write "#; Importing from ", dir,! + do ..setIgnore(.fm,verbose) + for + { + $$$next(i,dir) quit:i="" quit:dir="" + set rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) + + while rs.%Next() + { + set filename = rs.Name + + if $$$isDirectory(rs.Type) { + if recurse $$$push(filename) + continue + } + // check file filter + if ..inFilter(.fm,filename) continue + + set ext = $zcvt($p( filename, ".", * ),"l") + + if (ext = "dfi") { + set sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .dsloaded ) + + } else { + // load classes only + set sc = $system.OBJ.Load( filename, "k-d", .err, .loaded) + } + + if verbose $$$log + + } + // compile all the loaded classes to obey dependencies + set sc=$system.OBJ.CompileList( .loaded, "cukbr-d/multicompile",.err ) + + } + + if verbose { + + if $D(err) { + set i=$Order(err("")) + while i'="" { + write !,err(i) + set i=$O(err(i)) + } + } + + write !,!,"#; Imported from ", ..workdir() + } + return sc +} + +ClassMethod exportGlobals(mask = "") As %Status +{ + #include %occInclude + set statement=##class(%SQL.Statement).%New() + set sc=statement.%PrepareClassQuery("%SYS.GlobalQuery","NameSpaceListChui") + if $$$ISERR(sc) { do $system.OBJ.DisplayError(sc) } + set resultset=statement.%Execute($namespace) + while resultset.%Next() + { + set gname = resultset.%Get("Name") + + continue:$data(@("^"_gname))=0 + continue:$$$isGenerated(gname) + continue:$$$isPercented(gname) + continue:$$$isMapped(gname) + continue:'..checkMasks(gname, mask) + + set gname = gname_".GBL" + set filename = ..filename(gname) + do ..mkdir(gname) + + set sc = ..fixXMLLine(gname, filename_".xml") + } + return sc +} + +ClassMethod patch(filename = "", commitFrom = "", commitTo = "") As %Status +{ + set gln = ..gln() + set git = @gln@("git") + + set:commitTo="" commitTo=1 + if (git = 0) + { + set:(commitFrom="")||(commitFrom=1) commitFrom=0 + set sc = ##class(dev.diff.gitLocal).buildDiff(..workdir(), "HEAD~"_commitTo, "HEAD~"_commitFrom, .items) + return:$$$ISERR(sc) sc + } + elseif(git = 1) + { + set owner = @gln@("owner") + set repository = @gln@("repository") + set user = @gln@("user") + set password = @gln@("password") + set:commitFrom="" commitFrom=1 + + set sc = ##class(dev.diff.gitHub).Get(.items, owner, repository, user, password, commitFrom, commitTo) + return:$$$ISERR(sc) sc + } + + if (filename="") + { + set filename=$G(@gln@("projectName"),"patch") + set filename=filename_$p($zdt($h,8)," ")_$tr($p($p($zdt($h)," ",2),":",1,2),":")_".xml" + } + zw filename + set sc = $system.OBJ.Export(.items,filename) + + return sc +} + +ClassMethod fixXMLLine(documentName, filename) As %Status +{ + set stream = ##class(%Stream.FileBinary).%New() + do stream.LinkToFile(filename) + do $system.OBJ.ExportToStream(documentName, .stream) + + set matcher=##class(%Regex.Matcher).%New("") + set matcher.Text = stream.Read(stream.Size) + set data = matcher.ReplaceFirst("") + + do stream.Clear() + do stream.Write(data) + return stream.%Save() +} + +ClassMethod inFilter(ByRef filtermask, filename) As %Boolean +{ + set result=0 + for + { + if '$d(filtermask($I(i))) quit + set filter=$piece(filtermask(i),"*") + if $length(filename,filter)>1 set result=1 + } + + return result +} + +/// get the filtermask for the repository. +/// looks for .gitignore file and applies all the lines to filters +ClassMethod setIgnore(ByRef filtermask, verbose) +{ + // working with .gitignore file + // gitignore consists of masks of filtering files + set filename=..workdir()_"/.gitignore" + if '##class(%File).Exists(filename) quit + set file=##class(%File).%New(filename) + set sc=file.Open("R") + if 'sc + { + if verbose do $System.OBJ.DisplayError(sc) return + } + + While 'file.AtEnd + { + set filtermask($I(filtermask))=file.ReadLine() + } + quit +} + +/// get or set working directory for export/import source +ClassMethod workdir(workdir) +{ + set:$d(workdir) workdir = ##class(%File).NormalizeDirectory(workdir) + set gln = ..gln() + set:$d(workdir) @gln = workdir + + ///zu(12) namespace directory by default + #define nsdir $zu(12,"") + return $g(@gln, $$$nsdir) +} + +/// gl[obal] n[ame] - storage for settings +ClassMethod gln() [ CodeMode = expression, Private ] +{ +"^"_$classname() +} + +ClassMethod init(confile = "isc.json") +{ + quit:..workdir()="" "workdir is empty" + quit:'##class(%File).Exists(..workdir()_confile) "configuration file: "_confile_" not found in "_..workdir() + + set stream=##class(%Stream.FileCharacter).%New() + set sc=stream.LinkToFile(..workdir()_confile) + set conf={}.%FromJSON(stream.Read($$$MaxCacheInt)) + + set gln = "^dev.code" + + set @gln@("compileList")=conf.compileList + set @gln@("projectName")=conf.projectName + set @gln@("owner")=conf.owner + set @gln@("repository")=conf.repository + set @gln@("user")=conf.user + set @gln@("password")=conf.password + set @gln@("git")=conf.git + set:@gln@("git")="" @gln@("git")=0 + + write "compileList="_conf.compileList,! + write "projectName="_conf.projectName,! + write "owner="_conf.owner,! + write "repository="_conf.repository,! + write "user="_conf.user,! + write "password="_conf.password,! + write "git="_conf.git,! + + quit $$$OK +} + +/// export release file for list and project settings +ClassMethod release() +{ + set gln=..gln() + set list=$G(@gln@("compileList")) + if list="" write "Nothing to release. Run init method first" quit + + set release=$G(@gln@("projectName"),"release") + set release=release_$p($zdt($h,8)," ")_$tr($p($p($zdt($h)," ",2),":",1,2),":")_".xml" + do $System.OBJ.Export(list,release) + write "All objects with mask "_list_" has been exported to "_release +} + +ClassMethod compile() +{ + set gln=..gln() + set list=$G(@gln@("compileList")) + if list="" write "Nothing to compile. Run init method first" quit + do $System.OBJ.compile(list) +} + +/// test.dfi -> /dfi/test.dfi +/// test.cls -> /cls/test.cls +/// testpkg.test.cls -> /cls/testpkg/test.cls +/// etc +/// w ##class(dev.code).filename() +ClassMethod filename(code) +{ + #define log(%dir,%sc) w !, "mkdir ", %dir, " ", sc + + set wd = ..workdir() + + if '##class(%File).DirectoryExists( wd ) { + set sc = ##class(%File).CreateDirectoryChain( wd ) + $$$log(wd,sc) + } + + set ext = $p( code, ".", * ), ext = $zcvt( ext, "l" ) + + // for each type - different directory + set extensions = $lb("int","inc","mac","dfi","gbl") + if ($lf(extensions, ext)>0) + { + set wd = ##class(%File).NormalizeDirectory( ext, wd ) + } + + // directory must exist before any call (%File).NormalizeFilename( , wd) + if '##class(%File).DirectoryExists( wd ) + { + set sc = ##class(%File).CreateDirectoryChain( wd ) + $$$log(wd,sc) + } + + set filename = ##class(%File).NormalizeFilename( code, wd ) + + // lowercase file extension + set $p(filename,".",*)=ext + + #; for *.cls Package.Subpackage.ClassName.cls -> Folder/Subfolder/ClassName.cls + if (ext="cls") + { + set dirs = $piece( code, ".",1, *-2 ), dirs = $translate( dirs, ".", "/" ) + set relpath = dirs _ "/" _ $piece( code, ".", *-1, * ) ; + set filename = ##class(%File).NormalizeFilename( relpath, wd ) + } + + return filename +} + +/// import from workdir all files with ts newer than code ts in db +ClassMethod importUpdated(filemask = "*.*", qspec = "cku-d", ByRef err = "", recurse = 1, ByRef loaded = "", verbose = 1) As %Status +{ + #define push(%dir) s dirs( $i( dirs ) ) = %dir + #define next(%i,%dir) s %i=$o( dirs( "" ), 1, %dir ) k:%i'="" dirs(%i) + #define isDirectory(%type) ( %type = "D" ) + #define log w !, $piece(filename,..workdir(),2)," " w:sc "Ok" d:'sc $system.OBJ.DisplayError( sc ) + + set sc = 1, dirs = "", dir = ..workdir() $$$push(dir) + if verbose write "#; Importing updated from ", dir,! + do ..setIgnore(.fm,verbose) + for + { + $$$next(i,dir) quit:i="" quit:dir="" + + set rs = ##class(%File).FileSetFunc( dir, filemask, , 1 ) + + while rs.%Next() + { + set filename = rs.Name + + if $$$isDirectory( rs.Type ) + { + if ( recurse ) $$$push(filename) ;push directory + continue + } + + // check file filter + if ..inFilter(.fm,filename) continue + + set filets = rs.DateModified + set codename = ..codename( filename, .ext ) + set codets = ..codets( codename, ext ) + + if ( filets '] codets ) continue + + if (ext = "dfi"){ + set sc = ##class(%DeepSee.UserLibrary.Utils).%Import( filename, 1, 0, 0, "", .dsloaded ) + } else + { + #; drop existing code before import ( purge DateModified ) + #; s:codets'="" sc = ##class(%RoutineMgr).Delete( codename ) + kill err + set sc = $system.OBJ.Load( filename, qspec, .err, .loaded) + } + if verbose $$$log + } + } + write:verbose !,"#; Imported updated from ", ..workdir() + return sc +} + +/// presumable codename +ClassMethod codename(filename, ByRef ext = "") +{ + set ext = $p( filename, ".", * ), ext = $zcvt( ext, "l" ) + set path = ##class(%File).NormalizeDirectory( ext, ..workdir() ) + set codename = $p( filename, path, 2 ) + if ext = "dfi" { + set fullname = $tr( codename, "\", "/" ) ; return fullname for dfi in $$$IsWINDOWS + quit $p( fullname, ".", 1, *-1 ) ;remove extension + } + if (ext ="cls")!(ext="int")!(ext="inc")!(ext="mac") s codename=$tr(codename,"\/","..") + quit codename +} + +ClassMethod codets(codename, ext) +{ + set ts = "" + if ext'="dfi" { + set ts = ##class(%RoutineMgr).TS( codename ) + } else + { + set sql="Select timeModified From %DeepSee_UserLibrary.FolderItem Where fullname = ?" + set rs = ##class(%SQL.Statement).%ExecDirect( , sql, codename ) + if rs.%Next() + { + set utcts = rs.timeModified + set utch = $zdth( utcts, 3, , 3 ) ;utc internal format + set loch = $zdth( utch, -3 ) ; utc to local timezone + set ts = $zdt( loch, 3, ,0 ) ; local timestamp*/ + } + } + return $p( ts, "." ) ;remove ms +} + +// fix "-" into dfi files + +ClassMethod fixDashIntoName() +{ + set sql = "Select id, fullName as Name, documentName From %DeepSee_UserLibrary.FolderItem" + set rs = ##class(%SQL.Statement).%ExecDirect( .stm, sql ) + while rs.%Next() { + if $find(rs.Name, "-"){ + write rs.Name," -> " + set item = ##class(%DeepSee.UserLibrary.FolderItem).%OpenId(rs.id) + set item.name = $replace(item.name,"-"," ") + write item.name,! + do item.%Save() + } + } + kill rs +} + +ClassMethod mkdir(filename As %String) As %Status +{ + set sc = $$$OK + set path = ##class(%File).GetDirectory(filename) + if '##class(%File).DirectoryExists(path) + { + set sc = ##class(%File).CreateDirectoryChain(path) + write !, "mkdir ", path, " ", sc + } + return sc +} + +ClassMethod getVersion() [ CodeMode = expression ] +{ +"Version: "_..#version +} + +ClassMethod checkMasks(name As %String, masks As %String) As %Boolean +{ + return:masks="" 1 + return:name="" 0 + + set name = $zcvt(name,"l") + set masks = $zcvt(masks,"l") + + for i=1:1:$l(masks,",") + { + set pattern = "^" + set mask = $p(masks, ",", i) + + for index=1:1:$l(mask) + { + set char = $e(mask, index) + set pattern = pattern _ $case(char, + ".":"\.", + "?":".", + "*":".*", + :char + ) + } + set pattern = pattern _ "$|" + } + set pattern = $e(pattern,1,*-1) + set matcher=##class(%Regex.Matcher).%New(pattern, name) + do matcher.Locate() + return $select(matcher.Group="":0, 1:1) +} + +} diff --git a/src/dev/deepsee.cls b/src/dev/deepsee.cls new file mode 100755 index 0000000..4b0f419 --- /dev/null +++ b/src/dev/deepsee.cls @@ -0,0 +1,71 @@ +Class dev.deepsee +{ + +ClassMethod checkDataSource(onlyerror As %Boolean = 1) +{ + set sc = $$$OK + &sql(DECLARE C1 CURSOR FOR SELECT ID into :id FROM %DeepSee_Dashboard.Definition) + &sql(OPEN C1) + &sql(FETCH C1) + WHILE (SQLCODE = 0) + { + set flag = 1 + set dashbord = ##class(%DeepSee.Dashboard.Definition).%OpenId(id) + set widgets = dashbord.widgets + + for i=1:1:widgets.Count() + { + set widget = widgets.GetAt(i) + set pivotName = widget.dataSource + set sc = ..getMdx(pivotName, .MDX) + set sc = ..checkPivot(MDX) + + continue:sc&&onlyerror + + w:flag=1 dashbord.name,! + w " pivot: ",pivotName," status: "_$select(sc'=1:$System.Status.GetErrorText(sc),1:"OK"),! + set flag = 0 + } + &sql(FETCH C1) + } + &sql(CLOSE C1) +} + +ClassMethod checkPivot(MDX As %String) As %Status +{ + set sc = $$$OK + set rs = ##class(%DeepSee.ResultSet).%New() + + set sc = rs.%PrepareMDX(MDX) + return:$$$ISERR(sc) sc + + set sc = rs.%ExecuteAsynch() + return:$$$ISERR(sc) sc + + return sc +} + +ClassMethod getMdx(pPivotName As %String, Output MDX) As %Status +{ + #dim tPivot As %DeepSee.Dashboard.Pivot + #dim tPivotTable As %DeepSee.Component.pivotTable + set MDX = "" + + set tPivot = ##class(%DeepSee.UserLibrary.Utils).%OpenFolderItem(pPivotName,.sc) + return:'$IsObject(tPivot) $$$OK + return:$$$ISERR(sc) sc + + set tPivotTable = ##class(%DeepSee.Component.pivotTable).%New() + set sc = tPivot.%CopyToComponent(tPivotTable) + return:$$$ISERR(sc) sc + + set rs = tPivotTable.%CreateResultSet(.sc,,,,.MDX) // returns tQueryText - mdx without filters + return:$$$ISERR(sc) sc + + set MDX = $TR(MDX,$C(10),"") + + return sc +} + +} + diff --git a/cls/sc/diff/gitHub.cls b/src/dev/diff/gitHub.cls old mode 100644 new mode 100755 similarity index 98% rename from cls/sc/diff/gitHub.cls rename to src/dev/diff/gitHub.cls index 48f28c4..4b55178 --- a/cls/sc/diff/gitHub.cls +++ b/src/dev/diff/gitHub.cls @@ -1,4 +1,4 @@ -Class sc.diff.gitHub +Class dev.diff.gitHub { ClassMethod Get(Output items, Owner As %String = "", Repository As %String = "", Username As %String = "clminstaller", Password As %String = "clminstaller2016", CommitFrom As %Integer = "", CommitTo As %Integer = "") As %Status @@ -66,7 +66,7 @@ ClassMethod GetFileNameForReleaseBySHAs(Request As %Net.HttpRequest, SHAsList, O set fileName = files.GetAt(j).%data("filename") set:$L(fileName,".xml")'=1 fileName = $extract(fileName,1,*-4) continue:..IsCacheFile(fileName)=0 - do ##class(sc.diff.utils).ToCacheName(.fileName) + do ##class(dev.diff.utils).ToCacheName(.fileName) set filesForRelease(fileName) = "" } } diff --git a/cls/sc/diff/gitLocal.cls b/src/dev/diff/gitLocal.cls old mode 100644 new mode 100755 similarity index 87% rename from cls/sc/diff/gitLocal.cls rename to src/dev/diff/gitLocal.cls index 2bfe22b..4df6a54 --- a/cls/sc/diff/gitLocal.cls +++ b/src/dev/diff/gitLocal.cls @@ -1,4 +1,4 @@ -Class sc.diff.gitLocal +Class dev.diff.gitLocal { /// Get diff between two points in repository @@ -17,39 +17,44 @@ Class sc.diff.gitLocal ClassMethod buildDiff(repo As %String, sha1 As %String, sha2 As %String, Output items) As %Status { $$$TOE(sc, ..createFile(.tempFile)) + set oldDirectory = $system.Process.CurrentDirectory() do $system.Process.CurrentDirectory(repo) - $$$TOE(sc, ..execute($$$FormatText("git diff --name-status %1 %2 > %3 2>&3", sha1, sha2, tempFile))) + $$$TOE(sc, ..execute($$$FormatText("git diff --name-status %1 %2 > %3 2>&1", sha1, sha2, tempFile))) $$$TOE(sc, ..fileToString(tempFile, .diffRaw)) + set workdir = ##class(dev.code).workdir() + set path = ##class(dev.diff.utils).getLocalGitPathByWorkdir(workdir) + for i=1:1:$length(diffRaw, $c(10)) { set element = $piece(diffRaw, $c(10), i) set status = $e($piece(element, $c(9))) set file = $piece(element, $c(9), 2) - if ($l(file,"src/") < 2) continue + if ($l(file, path) < 2) continue - set isRelevantFile = ##class(sc.diff.utils).isRelevantFile(file) + set isRelevantFile = ##class(dev.diff.utils).isRelevantFile(file) if ((element="") || ('isRelevantFile)) continue if $length(element, $c(9))=2 { if ((status="M") || (status="U")) || (status="A") { - do ##class(sc.diff.utils).ToCacheName(.file) + do ##class(dev.diff.utils).ToCacheName(.file) set items(file) = "" } }elseif $length(element, $c(9))=3 { set file = $piece(element, $c(9), 3) - if ($l(file,"src/") < 2) continue + if ($l(file, path) < 2) continue if ((status="C") || (status="R")) { - do ##class(sc.diff.utils).ToCacheName(.newFile) + do ##class(dev.diff.utils).ToCacheName(.file) set items(file) = "" } } } + do $system.Process.CurrentDirectory(oldDirectory) return sc } diff --git a/cls/sc/diff/utils.cls b/src/dev/diff/utils.cls old mode 100644 new mode 100755 similarity index 64% rename from cls/sc/diff/utils.cls rename to src/dev/diff/utils.cls index 58e49f8..002603d --- a/cls/sc/diff/utils.cls +++ b/src/dev/diff/utils.cls @@ -1,11 +1,12 @@ -Class sc.diff.utils +Class dev.diff.utils { Parameter TRACKEDEXT As List = {$lb("xml", "cls", "csp", "csr", "mac", "int", "bas", "inc", "gbl", "prj", "obj", "pkg", "gof", "dfi", "pivot", "dashboard")}; ClassMethod ToCacheName(ByRef filename) { - do:$L(filename,".xml")>1 ..getExtension(.filename) + if $get(filename)="" break + do:$L($zcvt(filename,"L"),".xml")>1 ..getExtension(.filename) set str = $Select( $L(filename,"cls/")>1:$Replace($P(filename,"cls/",2),"/","."), $L(filename,"csp/")>1:$Replace($P(filename,"csp/",2),"/","."), @@ -29,8 +30,31 @@ ClassMethod getExtension(ByRef filename) set filename = $extract(filename,1,*-4)_".DFI" }elseif ($L(filename,"cls/") > 1) { set filename = $extract(filename,1,*-4)_".CLS" + }elseif ($L(filename,"gbl/") > 1) { + set filename = $p($extract(filename,1,*-4),"gbl/",2) } } +ClassMethod getLocalGitPathByWorkdir(workdir As %String) As %String +{ + set path = "" + if ($L(workdir,"/") > 1) { + if ($P(workdir,"/",*) = "") { + set path = $P(workdir,"/",*-1) + }else { + set path = $P(workdir,"/",*) + } + + }else { + if ($P(workdir,"\",*) = "") { + set path = $P(workdir,"\",*-1) + }else { + set path = $P(workdir,"\",*) + } + } + + return path_"/" +} + } diff --git a/src/dev/interoperability.cls b/src/dev/interoperability.cls new file mode 100644 index 0000000..e66434a --- /dev/null +++ b/src/dev/interoperability.cls @@ -0,0 +1,408 @@ +/// Class to export/import various config items. +/// Was created by https://github.com/eduard93 +Class dev.interoperability +{ + +// Query LUT() As %SQLQuery +// { +// SELECT DISTINCT TableName FROM Ens_Util.LookupTable +// } + +/// Export Lookup Tables into dir +/// +/// do ##class(dev.interoperability).ExportLUT() +ClassMethod ExportLUT(dir) +{ + set dir = ##class(%File).NormalizeDirectory(dir) + + set sql = "SELECT DISTINCT TableName FROM Ens_Util.LookupTable" + set rs = ##class(%SQL.Statement).%ExecDirect(, sql) + if rs.%SQLCODE '=0 quit "" + //set rs = ..LUTFunc() + + while rs.%Next() { + set lut = rs.TableName + /// LUTs are guaranteed to have distinct names + try { + $$$ThrowOnError($System.OBJ.Export(lut _ ".LUT", ..GenerateFileName(dir, lut), "-d")) + } catch ex { + write "New export failed. Trying legacy export",! + $$$ThrowOnError(##class(Ens.Util.LookupTable).%Export(..GenerateFileName(dir, lut), lut)) + } + } +} + +/// do ##class(dev.interoperability).ImportLUT() +ClassMethod ImportLUT(dir) +{ + #include %occErrors + write "Lookup Tables import from: " _ dir + set rs = ##class(%File).FileSetFunc(dir, "*.xml") + while rs.%Next() { + set tablePath = rs.Get("Name") + write "Importing: " _ tablePath,! + // table is the full path, the last part (denoted by *) is the actual file name + set tablePathNoExtension = $PIECE(tablePath, "/", *) + // asking for $PIECE with just delimiter asks for the first part, thus ignore anything after the . + set tablePathNoExtension = $PIECE(tablePathNoExtension, ".") + write "Importing Lookup Table in " _ tablePathNoExtension,! + // lookup table should be named the file name (without extension) + do ##class(Ens.Util.LookupTable).%ClearTable(tablePathNoExtension) + + // Try the new import first. + // It returns an error if no LUTs were found in the tablePath file + //Set sc = ##class(EnsPortal.LookupSettings).Import(tablePath) + //zw sc + //w $System.Status.DisplayError(sc),! + + set sc = $system.OBJ.Load(tablePath, "-d") + + // If we got an error, try legacy import + if $$$ISERR(sc) { + write "New import failed. Trying legacy import",! + set sc=##class(Ens.Util.LookupTable).%Import(tablePath) + if $$$ISOK(sc) { + write "Import successful",! + } + } + + // Error remains unfixed. Fail. + if $$$ISERR(sc) { + write "Lookup Table import failure: ", $System.Status.GetErrorText(sc),! + do $system.Process.Terminate(, 1) + } + } +} + +/// Export Custom HL7 schemas to a dir, one schema per file. +/// +/// do ##class(dev.interoperability).ExportCustomSchemas("C:\InterSystems") +ClassMethod ExportCustomSchemas(dir) +{ + #dim sc As %sc = $$$OK + set dir = ##class(%File).NormalizeDirectory(dir) + + // list all schemas + set rs = ##class(EnsLib.HL7.Schema).TypeCategoriesFunc() + while rs.%Next() { + // export only custom schemas + if rs.IsStandard = $$$NO { + set category = rs.Category + $$$THROWONERROR(sc, $System.OBJ.Export(category _ ".HL7", ..GenerateFileName(dir, category), "-d")) + } + } +} + +/// Import directory with custom HL7 schemas +/// +/// do ##class(dev.interoperability).ImportCustomSchemas("C:\InterSystems") +ClassMethod ImportCustomSchemas(dir) +{ + write "Custom Schemas import from: " _ dir + set sc = $System.OBJ.LoadDir(dir) + if $$$ISERR(sc) { + write "Schema import failure: ", $System.Status.GetErrorText(sc),! + do $system.Process.Terminate(, 1) + } +} + +/// Export Business Partners to one directory. One Business Partner per file. +/// +/// do ##class(dev.interoperability).ExportBusinessPartners("C:\InterSystems\BP") +ClassMethod ExportBusinessPartners(dir) +{ + set dir = ##class(%File).NormalizeDirectory(dir) + + // list all Business Partners + set rs = ##class(Ens.Config.BusinessPartner).ExtentFunc() + while rs.%Next() { + $$$THROWONERROR(sc, ##class(Ens.Config.BusinessPartner).%Export(rs.ID, ..GenerateFileName(dir, rs.ID))) + } +} + +/// Import Business Partners from a dir. This would overwrite partners with the same name, if any. +/// +/// do ##class(dev.interoperability).ImportBusinessPartners("C:\InterSystems\BP") +ClassMethod ImportBusinessPartners(dir) +{ + write "Business Partner import from: " _ dir, ! + set rs = ##class(%File).FileSetFunc(dir, "*.xml") + + while rs.%Next() { + set sc = ##class(Ens.Config.BusinessPartner).%Import(rs.Name, 1, 1) + if $$$ISERR(sc) { + write "Business Partner " _ rs.Name _ " import failure: ", $System.Status.GetErrorText(sc),! + do $system.Process.Terminate(, 1) + } + } +} + +Query Tasks() As %SQLQuery(SELECTMODE = "DISPLAY") +{ +SELECT ID +FROM %SYS.Task +WHERE Type = 'User' +} + +/// Export User Tasks to one directory. One Task per file. +/// +/// do ##class(dev.interoperability).ExportTasks("C:\InterSystems\HCC\Tasks") +ClassMethod ExportTasks(dir) +{ + set dir = ##class(%File).NormalizeDirectory(dir) + + // list all tasks + set rs = ..TasksFunc() + while rs.%Next() { + $$$THROWONERROR(sc, ##class(%SYS.TaskSuper).ExportTasks($lb(rs.ID), ..GenerateFileName(dir, rs.ID))) + } +} + +/// Import User Tasks from a dir. Tasks with the same JobGUID would be skipped. +/// +/// do ##class(dev.interoperability).ImportTasks("C:\InterSystems\HCC\Tasks") +ClassMethod ImportTasks(dir) +{ + write "Tasks import from: " _ dir, ! + set rs = ##class(%File).FileSetFunc(dir, "*.xml") + + while rs.%Next() { + set sc = ##class(%SYS.TaskSuper).ImportTasks(rs.Name) + if $$$ISERR(sc) { + write "Task " _ rs.Name _ " import failure: ", $System.Status.GetErrorText(sc),! + do $system.Process.Terminate(, 1) + } + } +} + +/// Export credentials to one directory. One credential per file. +/// NOT TO BE STORED IN SOURCE CONTROL +/// +/// do ##class(dev.interoperability).ExportCredentials("C:\InterSystems\HCC\Credentials") +ClassMethod ExportCredentials(dir) +{ + set dir = ##class(%File).NormalizeDirectory(dir) + + // list all tasks + set rs = ##class(Ens.Config.Credentials).ExtentFunc() + while rs.%Next() { + $$$THROWONERROR(sc, ..ExportCredential(rs.ID, ..GenerateFileName(dir, rs.ID))) + } +} + +ClassMethod ExportCredential(id, file) As %Status +{ + #dim sc As %Status = $$$OK + try { + set credential = ##class(Ens.Config.Credentials).%OpenId(id,,.sc) + if '$IsObject(credential) { + set sc = $$$ERROR($$$GeneralError,"Unable to find Credential: "_id) + quit + } + + // make sure we can open the file + open file:"WNU":0 else set sc = $$$ERROR($$$GeneralError,"Unable to open file: "_file) quit + close file + + set writer = ##class(%XML.Writer).%New() + set writer.Indent = 1 + set sc = writer.OutputToFile(file) + if $$$ISERR(sc) quit + set sc = writer.StartDocument() + if $$$ISERR(sc) quit + set sc = writer.RootObject(credential) + if $$$ISERR(sc) quit + set sc = writer.EndDocument() + if $$$ISERR(sc) quit + } catch(ex) { + set sc = ex.AsStatus() + } + if $$$ISERR(sc) { + write !,$System.Status.DisplayError(sc) + } + + quit sc +} + +/// Import credentials from one directory. +/// NOT TO BE STORED IN SOURCE CONTROL +/// +/// do ##class(dev.interoperability).ImportCredentials("C:\InterSystems\HCC\Credentials") +ClassMethod ImportCredentials(dir) +{ + write "Credential import from: " _ dir, ! + set rs = ##class(%File).FileSetFunc(dir, "*.xml") + + while rs.%Next() { + set sc = ..ImportCredential(rs.Name) + if $$$ISERR(sc) { + write "Credential " _ rs.Name _ " import failure: ", $System.Status.GetErrorText(sc),! + } + } +} + +/// Import one credential from file +ClassMethod ImportCredential(file) As %Status +{ + #dim sc As %Status = $$$OK + try { + // Create an instance of %XML.Reader + set reader = ##class(%XML.Reader).%New() + + // Begin processing of the file + set sc = reader.OpenFile(file) + if $$$ISERR(sc) quit + + Do reader.Correlate("Credentials", "Ens.Config.Credentials") + + // Read objects from xml file + while (reader.Next(.credential, .sc)) { + set exists = ##class(Ens.Config.Credentials).%ExistsId(credential.SystemName) + + if (exists=1) { + write "Replacing: ",credential.SystemName,! + set sc = ##class(Ens.Config.Credentials).%DeleteId(credential.SystemName) + if $$$ISERR(sc) quit + } else { + write "Importing: ", credential.SystemName,! + } + set sc = credential.%Save() + if $$$ISERR(sc) quit + } + if $$$ISERR(sc) quit + } catch(ex) { + set sc = ex.AsStatus() + } + quit sc +} + +/// Export SSL configurations to one directory. +/// One SSL config per file (with additional files for CAFile, CRLFile, CertificateFile, and PrivateKeyFile. +/// Set CAPath results in a directory copy. +/// +/// NOT TO BE STORED IN SOURCE CONTROL IF SSL CONFIG CONTAINS A PRIVATE KEY +/// +/// do ##class(dev.interoperability).ExportSSL("C:\InterSystems\HCC\SSL") +ClassMethod ExportSSL(dir) +{ + new $namespace + set $namespace = "%SYS" + set dir = ##class(%File).NormalizeDirectory(dir) + + // list all SSL configs + set rs = ##class(Security.SSLConfigs).ExtentFunc() + while rs.%Next() { + set sslFullFileName = ..GenerateFileName(dir, rs.ID) + set sslFileName = ##class(%File).GetFilename(sslFullFileName) + set sslFileNameNoExt = $p(sslFileName, ".", 1, *-1) + $$$THROWONERROR(sc, ##class(Security.SSLConfigs).Export(sslFullFileName, , rs.ID)) + $$$THROWONERROR(sc, ##class(Security.SSLConfigs).Get(rs.ID, .props)) + for file = "CAFile", "CRLFile", "CertificateFile", "PrivateKeyFile" { + if props(file)'="" { + set ext = $p(##class(%File).GetFilename(props(file)), ".", *, *) + set targetFile = ..GenerateFileName(dir, sslFileNameNoExt _ file, ext) + set rc = ##class(%File).CopyFile(props(file), targetFile, ,.return) + if rs = 0 { + $$$THROWONERROR(sc, $$$ERROR($$$GeneralError, $$$FormatText("Error copying file %1 into %2, which is %3 for SSL config %4. Error code %5", props(file), targetFile, file, rs.ID, return))) + } + } + } + if props("CAPath")'="" { + set rc = ##class(%File).CopyDir(props("CAPath"), dir _ sslFileNameNoExt) + if rs = 0 { + $$$THROWONERROR(sc, $$$ERROR($$$GeneralError, $$$FormatText("Error copying dir %1 into %2, which is CAPath dir for SSL config %3", props("CAPath"), target, dir _ sslFileNameNoExt, rs.ID))) + } + } + + } +} + +/// Import SSL configurations from one directory. +/// NOT TO BE STORED IN SOURCE CONTROL IF SSL CONFIG CONTAINS A PRIVATE KEY +/// +/// do ##class(dev.interoperability).ImportSSL("C:\InterSystems\HCC\SSL") +ClassMethod ImportSSL(dir) +{ + new $namespace + set $namespace = "%SYS" + set dir = ##class(%File).NormalizeDirectory(dir) + + write "SSL Configuration import from: " _ dir, ! + set rs = ##class(%File).FileSetFunc(dir, "*.xml") + + while rs.%Next() { + set sslFullFileName = rs.Name + set sslFileName = ##class(%File).GetFilename(sslFullFileName) + set sslFileNameNoExt = $p(sslFileName, ".", 1, *-1) + + // 1. Patch SSL Config XML to use correct paths + + // Create an instance of %XML.Reader + set reader = ##class(%XML.Reader).%New() + + // Begin processing of the file + $$$THROWONERROR(sc, reader.OpenFile(sslFullFileName)) + if $$$ISERR(sc) quit + + do reader.Correlate("SSLConfigs","Security.SSLConfigs") + + // Assuming one SSL config per file + do reader.Next(.ssl, .sc) + $$$THROWONERROR(sc, sc) + + for prop = "CAFile", "CRLFile", "CertificateFile", "PrivateKeyFile" { + if $property(ssl, prop)'="" { + set file = $ZSEARCH(dir _ sslFileNameNoExt _ prop _ "*") + if file="" { + throw ##class(%Exception.StatusException).CreateFromStatus($$$ERROR($$$GeneralError, $$$FormatText("SSL config %1 from file %2 refers to %3 which is not found at expected location %4", ssl.Name, sslFullFileName, prop, dir _ sslFileNameNoExt _ prop _ "*"))) + } + set $property(ssl, prop) = file + } + } + + if ssl.CAPath'="" { + if ##class(%File).DirectoryExists(dir _ sslFileNameNoExt) { + set ssl.CAPath = dir _ sslFileNameNoExt + } else { + throw ##class(%Exception.StatusException).CreateFromStatus($$$ERROR($$$GeneralError, $$$FormatText("SSL config %1 from file %2 refers to CAPath which is not found at expected location %3", ssl.Name, sslFullFileName, dir _ sslFileNameNoExt))) + } + } + + kill reader + + // 2. Write patched SSL config to file + + set writer=##class(%XML.Writer).%New() + $$$THROWONERROR(sc, writer.OutputToFile(sslFullFileName)) + set writer.Charset="UTF-8" + set writer.Indent=1 + $$$THROWONERROR(sc, writer.RootElement("SSLConfigsExport")) + $$$THROWONERROR(sc, writer.Object(ssl)) + $$$THROWONERROR(sc, writer.EndRootElement()) + $$$THROWONERROR(sc, writer.EndDocument()) + + kill writer + + // 3. Import new SSL config + $$$THROWONERROR(sc, ##class(Security.SSLConfigs).Import(sslFullFileName)) + } +} + +/// Generate a filename in a dir with a specific extension. +/// Filename is guaranteed to not exist OR be absolutely the same. +/// This is achieved by adding underscores to the start of the name if any conflicts exist. +/// +/// w ##class(dev.interoperability).GenerateFileName("C:\InterSystems\BP", "test") +ClassMethod GenerateFileName(dir, name, ext = "xml") As %String +{ + set dir = ##class(%File).NormalizeDirectory(dir) + set filename = dir _name _ "." _ ext + if '##class(%File).Exists(filename) { + quit filename + } elseif ##class(%File).NormalizeFilename(filename) = filename { + quit filename + } + quit ..GenerateFileName(dir, "_" _ name, ext) +} + +} \ No newline at end of file