Timothy Leavitt · Dec 16, 2015 go to post

Rather than projecting the property as a table, one option would be to write a custom class query that $orders over the index global, make the column name "KEYS", and make it an [SqlProc]. The SQL would then look like:

SELECT DISTINCT KEYS FROM MyClass_CustomQueryName()

It would be nice if there was a generic solution. My first thought was to add this query to an index class (i.e., one that extends %Library.CacheIndex) and generate <query>Fetch/Execute/Close methods for indices that on property(KEYS), but it seems that queries aren't supported as index members. The <indexName><query>Fetch/Execute/Close methods are generated in the persistent class, but there's no actual class query visible from SQL. Maybe %Library.FunctionalIndex would work if you don't mind reinventing the standard bitmap insert/update/delete, but I suspect that you'd end up losing FOR SOME %ELEMENT in this approach, and would need to replace it with %FIND.

I'll send you what I came up with while investigating - maybe it will provide food for thought.

Timothy Leavitt · Dec 17, 2015 go to post

I think SYS.Database (in %SYS), rather than Config.Databases, can accomplish what you want. Particularly, you can open an object representing a database, call its Delete() method, and then call %Save() on it. That seems to have the same effect you're looking for.

Here's a sample class (Demo.Recreate):

Include %occInclude

Class Demo.Recreate

{

ClassMethod Run(pDBDirectory As %String)

{

    new $Namespace

    zn "%SYS"

    try {    

        //Get the database

        set tDB = ##class(SYS.Database).%OpenId(pDBDirectory)

        If '$IsObject(tDB) {

            $$$ThrowStatus($$$ERROR($$$GeneralError,"Database "_pDBDirectory_" not found."))

        }

        

        write !,"Properties of database:",!

        zw tDB

        

        write !

        

        //For demonstration purposes: show contents of a global in that DB

        for i=1:1:10 {

            set ^["^^"_pDBDirectory]demo(i) = i

        }

        write "Contents of ^[""^^"_pDBDirectory_"""]demo: ",!

        zw ^["^^"_pDBDirectory]demo

        

        write !

        

        write "Deleting database..."

        $$$THROWONERROR(tSC,tDB.Delete())

        write " done.",!

        write "Recreating database..."

        $$$THROWONERROR(tSC,tDB.%Save())

        write " done.",!

        

        write !

        

        write !,"Properties of database:",!

        zw tDB

        

        write !

        

        //For demonstration purposes: show that contents of global in that DB are gone

        write "Contents of ^[""^^"_pDBDirectory_"""]demo: ",!

        zw ^["^^"_pDBDirectory]demo

        

        zw tDB

    } catch anyException {

        write anyException.DisplayString(),!

    }

}

}

Timothy Leavitt · Dec 18, 2015 go to post

I did a project similar to this using %Net.POP3 a few years back (internal ref: ISCX2452), but it's part of a much larger application, so the exact code probably would not be very useful.
The general pattern for using %Net.POP3 is:

 set tSC = server.Connect(servername,username,password)
 set tSC = server.GetMailBoxStatus(.numberOfMessages)
 for i=1:1:numberOfMessages {
  set tSC = server.Fetch(i,.msg)
  //msg is a %Net.MailMessage
  //optional: set tSC = server.DeleteMessage(i)
 }
 //IMPORTANT! QuitAndCommit() or QuitAndRollback()
 set tSC = server.QuitAndCommit()


Add your own status-checking, try/catch, etc. One important caution: always be careful to clean up the connection when you're done, with either QuitAndRollback or QuitAndCommit. This might be the cause of the error you noted in your last comment - an open connection could be left over from before, blocking additional connections. I think terminating the process will fix this. (I remember this causing all sorts of trouble/confusion on my old project.)

As a side note: if your application/use case is running on Ensemble, EnsLib.EMail.InboundAdapter will do a lot of the hard work for you. See for reference: http://docs.intersystems.com/ens20152/csp/docbook/DocBook.UI.Page.cls?K…

