Article
· Dec 1, 2016 8m read

RESTForms - REST API for your classes

In this article I would like to present the RESTForms project - generic REST API backend for modern web applications.

The idea behind the project is simple -after I wrote several REST APIs I realized that generally, REST API consists of two parts:

  • Work with persistent classes
  • Custom business logic

And, while you'll have to write your own custom business logic, RESTForms provides all things related to working with persistent classes right out of the box.
Use cases

  • You already have a data model in Caché and you want to expose some (or all) of the information in a form of REST API
  • You are developing a new Caché application and you want to provide a REST API

Client side

This project is developed as a web applications backend, so JS just gets it. No format conversion required.

Note: CRUD

4 operations can be done over an object or a collection:

  • Create
  • Read
  • Update
  • Delete

Features

What can you already do with RESTForms:

  • CRUD over exposed class - you can get class metadata, create, update and delete class properties
  • CRUD over object - you can get, create, update and delete objects
  • R over object collections (via SQL) - protected from SQL injections
  • Self-discovery – first you get a list of available classes, after that you can get class metadata, and based on that metadata you can do CRUD over object

Paths

Here's the table of the main paths, showcasing what can you do via RESTForms.

URL

Description

info

List of all available classes

info/all

Get metadata for all classes

info/:class

Class metadata

field/:class

Add property to class

field/:class

Modify class property

field/:class/:property

Delete class property

object/:class/:id

Retrieve object

object/:class/:id/:property

Retrieve one property of the object

object/:class

Create object

object/:class/:id

Update object from dynamic object

object/:class

Update object from object

object/:class/:id

Delete object

objects/:class/:query

(SQL) Get objects for the class by query

objects/:class/custom/:query

(SQL) Get objects for the class by custom query

How do I start using RESTForms?

  1. Import the project from GitHub (recommended method: add as a submodule to your own repo, or just download a release)
  2. For each class, you wish to expose via RESTForms
    • Inherit from adaptor class
    • Specify permissions (for example you may expose some classes as read-only)
    • Specify property used as a display value for an object
    • Specify display names for properties, you wish to display

Setup

  1. Download and import from latest release on release page 20161.xml (for Caché 2016.1) or 201162.xml (for Caché 2016.2+) into any namespace
  2. Create new web application /forms with Dispatch class Form.REST.Main
  3. Open http://localhost:57772/forms/test?Debug in the browser to validate install (should output  {"Status": "OK"} and possibly prompt for password).
  4.  If you want test data, call:
do ##class(Form.Util.Init).populateTestForms()

Example

First, you need to know what classes are available. To get that information, call:

http://localhost:57772/forms/form/info

You'll receive something like this as a response:

[
   { "name":"Company",     "class":"Form.Test.Company" },
   { "name":"Person",      "class":"Form.Test.Person"  },
   { "name":"Simple form", "class":"Form.Test.Simple"  }
]

There are currently 3 sample classes (provided with RESTForms), let's see metadata for Person (Form.Test.Person class). To get that information, call:

http://localhost:57772/forms/form/info/Form.Test.Person

In response you'll receive class metadata:

{  
   "name":"Person",
   "class":"Form.Test.Person",
   "displayProperty":"name",
   "objpermissions":"CRUD",
   "fields":[  
      { "name":"name",     "type":"%Library.String",    "collection":"", "displayName":"Name",          "required":0, "category":"datatype" },
      { "name":"dob",      "type":"%Library.Date",      "collection":"", "displayName":"Date of Birth", "required":0, "category":"datatype" },
      { "name":"ts",       "type":"%Library.TimeStamp", "collection":"", "displayName":"Timestamp",     "required":0, "category":"datatype" },
      { "name":"num",      "type":"%Library.Numeric",   "collection":"", "displayName":"Number",        "required":0, "category":"datatype" },
      { "name":"аge",      "type":"%Library.Integer",   "collection":"", "displayName":"Age",           "required":0, "category":"datatype" },
      { "name":"relative", "type":"Form.Test.Person",   "collection":"", "displayName":"Relative",      "required":0, "category":"form"     },
      { "name":"Home",     "type":"Form.Test.Address",  "collection":"", "displayName":"House",         "required":0, "category":"serial"   },
      { "name":"company",  "type":"Form.Test.Company",  "collection":"", "displayName":"Company",       "required":0, "category":"form"     }
   ]
}

What does all that mean?

Class metadata:

  • name - display name for the class
  • class - underlying persistent class
  • displayProperty - object property to use, when displaying object
  • objpermissions - what can a user do with an object. In current case, user can create new objects, modify existing ones, delete existing objects and get the

Property metadata:

  • name - property name - same as in the class definition
  • type - property class
  • collection - is list/array collection
  • displayName - display property name
  • required - is this property  required
  • category - property type class category. Follows usual Caché class categories, except all RESTForms enabled classes are shown as "form"

In class definition it looks like this:

/// Test form: Person
Class Form.Test.Person Extends (%Persistent, Form.Adaptor, %Populate)
{

/// Form name, not a global key so it can be anything
/// Set to empty string (like here) to not have a class as a form 
Parameter FORMNAME = "Person";

/// Default permissions
/// Objects of this form can be Created, Read, Updated and Deleted
/// Redefine this parameter to change permissions for everyone
/// Redefine checkPermission method (see Form.Security) for this class
/// to add custom security based on user/roles/etc.
Parameter OBJPERMISSIONS As %String = "CRUD";

/// Property used for basic information about the object
/// By default getObjectDisplayName method gets its value from it
Parameter DISPLAYPROPERTY As %String = "name";

/// Use value of this parameter in SQL, as ORDER BY clause value 
Parameter FORMORDERBY As %String = "dob";

/// Person's name.
Property name As %String(COLLATION = "TRUNCATE(250)", DISPLAYNAME = "Name", MAXLEN = 2000);

/// Person's Date of Birth.
Property dob As %Date(DISPLAYNAME = "Date of Birth", POPSPEC = "Date()");

Property ts As %TimeStamp(DISPLAYNAME = "Timestamp") [ InitialExpression = {$ZDATETIME($ZTIMESTAMP, 3, 1, 3)} ];

Property num As %Numeric(DISPLAYNAME = "Number") [ InitialExpression = "2.15" ];

/// Person's age.<br>
/// This is a calculated field whose value is derived from <property>DOB</property>.
Property аge As %Integer(DISPLAYNAME = "Age") [ Calculated, SqlComputeCode = { set {*}=##class(Form.Test.Person).currentAge({dob})}, SqlComputed, SqlComputeOnChange = dob ];

/// This class method calculates a current age given a date of birth <var>date</var>.
ClassMethod currentAge(date As %Date = "") As %Integer [ CodeMode = expression ]
{
$Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000))
}

/// Person's spouse.
/// This is a reference to another persistent object.
Property relative As Form.Test.Person(DISPLAYNAME = "Relative");

/// Person's home address. This uses an embedded object.
Property Home As Form.Test.Address(DISPLAYNAME = "House");

/// The company this person works for.
Relationship company As Form.Test.Company(DISPLAYNAME = "Company") [ Cardinality = one, Inverse = employees ];
}

RESTForms enabling a class

So, to make this class RESTForms enabled, I started with the usual persistent class and:

  1.  Extended it from Form.Adaptor
  2. Added FORMNAME parameter with the value - name of the class
  3. Added OBJPERMISSIONS parameter - CRUD for all permissions
  4. Added DISPLAYPROPERTY parameter - property name used to display object name
  5. Added FORMORDERBY parameter - default property to sort by for queries using RESTForms
  6. For each property I want to see in metadata I added DISPLAYNAME property parameter

That's all. After compilation, you can use the class with RESTForms.

As we generated some test data (see Installation, step 4), let's get Person with id 1. To get the object call:

http://localhost:57772/forms/form/object/Form.Test.Person/1

And, here's the response (generated data, may differ):

{
   "_class":"Form.Test.Person",
   "_id":1,
   "name":"Klingman,Rhonda H.",
   "dob":"1996-10-18",
   "ts":"2016-09-20T10:51:31.375Z",
   "num":2.15,
   "аge":20,
   "relative":null,
   "Home":{
      "_class":"Form.Test.Address",
      "House":430,
      "Street":"5337 Second Place",
      "City":"Jackson"
   },
   "company":{
      "_class":"Form.Test.Company",
      "_id":60,
      "name":"XenaSys.com",
      "employees":[
         null
      ]
   }
}

To modify the object (specifically, num property), call:

PUT http://localhost:57772/forms/form/object/Form.Test.Person

With this body:

{
   "_class":"Form.Test.Person",
   "_id":1,
   "num":3.15
}

Note that for better speed, only _class, _id and modified properties should be in the request body.

Now, let's create a new object. Call:

POST http://localhost:57772/forms/form/object/Form.Test.Person

With this body:

{
   "_class":"Form.Test.Person",
    "name":"Test person",
    "dob":"2000-01-18",
    "ts":"2016-09-20T10:51:31.375Z",
    "num":2.15,
    "company":{ "_class":"Form.Test.Company", "_id":1 }
}

If the object creation was successful, RESTForms would return an id:

{"Id": "101"}

Otherwise, an error would be returned in JSON format. Note that all persistent object properties should be referenced only by _class and _id  properties.

And finally, let's delete our new object. Call:

DELETE http://localhost:57772/forms/form/object/Form.Test.Person/101

That's full CRUD over Form.Test.Person class.

Demo

You can try RESTForms online here (user: Demo, pass: Demo) .

Additionally there is a RESTFormsUI application - editor for RESTForms data, check it out here (user: Demo, pass: Demo). Screenshot of the class list:

Conclusion

RESTForms does allmost of the work, required from the REST API as far as persistent classes are concerned.

What's next

In this article, I just started talking about RESTForms features. In the next article, I'd like to tell you about some advanced features - queries, that allow client to get slices of data safely, with no risk of SQL injections. Read about queries in the second part of the article.

There is also a RESTFormsUI - editor for RESTForms data.

Links

Discussion (23)2
Log in or sign up to continue

Download and import from latest release on release page 20161.xml (for Caché 2016.1) or 201162.xml (for Caché 2016.2+) into any namespace

If you have 2016.2+ Forms would not be compiled,  $-methods should be renamed.
You can do it with the following:

Import and compile in NameSpace classes from SystemMethodsRemover 

Run in terminal:

And it works.

Or if you have CacheUpdater installed, import and compile from the Github repo can be reached with this one command:

d ##class(CacheUpdater.Task).Update("intersystems-ru","SystemMethodsRemover")

Hello,

I'm trying to open RestFormsUI from my localhost. And I have a problem with giving rights to certain user to login into application. In Readme for the project it says "Webapp or Unknown user should be able to access the namespace/database". How am I to do it? Whatever I do at this point I get "Incorrect username and/or password" error. Apparently I'm doing something wrong :(

Doesn't work for me. I installed the 20162.xml classes on a 2017.2.1 system. It all seems to install correctly until I tried:

http://localhost:57772/forms/form/info/Form.Test.Person

I get this error:              

"error":"ERROR #5002: Cache error: <METHOD DOES NOT EXIST>zgetFormMetadata+15^Form.Info.1 *%SetAt,%Library.DynamicArray",

It looks like some underlying system methods have changed since this was written. How do I fix this please?