Timothy Leavitt · Feb 3, 2020 go to post

@Enrico Parisi - great catch, thank you! I've updated the article to avoid spreading misinformation. smiley

This highlights an interesting general point about error handling - you're much more likely to have an undetected bug in code that only runs in edge cases that you haven't tested. Measuring test coverage to close the feedback loop on unit test quality is a great way to highlight these areas. (I'll be writing up a post about that soon.)

Timothy Leavitt · Jan 15, 2020 go to post

How about (swapping in ^oddDEF if you don't care if it's been compiled):

ClassMethod PackageExists(package) As %Boolean
{
    Set prefix = package_"."
    Set firstClass = $Order(^oddCOM(prefix))
    Quit prefix = $Extract(firstClass,1,$Length(prefix))
}
Timothy Leavitt · Jan 15, 2020 go to post

That's interesting. I think it would really be:

If (^$LOCK("^MyGlobal(42)","OWNER") = $Job) {
    Lock -^MyGlobal(42)
}
Timothy Leavitt · Jan 15, 2020 go to post

1. Suppose $TLevel > (tInitTLevel + 1). That means that someone else's transaction was left open. You can't always guarantee that the code you're calling will behave by matching tstart with tcommit or trollback 1, but you can account for the possibility of your dependency misbehaving in your own transaction cleanup. Agreed on never using argumentless trollback.

2. Great point - updated accordingly.

Timothy Leavitt · Dec 16, 2019 go to post

I think the answers so far have missed the point. The number of arguments itself is variable. This is handy for things like building a complex SQL statement and set of arguments to pass to %SQL.Statement:%Execute, for example.

The data structure here is an integer-subscripted array with the top node set to the number of elements. (The top node is what's missing in the example above). Subscripts can be missing to leave the argument at that position undefined.

Here's a simple example:

Class DC.Demo.VarArgs
{

ClassMethod Driver()
{
    Do ..PrintN(1,2,3)
    
    Write !!
    For i=1:1:4 {
        Set arg($i(arg)) = i
    }
    Kill arg(3)
    ZWrite arg
    Write !
    Do ..PrintN(arg...)
}

ClassMethod PrintN(pArgs...)
{
    For i=1:1:$Get(pArgs) {
        Write $Get(pArgs(i),"<undefined>"),!
    }
}

}

Output is:

d ##class(DC.Demo.VarArgs).Driver()
1
2
3
arg=4
arg(1)=1
arg(2)=2
arg(4)=4
1
2
<undefined>
4
Timothy Leavitt · Dec 16, 2019 go to post

For bootstrap-table, I think the examples on their site are probably more useful than anything I could dig up. https://examples.bootstrap-table.com/#welcomes/large-data.html shows pretty good performance for a large dataset. Tabulator looks nice too though.

In any case it would probably be cleanest to load data via REST rather than rendering everything in the page in an HTML table and then using a library to make the table pretty.

Timothy Leavitt · Dec 12, 2019 go to post

Another option rather than having two versions of the whole codebase could be having a wrapper module around webterminal (i.e., another module that depends on webterminal) with hooks in webterminal to allow that wrapper to turn off projection-based installation-related features.

Timothy Leavitt · Dec 12, 2019 go to post

From the pros/cons, it seems the objectives are:

  • Maintain compatibility with normal installation (without ZPM)
  • Make side effects from installation/uninstallation auditable by putting them in module.xml

I'd suggest as one approach to accomplish both objectives:

  • Suppress the projection side effects when running in a package manager installation/uninstallation context (either by checking $STACK or using some trickier under-the-hood things with singletons from the package manager - regardless, be sure to unit test this behavior!).
  • Add "Resource Processor" classes (specified in module.xml with Preload="true" and not included in normal WebTerminal XML exports used for non-ZPM installation) - that is, classes extending %ZPM.PackageManager.Developer.Processor.Abstract and overriding the appropriate methods - to handle your custom installation things. You can then use these in your module manifest, provided that such inversion of control still works without bootstrapping issues following changes made in https://github.com/intersystems-community/zpm.
    • Generally-useful things like creating a %All namespace should probably be pushed back to zpm itself.
Timothy Leavitt · Nov 21, 2019 go to post

This is nifty! Note, you can make the extent manager happy by using:


Class DC.Demo.SometimesPersistent Extends %Persistent
{

Property Foo As %String;

ClassMethod Demo()
{
    New %storage,%fooD,%fooI,%fooS
    Set obj = ##class(DC.Demo.SometimesPersistent).%New()
    Set obj.Foo = "bar"
    Set %storage = "%foo"
    Write !,obj.%Save()
    Kill obj
    Set obj = ..%OpenId(1)
    w ! zw obj
    zw %fooD
}

Storage Default
{
<Data name="SometimesPersistentDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Foo</Value>
</Value>
</Data>
<DataLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</DataLocation>
<DefaultData>SometimesPersistentDefaultData</DefaultData>
<IdLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"D")</IdLocation>
<IndexLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"I")</IndexLocation>
<StreamLocation>@($Get(%storage,"^DC.Demo.SometimesPersistent")_"S")</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}
Timothy Leavitt · Oct 21, 2019 go to post

Hi Steve,

We (Application Services - internal applications @ InterSystems) use %UnitTest with some extensions. We have a base Unit Test case that has a "run this test" helper method (among other things), a wrapper around %UnitTest.Manager that makes it easier to run all the tests without deleting them (and with some other features optionally enabled, like test coverage measurement - see a video of my 2018 Global Summit presentation on this here). Our wrapper also loads all of the unit tests before running any, which allows unit tests to extend classes within the unit test root in different packages from their own.

I agree with @Eduard Lebedyuk that %UnitTest meets our needs well aside from the lack of parallelization. A parallel %UnitTest runner would be an interesting project indeed...

We routinely run unit tests via Jenkins CI, report test results in the jUnit format, and also report on code coverage (Cobertura-style) and a complexity/coverage scatter plot.

For automated UI testing, Selenium/Cucumber has worked well for older Zen/CSP UIs. True unit testing of newer UIs (e.g,. Jasmine and Karma for Angular) is handy too.

Timothy Leavitt · Oct 18, 2019 go to post

Disclaimer: I know more about what John is doing than is covered in the post.

It looks like, for the prebuilt themes, Angular Material itself uses Bazel (see introduction at https://angular.io/guide/bazel). The relevant bits are here:

https://github.com/angular/components/blob/master/src/material/prebuilt-themes/BUILD.bazel
https://github.com/angular/components/blob/master/src/material/core/BUILD.bazel
https://github.com/angular/components/blob/master/src/material/BUILD.bazel

I think that's probably the place to start, in terms of Angular 8 best practices.

Timothy Leavitt · Sep 5, 2019 go to post

Other answers are good. I'll also add, if you're running from Terminal, you can go to File > Logging... and have all the output logged to a file. (This works for other things that produce output too, not just CompileAll.)

Timothy Leavitt · Sep 5, 2019 go to post

Fun fact, you can also order by the column by number.

In this case:

select top 5 Description,Category,* from Cinema.Film order by 2

Timothy Leavitt · Sep 4, 2019 go to post

With correct web server configuration to route everything through the CSPGateway, the above example should handle URLs for other resources like that without issue (as long as the content served by the dashboard server has relative links to those endpoints, not absolute) - that's the point of the <base> element.

In my sample use case, it also handles several requests for images and other assets.

Timothy Leavitt · Sep 4, 2019 go to post

I don't think it's totally stupid. We're looking at doing something similar with another technology (JReport).

Here's a code sample to help you get started - just change the port / page / filters / permission checks appropriately.

Class Demo.CSPProxy Extends %CSP.Page
{

Parameter HOST = "127.0.0.1";

Parameter PORT = 8888;

Parameter PAGE = "jinfonet/runReport.jsp";

/// Event handler for <b>PreHTTP</b> event: this is invoked before
/// the HTTP headers for a CSP page have been sent.  All changes to the
/// <class>%CSP.Response</class> class, such as adding cookies, HTTP headers,
/// setting the content type etc. must be made from within the OnPreHTTP() method.
/// Also changes to the state of the CSP application such as changing
/// %session.EndSession or %session.AppTimeout must be made within the OnPreHTTP() method.
/// It is prefered that changes to %session.Preserve are also made in the OnPreHTTP() method
/// as this is more efficient, although it is supported in any section of the page.
/// Return <b>0</b> to prevent <method>OnPage</method> from being called.
ClassMethod OnPreHTTP() As %Boolean [ ServerOnly = 1 ]
{
    Set request = ##class(%Net.HttpRequest).%New()
    Set request.Server = ..#HOST
    Set request.Port = ..#PORT
    
    // TODO: Add other stuff here, like authentication.
    
    Set page = $Piece(%request.CgiEnvs("REQUEST_URI"),"/"_$classname()_".cls/",2)
    If (page = "") {
        Set %base = $classname()_".cls/"_$Piece(..#PAGE,"/",1,*-1)_"/"
        
        // TODO: add query parameters from %request to the URL requested below.
        $$$ThrowOnError(request.Get(..#PAGE))
    } Else {
        Set fullPage = "http://"_..#HOST_":"_..#PORT_"/"_page
        Do ##class(%Net.URLParser).Parse(fullPage,.parts)
        
        // TODO: Better way of checking the requested resource.
        If $Piece($Piece(parts("path"),"/",*),".",2) = "jsp" {
            Set %response.Status = ##class(%CSP.REST).#HTTP403FORBIDDEN
            Quit 0
        }
        $$$ThrowOnError(request.Send(%request.Method,page))
    }
    Set %data = request.HttpResponse.Data
        
    // TODO: Do any other headers matter?
        
    Set %response.Status = request.HttpResponse.StatusCode
    Set %response.ContentType = request.HttpResponse.ContentType
    Quit 1
}

/// Event handler for <b>PAGE</b> event: this is invoked in order to  
/// generate the content of a csp page.
ClassMethod OnPage() As %Status [ ServerOnly = 1 ]
{
    If (%response.ContentType [ "html") && $Data(%base) {
        &html<<base href="#(..EscapeHTML(%base))#">>
        Do %data.OutputToDevice()
    } Else {
        Do %data.OutputToDevice()
    }
    Quit $$$OK
}

}
Timothy Leavitt · Apr 1, 2019 go to post

A few weird things with custom login pages:

CSPSystem needs read permission on the DB containing them. (You'll need to close CSPGateway connections after making this change so that it can reconnect with the right privileges.)

It's specified with the ".cls" extension in the web application configuration, not just the classname.

Timothy Leavitt · Mar 12, 2019 go to post

In my experience, code coverage measurement is useful even in development. It helps identify areas that are untested as you're writing tests.

Timothy Leavitt · Mar 8, 2019 go to post

Hi Robert,

Access rights are part of my concern. You can connect using an SSL/TLS configuration without having read permission on %DB_CACHESYS (or the IRIS equivalent).

Timothy Leavitt · Jan 14, 2019 go to post

This sounds like it could be an issue with caching (at the browser level rather than at the CSPGateway level, given that the page loads just fine in a different browser). Perhaps try clearing the cache in the Android browser then reloading the page?

Timothy Leavitt · Jan 3, 2019 go to post

Generally speaking, I'd design this as a relationship to a set of "roles" that can be added or removed. (What if the person ceases to be a Doctor?) Methods of the person would dispatch to the roles.

Timothy Leavitt · Jan 3, 2019 go to post

"Why?" (a good question) and "Don't do that!" aside, here's a technical answer: override %OnDetermineClass to allow instances of the parent class to be treated as the subclass in question. For example:

Class DC.Demo.Extension.BaseRecord Extends %Persistent
{

Property Name As %String;

ClassMethod %OnDetermineClass(oid As %ObjectIdentity, ByRef class As %String) As %Status [ ServerOnly = 1 ]
{
  Set tSC = ##super(oid,.class)
  If (class = "DC.Demo.Extension.BaseRecord") {
    Set class = $classname()
  }
  Quit tSC
}

Storage Default
{
<Data name="BaseRecordDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^DC.Demo.Extension.BaseRecordD</DataLocation>
<DefaultData>BaseRecordDefaultData</DefaultData>
<IdLocation>^DC.Demo.Extension.BaseRecordD</IdLocation>
<IndexLocation>^DC.Demo.Extension.BaseRecordI</IndexLocation>
<StreamLocation>^DC.Demo.Extension.BaseRecordS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.SubRecord Extends BaseRecord
{

Property Foo As %String;

Storage Default
{
<Data name="SubRecordDefaultData">
<Subscript>"SubRecord"</Subscript>
<Value name="1">
<Value>Foo</Value>
</Value>
</Data>
<DefaultData>SubRecordDefaultData</DefaultData>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.Driver
{

ClassMethod Run()
{
  Do ##class(DC.Demo.Extension.BaseRecord).%KillExtent()

  Set tBaseRecord = ##class(DC.Demo.Extension.BaseRecord).%New()
  Set tBaseRecord.Name = "Fred"
  Write !,"Save base record: ",tBaseRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD

  Set tSubRecord = ##class(DC.Demo.Extension.SubRecord).%OpenId(1)
  Set tSubRecord.Foo = "Bar"
  Write !,"Open as sub record, name = ",tSubRecord.Name
  Write !,"Save sub record (converts to sub record): ",tSubRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD
}

}

Output is:

USER>d ##class(DC.Demo.Extension.Driver).Run()
 
Save base record: 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("","Fred")
 
Open as sub record, name = Fred
Save sub record (converts to sub record): 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("~DC.Demo.Extension.SubRecord~","Fred")
^DC.Demo.Extension.BaseRecordD(1,"SubRecord")=$lb("Bar")
Timothy Leavitt · Jan 2, 2019 go to post

My top place by day was 242 on day 8 (that was the only day that I actually did the puzzle at midnight though - I'm not at all a night owl).

My three wishes for ObjectScript would be:

  1. A greater variety of high-performance data structures with low-level support (rather than shoehorning everything into a local array/PPG/$ListBuild list to keep it wicked fast, or building more expensive objects that do exactly what I want)
  2. Libraries for working with those (and existing) data structures (or, better yet, an OO approach to everything, similar to %Library.DynamicArray/%Library.DynamicObject)
  3. Functional programming capabilities
Timothy Leavitt · Nov 16, 2018 go to post

The %System/%System/RoutineChange audit event is the only thing other than a source control hook that comes to mind. That's handy for detecting source code changes after the fact (when a class is compiled), but not intercepting attempts to change source code via an editor. What's your specific use case?

Timothy Leavitt · Jul 30, 2018 go to post

This isn't the answer to your actual question, but it's worth pointing out ##class(%Library.File).ManagerDirectory()  - instead of referencing the global, you could use:

##class(%Library.File).ManagerDirectory()_"LDAPKeyStore/"

Also, ##class(%SYS.System).GetInstanceName() returns the instance name; you shouldn't need to set that in a global, and (furthermore) the instance name isn't necessarily part of the path to the mgr directory (the two can be configured independently).

Other than that I think you'd be stuck with using XECUTE or $Xecute to do what you originally suggested.

Timothy Leavitt · Jul 12, 2018 go to post

After further review, I'm really not sure why the first/second queries don't use the index. The problem with the last query is that %Key is the index in the list, not anything about Tag itself.

Here's a solution that performs well in my testing:

Class DC.Demo.Tag Extends (%Persistent, %Populate)
{

Index Tag On Tag [ Unique ];

Property Tag As %String;

}

Class DC.Demo.Tagged Extends (%Persistent, %Populate)
{

Relationship HasTags As DC.Demo.HasTag [ Cardinality = children, Inverse = Tagged ];

ClassMethod Run()
{
    Do ..%KillExtent()
    Do ##class(DC.Demo.Tag).%KillExtent()
    
    Do ##class(DC.Demo.Tag).Populate(50)
    Do ..Populate(5000)
    Do ##class(DC.Demo.HasTag).Populate(10000)
}

}

Class DC.Demo.HasTag Extends (%Persistent, %Populate)
{

Relationship Tagged As DC.Demo.Tagged [ Cardinality = parent, Inverse = HasTags ];

Property Tag As DC.Demo.Tag [ Required ];

Index UniqueTag On Tag [ IdKey ];

Index TaggedByTag On (Tag, Tagged);

}