Eduard Lebedyuk · May 20, 2025 go to post

You're welcome. Here's a bit more info about how BPL BPs work. After you compile a BPL BP, two classes get created into the package with the same name as a full BPL class name:

  • Thread1 class contains methods S1, S2, ... SN, which correspond to activities within BPL
  • Context class has all context variables and also the next state which BPL would execute (i.e., S5)

Also BPL class is persistent and stores requests currently being processed.

BPL works by executing S methods in a Thread class and correspondingly updating the BPL class table, Context table, and Thread1 table where one message "being processed" is one row in a BPL table. After the request is processed, BPL deletes the BPL, Context, and Thread entries. Since BPL BPs are asynchronous, one BPL job can simultaneously process many requests by saving information between S calls and switching between different requests.
For example, BPL processed one request till it got to a sync activity - waiting for an answer from BO. It would save the current context to disk, with %NextState property (in Thread1 class) set to response activity S method, and work on other requests until BO answers. After BO answers, BPL would load Context into memory and execute the method corresponding to a state saved in %NextState property.

That's why registered objects as context properties can (and would) be lost between states - as they are not persisted.

Eduard Lebedyuk · May 19, 2025 go to post

HS.FHIR.DTL.vR4.Model.Resource.Patient is a registered object and not a persistent object, so there's no guarantee it will exist beyond a current BP State. You set context.patient in "Transform 1", so it will be gone from process memory after "Send to FHIR Repo 1" sends the request, but before it gets the reply back.

As a solution you can serialize FHIR resource to json and persist that.

Eduard Lebedyuk · May 16, 2025 go to post

Is it possible to search all globals within a namespace/db or at least to do a full text search on a list of globals?

Eduard Lebedyuk · May 8, 2025 go to post

Can't reproduce in IRIS for Windows (x86-64) 2022.1 (Build 209U) Tue May 31 2022 12:16:40 EDT:

But can reproduce on IRIS for Windows (x86-64) 2025.1 (Build 223U) Tue Mar 11 2025 18:14:42 EDT

Please file a WRC. Looks like something changed between 2022.1 and 2024.1.1

Eduard Lebedyuk · May 8, 2025 go to post

%Stream.Dynamic* are read-only pointers to DAO data. %Stream.Tmp* are full-fledged (r/w) in-memory streams.

Eduard Lebedyuk · May 8, 2025 go to post

You can still use classes, just add  %JSONIGNOREINVALIDFIELD to ignore unknown properties:

Parameter %JSONIGNOREINVALIDFIELD As BOOLEAN = 1;

Here's how your example can look like:

Class dc.Item Extends (%RegisteredObject, %JSON.Adaptor)
{

Parameter %JSONIGNOREINVALIDFIELD As BOOLEAN = 1;

Property pureId As %Integer;

Property portalUrl As %VarString;

}

and the main class:

Class dc.Response Extends (%RegisteredObject, %JSON.Adaptor)
{

Parameter %JSONIGNOREINVALIDFIELD As BOOLEAN = 1;

Property items As list Of Item;

/// do ##class(dc.Response).Test()
ClassMethod Test()
{
	set json = ..Sample()
	set obj = ..%New()
	$$$TOE(sc, obj.%JSONImport(json))
	do obj.DisplayItems()
}

Method DisplayItems()
{
	for i=1:1:..items.Count() {
		set item = ..items.GetAt(i)
		zw item
	}
}

ClassMethod Sample() As %String [ CodeMode = expression ]
{
{
  "count": 0,
  "pageInformation": {
    "offset": 0,
    "size": 0
  },
  "items": [
    {
      "pureId": 0,
      "uuid": "196ab1c9-6e60-4000-88cb-4b1795761180",
      "createdBy": "string",
      "createdDate": "1970-01-01T00:00:00.000Z",
      "modifiedBy": "string",
      "modifiedDate": "1970-01-01T00:00:00.000Z",
      "portalUrl": "string",
      "prettyUrlIdentifiers": [
        "string"
      ],
      "previousUuids": [
        "string"
      ],
      "version": "string",
      "startDateAsResearcher": "1970-01-01",
      "affiliationNote": "string",
      "dateOfBirth": "1970-01-01",
      "employeeStartDate": "1970-01-01",
      "employeeEndDate": "1970-01-01",
      "externalPositions": [
        {
          "pureId": 0,
          "appointment": {
            "uri": "string",
            "term": {
              "en_GB": "Some text"
            }
          },
          "appointmentString": {
            "en_GB": "Some text"
          },
          "period": {
            "startDate": {
              "year": 0,
              "month": 1,
              "day": 1
            },
            "endDate": {
              "year": 0,
              "month": 1,
              "day": 1
            }
          },
          "externalOrganization": {
            "uuid": "196ab1c9-6e60-4000-8b89-29269178a480",
            "systemName": "string"
          }
        }
    ]
    }
	]
}.%ToJSON()
}

}
Eduard Lebedyuk · May 8, 2025 go to post

