Eduard Lebedyuk · Feb 16, 2017 go to post

Filename is appended to directory, so if you have a directory: /tmp/ you can call

set tSC = ..Adapter.PutString("old/plan.txt",output)
set tSC = ..Adapter.PutString("new/plan.txt",output)

and it should produce two files /tmp/old/plan.txt and /tmp/new/plan.txt in two different subdirectories. of /tmp

Eduard Lebedyuk · Feb 16, 2017 go to post

The benefits of using list of %String  over just %String are:

  • You can easily index individual values
  • You can project and access values as a separate table

I actually had a use case for exactly this data. Requirements were:

  • There are alerts and users
  • Alerts are sent once an hour
  • Each alert is sent to multiple users
  • Each user should receive only one digest email with all relevant alerts

List of %String was extremely useful in this case. First I wrote a temp table:

/// Store alerts (for current process and runtime-only)
Class Util.Alert Extends %Persistent
{

/// Alert topic
Property topic As %String(MAXLEN = 1000) [ Required ];

/// Alert text
Property text As %String(MAXLEN = "");

/// Recipients
Property emails As list Of %String(SQLPROJECTION = "table/column", STORAGEDEFAULT = "array");

/// Index
Index emailsIndex On emails(ELEMENTS);

/// Add one Alert
/// w ##class(Util.Alert).add("Error", $lb("1@1.com","2@2.com"), "text")
ClassMethod add(topic As %String, text As %String, emails As %List) As %Status
{
    set obj = ..%New()
    set obj.topic = topic
    set obj.text = text
    
    if $listvalid(emails) {
        for i=1:1:$ll(emails){
            do obj.emails.Insert($lg(emails,i))
        }
    }
    return obj.%Save()
}

}

Next I wrote a business service which searched for alerts and added them to this table (not relevant for this discussion). And then once all alerts for an hour are in the Util.Alert table, sendEmails method can easily send alerts in digest mode:

/// Generate emails from alerts and send them
ClassMethod sendEmails()
{
    &sql(DECLARE C1 CURSOR FOR
         SELECT DISTINCT emails  
         INTO :email
         FROM Util.Alert
    )
 
    &sql(OPEN C1)
    &sql(FETCH C1)

    While (SQLCODE = 0) {
        set text = ..generateEmailText(email)
        do ..SendEmail(email, text)
        &sql(FETCH C1)
    }
    &sql(CLOSE C1)
}

/// Create email with all alerts for user
ClassMethod generateEmailText(email As %String) As %String
{
    set emailText = ""
    &sql(DECLARE C2 CURSOR FOR
         SELECT topic, text  
         INTO :topic, :text
         FROM Util.Alert
         WHERE FOR SOME %ELEMENT(emails) (%VALUE=:email)
    )

    &sql(OPEN C2)
    &sql(FETCH C2)

    While (SQLCODE = 0) {        
        set emailText = emailText _  topic _ ": " _ text _ $$$NL
         &sql(FETCH C2)
    }
    &sql(CLOSE C2)
    
    return emailText
}

Without list of %String SQL here would be far harder to write or slower.

Eduard Lebedyuk · Feb 14, 2017 go to post

First run:

csession CACHE

Then:

w ##class(%SYS.System).InstanceGUID() 

As %SYS is a % package it is available in every namespace.

Eduard Lebedyuk · Feb 10, 2017 go to post

Static files were edited outside of Studio.

Setting timeout to 0 and purging the cache once solved the problem.

Eduard Lebedyuk · Feb 10, 2017 go to post

Reading %CSP.StreamServer makes me think that I need to set "Set Files Timeout" to 0.

Testing that.

Eduard Lebedyuk · Feb 8, 2017 go to post

Workflow REST API is a separate project available on GitHub. You can install it on any version that supports REST, so Caché 2014.1+.

Eduard Lebedyuk · Feb 6, 2017 go to post

What is a "Business Server"?

You shouldn't specify Dispatch Class -it's for REST only.

Maybe add ?CfgItem=Weather to URL.

EnsLib.SOAP.GenericService is a generic service and it does not implement actual services, such as weather but send the request to actual business host -  "Weather BO" in your case.

Your original URL is correct as it references business host name instead of class name

Eduard Lebedyuk · Feb 5, 2017 go to post

jQuery is a library - a big collection of "classes" and methods you can use anywhere. It makes no assumptions about how your application is built, has no internal DOM tree representation, etc.