This adapter will delete successfully-processed e-mails. From a "syncing" perspective this keeps things simple. If you don't want the messages deleted, a simple workaround might be to subclass EnsLib.EMail.InboundAdapter and %Net.POP3, override DeleteMessage to make it a no-op in the %Net.POP3 subclass, and override the adapter's MailServer property to use your %Net.POP3 subclass. (This would be a bit less messy than overriding OnTask.)

Timothy Leavitt · Jan 20, 2016 go to post

This should definitely work. Are you setting the showTabBar property to true for the nested tabGroup? If not, that might explain why nothing is showing up.

Timothy Leavitt · Jan 20, 2016 go to post

I tend to use %INLIST $ListFromString(?) in cases like this. (Assuming that the list is something like IDs that wouldn't contain commas.)

So, in your example:

<tablePane  width="25%" id="testTable" sql="SELECT Id from Tracking_Data.Person WHERE Id %INLIST $ListFromString(?)" showQuery="true">
<parameter/>
</tablePane>
<button caption="Test" onclick="zenThis.composite.testTestTable();"/>

ClientMethod testTestTable() [ Language = javascript ]
{
  var table zenThis.composite.getChildById("testTable");
  table.setProperty('parameters'1, '1,2');
}

Timothy Leavitt · Jan 21, 2016 go to post

Another option, and a caveat about ^%SYS("SystemMode"):

It looks like you're using CCR.* "Environment" is a meaningful term in that context. In namespaces configured for CCR, you can retrieve the environment with $$Env^%buildccr, i.e.:

CCR>w $$Env^%buildccr

BASE

It is possible to have multiple namespaces on the same instance configured as different environments. ^%SYS("SystemMode") is set to the furthest-along environment (of BASE-TEST-LIVE) of any namespace on the instance. That is, if you have a namespace configured as BASE, and configure another one as TEST, ^%SYS("SystemMode") will be changed to TEST - and this is system-wide. Just something to keep in mind.

*CCR (Change Control Record) is an InterSystems in-house application used in sites where InterSystems does implementation work, so this isn't as generally relevant as ^%SYS("SystemMode").

Timothy Leavitt · Jan 21, 2016 go to post

It looks like that method only exists in Caché 2015.2.0+. You're probably running on an older version.

(write $zversion?)

Timothy Leavitt · Feb 4, 2016 go to post

At a Caché level (for namespaces, databases, code, and security), %Installer may be useful; see: http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GCI_manifest

For Ensemble, there are some additional deployment-related features that might do more for you in terms of settings and lookup tables. See: http://docs.intersystems.com/ens20152/csp/docbook/DocBook.UI.Page.cls?KEY=EGDV_deploying#EGDV_deployment_overview

Timothy Leavitt · Feb 5, 2016 go to post

"GOTO label" jumps to a specified label in the same context frame.

"DO label" adds a new context frame. Code at the specified label will execute until a QUIT/RETURN or the end of the routine, then execution will resume at the next line after the DO command.

So in this case it's running through the whole routine (including everything under the dataentry label, with dtype already set to something valid), including printing the message at the end. Then it proceeds to the next thing after the DO command, which is printing that line again.

For what it's worth, if you're building command-line tools, %Library.Prompt may save you a lot of time. See the class reference for more informatioan.

For example:

#include %syPrompt
    New options,dtype
    Write "Deployment Utility"
    Set options(1) = "Gateway"
    Set options(2) = "IHE"
    Do ##class(%Prompt).GetArray("Deployment Type?",.dtype,.options,,,,$$$InitialDisplayMask+$$$MatchArrayMask)
    Write !, "Starting ", dtype," deployment..."
    Quit

Timothy Leavitt · Feb 8, 2016 go to post

Here's some relevant documentation: http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GORIENT_ch_cos#GORIENT_cos_scope

One way to make this work as-is would be to put tstVar in the "public" list:

start

    set tstVar = "Green"

    do TestIt()

 
TestIt() [tstVar] {

    write tstVar 
}

Another would be to give the variable a name starting with a %:

start

    set %tstVar = "Green"

    do TestIt()

 
TestIt() {

    write %tstVar 
}

Timothy Leavitt · Feb 9, 2016 go to post

In various posts, you've talked about having an implementation tool that is run from the browser, and you've also talked about running it from terminal. Here's a design that would be well-suited to both:

* Settings that apply to the whole thing are properties of an object (class extends %RegisteredObject)

* There's a class method (static method in other languages) to get the settings' values interactively

* There's an instance method to run the setup on an instance of the class

Include %syPrompt
Class Demo.SetupTool Extends %RegisteredObject
{
Property Type As %String(VALUELIST = ",Gateway,IHE");
Property Setting1Value As %String [ InitialExpression = "ABC" ];
Property Setting2Value As %Boolean [ InitialExpression = ];
ClassMethod RunInteractive()
{
    Set tSettingsObject = ..%New()
    
    Write "Deployment Utility"
    Set options(1) = "Gateway"
    Set options(2) = "IHE"
    Do ##class(%Prompt).GetArray("Deployment Type?",.tType,.options,,,,$$$InitialDisplayMask+$$$MatchArrayMask)
    Set tSettingsObject.Type = tType
    
    Set tString = tSettingsObject.Setting1Value
    Do ##class(%Prompt).GetString("Enter a string: ",.tString)
    Set tSettingsObject.Setting1Value = tString
    
    Set tYesOrNo = tSettingsObject.Setting2Value
    Do ##class(%Prompt).GetYesNo("Is this a helpful demo? ",.tYesOrNo)
    Set tSettingsObject.Setting2Value = tYesOrNo
    
    Do tSettingsObject.DoSetup()
}
Method DoSetup()
{
    Write !,"Starting ",..Type," deployment...",!
    // Here, do things based on properties of this object.
    If '..Setting2Value {
        //Do something specific if Setting2Value is false.
        Write "Why not?",!
    }
}
}
 