Iris session can do it (assumes OS auth is enabled):

irissession <INSTANCE> -U<NAMESPACE> '##class(%SYSTEM.OBJ).Load("<file from linux folder>","ck")'

Note that there must be no whitespaces in the command arg.

More on automated deploy.

Eduard Lebedyuk · May 8, 2025 go to post

Something along these lines should work.

ClassMethod ToEnsStream(obj As %RegisteredObject, Output req As Ens.StreamContainer) As %Status
{
	#dim sc As %Status = $$$OK
	try {
		set stream = ##class(%Stream.GlobalCharacter).%New()
		if obj.%Extends("%JSON.Adaptor") {
			$$$TOE(sc, obj.%JSONExportToStream(.stream))
		} elseif obj.%Extends(##class(%DynamicAbstractObject).%ClassName(1)) {
			do obj.%ToJSON(.stream)
		} else {
			/// try %ZEN.Auxiliary.altJSONProvider:%ObjectToAET?
			throw ##class(%Exception.General).%New("<JSON>")
		}
		set req = ##class(Ens.StreamContainer).%New(stream)
	} catch ex {
		set sc = ex.AsStatus()
	}
	quit sc
}
Eduard Lebedyuk · May 6, 2025 go to post

Starting 2025.1InterSystems adds support for two alternative syntax flavors: LIMIT ... OFFSET ..., which is commonly used in other database platforms, and OFFSET ... FETCH ..., which is the official ANSI standard. Documentation.

Eduard Lebedyuk · May 6, 2025 go to post

Well, I have great news for you, Otto!

Starting from 2025.1, we have automatic database download from mirror member. Documentation. No copying required.

Eduard Lebedyuk · May 5, 2025 go to post

Use io operations like write, zwrite, zzdump - they would be written to an output file automatically, if set.

Eduard Lebedyuk · May 1, 2025 go to post

Looks like a bug. Please check with WRC.

Simplified your example a bit:

Class DC.ValueOfThis Extends %RegisteredObject
{

/// do ##class(DC.ValueOfThis).Test()
ClassMethod Test()
{
	write $zv,!!
	set obj=..%New()
	do obj.Work()
	write $$$FormatText("classmethod: $this %1, ..Value() %2", $this, ..Value()),!
	do obj.Work()
}

Method Work()
{
	write $$$FormatText("method: $this %1, ..Value() %2", $this, ..Value()),!
}

ClassMethod Value()
{
	quit $this
}

}
Eduard Lebedyuk · Apr 25, 2025 go to post

Here's a sample zzdump custom function for DTL:

