Timothy Leavitt · Jan 12, 2021 go to post

Here's a quick sample:

Class DC.Demo.XDataDemo
{

ClassMethod Driver()
{
    Set array = ..GetXDataContents("DC.Demo")
    zw array
}

ClassMethod GetXDataContents(package As %String) As %Library.ArrayOfObjects
{
    $$$ThrowOnError($System.OBJ.GetPackageList(.classes,package))
    Set array = ##class(%Library.ArrayOfObjects).%New()
    Set class = ""
    For {
        Set class = $Order(classes(class))
        Quit:class=""
        Set xDataName = ""
        For {
            Set xDataName = $$$defMemberNext(class,$$$cCLASSxdata,xDataName)
            Quit:xDataName=""
            Set xdata = ##class(%Dictionary.XDataDefinition).IDKEYOpen(class,xDataName,,.sc)
            $$$ThrowOnError(sc)
            Do array.SetAt(xdata.Data,class_":"_xDataName)
        }
    }
    Quit array
}

XData Foo
{
}

XData Bar
{
}

}

Note that this gets XData blocks defined in a class; if you want to get "inherited" XData blocks listed for each subclass along with the inherited content it's only slightly more complex:

ClassMethod GetXDataContents(package As %String) As %Library.ArrayOfObjects
{
    $$$ThrowOnError($System.OBJ.GetPackageList(.classes,package))
    Set array = ##class(%Library.ArrayOfObjects).%New()
    Set class = ""
    For {
        Set class = $Order(classes(class))
        Quit:class=""
        Set xDataName = ""
        For {
            Set xDataName = $$$comMemberNext(class,$$$cCLASSxdata,xDataName)
            Quit:xDataName=""
            Set origin = $$$comMemberKeyGet(class,$$$cCLASSxdata,xDataName,$$$cXDATAorigin)
            Set xdata = ##class(%Dictionary.XDataDefinition).IDKEYOpen(origin,xDataName,,.sc)
            $$$ThrowOnError(sc)
            Do array.SetAt(xdata.Data,class_":"_xDataName)
        }
    }
    Quit array
}
Timothy Leavitt · Jan 11, 2021 go to post

I was going through and looking at my old questions without accepted answers - this is the right answer but a SAMPLES namespace is harder to come by these days, so posting the actual code for my own future reference:

ClassMethod PersonSets(name As %String = "", state As %String = "MA") As %Integer [ ReturnResultsets, SqlName = PersonSets, SqlProc ]
{
        // %sqlcontext is automatically created for a method that defines SQLPROC

        // SQL result set classes can be easily prepared using dynamic SQL. %Prepare returns a
        // status value. The statement's prepare() method can also be called directly. prepare() throws
        // an exception if something goes wrong instead of returning a status value.
    set tStatement = ##class(%SQL.Statement).%New()
    try {
        do tStatement.prepare("select name,dob,spouse from sample.person where name %STARTSWITH ? order by 1")
        set tResult = tStatement.%Execute(name)
        do %sqlcontext.AddResultSet(tResult)
        do tStatement.prepare("select name,age,home_city,home_state from sample.person where home_state = ? order by 4, 1")
        set tResult = tStatement.%Execute(state)
        do %sqlcontext.AddResultSet(tResult)
        set tReturn = 1
    }
    catch tException {
        #dim tException as %Exception.AbstractException
        set %sqlcontext.%SQLCODE = tException.AsSQLCODE(), %sqlcontext.%Message = tException.SQLMessageString()
        set tReturn = 0
    }
    quit tReturn
}
Timothy Leavitt · Jan 11, 2021 go to post

HS.JSON.Path is intended for use with dynamic objects. With a dynamic object it'd look like (for example):

SAMPLE>set obj = {"destination":{"endpoint":"foo"}}
SAMPLE>w ##class(HS.JSON.Path).%Evaluate(obj,"$.destination.endpoint").%Get(0)
foo

Also, this should probably be a question rather than an announcement.

Timothy Leavitt · Jan 8, 2021 go to post

Here's an updated example for that. I've also updated the example to use %SQL.Statement, which is newer and better than %ResultSet; I didn't realize it would work with OnCreateResultSet initially.