Then, for example:

USER>d ##class(Demo.SetupTool).RunInteractive()
Deployment Utility
 
1) Gateway
2) IHE
 
Deployment Type? 1 Gateway
Enter a string:  ABC =>
Is this a helpful demo?  Yes => No
Starting Gateway deployment...
Why not?

Or, if you want to do the same kind of setup from a simple ZenMethod in a Zen page or something, you could set up a settings object and then call DoSetup() on it.

USER>set tSettings = ##class(Demo.SetupTool).%New()
 
USER>set tSettings.Type = "IHE"
 
USER>d tSettings.DoSetup()
 
Starting IHE deployment...

Timothy Leavitt · Feb 10, 2016 go to post

I agree. In some cases, having an initial default value determined when the object is saved - possibly based on values of other properties, which InitialExpression wouldn't allow - could be useful, if that's how SqlComputed + SqlComputeCode is expected to work. This probably isn't one of those cases.

Timothy Leavitt · Feb 12, 2016 go to post

Perhaps try:

SELECT NVL(SqlFieldName,Name) FROM %Dictionary.CompiledProperty

    WHERE parent = 'HS.IHE.ATNA.Repository.Aggregation' and Transient = 0

%Dictionary.PropertyDefinition only includes properties defined in a given class; %Dictionary.CompiledProperty includes properties that are inherited.

Timothy Leavitt · Feb 16, 2016 go to post

This has been bothering me a little bit; %Dictionary.* should really be a last-resort option, in my opinion, and it isn't easy to use it to get the full picture from an SQL perspective.

Here are some alternative/possibly-better solutions, using %SQL.StatementMetadata and INFORMATION_SCHEMA. It looks like INFORMATION_SCHEMA is more exactly what you were looking for, if you're running on a recent enough Caché version (2015.1+). I haven't been able to find documentation on it other than the class reference, though.

