Timothy Leavitt · Apr 12, 2017 go to post

The problem with using the strategy from that code snippet on breakpoints is that they're separate persistent objects - the approach in the code snippet is to make a temporary change to the object that will be exported, but this doesn't seem to work (after a few attempts with different approaches) for breakpoints without breaking things in Studio.

Having project items sorted alphabetically is helpful to avoid conflicts in source control with multiple people working on the same project at the same time.

Example:

User A adds a file. User B adds a file. User A commits their change. User B goes to commit their change and can't because a conflict is detected due to User A's change to the same line of code (because both added new project items at the end of the list).

Timothy Leavitt · Apr 12, 2017 go to post

This is a great list!

For (10), I always use a global starting with ^mtemp rather than ^debug (for example). By default, ^mtemp is mapped to CACHETEMP. This is handy for a few reasons:

  • Debugging global sets from all namespaces go to the same database.
  • This database is reinitialized each time the system starts, so it's a great place to put data you don't care about losing. (No need to fill/expand databases you do care about with debugging information, especially for the off chance that you accidentally forget to remove the debugging code.)
  • Since CACHETEMP isn't journaled, if the debugging information is logged in a transaction, those global sets won't be rolled back even if/when the transaction is.
  • Since CACHETEMP isn't journaled, if debug code sets a lot of globals, there won't be the extra space taken up by journals. (I once crashed a shared development environment when a process running overnight filled up the disk with journal files from setting the same debug global millions/billions of times.)

I find TDD most useful for cases where there's a simple input and output to/from complex server-side processing. It'd be great to see the unit test tool you built on GitHub/etc.!

One thing missing from your list is code review. This is particularly helpful for making sure you're *actually* doing #5-8. smiley

Timothy Leavitt · Apr 12, 2017 go to post

Some indirect answers that may be useful:

I don't tend to use the call stack view in the Studio debugger. For many cases where stack information is useful for debugging, I'll typically just add this to my code:

do LOG^%ETN 

Then run it, and then look in the application error log (visible in Management Portal at System Operation > System Logs > Application ErrorLog, or via do ^%ER) to see what the stack/variables were when it was called.

Also, you should be able to clear out locks (perhaps cautiously) in the management portal from System Operation > Locks > Manage Locks.

Timothy Leavitt · Apr 12, 2017 go to post

Here's a snippet from my Studio Extension (a subclass of one of the more standard extensions) that you might use and adapt in yours. It deals with a few other things that /diffexport misses - the timestamp, and the order of project items (which will normally have newly-added items at the bottom, out of alphabetical order). Unfortunately, I haven't found a way to handle breakpoints - probably the easiest way to get rid of those would be applying an XSLT to the file after it's been exported, which is pretty bad.

Method OnAfterSave(InternalName As %String, Object As %RegisteredObject) As %Status
{
    Set tFileName = ..ExternalName(InternalName)
    If tFileName = "" {
        Quit $$$OK
    }
    
    Set tName = $Piece(InternalName,".",1,*-1)
    Set tExt = $ZConvert($Piece(InternalName,".",*),"U")
    
    // Special handling for projects to ensure that newly-added items don't show up at the bottom of the XML export.
    // This tends to cause meaningless diffs (at best) and conflicts (at worst)
    If (tExt = "PRJ") {
        Set tProject = ##class(%Studio.Project).%OpenId(tName,,.tSC)
        If $IsObject(tProject) {
            // Save the project for real (we'll be in %OnAfterSave for the project when this happens,
            // but %Studio.SourceControl.Interface protects against <FRAMESTACK> by moving %SourceControl
            // to tmp, so this should be perfectly fine).
            // If the project is not saved, the items will be in the wrong order.
            If tProject.%Save() {
                // Reload the project. We need to save first to be sure all ProjectItem changes are commited.
                // This will load them up freshly, in the normal order.
                Do tProject.%Reload()
            }
            // Clear a few properties, since /diffexport won't be respected.
            // This won't actually be saved, but these things shouldn't be in the export to disk.
            Set tProject.LastModified = ""
            Set tProject.Target = ""
            Set tProject.TargetType = ""
        }
    }
    Quit ##super(.InternalName,.Object)
}
Timothy Leavitt · Apr 11, 2017 go to post

If you're looking to add RegEx-based validation to a property, see this post.

If you're looking to do RegEx matching in a query... I don't think there's a built in function for this (!), but it's easy enough to do in a stored procedure:

ClassMethod MatchesRegEx(pText As %String, pRegEx As %String) As %Boolean [ SqlProc ]
{
    Quit $Match(pText,pRegEx)
}
Timothy Leavitt · Apr 10, 2017 go to post

See class documentation for Security.Applications.

Example:

Class DC.Demo.DeepSeeEnable
{

ClassMethod SetDSFlag(pApplicationName As %String, pValue As %Boolean = 1) As %Status
{
    Set tSC = $$$OK
    Try {
        New $Namespace
        Set $Namespace = "%SYS"
        
        Set tApplication = ##class(Security.Applications).%OpenId(pApplicationName,,.tSC)
        If $$$ISERR(tSC) {
            Quit
        }
        
        Set tApplication.DeepSeeEnabled = pValue
        Set tSC = tApplication.%Save()
    } Catch e {
        Set tSC = e.AsStatus()
    }
    Quit tSC
}

}

Use:

USER>s sc = ##class(DC.Demo.DeepSeeEnable).SetDSFlag("/csp/user",1)
 
USER>w sc
1
Timothy Leavitt · Apr 10, 2017 go to post

Hi Sebastian,

In the past, when I've tried to be creative and make <dataCombo> work more like a <select>, the better solution has been to just use a <select>, possibly customized a little bit to make it behave nicer with changing query parameter values. What's the reasoning for using a <dataCombo> rather than a <select> in your case?

Here's some custom component code that might serve as a basis for a full solution. The trick is setting editable="true", overriding findSelectedItem to select the best match to the input text, and calling findSelectedItem after changes, using the built-in timer for the sake of simplicity. Compare to a normal dataCombo for reference.

/// dataCombo subclass with limited support for responding to keyboard events
Class DC.Demo.ZEN.Component.dataCombo Extends %ZEN.Component.dataCombo [ System = 3 ]
{

///  This is the XML namespace used for library components.
Parameter NAMESPACE = "http://www.intersystems.com/zen/dc/demo";

/// Always editable, of course.
Property editable As %ZEN.Datatype.boolean [ InitialExpression = 1 ];

/// Onclick, show the dropdown.
Property onclick As %ZEN.Datatype.eventHandler [ InitialExpression = "zenThis.showDropdown();" ];

/// Find and select item within the dropdown that matches current control value.
/// This is called when the dropdown appears to make sure that the current
/// item is highlighted.
ClientMethod findSelectedItem(force, update, select) [ Language = javascript ]
{
    force = ('undefined'!=force)?force:false;
    update = ('undefined'!=update)?update:true;
    select = ('undefined'!=select)?select:false;
    var inputValue = this.findElement('input').value.toUpperCase();
    this.keyMode = true;
    if (force||this.isDropdownVisible) {
        var count = this.getOptionCount();
        var dVal = new Array();
        for (var idx = 0; idx < count; idx++) {
            dVal[idx] = this.getOptionText(idx).toUpperCase();
            if ((inputValue <= dVal[idx])&&((idx == 0)||(inputValue > dVal[idx-1]))) {
                this.selectItem(idx,update,select);
                break;
            }
        }
    }
}

/// Start (or restart) timer normally used by "timer" mode (overridden to apply to all modes)
/// Users should not call this method.
ClientMethod startTimer() [ Internal, Language = javascript ]
{
    this.clearTimer();
    this.actionTimerId = self.setTimeout("zenPage.getComponent("+this.index+").timerHandler()",this.delay);
}

/// Clear timer normally used by "timer" mode (overridden to apply to all modes)
/// Users should not call this method.
ClientMethod clearTimer() [ Internal, Language = javascript ]
{
    if (this.actionTimerId) {
        self.clearTimeout(this.actionTimerId);
        this.actionTimerId = null;
    }
}

/// Timer event handler normally used by "timer" mode (overridden to apply to all modes)
/// Users should not call this method.
ClientMethod timerHandler() [ Internal, Language = javascript ]
{
    if (this.isDropdownVisible) {
        // refresh drop down only if searchKeyLen is not defined!
        if ((this.searchKeyLen != '') && (this.searchKeyLen > 0)) {
            this.renderDropdown();
        } else {
            this.findSelectedItem(); //Just find the selected item.
        }
    }
    else {
        this.showDropdown();
    }
}

/// Change handler for input control.
/// Users should not call this method.
ClientMethod inputChangeHandler() [ Internal, Language = javascript ]
{
    this.invokeSuper('inputChangeHandler');
    
    //If input was cleared, and the actual control value was changed, then clear the value and notify.
    var input = this.findElement('input');
    if (input.value == '') {
        if (this.getValue() != '') {
            this.setValue('');
            this.onchangeHandler();
        }
    }
}

/// Notification that this component is about to stop being modal.
ClientMethod onEndModalHandler(zindex) [ Language = javascript ]
{
    this.findSelectedItem(true,false,true);
    this.invokeSuper('onEndModalHandler',arguments);
}

}