Class DC.Demo.ZenPage Extends %ZEN.Component.page
{

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<fieldSet legend="Filter" layout="horizontal">
<text label="Name Starts With:" onchange="zen('myTable').setProperty('parameters',1,zenThis.getValue())" />
<dateText label="Start Date:" onchange="zen('myTable').setProperty('parameters',2,zenThis.getValue())" />
<dateText label="End Date:" onchange="zen('myTable').setProperty('parameters',3,zenThis.getValue())" />
</fieldSet>
<tablePane id="myTable" OnCreateResultSet="CreateResultSet">
<parameter value="" />
<parameter value="" />
<parameter value="" />
</tablePane>
<button onclick="zenPage.Populate()" caption="Repopulate Data" />
</page>
}

ClassMethod CreateResultSet(Output pSC As %Status, pInfo As %ZEN.Auxiliary.QueryInfo) As %SQL.Statement
{
    Set nameFilter = pInfo.parms(1)
    Set startDateFilter = pInfo.parms(2) // Will be in ODBC format
    Set endDateFilter = pInfo.parms(3) // Will be in ODBC format
    Set query = ##class(%SQL.Statement).%New()
    Set query.%SelectMode = 1
    Set sql = "select Name, SomeDate from DC_Demo.SampleData"
    Set conditions = ""
    If (nameFilter '= "") {
        Set conditions = conditions_$ListBuild("Name %STARTSWITH ?")
        Set parameters($i(parameters)) = nameFilter
    }
    If (startDateFilter '= "") && (endDateFilter '= "") {
        // Yes, this could just be independent AND'ed conditions on start/end date,
        // which would reduce code complexity, but you wanted to see BETWEEN, so... :)
        Set conditions = conditions_$ListBuild("SomeDate BETWEEN ? and ?")
        Set parameters($i(parameters)) = startDateFilter
        Set parameters($i(parameters)) = endDateFilter
    } ElseIf (startDateFilter '= "") {
        Set conditions = conditions_$ListBuild("SomeDate >= ?")
        Set parameters($i(parameters)) = startDateFilter
    } ElseIf (endDateFilter '= "") {
        Set conditions = conditions_$ListBuild("SomeDate <= ?")
        Set parameters($i(parameters)) = endDateFilter
    }
    If (conditions '= "") {
        Set sql = sql _ " where "_$ListToString(conditions," and ")
    }
    Set pSC = query.%Prepare(sql)
    If $$$ISERR(pSC) {
        Quit $$$NULLOREF
    }
    
    //Important: Reduce to only the parameters specified/used.
    Kill pInfo.parms
    Merge pInfo.parms = parameters
    Quit query
}

ClassMethod Populate() [ ZenMethod ]
{
    Do ##class(DC.Demo.SampleData).%KillExtent()
    Do ##class(DC.Demo.SampleData).Populate(20,,,,0)
    &js<zen('myTable').executeQuery();>
}

}

