go to post Stephen Wilson · May 17, 2019 Great article!Can you enable HTTP Bearer token authentication on Web Applications via the System Management Portal?All I see under 'Allowed Authentication Methods' are options for:UnauthenticatedPassword Login CookiePerhaps I am missing a trick somewhere?
go to post Stephen Wilson · Mar 15, 2019 What does your Edit Web Application settings look like? What allowed authentication methods are enabled?
go to post Stephen Wilson · Nov 21, 2018 Congrats @Scott Roth. I enjoy reading your posts and I'm sure you will do well as a DC moderator.
go to post Stephen Wilson · Nov 20, 2018 Pretty embarassing result for me. I misinterpretted how ObjectScript treats equals(=) operator when used with a write and thus lost a lot of marks. Its never something I've come across but now I am well and truely informed. Good article. From a support perspective, I often have to evaluate expressions in the terminal to check my understanding is correct. It can be mighty fustrating if you spend all day assuming an expression is interpretted one way, only to find your assumption to be incorrect! Agree with Alexander Koblov. Avoid ambiguous or easily misinterrepted code. Also use brackets, they are your friends.
go to post Stephen Wilson · Oct 25, 2018 Another great resource is the REST and Relaxation webinar. If your external service returns the above JSON body, you can work with the JSON directly. Get the JSON from the HTTP Request Body ClassMethod GetJSONData() As %DynamicObject { // Throw our own exception if the request body does not contain valid JSON. Try { Set obj = ##class(%DynamicObject).%FromJSON(%request.Content) } Catch ex { Throw ##class(MyProject.Exception.BadRequest).%New("Invalid JSON") } Quit obj }Call the method from within your %CSP.REST class. The URL and HTTP method will be your event trigger.Class MyProject.Patient Extends %CSP.REST <Routes> <Route Url="/postPatient" Method="Post" Call="PostPatient" /> </Routes>ClassMethod PostPatient() As Status{ Set formData = ##class(MyProject.API.Service).GetJSONData() // Get the tag element Set tag = formData.tag // Loop through a JSON Array Set siteIter = formData.patient.site.%GetIterator() if ( $GET(siteIter)'="" { while(siteIter.%GetNext(.key, .value) { Set code = value.code Set mrn = value.mrn } }return $$$OK}The syntax here may not be 100% correct but I hope this gives you some ideas. Try to keep your JSON object as simple as possibleWhen you get the information from the JSON body you can decide whether to create a new object or modify an existing object using the %Persistent methods %OpenId() %ExistsId() and %New(). I would model your classes based on how you wish to store the data contained within the HTTP request rather than the request itself. If you are able to obtain information from the request itself eg. "tag: post" then you probably do not need to store that in the database.You can set the response status you wish to return bySet %response.Status = ..#HTTP400BADREQUEST so you may not need to encode things in JSON that are part of the request. I use AutoMapper and NewtownsoftJson .NET libraries for mapping JSON to C# objects/models and work with the JSON and ObjectScript classes directly when saving JSON data to the database.Update: There's some useful documentation in the Using JSON in Caché Guide, including JSON Serialization and de-serialization to dynamic entities using %FromJSON and %ToJSON methods
go to post Stephen Wilson · Oct 24, 2018 @Michael Cohen Thanks for the links although the Intersystems documentation linked appears to reference .NET 4 Entity Framework rather than .NET Core EF. They are two different platforms and is most likely why you could not find ConfigureServices in the Intersystems docs. For a Micosoft SQL Server, your ConfigureServices method could have services.AddDbContext<MyMSDBContext>(cfg => { cfg.UseSqlServer(_config.GetConnectionString("MyConnectionString")); });but I haven't seen any examples in Caché! Perhaps there is a development opportuntiy to write a NuGet package? Here's a Microsoft link to other popular database providers supported within .NET Core.PS. If you use RESTful HTTP methods to talk to the database then you don't need a database provider as the data can be returned as part of the HTTP Response.
go to post Stephen Wilson · Oct 18, 2018 My understanding is that the Globals API namespace reference using Intersystems.Globals; is bundled with the InterSystems.CacheExtreme.dll which relies on the traditional .NET Framework ie. not .NET Core. Some Global API documentation recommends you use both DLLs mentioned in the question description above.I am currently developing a .NetCore 2.1 MVC Web Application that uses Bootstrap 4, Datatables.net, Newtownsoft Json Serailizer (AutoMapper is also good), Moment.js, Tempus Dominus Bootstrap 4 Themed Datepicker, Caché ObjectScript %Persistent classes mapped to Globals (see The Art of Mapping Globals Series), Cache ObjectScript %CSP.REST classes for mapping web api calls (see Rest and Relatation Demo) and basic authentication over HTTPS. Using this development stack, I do not need to generate proxy classes using the Caché Object Binding Wizard and I have no need for either DLL but I do not have direct access to globals from my .NET classes. The backend classmethods work at the class/object level in response to HTTP requests (Web API calls) to manipulate Global values in the database.
go to post Stephen Wilson · Oct 9, 2018 Thanks @Vitaliy Serdtsev. I never knew about this Globals API.
go to post Stephen Wilson · Oct 5, 2018 If you have never used Wireshark before or haven't a deep understanding of the TCP/IP suite of protocols then Wireshark might be overkill for your needs. I have only used it in a Lab or Development environment. You also have to consider the disk storage requirements and governance issues around full-packet capture eg. HPIAA, PCI-DSS. You might want to check Eduard Lebedyuk's article on Debugging Web for additional tools and tips.For troubleshooting these issues, we enable a monitoring global that records every character recieved within the TCP Stream START do USE read *CHAR:2 else do WAIT do CHARMONITOR goto START CHARMONITOR if ^AC=1 I CHAR'=-1 set X=^AC1,^AC1(X+1)="*"_$char(CHAR)_"*"_CHAR_"*",^AC1=X+1 ; Monitor character by character set ^CALLED("CHARMON")=$ZD(+$H,4)_"*"_$ZT($P($H,",",2))_"*"_CHAR quitYou can try a re-connect using $ZTRAP or try-catch. In this example, we do a maximum of 10 re-connect attempts set $ZT="ERROR" ERROR if ($ZE["<READ>") { set ERRORCOUNT=ERRORCOUNT+1 if (ERRORCOUNT<11) { set ^TCPLOG("ERROR",$ZD(+$H,4),$ZT($P($H,",",2)),ERRORCOUNT)=$ZE close "|TCP|"_PORT hang 30 goto OPEN } else { do ^%ET } } else { do ^%ET } quitYou might also look at extending the TCP timeout value to see if that makes a difference to the volume of errors. Check out this Ensemble Example of a SOAP Web Service
go to post Stephen Wilson · Oct 4, 2018 It's a real-world scenario where you can have something as true, false or undefined. In C# .NET you might use bool? type for this. If you are populating a checkbox HTML element, you will have to consider the three possible outcomes. Since I'm mapping classes to pre-existing global structures it's important to understand how the object properties map to these pre-existing global structures.
go to post Stephen Wilson · Oct 4, 2018 @Sean Connelly many thanks for the ideas. Did you need to run the x.%Save() before the global storage was updated?
go to post Stephen Wilson · Oct 4, 2018 Ok so lets kill off the AKI node in the global and instansiate the object by passing in the IDKEYk ^CODE("TNO","BIO",291,"AKI")set myobject= ##class(Code.TestDetails).%OpenId("BIO||291")If you do if (myobject.AKI="") what are you really asking:1) if ($GET(^CODE("TNO","BIO",291,"AKI"))="") 2) if ($DATA(^CODE("TNO","BIO",291,"AKI"))=0)I think it is option 1 and I was wondering if there was a $DATA operation that would work at the object level. I see %DynamicObject has a %IsDefined("propertyName") method but this is more often seen in parsing JSON objects and used to detect { "AKI": null }Perhaps this is what I'm looking for:Set isNullBool = ##class(Code.TestDetails).%ObjectIsNull(myobject.AKI)zwrite isNullBoolAny other thoughts?
go to post Stephen Wilson · Sep 14, 2018 Same idea within a shell session. I often like to evaluate expressions to check they work as expected before writing any significant code.set val1 = 10set val2 = 11set operator = "<"// No need for extra double-quotes in logical expressionset testStatement="set testResult=(val1_operator_val2)"xecute testStatementzwrite testResult
go to post Stephen Wilson · Sep 11, 2018 Rather than pass the traverseRelationships Boolean to the instance method toJSON(traverseRelationships) in User.Widget, could you change URL map/URL route to use a QueryString parameter? eg 1. http://{{SERVER}}:{{WEB_PORT}}/widgetsdirect/rest/Stephen?traverseRelationships=trueeg2. http://{{SERVER}}:{{WEB_PORT}}/widgetsdirect/rest/Stephen?traverseRelationships=falseHow do you map URL querystring parameters to the %CSP.REST class in HTTP Get Requests?
go to post Stephen Wilson · Aug 16, 2018 I think Kerberos is only required if you a connecting to a MS SQL Server database from a non-Windows environment using the JDBC driver. If you are in a Windows environment you will probably use NTLMv2. SQL Squirrel can be used for troubleshooting connection strings or you can write a simple console based Java application. You might also check the account outside of using JDBC using SQL Server Management Studio. You need to verify your system requirements are correct for the JRE/JDK/JDBC version and the MS SQL Server version. Depending on your version requirements the connection string will vary.
go to post Stephen Wilson · Aug 1, 2018 Consider the line in C#CACHEObject.ContainerImco containerImco = new CACHEObject.ContainerImco(cacheConnection);Does this 'ContainerImco' object have a constructor that takes in a CacheConnection object? Have you opened the connection?From the error it looks like its failing when trying to get the connection. It seems a bit weird to have a collection of CacheConnection objects.
go to post Stephen Wilson · Aug 1, 2018 Your code is quite difficult to read without proper styling. I recommend the 'Special Container'. We created a DLL from a c# class library generated from the .NET Object Binding Wizard and placed the DLL in our bin/ folder.Assuming lcontainerImco.Connection is a CacheConnection object from 'Intersystems.Data.CacheClient' and you have imported 'Intersystems.Data.CacheTypes'. The following should work.CacheConnection CacheConnect = new CacheConnection();CacheConnect.ConnectionString = server + "Port = " + connectionPort + "; " + "Namespace = USER; "+ "Password = " + password + ";" + "User ID = " username + "; " + "pooling = false;";CacheConnect.Open();There is also a little known issue about certain special characters in the password causing parsing problems, which is identifiable from the following stack trace:at InterSystems.Data.CacheClient.CacheADOConnection.createConnectionKeyString()\r\n atInterSystems.Data.CacheClient.CacheADOConnection.ParseConnectionStringInternal(String connectionString)\r\n at InterSystems.Data.CacheClient.CacheADOConnection.set_ConnectionString(String value)\r\nBad characters in the password include pairing and delimiting characters eg. equals sign, single-quotes and backslashes, pound sign symbol and not symbol. This was reported to WRC two years ago but never resolved, so we created a work-a-round solution. Refer to the documentation relevant to your Caché version for all the valid connection string parameters (I have linked the latest release) and be sure to wrap your connection in a try-catch structure.
go to post Stephen Wilson · Jul 24, 2018 You would probably get a syntax error if LD resolved to an empty string (""). I would put a trace/watchpoint on LD or alternatively log its contents to a global to verify it is not null. You might also want to check the definition #define LDAPServer $Get(^OSUMCLDAP("Server")) to see if it is correct and verify whether it should or should not contain a port number.Here's another sample SimpleBinds() operation using server ldapserver1.mycoolcompany.com port 51000 /// Return 1 if LDAP SimpleBind successful. /// Return 0 if LDAP SimpleBind unsuccessfulClassMethod Connect(ByRef Username As %String, ByRef Password As %String) As %Boolean { set userContext = "uid="_Username_",ou=aixuser,cn=aixsecdb,cn=aixdata,o=mycoolcompany_aix" set ldapConnection = ##class(%SYS.LDAP).Init("ldapserver1.mycoolcompany.com",51000) set status =##class(%SYS.LDAP).SimpleBinds(ldapConnection,userContext,Password) if (status=$$$LDAPSUCCESS) { write !, "LDAP SimpleBind Successful!" return 1 } else { write !,"LDAP SimpleBind failed!" write !,"Error code : ",status write !,"Error message : ",##Class(%SYS.LDAP).Err2String(status) return 0 }}The Init() method documentation has some good troubleshooting tips under 'Error Codes' for connecting using SSL/TLS
go to post Stephen Wilson · Jul 4, 2018 I am enjoying this tutorial and the cursor name thing did confuse me for a little bit. In addition, you need to have SET widgetObj = {}somewhere in your code otherwise, it won't compile. Is there anyway to contribute on the Github page? There doesn't appear to be repo I can submit pull requests to.
go to post Stephen Wilson · Jul 4, 2018 Can you clarify what 'Caché Nodejs supports Node 7' means in the release notes? It appears to suggest node-7-10-1 should work with the cache610.node adapter.