In case you're unfamiliar with custom components: you can include this one in a Zen page like this:

<page xmlns="http://www.intersystems.com/zen" xmlns:demo="http://www.intersystems.com/zen/dc/demo">
<demo:dataCombo sql="select 'your query here'/>
</page>

Or like this:

<page xmlns="http://www.intersystems.com/zen" >
<dataCombo xmlns="http://www.intersystems.com/zen/dc/demo" sql="select 'your query here'" />
</page>
Timothy Leavitt · Apr 10, 2017 go to post

Of course, for pages that don't have such handy links, there's Ctrl + End and Ctrl + Home... smiley

Timothy Leavitt · Mar 28, 2017 go to post

Here's what you could use to get the first three pieces, all optional:

 Url="/([^/]*)/?([^/]*)/?([^/]*)"

Note that if there's nothing matched for the nth capturing group, it'll pass an empty string as the nth argument. (So defaults in your method signature won't be helpful.)

Timothy Leavitt · Mar 28, 2017 go to post

Eduard actually reported the same error occuring in a different case a while ago. I added some notes to the bug report earlier (138510).

Timothy Leavitt · Mar 28, 2017 go to post

Right - the first one happens to work in this case, but the second one should work in any case where a comma isn't accepted (and should evaluate to the correct string).

Timothy Leavitt · Mar 28, 2017 go to post

That isn't valid XML - I think it'd need to be:

 <Var Name="AddClassesErrors" Value="&quot;,5202,5373,&quot;" />

The &quot; makes it smarter about it being one string, and the extra commas should make it work with the test condition in %Installer.Install:Import:

 ((","_pIgnoreErrors_",")[(","_$P($system.Status.GetErrorCodes(tSC),",")_","))

This is messy! sad

EDIT: Better option:

 <Var Name="AddClassesErrors" Value="#{&quot;5202,5373&quot;}" />
Timothy Leavitt · Mar 28, 2017 go to post

This just looks like a bug - the generated code is:

 Do tInstaller.Import(tNSName,tInstaller.Evaluate("${AddonDir}/AddClasses.xml"),"ck","5202","5373","0")

While you would expect:

 Do tInstaller.Import(tNSName,tInstaller.Evaluate("${AddonDir}/AddClasses.xml"),"ck","5202,5373","0")

Here's a possible workaround (untested, but the generated code looks better):

<Var Name="AddClassesErrors" Value="5202,5373" />
<If Condition='#{##class(%File).Exists("${AddonDir}/AddClasses.xml")}'>
  <Import File="${AddonDir}/AddClasses.xml" IgnoreErrors="${AddClassesErrors}" Flags="ck" />
</If>

EDIT: actual workaround (see discussion below) is to use #{<COS_expression>} (see documentation).

<Var Name="AddClassesErrors" Value="#{&quot;5202,5373&quot;}" />
<If Condition='#{##class(%File).Exists("${AddonDir}/AddClasses.xml")}'>
  <Import File="${AddonDir}/AddClasses.xml" IgnoreErrors="${AddClassesErrors}" Flags="ck" />
</If>
Timothy Leavitt · Mar 24, 2017 go to post

Also, if I plug:

do ##class(App.Use2).Test()

into the original code sample, I see App.Use:Test+1 in the Location property of the exception. How are you ending up with App.Use2:Test+1?

Timothy Leavitt · Mar 24, 2017 go to post

Yes - this is in the compiled class metadata.

 $$$comMemberKeyGet("App.Use2",$$$cCLASSmethod,"Test",$$$cMETHorigin)

Or see the Origin property in %Dictionary.CompiledMethod (same for other class members as well).

Timothy Leavitt · Mar 7, 2017 go to post

Here's a solution that works for me:

s string="http://www.google.com"
set matcher=##class(%Regex.Matcher).%New("(\b(https?|ftp)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])",string)
w matcher.ReplaceAll("<a href='$1' target='_blank'>$1</a>")

Key changes:

  • Remove the enclosing / /gim - this is part of the regex syntax in JS.
  • Add two a-z ranges for case insensitivity
  • Remove JS escaping of slashes (not necessary)
Timothy Leavitt · Mar 2, 2017 go to post

I did, but I added it as a comment rather than an answer, so I can't mark it as the accepted answer. Regardless, I followed up this morning, and have been advised that the Management Portal's behavior is a bug and may be "fixed" in the future. The preferred solution would be additional configuration, either at the webserver level or adding more web applications.

Timothy Leavitt · Jan 11, 2017 go to post

Actually, this seems to just work (as part of a %CSP.REST subclass, used in a properly configured web application - the Management Portal UI disables the "CSP Files Physical Path" field if you enter a dispatch class, but will save it if you add the physical path first):

XData UrlMap [ XMLNamespace = "http://www.intersystems.com/urlmap" ]
{
<Routes>
<!-- ... various Routes/Forwards like in a typical REST API ... -->
<!-- Other routes: pass through to file system -->
<Route Url="/(.*)" Method="GET" Call="ServeStaticFile" />
</Routes>
}

ClassMethod ServeStaticFile(pPath As %String) As %Status
{
    #dim %request As %CSP.Request
    Do %request.Set("FILE",%request.Application_pPath)
    Quit ##class(%CSP.StreamServer).Page()
}

If there's nothing more to it than that... smiley

Timothy Leavitt · Dec 15, 2016 go to post

Looking at that documentation, one difference between LIST and GROUP_CONCAT is that GROUP_CONCAT lets you specify the separator, while LIST always uses a comma.

If you wanted to use a different separator, and your data will never contain commas, then it's as easy as (for example):

select home_city as "City", count(*) as "Count", REPLACE(LIST(Name),',',' ') as "Names"
from sample.person
group by home_city

If "your data will never contain commas" is a bad assumption (as it is in the case of Name in Sample.Person), the solution is to use %DLIST and $ListToString.

select home_city as "City", count(*) as "Count", $ListToString(%DLIST(Name),' ') as "Names"
from sample.person
group by home_city

%DLIST builds a $ListBuild list, and $ListToString joins the list elements with the specified separator.

%DLIST is useful in other cases too - for example, if your data might contain commas and you want to iterate over the aggregated data after running dynamic SQL.

Timothy Leavitt · Dec 9, 2016 go to post

One way to do this is comparing $listbuilds of the variable in question. Note also that you can coerce any value to a number with the "+" operator. So, for example:

USER>set var = 1
 
USER>w $lb(var)=$lb(+var)
1  <-- var is a number.
USER>set var = "1"
 
USER>w $lb(var)=$lb(+var)
0  <-- var is a not a number.

There's not really a simple built-in function to do exactly what you want though (at least that I know of).

Timothy Leavitt · Dec 7, 2016 go to post

It's defined in %cspInclude.inc as:

#define URLENCODE(%string) $zconvert($zconvert(%string,"O",$replace($$$GETIO,"JSML","UTF8")),"O","URL")

See documentation on $ZCONVERT as well. This mentions:

“UTF8” which converts (output mode) 16-bit Unicode characters to a series of 8-bit characters. Thus, the characters $CHAR(0) through $CHAR(127) are the same in RAW and UTF8 mode; characters $CHAR(128) and above are converted.

“URL” which adds (output mode) or removes (input mode) URL parameter escape characters to a string. Characters higher than $CHAR(255) are represented in Unicode hexadecimal notation: $CHAR(256) = %u0100.

So, in short, $$$URLENCODE converts the input from (possibly) 16-bit to 8-bit characters, then adds URL parameter escape characters where they're needed. (For example, escaping slashes.)

Timothy Leavitt · Nov 30, 2016 go to post

Another option to consider, if you have the flexibility to do so, would be putting an [IDKey] index on the code property of the code tables, having the code properties in ICDAutoCodeDefn refer to the code table classes rather than being of type %String, and then using implicit joins. I suspect (but haven't verified) that this would perform better than calculated/transient properties, and it's much easier to follow/maintain than normal JOINs.

Here's a full example. Looking at a general code table class:

Class DC.Demo.CodeTables.CodeTable Extends %Persistent [ Abstract, NoExtent ]
{
Index Code On Code [ IdKey ];
Property Code As %String;
Property Description As %String;
}

In this example, a specific code table would then extend that class, but don't need to add anything:

Class DC.Demo.CodeTables.Team Extends DC.Demo.CodeTables.CodeTable
{
}

Class DC.Demo.CodeTables.Position Extends DC.Demo.CodeTables.CodeTable
{
}

Then, in the class that refers to these code tables:

Class DC.Demo.CodeTables.Players Extends %Persistent
{
Property Name As %String(MAXLEN = 100);
Property Position As DC.Demo.CodeTables.Position;
Property Team As DC.Demo.CodeTables.Team;
ForeignKey Position(Position) References DC.Demo.CodeTables.Position();
ForeignKey Team(Team) References DC.Demo.CodeTables.Team();
}

(It's worth considering using foreign keys in cases like this.)

To demonstrate how this ends up working from an SQL perspective:

Class DC.Demo.CodeTables.Driver
{

ClassMethod Run()
{
    Do ##class(DC.Demo.CodeTables.Players).%KillExtent()
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')")
    Do ..ShowQueryResults("insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')")
    Do ..ShowQueryResults("insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')")
    Do ..ShowQueryResults("select Name,Position->Description ""Position"",Team->Description ""Team"" from DC_Demo_CodeTables.Players")
}

ClassMethod ShowQueryResults(pQuery As %String, pParams...)
{
    Write !,"Running query: ",pQuery,!
    Do ##class(%SQL.Statement).%ExecDirect(,pQuery,pParams...).%Display()
}

}

The output from the Run() method is:

USER>Do ##class(DC.Demo.CodeTables.Driver).Run()

Running query: insert or update into DC_Demo_CodeTables.Position (Code,Description) values ('TE','Tight End')
1 Row Affected
Running query: insert or update into DC_Demo_CodeTables.Team (Code,Description) values  ('NE','New England Patriots')
1 Row Affected
Running query: insert into DC_Demo_CodeTables.Players (Name,Position,Team) values  ('Rob Gronkowski','TE','NE')
1 Row Affected
Running query: select Name,Position->Description "Position",Team->Description "Team" from DC_Demo_CodeTables.Players
Name    Position    Team
Rob Gronkowski    Tight End    New England Patriots

1 Rows(s) Affected
Timothy Leavitt · Nov 28, 2016 go to post

The Zen equivalent of ##super for JS is invokeSuper. Typically you'd just use:

this.invokeSuper('nameOfMethod',arguments);

All Zen components (including pages) have this. Here's an example from %ZEN.Component.dataCombo:

///  Set the value of a named property.<br>
ClientMethod setProperty(property, value, value2) [ Language = javascript ]
{
    switch(property) {
    case 'itemCount':
        break;
    case 'parameters':
        // set value of parameter: note that value will
        // be 1-based, so we have to convert it.
        // changing parameter always forces a query execution,
        // even in snapshot mode.
        if ('' != value) {
            value = value - 1;
            if (this.parameters[value]) {
                if (this.parameters[value].value != value2) {
                    this.parameters[value].value = value2;
                    this.clearCache();
                }
            }
        }
        break;
    default:
        // dispatch
        return this.invokeSuper('setProperty',arguments);
        break;
    }

    return true;
}
Timothy Leavitt · Nov 17, 2016 go to post

Hi Sebastian,

Sorry this went unanswered for so long. In case you haven't figured it out yet, one solution is:

    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)

%AddComponent registers the parameter with the page, so you won't get that error. (There might be a better way to do this, but %AddComponent at least seems to work.)

Here's a full example:

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

Property count As %ZEN.Datatype.integer [ InitialExpression = 0 ];

/// This XML block defines the contents of this page.
XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" title="">
<button onclick="zenPage.AddCombo();" caption="Add a Combo Box!" />
</page>
}

Method AddCombo() [ ZenMethod ]
{
    #dim %page As DC.Demo.DynamicComboPage
    Set ..count = ..count + 1
    Set tParm = ##class(%ZEN.Auxiliary.parameter).%New()
    Set tParm.id = "parameter_"_..count
    Set tParm.name = "parameter_"_..count

    Set tExam = ##class(%ZEN.Component.dataCombo).%New()
    Set tExam.id = "dataCombo_"_..count
    Set tExam.label = "Test"
    
    Do tExam.parameters.Insert(tParm)
    Do %page.%AddComponent(tParm)
    Do %page.%AddChild(tExam)
}

}
Timothy Leavitt · Nov 17, 2016 go to post

I think these error codes correspond to Windows system error codes - see this page.

So -2 corresponds to "ERROR_FILE_NOT_FOUND" and -3 to "ERROR_PATH_NOT_FOUND"

It seems more likely that you would get -2 from ##class(%File).Delete(file,.return). passing a directory that simply doesn't exist to RemoveDirectory results in -3. Is RemoveDirectory actually returning that error code, or perhaps a different one?