Note - I got something slightly wrong in the original example. The responsibility of OnCreateResultSet is just to create the ResultSet, not to execute the query. Most importantly, the parameters in the QueryInfo object need to be reset to just the subset used in our method of generating the query; otherwise, the original parms array would be used to execute the query, which could produce incorrect results (but didn't happen to in the simpler example). An alternative approach would be adding a server-side callback for ExecuteResultSet as well, but in this case it's simpler to just manipulate the parms array.

Timothy Leavitt · Jan 8, 2021 go to post

Also, @ED Coder , it's perhaps worth stepping back and asking the context of your work. If you're looking to build a brand new application of any real complexity, Zen probably isn't the best approach these days. I really enjoy Zen and have spent many years working with it, but starting on a new project I'd probably look toward a popular client-side framework like Angular or React and using REST APIs to interact with the database (probably using https://openexchange.intersystems.com/package/apps-rest to match the rapid development features of Zen in a REST API context).

If you're jumping into a large, existing Zen application and trying to make sense of the technology, that's a lot more understandable. Just curious. smiley

Timothy Leavitt · Jan 8, 2021 go to post

A ZenMethod is written in ObjectScript, not JavaScript, so that's why getValue() doesn't work. You can only access page components in a ClassMethod if you pass them in as arguments; otherwise, you need to make the method an instance method (that is, just Method readValues). Then you can use:

Set clinic = ..%GetComponentById('clinic').value

If you do need to do things in JavaScript in a ZenMethod, you can do them in an &js block - e.g.:

&js<alert(#(..QuoteJS(clinic))#);>

Which, in combination with the line above, would safely quote the string "clinic" for use in JavaScript (e.g., escaping quotes within the string), and this JavaScript will run on the clientafter the method returns. (That is, it isn't immediate; if you had a "hang 5" command after the &js block you wouldn't see an alert on the client until after the method ends.)

Instance methods in Zen are expensive because the whole page needs to be serialized and sent to the server (so that you can access and potentially modify all the components on the page). However, your method signature suggests that it's expecting a Zen proxyObject. You could build a Zen proxyObject with all of the form field values you care about in a JavaScript method on the client, and send that to the server by passing it to your ClassMethod, and that would be more efficient (if all you want to do is retrieve data from the form).

Timothy Leavitt · Jan 7, 2021 go to post

This also demonstrates how to securely add multiple filters to a query - using the ? syntax, *not* just concatenating strings in directly. The "variadic arguments" syntax where you pass in an integer-subscripted array to be any number of arguments (including 0) is super handy for this.

Timothy Leavitt · Jan 7, 2021 go to post

I generally don't use OnCreateResultSet, but here's a sample with it:

Class DC.Demo.ZenPage Extends %ZEN.Component.page
{

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<fieldSet legend="Filter" layout="horizontal">
<text label="Name Starts With:" onchange="zen('myTable').setProperty('parameters',1,zenThis.getValue())" />
<dateText label="Date:" onchange="zen('myTable').setProperty('parameters',2,zenThis.getValue())" />
<button onclick="zen('myTable').executeQuery()" caption="Filter" />
</fieldSet>
<tablePane id="myTable" OnCreateResultSet="CreateResultSet">
<parameter value="" />
<parameter value="" />
</tablePane>
<button onclick="zenPage.Populate()" caption="Repopulate Data" />
</page>
}

ClassMethod CreateResultSet(Output pSC As %Status, pInfo As %ZEN.Auxiliary.QueryInfo) As %ResultSet
{
    Set nameFilter = pInfo.parms(1)
    Set dateFilter = pInfo.parms(2) // Will be in ODBC format
    Set query = ##class(%ResultSet).%New()
    Set query.RuntimeMode = 1 // ODBC
    Set sql = "select Name, SomeDate from DC_Demo.SampleData"
    Set conditions = ""
    If (nameFilter '= "") {
        Set conditions = conditions_$ListBuild("Name %STARTSWITH ?")
        Set parameters($i(parameters)) = nameFilter
    }
    If (dateFilter '= "") {
        Set conditions = conditions_$ListBuild("SomeDate = ?")
        Set parameters($i(parameters)) = dateFilter
    }
    If (conditions '= "") {
        Set sql = sql _ " where "_$ListToString(conditions," and ")
    }
    Set pSC = query.Prepare(sql)
    If $$$ISERR(pSC) {
        Quit $$$NULLOREF
    }
    Set pSC = query.Execute(parameters...)
    If $$$ISERR(pSC) {
        Quit $$$NULLOREF
    }
    Quit query
}

ClassMethod Populate() [ ZenMethod ]
{
    Do ##class(DC.Demo.SampleData).%KillExtent()
    Do ##class(DC.Demo.SampleData).Populate(20,,,,0)
    &js<zen('myTable').executeQuery();>
}

}

And the data behind it (minus storage definition):

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

Property Name As %String;

Property SomeDate As %Date;

}
Timothy Leavitt · Jan 7, 2021 go to post

Note - it's generally better practice to use

zenPage.getComponentById('comboboxEdit').getValue()

or equivalently (and shorter),

zen('comboboxEdit').getValue()
Timothy Leavitt · Nov 16, 2020 go to post

Rather than setting the global, could just Do $SYSTEM.Version.SystemMode(newMode) where newMode is LIVE, TEST, FAILOVER, or DEVELOPMENT (case-insensitive).

Timothy Leavitt · Nov 16, 2020 go to post

re: "creative minds" - been there, done that, should probably put it on the Open Exchange at some point... ;)