/// NOTE: It could be good to validate pTableName to avoid SQL injection. (Outside the scope of this demo.)
/// This works pre-2015.1 (since %SQL.Statement was introduced - maybe 2012.2+?)
ClassMethod GetTableColumns(pTableName As %String) As %List
{
    #dim tResult As %SQL.StatementResult
    #dim tMetadata As %SQL.StatementMetadata
    Set tStmt = ##class(%SQL.Statement).%New()
    $$$ThrowOnError(tStmt.%Prepare("select top 0 * from "_pTableName))
    Set tResult = tStmt.%Execute(), tMetadata = tResult.%GetMetadata()
    Set tCols = ""
    For i=1:1:tMetadata.columnCount {
        Set tCols = tCols_$ListBuild(tMetadata.columns.GetAt(i).colName)
    }
    Quit tCols
}
/// This will only work on 2015.1+; INFORMATION_SCHEMA is a new feature. For more information, see the class reference for it in the documentation.
ClassMethod GetTableColumnsNew(pTableName As %String) As %List
{
    #dim tResult As %SQL.StatementResult
    Set tStmt = ##class(%SQL.Statement).%New()
    Set tSchema = $Piece(pTableName,".")
    Set tTableName = $Piece(pTableName,".",2)
    $$$ThrowOnError(tStmt.%Prepare("select COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = ? and TABLE_NAME = ?"))
    Set tResult = tStmt.%Execute(tSchema,tTableName)
    Set tCols = ""
    While tResult.%Next(.tSC) {
        Set tCols = tCols_$ListBuild(tResult.%Get("COLUMN_NAME"))
    }
    $$$ThrowOnError(tSC)
    Quit tCols
}

Using this:

SAMPLES>set cols = ##class(Demo.TableColumns).GetTableColumns("Sample.Person")
SAMPLES>w $lts(cols)
ID,Age,DOB,FavoriteColors,Name,SSN,Spouse,Home_City,Home_State,Home_Street,Home_Zip,Office_City,Office_State,Office_Street,Office_Zip
SAMPLES>set cols = ##class(Demo.TableColumns).GetTableColumnsNew("Sample.Person")
SAMPLES>w $lts(cols)                                                            ID,Age,DOB,FavoriteColors,Name,SSN,Spouse,Home_City,Home_State,Home_Street,Home_Zip,Office_City,Office_State,Office_Street,Office_Zip

Timothy Leavitt · Feb 16, 2016 go to post

The original suggestion might still work, but there are caveats with serial objects and probably some other edge cases.

What's your use case, if you don't mind elaborating? Why is SQL syntax necessary? What Caché version are you running (run in terminal: write $zversion)?

Timothy Leavitt · Feb 18, 2016 go to post

Specifics on %CSP.Page / cspbind error handling:

There are alternatives to modifying system-level error message translations. Even if the localization is changed, the end user would probably still see "SQLCODE -139," which isn't helpful. What you probably want to show the user is "Someone else has modified this record. Please reload the page and try again." or something similar.

Using %CSP.Page and a form with cspbind, there's no way to customize the error handling in the generated <form name>_save function. It seems to always show any error message in an alert. An alternative would be to use a similar approach to form.csp and formsubmit.csp in the SAMPLES namespace, submitting the form and calling the <form name>Submit method of the page rather than using a hyperevent (in form_save()). Note that you don't need a separate page to handle the form submit; one page can do it all.

Of course, if the form is saved with a POST rather than a hyperevent, and optimistic concurrency control is in use, it might be worth a separate call to the server immediately before submitting the form for an optimistic concurrency check. If someone else edited the record, the page could tell the user rather than submitting the form. This wouldn't prevent the error in question, but would at least make it much less likely.

(Side note: %CSP.Page/cspbind is very old technology. There are better tools available now, but if you're stuck with it in a large existing project, that's understandable.)

General thoughts on showing users friendly error messages:

