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?
- Import the project from GitHub (recommended method: add as a submodule to your own repo, or just download a release)
- 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
- 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
- Create new web application /forms with Dispatch class Form.REST.Main
- Open http://localhost:57772/forms/test?Debug in the browser to validate install (should output {"Status": "OK"} and possibly prompt for password).
- 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:
- Extended it from Form.Adaptor
- Added FORMNAME parameter with the value - name of the class
- Added OBJPERMISSIONS parameter - CRUD for all permissions
- Added DISPLAYPROPERTY parameter - property name used to display object name
- Added FORMORDERBY parameter - default property to sort by for queries using RESTForms
- 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
Eduard, thanks for the very promising article!
If I decide to use your framework, how can I deploy the library in my solution? What are the best practices for that?
I think packed with the rest of the solution would be okay, and during installation you can automatically check GitHub for latest release and download it, if is's newer.
@Eduard the RESTForms is a great idea!
is it possible to adapt this RestAPI concept to work without using the CSP application ??
kevin
What do you mean? Do you want to call RESTForms from some other (non-web) context? Can you please expand your question.
the current implementation is CSP based and uses (user based) csp licences for the interaction. Because of the way csp retains that licence for a period after "finishing with the sesssion", it would be nice to be able to use a similar implementation that does make any use of the CSP licences
kevin
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")
Release page offers releases for both 2016.1 and 2016.2+. And in 2016.2+ $ methods are already removed.
If however you want to use latest commit from repo, then you need to run SMR.
Ed!
Consider you have another Github repo in which you want to use REST Forms repo. What is the best way to achieve this?
The best way to add RESTForms (or any other repository) into your repository is submodules. Submodule is a pointer to specific commit from another repository. On disk it looks like folder. More info on submodules. To add RESTForms execute in git cli:
Hi, did you check user name and/or password of Demo RestFormsUI? I cannot get access to that page...
Reenabled Demo user.
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 :(
Have you tried providing user credentials with %All access?
If you're getting this error for user with %All, and it's a dev box, add %All to /forms application.
Webapp roles:
RestForms App:
Roles:
RestFormsUI App:
Everything seems to be in order. Can you post an error from browser dev tools?
New RESTForms release should fix that error.
Thanks a lot for you assistance!
Well done. I will give this one a try in my next projects.
Eduard, great project!
Please, convert the source code into UDL! ;)
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:
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?
Added latest release, check it out.
Should be fixed now.
Ah, that's better, thanks for the quick fix!