Timothy Leavitt · Nov 16, 2020 go to post

From my experience, foreign keys are really underrated/underused among ObjectScript developers. With relationships you don't need to worry about them, but really any time you have an object-valued property (not a relationship) there should almost certainly be a foreign key defined on it. (Same thing goes of course for non-object references to uniquely identifying fields in other tables.)

Timothy Leavitt · Oct 16, 2020 go to post

I'm currently looking in to this and hope to have it fixed in short order. Thank you for letting us know. We apologize for the inconvenience and will let you know once the issue has been addressed.

Timothy Leavitt · Sep 21, 2020 go to post

I already use this approach as much as possible, both for the applications I develop and my Open Exchange projects. It makes it easy to trace from test to tested unit, and (in the community package manager world) it avoids collisions between different packages all trying to use the same unit test package. I strongly agree with Evgeny's recommendation.

Timothy Leavitt · Aug 31, 2020 go to post

FYI, we're looking to add automatic OpenAPI generation to https://github.com/intersystems/apps-rest at some point in the reasonably-near future. (We had an intern work on it over the summer, and are just kicking the tires on it a bit on our own REST models/APIs before unleashing it on the world.)

Timothy Leavitt · Aug 25, 2020 go to post

I'm intrigued to hear about expression indices - sounds really cool.

Without those, another option is just to have a separate class/table. Suppose the key to the AR array is the address type (Home, Office, etc.); then you could have:

Class Sample.Person1 Extends (%Persistent, %Populate)
{

Property Name As %String;

Relationship Addresses As Sample.PersonAddress [ Cardinality = children, Inverse = Person ];

}

Class Sample.PersonAddress Extends (%Persistent, %Populate)
{

Relationship Person As Sample.Person1 [ Cardinality = parent, Inverse = Addresses ];

Property Type As %String;

Property Address As Sample.Address;

}

Sample.PersonAddress then can have whatever plain old normal indices you want (except bitmap indices - if you want those, make it one-to-many instead of parent/child).

Generally: any time you add an array property - especially an array of objects - it's worth stepping back and thinking about whether it should just be its own full-blown class/table.

Timothy Leavitt · Aug 12, 2020 go to post

@Richard Schilke , you should be able to share a session by specifying the same CSP session cookie path for your REST web application and the web application(s) through which your Zen pages are accessed. Alternatively, you could assign the web applications the same GroupById in their web application configuration.

You likely also need to configure your REST handler class (your subclass of AppS.REST.Handler) to use CSP sessions (from your earlier description, I assumed you had). This is done by overriding the UseSession class parameter and setting it to 1 (instead of the default 0).

To reference header data in the UserInfo classmethod, you should just be able to use %request (an instance of %CSP.Request) and %response (an instance of %CSP.Response) as appropriate for request/response headers.

Timothy Leavitt · Aug 12, 2020 go to post

@Richard Schilke - great!

We have support for filtering/sorting on the collection endpoints already, though perhaps not fully documented. Pagination is a challenge from a REST standpoint but I'd love to add support for it (perhaps in conjunction with "advanced search") at some point. I'm certainly open to ideas on the implementation there. :)

Users are the best, because if you don't have them, it's all just pointlessly academic. ;)

Timothy Leavitt · Aug 12, 2020 go to post

@Richard Schilke - on further review, it's an issue with the Action map. See my response in https://github.com/intersystems/apps-rest/issues/7 (and thank you for filing the issue!). I'll still create a new release soon to pick up the projection bug you found.

Regarding headers - you can reference %request anywhere in the REST model classes, it just breaks abstraction a bit. (And for the sake of unit testing, it would be good to behave reasonably if %request happens not to be defined, unless your planning on using Forgery or equivalent.)

Regarding sessions - yes, you can share a session with a Zen application via a common session cookie path or using GroupById. You can reference this as needed as well, though I'd recommend wrapping any %session (or even %request) dependencies in the user context object that gets passed to CheckPermissions().