The more general problem is: "Often, the error codes and messages the system produces are not helpful to an end user. How do you show the user the information they need when an error occurs?"

This gets more complicated because error messages can come from very different sources - for example, SQL, an object %Save, a variable being undefined, or the application detecting a user error.

The standard for the large Caché-based application I work on is:

  • Within UI classes (class-based CSP or Zen, in our case), server-side code is wrapped in try-catch blocks.
  • Different types of exceptions (subclasses of %Exception.AbstractException) are thrown for the different types of errors that occur. These may be:
    • SQLCODEs and associated messages from embedded or dynamic SQL
    • Error %Status codes from object %Saves and various other things
    • General internal errors in application business logic
    • User errors detected in business logic. (These are treated differently for logging purposes; user errors may be more common than system errors and might not be logged.)
    • System errors (<UNDEFINED>, <SUBSCRIPT>, etc.)
  • Any of these types of exceptions. except system errors, may include a user-friendly message explaining what went wrong.
  • If an exception is caught, a method is called to (1) log that an error occurred and (2) either provide a description of the error that the user can understand, or else just say that an error happened (with the confusing details omitted, although they are logged).
    • If the exception already has a friendly message for the user, it's returned.
    • Otherwise, for error SQLCODEs and error %Status codes, we provide general messages for concurrency-related errors and some other common error codes. We also have a system for interpreting foreign and unique key violations (from an SQLCODE or %Status) and providing more descriptive messages based on those.
    • Worst case, the message "An internal error occurred (log ID _____)" is returned.
  • The message is shown to the user in red text, an alert dialog (ideally not a vanilla JavaScript alert), or however else makes sense in context.
  • There are macros for the different types of exceptions and for getting the user-friendly message, for ease of maintainability.
Timothy Leavitt · Feb 26, 2016 go to post

Eduard,

It would help if there was further explanation of your use case. What is the "additional check" in question? Is it a flag on the user at the application level? Is it a call to some other system?

For simplicity, let's suppose your application has a role called TerminalUser that has the %Service_Console:U permission, and that it's the only role an application user might have that grants that permission.

If the "additional check" is based on a flag in your application, your application could simply add/remove the TerminalUser role when that flag changes. See class documentation for Security.* in the %SYS namespace. This might rely on a "privileged routine application" to escalate roles if the user changing this flag wouldn't normally have the necessary privileges to change security settings.

In either case, ZAUTHENTICATE might still work, although it wouldn't be as elegant. It could perform whatever additional checks are needed (against your application or some external system), and possibly add or remove the TerminalUser role based on the results. ZAUTHENTICATE would always return an error, so it falls back to password login. At that point the user could authenticate successfully but might not be allowed to use terminal if the TerminalUser role was removed. (I haven't tried this, but it seems like it could work.)

If you're using OS or Kerberos authentication, ZAUTHORIZE could also be relevant.

Timothy Leavitt · Mar 3, 2016 go to post

%ZEN.ObjectProjection demonstrates a few answers to your second question. There are probably more differences/advantages, but here are a few:

  • Projections can call methods of the class that was just compiled, while generator methods can't call methods of the class that's being compiled. Example: %ZEN.ObjectProjection calls the %GetIncludeInfo method of each class that was compiled.
  • Projections can avoid repeated work by queuing classes in CreateProjection and using the EndCompile method (as %ZEN.ObjectProjection does). In the case of the generated JS and CSS files for Zen, multiple classes may contribute content to the same file, so the file must be regenerated when any of these classes is compiled. If many such classes are compiled at the same time, the projection only regenerates each impacted file once. I don't think there's a good way to do the same thing in a generator method.
Timothy Leavitt · Mar 7, 2016 go to post

For what it's worth, a more direct answer to the question "Who does Windows think I am?" can be found programmatically with:

$System.Process.UserName()

(2015.1+; see class reference)

Timothy Leavitt · Mar 10, 2016 go to post

You could probably edit the post, then click the "Source" button, and remove them from the HTML.