Angular JS is an MVC framework - it forces your application to be structured in a very specific way, it manipulates DOM tree of the page according to specified rules. And well, it also provides a big collection of "classes" and methods you can use in your the code, but only the in parts you wrote in compliance with Angular architecture (scopes/controllers/services).

Eduard Lebedyuk · Feb 4, 2017 go to post

Please consider removing or at the very least modifying the description of:

  intersystems-ru~REST         0.8.0-a  Basic classes for REST web API on InterSystems Cache

It was only relevant with 2014-2015 REST and does not represent best practices for REST APIs in  2016.1+.

Eduard Lebedyuk · Feb 4, 2017 go to post

There are some problems with many namespace. I found that after I hit about a hundred namespaces on an instance it's time to purge. 50 seems to be the upper limit of easily manageable for me tbh. More, and scrolling in Studio/SMP takes too much time.

Eduard Lebedyuk · Feb 4, 2017 go to post

Do you have a call to CreateBusinessService somewhere in your code? It allows doing business service calls from non-Ensebble context (by well making current job Ensemble job). It's called like this (from Demo.ZenService.Zen.WeatherReportForm):
 

Set tSC=##class(Ens.Director).CreateBusinessService("Zen Service",.tService)

Where first arguments is a host name of a business service. In your case the error appeared because the first argument is equal to EnsLib.SOAP.GenericService which is not present in currently running Ensemble production. You need to do one of the:

  • Check that you're running a correct production
  • Change first argument of CreateBusinessService method from EnsLib.SOAP.GenericService to something that exists in current production
  • Create business service in production with the name EnsLib.SOAP.GenericService
Eduard Lebedyuk · Feb 4, 2017 go to post

I'd like to add some commentary to Angular 1/2 comparison.

Angular 1:

- easier to hook to cache (can be used by calling the library from a csp and calling services cache rest)

That's same as with Angular 2.

- flatter learning curve if jquery is already known

- being so similar to jquery

jQuery has little in common with Angular. Angular (1 or 2) is an MVC framework and jQuery is the utilities library.

- not needed for really small projects

Yes, for both Angular 1 and 2.

- easier to maintain, perhaps, for someone who knows web, html and jquery

Highly debatable. Learning curve for Angular 1 is fairly steep.

Angular 2.

- does not work in browsers older than IE9

Angular 1 also does not work in browsers older than IE9

- keeps changing, it is not completely stable yet?

It has a stable version for a few months already.

Now, with that said, I'd like to recommend Angular 2. The main advantage in my opinion is that many fundamental problems present in Angular 1 were solved in Angular 2. Here's some links elaborating on differences between Angular 1 and 2:

  • Short comparison of differences between Angular 1 and 2 is available here.
  • More detailed comparison is available here.
Eduard Lebedyuk · Feb 2, 2017 go to post

That is almost what I started this topic with. I get a compilation error with this code. And fields data_Year and data_Month are not getting created it seems.

Eduard Lebedyuk · Feb 2, 2017 go to post

Thank you!

I copied your example, but SQL returns results saved on disk, which is expected as the data property is triggered computed

Class CS.Persistent Extends %Persistent
{

Property data As CS.Serial [ SqlComputeCode = {set {*} = ##class(CS.Persistent).dataGetStatic()}, SqlComputed ];

/// data getter method in SQL context
ClassMethod dataGetStatic() As %List
{
    quit $lb(2017,1)
}

/// do ##class(CS.Persistent).test()
ClassMethod test()
{
    do ..%KillExtent()
    do $system.SQL.PurgeForTable($classname())
    set obj = ##class(CS.Persistent).%New()
    set obj.data.Month=-1
    set obj.data.Year=0
    set sc = obj.%Save()
    write !,"Save: ",$s($$$ISOK(sc):"OK", 1:$System.Status.GetErrorText(sc)),!
    zw ^CS.PersistentD
    do ##class(%SQL.Statement).%ExecDirect(,"select * from "_$classname()).%Display()
}
}

I execute in a terminal:

do ##class(CS.Persistent).test()

And I receive the following output:

Save: OK
^CS.PersistentD=1
^CS.PersistentD(1)=$lb("",$lb(0,-1))
ID      data_Month      data_Year
1       -1              0

Can I make a serial class always computed?

I want to receive the following output:

Save: OK
^CS.PersistentD=1
^CS.PersistentD(1)=$lb("",$lb(0,-1))
ID      data_Month      data_Year
1       1               2017