Class Utils.Functions Extends Ens.Util.FunctionSet
{

/// w ##class(Utils.Functions).ZZDUMP("abc")
ClassMethod ZZDUMP(var) As %String
{
	set id = $random(100000000000000000)
	while $d(^ISC.AsyncQueue(id)) {
		set id = $random(100000000000000000)
	}
	set str = ""
	
	try {	
		$$$TOE(sc, ##class(%Api.Atelier.v1).MonitorBeginCapture(id))
		if '$data(var) {
			write "<UNDEFINED>"
		} else {
			if '$isobject(var) {
				zzdump var
			} else {
				if var.%IsA(##class(%DynamicAbstractObject).%ClassName(1)) {
					zzdump var.%ToJSON()
				} elseif var.%IsA(##class(%Stream.Object).%ClassName(1)) {
					do var.Rewind()
					zzdump var.Read()
				} elseif var.%IsA(##class(EnsLib.HL7.Message).%ClassName(1)) {
					zzdump var.OutputToString()
				} else {
					// zzdump will output OREF string.
					zw var 
				}
			}
		}
		$$$TOE(sc, ##class(%Api.Atelier.v1).MonitorEndCapture(id))
	
		for i=1:1:^ISC.AsyncQueue(id,"cout","i") {
			set str = str _ ^ISC.AsyncQueue(id,"cout",i) _ $$$NL
		}
	} catch ex {
		do ##class(%Api.Atelier.v1).MonitorEndCapture(id)
	}
	kill ^ISC.AsyncQueue(id)
	quit str
}

}
Eduard Lebedyuk · Apr 23, 2025 go to post

Direct set still works (must be executed in %SYS):

set ^SYS("Portal","UsersD",$username,"Community")=$lb("","https://community.intersystems.com")
Eduard Lebedyuk · Apr 22, 2025 go to post

If that is okay, please give me the wrc#, I'm interested too. Looked through the sources, but still no idea what's going on here.

Eduard Lebedyuk · Apr 22, 2025 go to post

Try:

 set result = $SYSTEM.SQL.Execute("CALL osuwmc_Utils_EnterpriseDirDb.InterfaceCheckConnectMedCtrID('$Get(MedCtrID)')")
 set rs = result.%NextResult()
 while rs.%Next(){
  set Loc = rs.Get("OSUguestRoleDTL")
 }

Maybe you'll need to call %NextResult() twice.

Eduard Lebedyuk · Apr 17, 2025 go to post

Any valid alphanumeric section name will work. For example:

Class Utils.BO Extends Ens.BusinessOperation
{
Property MySetting;
Parameter SETTINGS = "MySetting:My Custom Category";
}

Will create a new My Custom Category for this business host:

Settings names can be localized by following this guide (categories too probably, but I haven't tried). Domain would be Ensemble.

Eduard Lebedyuk · Apr 14, 2025 go to post

I think you can guarantee that the picked list would:

  1. Provide the fullest possible coverage of numbers
  2. Skip at least fully superfluous lists

And do it in O(2n) where n is list count (assuming lists are of similar length).

 

Before anything, zero-init a list for each number (called a number list). You'll need to do two passes over your lists.  

On a first pass, check each list value against your number list. If at least one corresponding value in a number list is a zero (meaning our current list has a number we did not encounter before), add the list to the result and increment each position in a number list that is present in a current list by 1.

In our case:

 

Numbers: 0, 0, 0, 0, 0, 0, 0, 0, 0

List(1)="3,5,6,7,9"

As Numbers(3)==0, we add List(1) to the output and modify Numbers:

Numbers: 0, 0, 1, 0, 1, 1, 1, 0, 1

 

In a similar vein, we iterate all our lists (and add all of them, actually); our Numbers after the first pass should look like this:

Numbers: 1, 2, 1, 2, 2, 3, 2, 2, 4

Lists: 1, 2, 3, 4, 5

 

Now do a second pass, only over lists added on a first pass. If every element in a list has a value >1 in a number list, remove the list and decrease the corresponding number list by 1.

 

List(1)="3,5,6,7,9"

Numbers: 1, 2, 1, 2, 2, 3, 2, 2, 4

Numbers(3)==1, so this list remains.

 

List(2)="1,2,6,9"

Numbers(1)==1, so this list remains.

 

List(3)="5,8,9"

Numbers(5)==2>1, Numbers(8)==2>1,  Numbers(5)==4>1,  so we are removing this list, new numbers:

Numbers: 1, 2, 1, 2, 1, 3, 2, 1, 3

 

List(4)="2,4,6,8"

Numbers(8)==1, so this list remains.

 

List(5)="4,7,9"

Numbers(4)==2>1, Numbers(7)==2>1,  Numbers(9)==3>1,  so we are removing this list, new numbers:

Numbers: 1, 2, 1, 1, 1, 3, 1, 1, 2

Lists: 1, 2, 4

 

This, however, does not guarantee that it's a minimum amount of lists, but entirely superfluous lists would be removed, and all possible numbers would be present (have at least one reference in a number list).

 

Another way I thought it could be resolved is by transposing the lists into numbers like this:


Number(1)=$lb(2)
Number(2)=$lb(2, 4)
Number(3)=$lb(1)
Number(4)=$lb(4, 5)
Number(5)=$lb(1, 3)
Number(6)=$lb(1, 2, 4)
Number(7)=$lb(1, 5)
Number(8)=$lb(3, 4)
Number(9)=$lb(1, 2, 3, 5)


After that is done, any number with just one reference must be picked (meaning it's present in only one list). In our case, numbers 1 and 3, resulting in picking lists 2 and 1.


All numbers in lists 1 and 2 must also be picked: 1, 2, 3, 5, 6, 7, 9


Next, we delete Numbers that we already picked, leaving us with:

Number(4)=$lb(4, 5)
Number(8)=$lb(3, 4)

From the remaining Numbers, we need to remove lists that we already picked (so 1 and 2), but in your example, they are not present anyway.


However, after this cleanup, we might encounter a situation where a number is present in only one list. In that case, the first step needs to be redone again until we arrive at a situation where no number is present only in one list, so in our case:


Number(4)=$lb(4, 5)
Number(8)=$lb(3, 4)


After that, pick a list with the largest amount of different numbers - 4 in our case and repeat from the beginning. Eventually, you'll arrive at empty Numbers local, meaning the task is complete.

Eduard Lebedyuk · Apr 14, 2025 go to post

Starting from 2024.1 IRIS Native disallows routine invocations. Please use class methods instead. 

For reference, these changes can be identified as DP-422635 and DP-424156.

Eduard Lebedyuk · Mar 10, 2025 go to post

In the main method of EnsLib.HTTP.OutboundAdapter - SendFormDataArray line 5 you can see the following code:

#; Create an Http Request Object
Set tHttpRequest=$S($$$IsdefObject(pHttpRequestIn):pHttpRequestIn,1:##class(%Net.HttpRequest).%New())  $$$ASSERT($IsObject(tHttpRequest)&&tHttpRequest.%IsA("%Net.HttpRequest"))

Which creates a new empty %Net.HttpRequest object, unless pHttpRequestIn (3rd arg) has been passed with a custom request object. Most wrapper methods (VERB, VERBURL and VERBFormDataArray) do not pass pHttpRequestIn so you should get a fresh request object every time, but SendFormData and SendFormDataURL would pass pHttpRequestIn  from caller, if given.

Another place to look into is a Send call:

Set tSC=tHttpRequest.Send($ZCVT(pOp,"U"),$G(pURL,..URL),..#DEBUG)

It has the following signature:

Method Send(type As %String, location As %String, test As %Integer = 0, reset As %Boolean = 1) As %Status

reset arg defaults to 1, and when it's true the Reset method of %Net.HttpRequest is called after every request. Reset method removes headers (among other things). As you can see in the implementation:

Kill i%Headers,i%FormData,i%Params

EnsLib.HTTP.OutboundAdapter never calls Send with reset=0, so the request should be reset every time.

That said, how do you call SendFormDataArray?

I took a look at the article you linked to. Can you add logging to GetRequest method? It should be called on every business operation message.