├── CACHE.postman_environment.json ├── Form.inc.xml ├── Form ├── Adaptor.cls.xml ├── Field.cls.xml ├── File.cls.xml ├── Generators.cls.xml ├── Info.cls.xml ├── JSON │ ├── OBJ.cls.xml │ └── SQL.cls.xml ├── Property.cls.xml ├── REST │ ├── Abstract.cls.xml │ ├── Field.cls.xml │ ├── File.cls.xml │ ├── Form.cls.xml │ ├── Main.cls.xml │ ├── Object.cls.xml │ └── Objects.cls.xml ├── Security.cls.xml ├── Settings.cls.xml ├── Test │ ├── Address.cls.xml │ ├── Company.cls.xml │ ├── Person.cls.xml │ └── Simple.cls.xml └── Util │ ├── Converter.cls.xml │ ├── Init.cls.xml │ └── Translate.cls.xml ├── INTRO_RU.md ├── LICENSE ├── README.md ├── RESTForms.postman_collection.json └── sc-list.txt /CACHE.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f50753eb-56ac-9c48-7516-2731bc324c72", 3 | "name": "CACHE@localhost", 4 | "values": [ 5 | { 6 | "key": "host", 7 | "value": "localhost", 8 | "type": "text", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "port", 13 | "value": "57772", 14 | "type": "text", 15 | "enabled": true 16 | }, 17 | { 18 | "key": "ns", 19 | "value": "SAMPLES", 20 | "type": "text", 21 | "enabled": true 22 | }, 23 | { 24 | "key": "user", 25 | "value": "_SYSTEM", 26 | "type": "text", 27 | "enabled": true 28 | }, 29 | { 30 | "key": "pass", 31 | "value": "SYS", 32 | "type": "text", 33 | "enabled": true 34 | } 35 | ], 36 | "timestamp": 1485372526371, 37 | "_postman_variable_scope": "environment", 38 | "_postman_exported_at": "2017-01-25T19:36:53.787Z", 39 | "_postman_exported_using": "Postman/4.9.3" 40 | } -------------------------------------------------------------------------------- /Form.inc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45 | 46 | -------------------------------------------------------------------------------- /Form/Adaptor.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Base form adaptor class, all forms must inherit from this class 6 | 1 7 | Form.Info 8 | Form 9 | Form.Field 10 | Form.Security,Form.Generators 11 | 64090,75147.034342 12 | Form.Info 13 | 14 | 15 | 16 | Form name, not a global key so it can be anything 17 | Set to empty string (like here) to not have a class as a form 18 | %String 19 | 20 | 21 | 22 | 23 | Default permissions 24 | Objects of this form can be Created, Read, Updated and Deleted 25 | Redefine this parameter to change permissions for everyone 26 | Redefine checkPermission method (see Form.Security) for this class 27 | to add custom security based on user/roles/etc. 28 | %String 29 | CRUD 30 | 31 | 32 | 33 | 34 | Property used for basic information about the object 35 | By default getObjectDisplayName method gets its value from it 36 | %String 37 | displayName 38 | 39 | 40 | 41 | 42 | Use value of this parameter in SQL, as ORDER BY clause value 43 | %String 44 | 45 | 46 | 47 | 48 | Выводит объект Id в формате JSON на текущее устройство 49 | Переопределите этот метод для конкретной формы, если есть какие-то особенности её обработки 50 | Возможно: заменить метод на генератор, который будет генерить код для формы 51 | Подразумевается, что форма хранимая 52 | 1 53 | id:%Integer="" 54 | %Status 55 | 61 | 62 | 63 | 64 | 65 | Открывает объект по Id и перезаписывает все его свойства 66 | соответствующими значениями из object 67 | После чего сохраняет объект 68 | 1 69 | id:%Integer="",object="" 70 | %Status 71 | 81 | 82 | 83 | 84 | 85 | Устанавливает все поля текущего объекта из переданного объекта 86 | Это может быть динамический объект или объект того же класса 87 | objectgenerator 88 | object 89 | %Status 90 | 104 | 105 | 106 | 107 | 108 | Get basic information about one object 109 | 1 110 | 1 111 | id:%Integer 112 | %DynamicObject 113 | 119 | 120 | 121 | 122 | 124 | list - contains objects ids (not oids)]]> 125 | 1 126 | list:%ListOfDataTypes 127 | %DynamicArray 128 | 137 | 138 | 139 | 140 | 141 | Get value of a property specified in DISPLAYPROPERTY parameter. 142 | If it is stored, then GetStored value would be taken 143 | If it is calculated, then the object would be opened and the value would be calculated 144 | Redefine to implement your custom logic. 145 | Note, that if you redefine this method, it must be availible as an sql procedure and would be used as a display property. 146 | Also remenber to limit result length to 250-490 symbols depending on a collation in use. 147 | 1 148 | objectgenerator 149 | id:%Integer 150 | generateMetadata 151 | %String 152 | 1 153 | MAXLEN=250 154 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /Form/Field.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form field properties 6 | 1 7 | 64090,75443.892382 8 | 9 | 10 | 11 | Field display name 12 | %String 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Form/File.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %Persistent 5 | 64090,76829.939948 6 | 7 | 8 | 9 | File name as supplied by user 10 | %String 11 | 1 12 | 13 | 14 | 15 | 16 | 17 | 18 | Directory, where file is stored 19 | %String 20 | 1 21 | 22 | 23 | 24 | 25 | 26 | 27 | Full path to where file is stored 28 | %String 29 | set {*} = {dir} _ {attachmentHASH} 30 | 1 31 | attachmentHASH,dir 32 | 33 | 34 | 35 | 36 | 37 | 38 | Description 39 | %String 40 | 41 | 42 | 43 | 44 | 45 | Attachmentt SHA Hash in Base64 46 | %String 47 | 48 | 49 | 50 | 51 | The file itself (data is stored on disk, it's just a link) 52 | %FileBinaryStream 53 | 54 | 55 | 56 | %New method to 58 | provide notification that a new instance of an object is being created. 59 | 60 |

If this method returns an error then the object will not be created. 61 |

It is passed the arguments provided in the %New call. 62 | When customizing this method, override the arguments with whatever variables and types you expect to receive from %New(). 63 | For example, if you're going to call %New, passing 2 arguments, %OnNew's signature could be: 64 |

Method %OnNew(dob as %Date = "", name as %Name = "") as %Status 65 | If instead of returning a %Status code this returns an oref and this oref is a subclass of the current 66 | class then this oref will be the one returned to the caller of %New method.]]> 67 | name:%String="",dir:%String="",description:%String="",stream:%Stream.Object=##class(%FileBinaryStream).%New() 68 | 1 69 | %Status 70 | 1 71 | 82 | 83 | 84 | 85 | 86 | Serve file in web context 87 | %Status 88 | 100 | 101 | 102 | 103 | %Library.CacheStorage 104 | ^Form.FileD 105 | FileDefaultData 106 | ^Form.FileD 107 | ^Form.FileI 108 | ^Form.FileS 109 | 110 | 111 | %%CLASSNAME 112 | 113 | 114 | name 115 | 116 | 117 | description 118 | 119 | 120 | attachmentGUID 121 | 122 | 123 | attachmentHASH 124 | 125 | 126 | stream 127 | 128 | 129 | realName 130 | 131 | 132 | dir 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /Form/Generators.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Various generators called on form compilation which does not actually generate any code 6 | Generators that actually generate the code should be placed into Form.Adaptor class 7 | 1 8 | 64110,50058.313465 9 | 10 | 11 | 12 | End form callback method 13 | 1 14 | 15 | %Status 16 | 18 | 19 | 20 | 21 | 22 | Validate FORMORDERBY parameter value if present 23 | 1 24 | 1 25 | objectgenerator 26 | generateMetadata 27 | %String 28 | 45 | 46 | 47 | 48 | 49 | Validate FORMORDERBY parameter value if present 50 | 1 51 | 1 52 | objectgenerator 53 | generateMetadata 54 | %String 55 | 80 | 81 | 82 | 83 | 84 | Fill ^CacheMsg global for further translation 85 | 1 86 | 1 87 | objectgenerator 88 | generateMetadata 89 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Form/Info.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | Form 6 | 64090,75344.051371 7 | 8 | 9 | 10 | Get all forms list 11 | w ##class(Form.Info).getFormsList().$toJSONFormat() 12 | 1 13 | %DynamicArray 14 | 33 | 34 | 35 | 36 | 37 | Get all forms metadata 38 | w ##class(Form.Info).getFormsMetadata().$toJSONFormat() 39 | 1 40 | %DynamicObject 41 | 50 | 51 | 52 | 53 | 54 | Check that form with this classname exist 55 | 1 56 | className:%String 57 | %Boolean 58 | 66 | 67 | 68 | 69 | 70 | Check that form with this classname exist 71 | 1 72 | className:%String 73 | %Status 74 | 82 | 83 | 84 | 85 | w ##class(Form.Info).getFormMetadata("Form.Test.Simple").$toJSONFormat()]]> 91 | 1 92 | className:%String="" 93 | %DynamicObject 94 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /Form/JSON/OBJ.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Преобразует объект класса Cache в JSON. 6 | Изменения ищутся по слову FORMS 7 | %ZEN.Auxiliary.jsonProvider 8 | 64099,77101.820492 9 | Form.Settings 10 | 11 | 12 | JSON generation what to set as pVisited (it won't be revisited again, or skipped on duplicate hit)]]> 14 | 1 15 | objectgenerator 16 | 17 | 26 | 27 | 28 | 29 | Obj parsing, check what forms can be modified by user ]]> 31 | 1 32 | objectgenerator 33 | pObject:Form.Adaptor,pLevel:%Integer 34 | 43 | 44 | 45 | 46 | pObject to 48 | the current device using JSON notation. 49 | pFormat is a flags string to control output formatting options.
50 | The following character option codes are supported:
51 | 1-9 : indent with this number of spaces (4 is the default with the 'i' format specifier)
52 | a - output null arrays/objects
53 | b - line break before opening { of objects
54 | c - output the Caché-specific "_class" and "_id" properties
55 | d - output Caché numeric properties that have value "" as null
56 | e - output empty object properties
57 | i - indent with 4 spaces unless 't' or 1-9
58 | l - output empty lists
59 | n - newline (lf)
60 | o - output empty arrays/objects
61 | q - output numeric values unquoted even when they come from a non-numeric property
62 | s - use strict JSON output - NOTE: special care should be taken when sending data to a browser, as using this flag 63 | may expose you to cross site scripting (XSS) vulnerabilities if the data is sent inside <script> tags. Zen uses 64 | this technique extensively, so this flag should NOT be specified for jsonProviders in Zen pages.
65 | t - indent with tab character
66 | u - output pre-converted to UTF-8 instead of in native internal format
67 | w - Windows-style cr/lf newline
]]>
68 | 1 69 | 70 | %Status 71 | 1 "," 108 | If $IsObject(tValue) { 109 | If (tValue.%IsA("%ZEN.proxyObject")) { 110 | Set tSC = tValue.%ToJSON(pLevel+1,pFormat) 111 | Quit:$$$ISERR(tSC) 112 | } Else { 113 | Set tSC = ..%ObjectToJSON(tValue,.pVisited, pLevel+1, pFormat) 114 | Quit:$$$ISERR(tSC) 115 | } 116 | } Else { 117 | Write $$$ZENJSONVALUE(tValue,pFormat) 118 | } 119 | } 120 | Quit:$$$ISERR(tSC) 121 | If tIncludeWhitespace Set tIndent="", $P(tIndent,tTab,pLevel+1)="" Write tLF_tIndent 122 | Write "]" 123 | } 124 | Quit 125 | } 126 | ElseIf (pObject.%Extends("%Stream.Object")) { 127 | Write """" 128 | #; Initialize stream read length, if needed 129 | If '$data(tStreamMaxReadLen) Set tStreamMaxReadLen = ($$$MaxLocalLength\2) 130 | Do pObject.Rewind() 131 | While 'pObject.AtEnd { 132 | Write $$$ZENJSONESCAPE(pObject.Read(tStreamMaxReadLen),pFormat) 133 | } 134 | Write """" 135 | Quit 136 | } 137 | 138 | If pFormat["o" || 'pLevel { 139 | Set tPropCount = "" 140 | If (tIncludeWhitespace && pLevel) Set tIndent="", $P(tIndent,tTab,pLevel+1)="" Write $S(pFormat["b":tLF_tIndent,1:" ") 141 | Write "{" 142 | } Else { 143 | Set tPropCount = 0 144 | } 145 | If pFormat["c" { 146 | // add class name to model 147 | Do nextProp 148 | Write $$$ZENJSONPAIR("_class",tClass,pFormat) 149 | // add id for persistent objects 150 | If (pObject.%IsA("%Library.Persistent")) { 151 | Do nextProp 152 | Set tID = pObject.%Id() 153 | Write $$$ZENJSONPAIR("_id",tID,pFormat) 154 | } 155 | } 156 | 157 | #; Special treatment for top-level array: output no matter what 158 | If pObject.%Extends("%Collection.AbstractArray") { 159 | #; write out (eligible) array elements/properties 160 | If pObject.%Extends("%Collection.AbstractArrayOfObj") { 161 | #; object elements 162 | Set tKey="" For { Set tValue = pObject.GetNext(.tKey) Quit:""=tKey 163 | If $IsObject(tValue) { 164 | If tValue.%Extends("%Stream.Object")||tValue.%Extends("%IO.I.Stream") { 165 | Do tValue.Rewind() 166 | If (pFormat["e" || tValue.Size()) { 167 | Do nextProp 168 | Write $$$ZENJSONPROP(tKey,pFormat)_":""" 169 | #; Initialize stream read length, if needed 170 | If '$data(tStreamMaxReadLen) Set tStreamMaxReadLen = ($$$MaxLocalLength\2) 171 | #; Rewind non-%IO streams if needed 172 | If tValue.AtEnd && tValue.%Extends("%Stream.Object") Do tValue.Rewind() 173 | While 'tValue.AtEnd { 174 | Write $$$ZENJSONESCAPE(tValue.Read(tStreamMaxReadLen),pFormat) 175 | } 176 | Write """" 177 | } 178 | } ElseIf pFormat["o" || ..hasObjContent(tValue,.pVisited,pFormat) { 179 | Do nextProp 180 | Write $$$ZENJSONPROP(tKey,pFormat)_":" 181 | Set tSC = ..%ObjectToJSON(tValue,.pVisited, pLevel+1,pFormat) 182 | Quit:$$$ISERR(tSC) 183 | } 184 | } ElseIf pFormat["a" { 185 | Do nextProp 186 | Write $$$ZENJSONPROP(tKey,pFormat)_":null" 187 | } 188 | } ; end tKey object array loop 189 | } Else { 190 | #; scalar array elements 191 | Set tKey="" For { Set tValue = pObject.GetNext(.tKey) Quit:""=tKey 192 | If (pFormat["e") || (tValue'="") { 193 | Do nextProp 194 | Write $$$ZENJSONPAIR(tKey,tValue,pFormat) 195 | } 196 | } ; end tKey scalar array loop 197 | } 198 | If tPropCount'=0 { 199 | #; either we wrote at least one property or we wrote an empty '{' due to "o" mode or level zero 200 | If tIncludeWhitespace Set tIndent="", $P(tIndent,tTab,pLevel+1)="" Write tLF_tIndent 201 | Write "}" 202 | } 203 | Quit 204 | } 205 | #; else: main object is not a collection 206 | 207 | #; loop over properties using class meta-data 208 | Do ..getOrderedProps(tClass,.tProps) 209 | Set tSeq="" For { Set tSeq=$O(tProps(tSeq),1,tPropName) Quit:""=tSeq 210 | 211 | // Skipping private and internal properties 212 | Set tPrivate = +$$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPprivate) 213 | Set tInternal = +$$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPinternal) 214 | Continue:tPrivate||tInternal||(tPropName["%") 215 | 216 | Set tType = $$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPtype) 217 | Set tClsType = $$$getClassType(tType) 218 | Set tClientType = $$$comClassKeyGet(tType,$$$cCLASSclientdatatype) 219 | Set tCollection = $$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPcollection) 220 | If (tClsType '= "datatype") { 221 | #; Check for the case where we have a property declared as a %ListOf** 222 | If ($classmethod(tType,"%IsA","%Collection.AbstractList")) { 223 | Set tCollection = "list" 224 | If ($classmethod(tType,"%IsA","%Collection.AbstractListOfDT")) { 225 | #; Reset object information for %ListOfDataTypes 226 | Set tClientType = "VARCHAR" 227 | Set tDataType = "" 228 | } 229 | } 230 | } Else { 231 | Set tDataType=$Case(tClientType, "BOOLEAN":"b", "INTEGER":"n","NUMERIC":"n","FLOAT":"n", "TIMESTAMP":"u", "DATE":"d", "TIME":"t", :"") 232 | } 233 | Set tMultiDim = 0 234 | If (tCollection="array") { 235 | Set tCardinality = $$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPcardinality) 236 | Set tInverse = $$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPinverse) 237 | If ((tCardinality'="")&&(tInverse'="")) { 238 | // treat relationship as list 239 | Set tCollection = "list" 240 | } 241 | } ElseIf (tCollection = "") { 242 | Set tMultiDim = +$$$comMemberKeyGet(tClass,$$$cCLASSproperty,tPropName,$$$cPROPmultidimensional) 243 | } 244 | Continue:tMultiDim 245 | 246 | // FORMS - display datatypes in JSON format 247 | Set tValue = ##class(Form.Util.Converter).logicalToJSON(tType, $property(pObject,tPropName)) 248 | // FORMS - END - display datatypes in JSON format 249 | 250 | #; If the value is "" or $c(0) and we are NOT including empty properties, skip if we are not a collection, object or stream 251 | If (((tValue = "") || (tValue = $c(0))) && (pFormat'["e") && (tCollection = "") && $Case(tClientType, "HANDLE": 0, "CHARACTERSTREAM": 0, "BINARYSTREAM": 0, :1)) { 252 | Continue 253 | } 254 | // Write the property if not inhibited 255 | If (tCollection="list") { 256 | // list collection 257 | If '$IsObject(tValue) { 258 | Set tCount = 0 259 | } Else { 260 | Set tList = tValue 261 | Set tCount = tList.Count() 262 | } 263 | If (pFormat["l" || tCount) { 264 | Do nextProp 265 | Write $$$ZENJSONPROP(tPropName,pFormat)_":[" 266 | For n = 1:1:tCount { 267 | Set tValue = tList.GetAt(n) 268 | Write:n>1 "," 269 | If (tClientType = "HANDLE") { 270 | #; object items 271 | If $IsObject(tValue) { 272 | Set tSC = ..%ObjectToJSON(tValue,.pVisited, pLevel+1, pFormat) 273 | Quit:$$$ISERR(tSC) 274 | } Else { 275 | Write "null" ; not conditional because it has to hold the place in the list 276 | } 277 | } Else { 278 | #; scalar list item ; converts $List to empty string! 279 | Write $S(tDataType="b":$S(tValue:"true",1:"false") 280 | , ((tDataType="n")||(pFormat["q"))&&$$$ZENJSISNUM(tValue):$$$ZENJSNUM(tValue) 281 | , (tDataType="n")&&(tValue="")&&(pFormat["d"):"null" 282 | , ($C(0)=tValue)||$ListValid(tValue):"""""" 283 | //, "dtu"[tDataType:$S(pFormat["u":$$$ZENJSUSTR(..formatDateTime(tValue,tType,tDataType,pFormat)), 1:$$$ZENJSSTR(..formatDateTime(tValue,tType,tDataType,pFormat))) 284 | , 1:$$$ZENJSONSTR(tValue,pFormat)) 285 | } 286 | } 287 | Write "]" 288 | } 289 | } 290 | ElseIf (tCollection="array") { 291 | // array collection (object on client) 292 | If '$IsObject(tValue) { 293 | Set tKey = "" 294 | } Else { 295 | Set tArray = tValue 296 | Set tKey = tArray.Next("") 297 | If pFormat'["o" && (""'=tKey) { 298 | #; look ahead to see if there is any content 299 | Set tHasArrayContent=0, k=tKey While (k '= "") { Set tValue = tArray.GetAt(k) 300 | If (tClientType = "HANDLE") { 301 | If $IsObject(tValue) { 302 | If ..hasObjContent(tValue,.pVisited,pFormat) Set tHasArrayContent=1 Quit 303 | } ElseIf (pFormat["a") { 304 | Set tHasArrayContent=1 Quit 305 | } 306 | } Else { 307 | If $S(tDataType="b":1 308 | , $C(0)=tValue||$ListValid(tValue):pFormat["e" 309 | //, "dtu"[tDataType:$S(pFormat["u":$$$ZENJSUSTR(..formatDateTime(tValue,tType,tDataType,pFormat)), 1:$$$ZENJSSTR(..formatDateTime(tValue,tType,tDataType,pFormat))) 310 | , 1:""'=tValue||(pFormat["e")) { 311 | Set tHasArrayContent=1 Quit 312 | } 313 | } 314 | Set k = tArray.Next(k) 315 | } 316 | } 317 | } 318 | If (pFormat["o" || (""'=tKey && tHasArrayContent)) { 319 | Do nextProp 320 | Write $$$ZENJSONPROP(tPropName,pFormat)_": {" 321 | Set n = 0 322 | While (tKey '= "") { Set tValue = tArray.GetAt(tKey) 323 | If (tClientType = "HANDLE") { 324 | #; object elements 325 | If $IsObject(tValue) { 326 | Set n = n+1 327 | Write $S(n>1:",",1:"")_$$$ZENJSONPROP(tKey,pFormat)_":" 328 | Set tSC = ..%ObjectToJSON(tValue,.pVisited, pLevel+1, pFormat) 329 | Quit:$$$ISERR(tSC) 330 | } ElseIf (pFormat["a") { 331 | Set n = n+1 332 | Write $S(n>1:",",1:"")_$$$ZENJSONPROP(tKey,pFormat)_":null" 333 | } 334 | } Else { 335 | #; scalar array item ; converts $List to empty string! 336 | Set tStr = $S(tDataType="b":$S(tValue:"true",1:"false") 337 | , ((tDataType="n")||(pFormat["q"))&&$$$ZENJSISNUM(tValue):$$$ZENJSNUM(tValue) 338 | , (tDataType="n")&&(tValue="")&&(pFormat["d"):"null" 339 | , ($C(0)=tValue)||$ListValid(tValue):"""""" 340 | //, "dtu"[tDataType:$S(pFormat["u":$$$ZENJSUSTR(..formatDateTime(tValue,tType,tDataType,pFormat)), 1:$$$ZENJSSTR(..formatDateTime(tValue,tType,tDataType,pFormat))) 341 | , 1:$$$ZENJSONSTR(tValue,pFormat)) 342 | If (pFormat["e") || (tStr'="""""") { 343 | Set n = n+1 344 | Write $S(n>1:",",1:"")_$$$ZENJSONPROP(tKey,pFormat)_":"_tStr 345 | } 346 | } 347 | Set tKey = tArray.Next(tKey) 348 | } 349 | Write "}" 350 | } 351 | } 352 | ElseIf (tClientType = "HANDLE") { 353 | // object 354 | If $IsObject(tValue) { 355 | If ..hasObjContent(tValue,.pVisited,pFormat) || (pFormat["o") { 356 | Do nextProp 357 | Write $$$ZENJSONPROP(tPropName,pFormat)_":" 358 | Set tSC = ..%ObjectToJSON(tValue,.pVisited, pLevel+1, pFormat) 359 | Quit:$$$ISERR(tSC) 360 | } 361 | } ElseIf (pFormat["a") { 362 | Do nextProp 363 | Write $$$ZENJSONPROP(tPropName,pFormat)_":null" 364 | } 365 | } 366 | ElseIf (tClientType = "CHARACTERSTREAM") { 367 | If $IsObject(tValue) { 368 | If tValue.Size || (pFormat["e") { 369 | Do nextProp 370 | Write $$$ZENJSONPROP(tPropName,pFormat)_":""" 371 | If tValue.Size { 372 | #; Initialize stream read length, if needed 373 | If '$data(tStreamMaxReadLen) Set tStreamMaxReadLen = ($$$MaxLocalLength\2) 374 | Do tValue.Rewind() 375 | While 'tValue.AtEnd { 376 | Write $$$ZENJSONESCAPE(tValue.Read(tStreamMaxReadLen),pFormat) 377 | } 378 | } 379 | Write """" 380 | } 381 | } ElseIf (pFormat["a") { 382 | Do nextProp 383 | Write $$$ZENJSONPROP(tPropName,pFormat)_":null" 384 | } 385 | } 386 | ElseIf (tClientType = "BINARYSTREAM") { 387 | Do nextProp 388 | Write $$$ZENJSONPROP(tPropName,pFormat)_":null" 389 | } 390 | Else { 391 | #; scalar item ; converts $List to empty string! 392 | Set tStr = $S(tDataType="b":$S(tValue:"true",1:"false") 393 | , ((tDataType="n")||(pFormat["q"))&&$$$ZENJSISNUM(tValue):$$$ZENJSNUM(tValue) 394 | , (tDataType="n")&&(tValue="")&&(pFormat["d"):"null" 395 | , ($C(0)=tValue)||$ListValid(tValue):"""""" 396 | //, "dtu"[tDataType:$S(pFormat["u":$$$ZENJSUSTR(..formatDateTime(tValue,tType,tDataType,pFormat)), 1:$$$ZENJSSTR(..formatDateTime(tValue,tType,tDataType,pFormat))) 397 | , 1:$$$ZENJSONSTR(tValue,pFormat)) 398 | 399 | If (pFormat["e") || (tStr'="""""") { 400 | Do nextProp 401 | Write $$$ZENJSONPROP(tPropName,pFormat)_":"_tStr 402 | } 403 | } 404 | } ; end properties loop 405 | Quit:$$$ISERR(tSC) 406 | 407 | If tPropCount'=0 { 408 | #; either we wrote at least one property or we wrote an empty '{' due to "o" mode or level zero 409 | If tIncludeWhitespace Set tIndent="", $P(tIndent,tTab,pLevel+1)="" Write tLF_tIndent 410 | Write "}" 411 | } 412 | } 413 | Catch ex { 414 | Set tSC = ex.AsStatus() 415 | } 416 | Quit tSC 417 | 418 | nextProp 419 | If tPropCount=0 { 420 | If (tIncludeWhitespace && pLevel) Set tIndent="", $P(tIndent,tTab,pLevel+1)="" Write $S(pFormat["b":tLF_tIndent,1:" ") 421 | Write "{" 422 | } ElseIf tPropCount { 423 | Write "," 424 | } ; else tPropCount="" means we already did the starting '{' due to "o" mode 425 | Set tPropCount = tPropCount + 1 426 | If tIncludeWhitespace Set tIndent="", $P(tIndent,tTab,pLevel+2)="" Write tLF_tIndent 427 | Quit 428 | ]]> 429 |
430 | 431 | 432 | pJSON containing JSON notation 434 | and convert it to an object instance pObject.
435 | pJSON could also be a character stream.
436 | pClass is the name of the class to create to hold 437 | the instantiated object. This class must match the data within the JSON 438 | notation. If pClass is empty (""), then an instance 439 | of the generic class %ZEN.proxyObject will be created. 440 | pCharsProcessed and pLevel are used 441 | internally and do not have to be supplied. 442 | pIgnoreUnknownProps controls whether we will 443 | ignore errors when we process a property that isn't expected. The default 444 | behaviour is to treat this as an error.
445 | Note that this method assumes well-formed JSON notation: it does 446 | not perform complete error checking.]]>
447 | 1 448 | 1 449 | pJSON:%String,pClass:%String="",*pObject:%RegisteredObject,*pCharsProcessed:%Integer,pLevel:%Integer=1,pFirstChar:%String="",pIgnoreUnknownProps:%Boolean=0 450 | %Status 451 | $L(pJSON)) Quit 492 | Set ch = $E(pJSON,p) 493 | } 494 | 495 | Set p = p + 1 496 | Set pCharsProcessed = pCharsProcessed + 1 497 | If (tState = 0) { 498 | If (ch = "{") { 499 | // start of object 500 | // we will hold the property values in here until the end 501 | Kill tPropValues 502 | Set pObject = "" 503 | Set tState = 1 504 | } 505 | ElseIf (ch = "[") { 506 | Set tJSONArray = 1 507 | Kill tPropValues 508 | Set pObject = "" 509 | Set tCollectionClass = $select((pClass '= "")&&$classmethod(pClass,"%Extends","%Collection.AbstractList"): pClass, 1 :"") 510 | // start of list/array-valued property 511 | Set tInArray = 1 512 | Set tArrayType = "list" 513 | Kill tArray 514 | Set tArrayIndex = 0 515 | Set tToken = "" 516 | Set tIsString = 0 517 | Set tState = 5 518 | Set tArrayState = "value" 519 | } 520 | ElseIf '$$$WHITESPACE(ch) { 521 | Set tSC = $$$ERROR($$$GeneralError,"Expected { at start of JSON text.") 522 | Quit 523 | } 524 | } 525 | ElseIf (tState = 1) { 526 | If (ch = "}") { 527 | // end of object 528 | // create object, stuff properties into it 529 | Set pClass = $G(tPropValues("_class"),pClass) 530 | Set tClass = $G(tPropValues("_class"),tClass) 531 | 532 | // FORMS - instantiate object by id 533 | Set tId = $G(tPropValues("_id")) 534 | 535 | If tId'="" { 536 | Set pObject = $classmethod(tClass,"%OpenId", tId) 537 | } Else { 538 | Set pObject = $classmethod(tClass,"%New") 539 | } 540 | // FORMS - END - instantiate object by id 541 | 542 | Set p = $O(tPropValues("")) 543 | While (p'="") { 544 | If (p '= "_class") && (p '= "_id") { 545 | Try { 546 | // test for stream property 547 | Set tStream = $property(pObject,p) 548 | If ($IsObject(tStream) && (tStream.%Extends("%Stream.Object") || tStream.%Extends("%IO.I.Stream"))) { 549 | Do tStream.Rewind() 550 | Do tStream.Write($G(tPropValues(p))) 551 | } 552 | Else { 553 | Set $property(pObject,p) = $G(tPropValues(p)) 554 | } 555 | } 556 | Catch ex { 557 | If $case(ex.Name, "" : 0, "": 'pIgnoreUnknownProps, :1) Throw ex 558 | } 559 | } 560 | Set p = $O(tPropValues(p)) 561 | } 562 | Quit 563 | } 564 | ElseIf (ch = """") && ('tPropQuoted) { 565 | Set tPropQuoted = 1 566 | } 567 | ElseIf ('$$$WHITESPACE(ch) && (ch'="")) { 568 | // start of property name 569 | Set tToken = ch 570 | Set tState = 2 571 | } 572 | } 573 | ElseIf (tState = 2) { 574 | // property name 575 | If (ch = "\") { 576 | Set tState = "2a" 577 | } 578 | ElseIf (tPropQuoted) { 579 | If (ch = """") { 580 | Set tPropQuoted = 0 581 | } 582 | Else { 583 | Set:'$IsObject(tToken) tToken = tToken _ ch 584 | } 585 | } 586 | Else { 587 | If (ch = ":") { 588 | Set tProperty = tToken 589 | #; Set tProperty = $select($IsObject(tToken): tToken, 1: ..%UnescapeJSONString(tToken)) 590 | Set tToken = "" 591 | Set tState = 3 592 | Set tIsString = 0 593 | } 594 | ElseIf ('$$$WHITESPACE(ch)) { 595 | Set:'$IsObject(tToken) tToken = tToken _ ch 596 | } 597 | } 598 | } 599 | // NOTE: States 2a, 2b and 2c are defined as the last few states as we expect escaped property names to be very rare 600 | ElseIf (tState = 3) { 601 | // value 602 | If (ch = ",") { 603 | // end of value 604 | If (tIsString || $IsObject(tToken)) { 605 | Set tValue = tToken 606 | } 607 | Else { 608 | Set tValue = $Case(tToken,"null":"","true":1,"false":0,:+tToken) 609 | } 610 | If (tProperty '= "") { 611 | Set tPropValues(tProperty) = tValue 612 | } 613 | Set pClass = $G(tPropValues("_class"),pClass) 614 | Set tToken = "" 615 | Set tValue = "" 616 | Set tState = 1 617 | } 618 | ElseIf (ch = "}") { 619 | // end of value and object 620 | If (tIsString || $IsObject(tToken)) { 621 | Set tValue = tToken 622 | } 623 | Else { 624 | Set tValue = $Case(tToken,"null":"","true":1,"false":0,:+tToken) 625 | } 626 | If (tProperty '= "") { 627 | Set tPropValues(tProperty) = tValue 628 | } 629 | 630 | // create object, stuff properties into it 631 | Set pClass = $G(tPropValues("_class"),pClass) 632 | Set tClass = $G(tPropValues("_class"),tClass) 633 | 634 | // FORMS - instantiate object by id 635 | Set tId = $G(tPropValues("_id")) 636 | 637 | If tId'="" { 638 | Set pObject = $classmethod(tClass,"%OpenId", tId) 639 | If ((##class(Form.Info).formExists(pObject.%ClassName(1))) && ('..CanModify(pObject, pLevel))) { 640 | /// FORMS - object is a form, but not a base form 641 | /// so we open it and that's it 642 | /// CanModify method can be affected by canModify setting 643 | Quit 644 | } 645 | } Else { 646 | Set pObject = $classmethod(tClass,"%New") 647 | } 648 | // FORMS - END - instantiate object by id 649 | 650 | Set p = $O(tPropValues("")) 651 | While (p'="") { 652 | If (p '= "_class") && (p '= "_id") { 653 | Try { 654 | // test for stream property 655 | Set tStream = $property(pObject,p) 656 | If ($IsObject(tStream) && (tStream.%Extends("%Stream.Object") || tStream.%Extends("%IO.I.Stream"))) { 657 | Do tStream.Rewind() 658 | Do tStream.Write($G(tPropValues(p))) 659 | } ElseIf ($IsObject(tStream) && tStream.%Extends("%Library.Persistent")) { 660 | // FORMS - set persistent properties 661 | If $IsObject($G(tPropValues(p))) { 662 | Set tStream = tPropValues(p) 663 | } Else { 664 | Set tStream = tStream.%OpenId($G(tPropValues(p)),,.st) 665 | } 666 | 667 | Set $property(pObject,p) = tStream 668 | // set relashonship 669 | } ElseIf ($$$comMemberKeyGet($classname(pObject),$$$cCLASSproperty,p,$$$cPROPrelationship)) { 670 | Set cardinality = $$$defMemberKeyGet($classname(pObject),$$$cCLASSproperty,p,$$$cPROPcardinality) 671 | 672 | If cardinality = "one" { 673 | Set $property(pObject,p) = tPropValues(p) 674 | } Else { 675 | // TODO: parent-child 676 | #dim list As %Collection.ListOfObj 677 | Set list = tPropValues(p) 678 | For i = 1:1:list.Count() { 679 | Do $method($property(pObject,p),"Insert",list.GetAt(i)) 680 | } 681 | } 682 | }Else { 683 | // set datatypes 684 | Set tPropClassName = ##class(Form.Property).getPropertyType($classname(pObject), p) 685 | Set $property(pObject,p) = ##class(Form.Util.Converter).jsonToLogical(tPropClassName, $G(tPropValues(p))) 686 | // FORMS - END - set persistent properties 687 | } 688 | } 689 | Catch ex { 690 | If $case(ex.Name, "": 0, "": 'pIgnoreUnknownProps, :1) Throw ex 691 | } 692 | } 693 | Set p = $O(tPropValues(p)) 694 | } 695 | Set tToken = "" 696 | Set tValue = "" 697 | Quit 698 | } 699 | ElseIf (ch = "{") { 700 | // start of object-valued property 701 | Set pClass = $G(tPropValues("_class"),pClass) 702 | 703 | If ((pClass="")||(tProperty="")) { 704 | Set tChildClass = "" 705 | Set tCollection = "" 706 | } 707 | Else { 708 | // lookup type in meta data 709 | Set tChildClass = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPtype) 710 | Set tCollection = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPcollection) 711 | } 712 | // Note: This if block assumes pClass and tProperty are defined when tCollection '= "" 713 | If (tCollection = "array") { 714 | // start of array-valued property 715 | Set tArrayType = "array" 716 | Set tArrayKey = "" 717 | Set tInArray = 1 718 | Kill tArray 719 | Set tToken = "" 720 | Set tIsString = 0 721 | Set tState = 5 722 | Set tArrayState = "name" 723 | 724 | // look up the runtime type of the array 725 | // set tCollectionClass to the runtime type if the runtime type is not in %Library or %Collection 726 | Set tCollectionClass = "" 727 | Set tArrayRuntimeType = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPruntimetype) 728 | If (tArrayRuntimeType '= "") { 729 | Set tArrayRuntimePackage = $piece(tArrayRuntimeType,".",1) 730 | If (tArrayRuntimePackage '= "%Library") && (tArrayRuntimePackage '= "%Collection") { 731 | Set tCollectionClass = tArrayRuntimeType 732 | } 733 | } 734 | } 735 | Else { 736 | If ($IsObject(pJSON)) { 737 | Set tSubJSON = pJSON 738 | Set tPoke = ch // simulate stream unwind 739 | } 740 | Else { 741 | Set tSubJSON = $E(pJSON,p-1,*) 742 | Set tPoke = "" 743 | } 744 | Set tSC = ..%ParseJSON(tSubJSON,tChildClass,.tToken,.tChars,pLevel+1,tPoke,pIgnoreUnknownProps) 745 | If $$$ISERR(tSC) Quit 746 | Set p = p + tChars - 1 747 | Set pCharsProcessed = pCharsProcessed + tChars - 1 748 | } 749 | } 750 | ElseIf (ch = "[") { 751 | Set tCollectionClass = "" 752 | If ((pClass'="")&&(tProperty'="")) { 753 | // lookup type in meta data 754 | // we could have a normal collection: List Of PropType 755 | // OR 756 | // the proptype could be a subclass of a collection 757 | Set tCollectionClass = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPtype) 758 | If (tCollectionClass '= "") && (($$$comClassKeyGet(tCollectionClass,$$$cCLASSclasstype)="datatype") || '$classmethod(tCollectionClass,"%Extends","%Collection.AbstractIterator")) { 759 | // use "built-in" collection 760 | Set tCollectionClass = "" 761 | } 762 | } 763 | 764 | // start of list/array-valued property 765 | Set tInArray = 1 766 | Set tArrayType = "list" 767 | Kill tArray 768 | Set tArrayIndex = 0 769 | Set tToken = "" 770 | Set tIsString = 0 771 | Set tState = 5 772 | Set tArrayState = "value" 773 | } 774 | ElseIf ((ch = """")||(ch = "'")) { 775 | // start of string 776 | Set tToken = "" 777 | Set tIsString = 1 778 | Set tQuote = ch 779 | Set tState = 4 780 | } 781 | ElseIf ('$$$WHITESPACE(ch)) { 782 | // must be a numeric value, or true,false,or null 783 | Set:'$IsObject(tToken) tToken = tToken _ ch 784 | } 785 | } 786 | ElseIf (tState = 4) { 787 | // string literal 788 | If (ch = "\") { 789 | // escape? 790 | Set tState = "4a" 791 | } 792 | ElseIf (ch = tQuote) { 793 | // end of string 794 | If (tInArray) { 795 | Set tState = 5 796 | } 797 | Else { 798 | Set tState = 3 799 | } 800 | } 801 | Else { 802 | Set:'$IsObject(tToken) tToken = tToken _ ch 803 | } 804 | } 805 | // NOTE: States 4a, 4b and 4c are defined *after* state 5 as we expect escaped text less often than arrays (state 5) 806 | ElseIf (tState = 5) { 807 | // array items 808 | If (ch = ",") { 809 | // end of array item 810 | If (tArrayType = "list") { 811 | Set tArrayIndex = tArrayIndex + 1 812 | } 813 | If (tIsString || $IsObject(tToken)) { 814 | Set tValue = tToken 815 | } 816 | Else { 817 | Set tValue = $Case(tToken,"null":"","true":1,"false":0,:+tToken) 818 | } 819 | If (tArrayType = "list") { 820 | Set tArray(tArrayIndex) = tValue 821 | } 822 | ElseIf (tArrayKey'="") { 823 | Set tArray(tArrayKey) = tValue 824 | } 825 | Set tToken = "" 826 | Set tArrayKey = "" 827 | Set tIsString = 0 828 | If (tArrayType = "list") { 829 | Set tArrayState = "value" 830 | } 831 | Else { 832 | Set tArrayState = "name" 833 | } 834 | } 835 | ElseIf ((tArrayType="list")&&(ch = "]")) { 836 | // end of list array 837 | If (tToken '= "") { 838 | Set tArrayIndex = tArrayIndex + 1 839 | If (tIsString || $IsObject(tToken)) { 840 | Set tValue = tToken 841 | } 842 | Else { 843 | Set tValue = $Case(tToken,"null":"","true":1,"false":0,:+tToken) 844 | } 845 | Set tArray(tArrayIndex) = tValue 846 | } 847 | 848 | If ($G(tCollectionClass)'="") { 849 | Set tListObj = $classmethod(tCollectionClass,"%New") 850 | } 851 | Else { 852 | #; Look for first non-"" value to determine whether the list contains objects or datatypes 853 | Set tUseObjectArray = 1 854 | Set n = $O(tArray("")) 855 | While n { 856 | If $IsObject($G(tArray(n))) Quit 857 | If ($G(tArray(n)) '= "") { 858 | Set tUseObjectArray = 0 859 | Quit 860 | } 861 | Set n = $O(tArray(n)) 862 | } 863 | Set tListObj = $select(tUseObjectArray: ##class(%Library.ListOfObjects).%New(), 1: ##class(%Library.ListOfDataTypes).%New()) 864 | } 865 | Set tCollectionClass = "" 866 | Set n = $O(tArray("")) 867 | While (n'="") { 868 | Do tListObj.Insert(tArray(n)) 869 | Set n = $O(tArray(n)) 870 | } 871 | 872 | Set tToken = tListObj 873 | Set tListObj = "" 874 | Set tInArray = 0 875 | Kill tArray 876 | Set tArrayIndex = 0 877 | Set tState = 3 878 | If tJSONArray { 879 | Set pObject = tToken 880 | Set tJSONArray = 0 881 | Quit 882 | } 883 | } 884 | ElseIf ((tArrayType="array")&&(ch = "}")) { 885 | // end of array 886 | If (tToken '= "") { 887 | If (tIsString || $IsObject(tToken)) { 888 | Set tValue = tToken 889 | } 890 | Else { 891 | Set tValue = $Case(tToken,"null":"","true":1,"false":0,:+tToken) 892 | } 893 | If (tArrayKey'="") { 894 | Set tArray(tArrayKey) = tValue 895 | } 896 | } 897 | 898 | If ($G(tCollectionClass)'="") { 899 | Set tArrayObj = $classmethod(tCollectionClass,"%New") 900 | } 901 | Else { 902 | Set tUseObjectArray = 1 903 | Set n = $O(tArray("")) 904 | While n '= "" { 905 | If $IsObject($G(tArray(n))) Quit 906 | If ($G(tArray(n)) '= "") { 907 | Set tUseObjectArray = 0 908 | Quit 909 | } 910 | Set n = $O(tArray(n)) 911 | } 912 | Set tArrayObj = $select(tUseObjectArray: ##class(%Library.ArrayOfObjects).%New(), 1: ##class(%Library.ArrayOfDataTypes).%New()) 913 | } 914 | Set tCollectionClass = "" 915 | 916 | Set n = $O(tArray("")) 917 | While (n'="") { 918 | Do tArrayObj.SetAt(tArray(n),n) 919 | Set n = $O(tArray(n)) 920 | } 921 | 922 | Set tToken = tArrayObj 923 | Set tArrayObj = "" 924 | Set tInArray = 0 925 | Kill tArray 926 | Set tArrayIndex = 0 927 | Set tArrayKey = "" 928 | Set tState = 3 929 | } 930 | ElseIf (ch = "{") { 931 | // object-valued item: token is the object 932 | If (pClass'="") && (tProperty="") && $classmethod(pClass,"%Extends","%Library.ListOfObjects") { 933 | Set tPropElementType = $parameter(pClass,"ELEMENTTYPE") 934 | Set tChildClass = $select(tPropElementType = "%RegisteredObject": "", 1: tPropElementType) 935 | } 936 | ElseIf ((pClass="")||(tProperty="")) { 937 | Set tChildClass = "" 938 | } 939 | Else { 940 | // lookup type in meta data 941 | If (tCollectionClass="") { 942 | // property types tells us the type of items in the collection 943 | Set tChildClass = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPtype) 944 | } 945 | Else { 946 | // we have to get the element type from the collection class 947 | Set tChildClass = $parameter(tCollectionClass,"ELEMENTTYPE") 948 | } 949 | } 950 | If ($IsObject(pJSON)) { 951 | Set tSubJSON = pJSON 952 | Set tPoke = ch // simulate stream unwind 953 | } 954 | Else { 955 | Set tSubJSON = $E(pJSON,p-1,*) 956 | Set tPoke = "" 957 | } 958 | 959 | Set tSC = ..%ParseJSON(tSubJSON,tChildClass,.tToken,.tChars,pLevel+1,tPoke,pIgnoreUnknownProps) 960 | If $$$ISERR(tSC) Quit 961 | Set p = p + tChars - 1 962 | Set pCharsProcessed = pCharsProcessed + tChars - 1 963 | } 964 | ElseIf (ch = "[") { 965 | If ((pClass="")||(tProperty="")) { 966 | Set tChildCollectionClass = "" 967 | } 968 | Else { 969 | Set tChildCollectionClass = $$$comMemberKeyGet(pClass,$$$cCLASSproperty,tProperty,$$$cPROPtype) 970 | If (tChildCollectionClass '= "") && (($$$comClassKeyGet(tCollectionClass,$$$cCLASSclasstype)="datatype") || '$classmethod(tCollectionClass,"%Extends","%Collection.AbstractIterator")) { 971 | // use "built-in" collection 972 | Set tChildCollectionClass = "" 973 | } 974 | } 975 | If ($IsObject(pJSON)) { 976 | Set tSubJSON = pJSON 977 | Set tPoke = ch // simulate stream unwind 978 | } 979 | Else { 980 | Set tSubJSON = $E(pJSON,p-1,*) 981 | Set tPoke = "" 982 | } 983 | Set tSC = ..%ParseJSON(tSubJSON,tChildCollectionClass,.tToken,.tChars,pLevel+1,tPoke,pIgnoreUnknownProps) 984 | If $$$ISERR(tSC) Quit 985 | Set p = p + tChars - 1 986 | Set pCharsProcessed = pCharsProcessed + tChars - 1 987 | } 988 | ElseIf ((ch = """")||(ch = "'")) { 989 | // start of string 990 | Set tToken = "" 991 | Set tIsString = 1 992 | Set tQuote = ch 993 | Set tState = 4 994 | } 995 | ElseIf ((tArrayType="array")&&(ch=":")) { 996 | // end of name 997 | If (tArrayState = "name") { 998 | Set tArrayState = "value" 999 | Set tArrayKey = tToken 1000 | Set tToken = "" 1001 | } 1002 | } 1003 | ElseIf ('$$$WHITESPACE(ch)) { 1004 | // literal 1005 | Set:'$IsObject(tToken) tToken = tToken _ ch 1006 | } 1007 | } 1008 | // NOTE: States 4a, 4b and 4c precede states 2a, 2b and 2c as we expect literal strings to need escaping more often than property names 1009 | ElseIf (tState = "4a") { 1010 | // \ in string 1011 | If (ch = "u") { 1012 | Set tUnicodeHex = "" 1013 | Set tState = "4b" 1014 | } 1015 | // add special case support for \xNN escape sequences that are valid in Javascript 1016 | ElseIf (ch = "x") { 1017 | Set tHex = "" 1018 | Set tState = "4c" 1019 | } 1020 | Else { 1021 | // Support escape sequences defined in RFC 4627, as well as \' 1022 | Set tToken = tToken _ $Case(ch, "\": "\", "'": "'", """": """", "/": "/", "b": $char(8), "f": $char(12), "n": $char(10), "r": $char(13), "t": $char(9), : "\" _ ch) 1023 | Set tState = 4 1024 | } 1025 | } 1026 | ElseIf (tState = "4b") { 1027 | // in \uXXXX escape sequence 1028 | Set tUnicodeHex = tUnicodeHex _ ch 1029 | If ($length(tUnicodeHex) = 4) { 1030 | // Check that we do actually have a Hex value 1031 | If $$$MATCHHEXCHARS(tUnicodeHex,4) { 1032 | Set tUnicodeDecimal = $zhex(tUnicodeHex) 1033 | Set tToken = tToken _ $char(tUnicodeDecimal) 1034 | } 1035 | Else { 1036 | Set tToken = tToken _ "\u" _ tUnicodeHex 1037 | } 1038 | Set tState = 4 1039 | } 1040 | } 1041 | ElseIf (tState = "4c") { 1042 | // in \xNN escape sequence 1043 | Set tHex = tHex _ ch 1044 | If ($length(tHex) = 2) { 1045 | // Check that we do actually have a Hex value 1046 | If $$$MATCHHEXCHARS(tHex,2) { 1047 | Set tCodeDecimal = $zhex(tHex) 1048 | Set tToken = tToken _ $char(tCodeDecimal) 1049 | } 1050 | Else { // Not a hex escape 1051 | Set tToken = tToken _ "\x" _ tHex 1052 | } 1053 | Set tState = 4 1054 | } 1055 | } 1056 | ElseIf (tState = "2a") { 1057 | // \ in property name 1058 | If (ch = "u") { 1059 | Set tUnicodeHex = "" 1060 | Set tState = "2b" 1061 | } 1062 | // add special case support for \xNN escape sequences that are valid in Javascript 1063 | ElseIf (ch = "x") { 1064 | Set tHex = "" 1065 | Set tState = "2c" 1066 | } 1067 | Else { 1068 | // Support escape sequences defined in RFC 4627, as well as \' 1069 | Set tToken = tToken _ $Case(ch, "\": "\", "'": "'", """": """", "/": "/", "b": $char(8), "f": $char(12), "n": $char(10), "r": $char(13), "t": $char(9), : "\" _ ch) 1070 | Set tState = 2 1071 | } 1072 | } 1073 | ElseIf (tState = "2b") { 1074 | // in \uXXXX escape sequence 1075 | Set tUnicodeHex = tUnicodeHex _ ch 1076 | If ($length(tUnicodeHex) = 4) { 1077 | #; Check that we do actually have a Hex value 1078 | If $$$MATCHHEXCHARS(tUnicodeHex,4) { 1079 | Set tUnicodeDecimal = $zhex(tUnicodeHex) 1080 | Set tToken = tToken _ $char(tUnicodeDecimal) 1081 | } 1082 | Else { 1083 | Set tToken = tToken _ "\u" _ tUnicodeHex 1084 | } 1085 | Set tState = 2 1086 | } 1087 | } 1088 | ElseIf (tState = "2c") { 1089 | // in \xNN escape sequence 1090 | Set tHex = tHex _ ch 1091 | If ($length(tHex) = 2) { 1092 | #; Check that we do actually have a Hex value 1093 | If $$$MATCHHEXCHARS(tHex,2) { 1094 | Set tCodeDecimal = $zhex(tHex) 1095 | Set tToken = tToken _ $char(tCodeDecimal) 1096 | } 1097 | Else { // Not a hex escape 1098 | Set tToken = tToken _ "\x" _ tHex 1099 | } 1100 | Set tState = 2 1101 | } 1102 | } 1103 | } 1104 | } 1105 | Catch ex { 1106 | // Do ..%WriteJSONToFile(pJSON,"jsonout.txt") 1107 | Set tSC = ex.AsStatus() 1108 | } 1109 | Quit tSC 1110 | ]]> 1111 |
1112 |
1113 |
1114 | -------------------------------------------------------------------------------- /Form/JSON/SQL.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Form 5 | %ZEN.Auxiliary.altJSONSQLProvider 6 | 64091,68899.667254 7 | 8 | 9 | 10 | conditionClass = main class 11 | conditionClass(alias) = some other class 12 | 1 13 | 14 | %Status 15 | 42 | 43 | 44 | 45 | w ##class(Form.JSON.SQL).ParseWhere("Text eq Admin Text neq 1", , "Form.Test.Simple", .out) 48 | result: out=" WHERE Text='Admin' AND Text='1'"]]> 49 | 1 50 | 51 | %Status 52 | in filter values eith whitespaces 74 | set value = $TR(value,"'"";") 75 | 76 | if $l(column, ".") = 1 { 77 | set columnValid = ..ParseColumn(.column, conditionClass, .propertyClass) 78 | return:'columnValid $$$ERROR($$$GeneralError,"Filter column '" _column _ "' is not valid for " _ conditionClass) 79 | set:$system.SQL.IsReservedWord(column) column = """" _ column _ """" 80 | } elseif $l(column, ".") = 2 { 81 | set alias = $p(column, ".", 1) 82 | return:'$d(conditionClass(alias)) $$$ERROR($$$GeneralError,"Unknown alias " _ alias) 83 | 84 | set column = $p(column, ".", 2) 85 | set columnValid = ..ParseColumn(.column, conditionClass(alias), .propertyClass) 86 | return:'columnValid $$$ERROR($$$GeneralError,"Filter column '" _column _ "' is not valid for " _ conditionClass(alias)) 87 | 88 | set:$system.SQL.IsReservedWord(column) column = """" _ column _ """" 89 | set column = alias _ "." _ column 90 | } else { 91 | return $$$ERROR($$$GeneralError,"Filter column '" _column _ "' is not valid for " _ conditionClass) 92 | } 93 | 94 | set value = ##class(Form.Util.Converter).jsonToLogical(propertyClass, value) 95 | 96 | set sqlpredicate = ..TranslatePredicate(predicate) 97 | if sqlpredicate="" { 98 | return $$$ERROR($$$GeneralError,"Incorrect filter predicate") 99 | } 100 | 101 | #define sq(%str) "'" _ %str _ "'" 102 | if sqlpredicate = "IN" { 103 | 104 | set valTemp = "(" 105 | for j=1:1:$length(value, "~") { 106 | set curVal = $piece(value, "~", j) 107 | set curVal = $$$sq(curVal) 108 | set:curVal="'$$$NULL'" curVal = "NULL" 109 | set:((collation'="") && (curVal'="NULL")) curVal = collation _ "(" _ curVal _ ")" 110 | 111 | set valTemp = valTemp _ curVal 112 | set:j'=$length(value, "~") valTemp = valTemp _ ", " 113 | } 114 | set value = valTemp _ ")" 115 | } else { 116 | // bake ((value)) values as a (('value')) to help the optimizer 117 | if (($e(value,1,2)="((") && ($e(value,*-1,*)="))")) { 118 | set value = $e(value, 3,*-3) 119 | set value = $$$sq(value) 120 | set value = "((" _ value _ "))" 121 | } else { 122 | set value = $$$sq(value) 123 | } 124 | set:value="'$$$NULL'" value = "NULL" 125 | } 126 | 127 | if collation'="" { 128 | set:((value'="NULL") && (sqlpredicate '="IN")) value = collation _ "(" _ value _ ")" 129 | set column = collation _ "(" _ column _ ")" 130 | } 131 | 132 | set condition = condition _ column _ " " _ sqlpredicate _ " " _ value 133 | } 134 | return $$$OK 135 | ]]> 136 | 137 | 138 | 139 | w ##class(Form.JSON.SQL).ParseColumn("company->addrerss_City", "Form.Test.Person", .c)]]> 142 | 1 143 | 144 | %Boolean 145 | ") // Serial references 148 | set columnOut = "" 149 | 150 | set length = $length(column,"->") 151 | for j = 1:1:length { 152 | // Determine current class context and property name 153 | set currentClass = $case(j, 1:conditionClass, :##class(Form.Property).getPropertyType(currentClass, currentProperty)) // first piece - conditionClass, otherwise - previous class 154 | set currentProperty = $piece(column,"->",j) 155 | 156 | // Last property may be an ID (which is not a %Dictionary.CompiledProperty cilumn, 157 | // so quit from the for cycle 158 | if ((j=length) && ($zcvt(currentProperty, "U") = "ID")) { 159 | set columnOut = columnOut _ "ID" 160 | quit 161 | } 162 | 163 | // Property does not exist. Abort. 164 | if '##class(%Dictionary.CompiledProperty).IDKEYExists(currentClass, currentProperty) { 165 | return $$$NO 166 | } 167 | 168 | // Get property SQL name (usually equal to property name) 169 | set currentPropertySQLName = ##class(Form.Property).getPropertySQLName(currentClass, currentProperty) 170 | set:$system.SQL.IsReservedWord(currentPropertySQLName) currentPropertySQLName = """" _ currentPropertySQLName _ """" 171 | 172 | // Append property SQL name to resulting sql expression 173 | set columnOut = columnOut _ currentPropertySQLName 174 | 175 | // Append _ or -> separator if this is not a last column 176 | if j'=length { 177 | set propertyType = ##class(Form.Property).getPropertyType(currentClass, currentProperty) 178 | if $$$classIsSerial(propertyType) { 179 | set separator = "_" 180 | } else { 181 | set separator = "->" 182 | } 183 | set columnOut = columnOut _ separator 184 | } 185 | } 186 | set finalPropertyClass = ##class(Form.Property).getPropertyType(currentClass, currentProperty) 187 | set column = columnOut 188 | 189 | return $$$YES 190 | ]]> 191 | 192 | 193 | 194 | 195 | Translate predicate from js into sql 196 | 1 197 | predicate="" 198 | %String 199 | =" 204 | q:predicate="gt" ">" 205 | q:predicate="lte" "<=" 206 | q:predicate="lt" "<" 207 | q:predicate="startswith" "%STARTSWITH" 208 | q:predicate="contains" "[" 209 | q:predicate="doesnotcontain" "NOT[" 210 | q:predicate="in" "IN" 211 | q:predicate="is" "IS" 212 | q:predicate="isnot" "IS NOT" 213 | q:predicate="like" "LIKE" 214 | 215 | q "" 216 | ]]> 217 | 218 | 219 | 220 | 221 | w ##class(Form.JSON.SQL).ParseOrderBy("Value desc", "Form.TestRef") 222 | 1 223 | input,class,*condition 224 | %Status 225 | 274 | 275 | 276 | 277 | 278 | Draw JSON output. 279 | nocount - do not output count of rows 280 | 1 281 | nocount:%Boolean=$$$NO 282 | %Status 283 | 389 | 390 | 391 | 392 | *tSC:%Status,pInfo:%ZEN.Auxiliary.QueryInfo 393 | %ResultSet 394 | 402 | 403 | 404 | 405 | -------------------------------------------------------------------------------- /Form/Property.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Methods for form field === class property 6 | 1 7 | Form 8 | 64090,75759.538379 9 | 10 | 11 | 12 | Get metadata for one field of a form. Equal to propertyToMetadata method, 13 | but uses direct global references instead of object access 14 | 1 15 | className:%String="",name="" 16 | %DynamicObject 17 | 34 | 35 | 36 | 37 | 38 | Determine class type 39 | 1 40 | className:%String="" 41 | %String 42 | 47 | 48 | 49 | 50 | 51 | Add class property 52 | className - class 53 | name - property name 54 | type - property type 55 | collection - is a collection (list, array) 56 | displayName - displayname parameter value 57 | required - is it required (0/1) 58 | 1 59 | className:%String="",name:%String,type:%String="%String",collection:%String(VALUELIST=",list,array")="",displayName:%String="",required:%Boolean=$$$NO 60 | %Status 61 | 89 | 90 | 91 | 92 | 93 | Modify class property 94 | className - class 95 | name - property name 96 | type - property type 97 | collection - is a collection (list, array) 98 | displayName - displayname parameter value 99 | required - is it required (0/1) 100 | 1 101 | className:%String="",name:%String,type:%String="%String",collection:%String(VALUELIST=",list,array")="",displayName:%String="",required:%Boolean=$$$NO 102 | %Status 103 | 122 | 123 | 124 | 125 | 126 | Удалить свойство класса 127 | className - класс 128 | name - имя свойства 129 | 1 130 | className:%String="",name:%String 131 | %Status 132 | 143 | 144 | 145 | 146 | 147 | Скомпилировать класс, вернуть статус. 148 | Не выводить ничего на устройство. 149 | 1 150 | expression 151 | className:%String="" 152 | %Status 153 | 155 | 156 | 157 | 158 | 159 | Get property DISPLAYNAME 160 | w ##class(Form.Property).getPropertyDisplayName("Form.Test.Simple", "text") 161 | 1 162 | className:%String="",name:%String 163 | %String 164 | 170 | 171 | 172 | 173 | 174 | Get property Type 175 | w ##class(Form.Property).getPropertyType("Form.Test.Simple", "text") 176 | 1 177 | className:%String="",name:%String 178 | %String 179 | 184 | 185 | 186 | 187 | 188 | Get property param 189 | w ##class(Form.Property).getPropertyParam("Form.Test.Simple", "text", "VALUELIST") 190 | 1 191 | expression 192 | className:%String="",name:%String="",param="" 193 | %String 194 | 196 | 197 | 198 | 199 | 200 | Get property SQL name 201 | w ##class(Form.Property).getPropertySQLName("Form.Test.Simple", "text") 202 | 1 203 | expression 204 | className:%String="",name:%String="" 205 | %String 206 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /Form/REST/Abstract.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Абстрактный класс, реализующий техническую обработку входящего запроса. 6 | %occErrors,%ZEN.Utils,Form 7 | %CSP.REST 8 | 64090,76470.397537 9 | 10 | 11 | application/json 12 | 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | Integer 20 | 1 21 | 22 | 23 | 24 | 25 | This method takes a status, renders it as json (if requested) and outputs the result 26 | 1 27 | 1 28 | pSC:%Status 29 | %Status 30 | 66 | 67 | 68 | 69 | 1 70 | %ZEN.proxyObject 71 | 81 | 82 | 83 | 84 | 1 85 | %ListOfDataTypes 86 | 101 | 102 | 103 | 104 | 105 | This method Gets called prior to dispatch of the request. Put any common code here 106 | that you want to be executed for EVERY request. If pContinue is set to 0, the 107 | request will NOT be dispatched according to the UrlMap. If this case it's the 108 | responsibility of the user to return a response. 109 | 1 110 | 111 | %Status 112 | 114 | 115 | 116 | 117 | 118 | Конвертируем %request.Content в UTF8 и в объект класса %ZEN.proxyObject 119 | 1 120 | %Status 121 | 143 | 144 | 145 | 146 | 1 147 | class:%String,action:%String(VALUELIST="C,R,U,D") 148 | %Status 149 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /Form/REST/Field.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Брокер работы с полями форм 6 | Form.REST.Abstract 7 | 64090,79242.449407 8 | 9 | 10 | http://www.intersystems.com/urlmap 11 | 13 | 14 | 15 | 16 | 17 | ]]> 18 | 19 | 20 | 21 | 22 | Добавить поле в форме 23 | URL запроса: POST /form/field/:form 24 | Тело: 25 | { 26 | "name":"services", 27 | "collection":"", 28 | "displayName":"Ресурс", 29 | "type":"%Library.String", 30 | "required": "0" 31 | } 32 | 1 33 | class:%String="" 34 | %Status 35 | 52 | 53 | 54 | 55 | 56 | Изменить поле в форме 57 | URL запроса: PUT /form/field/:form 58 | Тело: 59 | { 60 | "name":"services", 61 | "collection":"", 62 | "displayName":"Ресурс", 63 | "type":"%String", 64 | "required": "0" 65 | } 66 | 1 67 | class:%String="" 68 | %Status 69 | 86 | 87 | 88 | 89 | 90 | Удалить поле в форме 91 | 1 92 | class:%String="",property:%String="" 93 | %Status 94 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Form/REST/File.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This broker does file-related work. 6 | All file properies are assumed to be of Form.File class 7 | Form.REST.Abstract 8 | 64091,74902.525566 9 | 10 | 11 | http://www.intersystems.com/urlmap 12 | 14 | 15 | 16 | 17 | 18 | 19 | ]]> 20 | 21 | 22 | 23 | 24 | class and id are object identificators. 25 | If it's not a list property, then the first file is set as property value 26 | 1 27 | class:%String="",id:%Integer="",property:%String="" 28 | %Status 29 | 75 | 76 | 77 | 78 | 79 | Clear property value for object with class/id 80 | do ##class(Form.REST.File).deleteFiles("Form.Test.Simple", 1, "file") 81 | 1 82 | class:%String="",id:%Integer="",property:%String="" 83 | %Status 84 | 103 | 104 | 105 | 106 | 107 | Delete value with name=filename from property for object with class/id 108 | 1 109 | class:%String="",id:%Integer="",property:%String="",filename="" 110 | %Status 111 | 162 | 163 | 164 | 165 | 166 | Serve file with name=filename from property for object with class/id 167 | 1 168 | class:%String="",id:%Integer="",property:%String="",filename="" 169 | %Status 170 | 214 | 215 | 216 | 217 | 218 | Get directory path based on structure: basedir/class/id/property 219 | 1 220 | class:%String,id:%String,property:%String 221 | %String 222 | 232 | 233 | 234 | 235 | 236 | Get file extension ext and filename (without extension) name from filename (without path) 237 | 1 238 | filename:%String,*name:%String,*ext:%String 239 | 1 { 242 | set name = $piece(filename, ".", 1, *-1) 243 | set ext = $piece(filename, ".", *) 244 | } else { 245 | set name = filename 246 | set ext = "" 247 | } 248 | ]]> 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /Form/REST/Form.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Класс работы с формами и их содержимым 6 | Form.REST.Abstract 7 | 64090,79002.958751 8 | 9 | 10 | UTF-8 11 | 12 | 13 | 14 | http://www.intersystems.com/urlmap 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ]]> 27 | 28 | 29 | 30 | 31 | Get available forms list as a JSON array 32 | 1 33 | %Status 34 | 40 | 41 | 42 | 43 | 44 | Get available forms metadata as JSON object 45 | 1 46 | %Status 47 | 53 | 54 | 55 | 56 | 57 | Get form metainformation by name 58 | 1 59 | form:%String="" 60 | %Status 61 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /Form/REST/Main.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Form.REST.Abstract 5 | 64090,76630.913942 6 | 7 | 8 | http://www.intersystems.com/urlmap 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ]]> 18 | 19 | 20 | 21 | 22 | Выход из системы 23 | 1 24 | %Status 25 | 31 | 32 | 33 | 34 | 35 | Тестовый метод 36 | 1 37 | %Status 38 | 42 | 43 | 44 | 45 | 1 46 | %Status 47 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Form/REST/Object.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Брокер работы с индивидуальными объектами 6 | Form.REST.Abstract 7 | 64090,79332.572705 8 | 9 | 10 | UTF-8 11 | 12 | 13 | 14 | http://www.intersystems.com/urlmap 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ]]> 25 | 26 | 27 | 28 | 29 | Get json representation of an object by its class and id 30 | 1 31 | class:%String="",id:%Integer="" 32 | %Status 33 | 44 | 45 | 46 | 47 | 48 | Get value of property for object of class with id identifier. Does not open objects if possible 49 | 1 50 | class:%String="",id:%Integer="",property:%String="" 51 | %Status 52 | 73 | 74 | 75 | 76 | 77 | Создание нового объекта для формы form (берём из %request.Content) 78 | Возвращает {"Id": "Значение Id"} при успехе 79 | 1 80 | class:%String 81 | %Status 82 | 101 | 102 | 103 | 104 | 105 | Обновление объекта id для формы form (берём из %request.Content) 106 | 1 107 | class:%String,id:%Integer="" 108 | %Status 109 | 119 | 120 | 121 | 122 | 123 | Обновление объекта id для формы form (берём из %request.Content) 124 | 1 125 | class:%String 126 | %Status 127 | 141 | 142 | 143 | 144 | 145 | Удаление объекта id для формы form 146 | 1 147 | class:%String="",id:%Integer="" 148 | %Status 149 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /Form/REST/Objects.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Broker class to work with queries 6 | Form.REST.Abstract 7 | 64090,79332.572705 8 | 9 | 10 | 11 | Query to return all availible information about form objects 12 | *, %CLASSNAME AS _class 13 | 14 | 15 | 16 | 17 | Query to return objects count 18 | count(1) "count" 19 | 20 | 21 | 22 | http://www.intersystems.com/urlmap 23 | 25 | 26 | 27 | 28 | ]]> 29 | 30 | 31 | 32 | 33 | Get all form objects 34 | 1 35 | class:%String="",queryType:%String 36 | %Status 37 | 52 | 53 | 54 | 55 | 58 | 1. Uppercase parameter values defined in this class
59 | 2. ClassMethods, defined in this class with the name: queryQUERYTYPE]]>
60 | 1 61 | queryType:%String,class:%String,*queryBase:%String 62 | %Status 63 | 90 |
91 | 92 | 93 | w ##class(Form.REST.Objects).queryINFO("Form.Test.Simple")]]> 96 | 1 97 | class:%String 98 | %String 99 | 111 | 112 | 113 | 114 | w ##class(Form.REST.Objects).queryINFOCLASS("Form.Test.Simple")]]> 117 | 1 118 | class:%String 119 | %String 120 | 124 | 125 | 126 | 127 | 128 | Get form objects by a custom query 129 | 1 130 | class:%String="",queryType:%String 131 | %Status 132 | 152 | 153 |
154 |
155 | -------------------------------------------------------------------------------- /Form/Security.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 64099,78607.760597 6 | 7 | 8 | 9 | Default permissions to access form object 10 | %String 11 | CRUD 12 | 13 | 14 | 15 | 16 | This is an override method to perform an actual permission check 17 | By default it uses PERMISSIONS parameter, but the form can override 18 | based on a role/user/whatever 19 | 1 20 | action:%String(VALUELIST="C,R,U,D") 21 | %Boolean 22 | 26 | 27 | 28 | 29 | 30 | Check, if the action we want to perform can be done 31 | This method is final. Override checkPermission method 32 | 1 33 | 1 34 | action:%String(VALUELIST="C,R,U,D") 35 | %Status 36 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Form/Settings.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Get and set settings 6 | 1 7 | Form 8 | 64110,52980.392815 9 | 10 | 11 | w {}.$toJSON()]]> 17 | 1 18 | expression 19 | name:%String 20 | %String 21 | 23 | 24 | 25 | 26 | 27 | Set setting "name" value 28 | 1 29 | name:%String="",value:%String="" 30 | %Status 31 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Form/Test/Address.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %SerialObject,%Populate 5 | 64101,39590.214377 6 | 7 | 8 | %Integer 9 | 10 | 11 | 12 | 13 | 14 | 15 | The street address. 16 | %String 17 | 18 | 19 | 20 | 21 | 22 | 23 | The city name. 24 | %String 25 | 26 | 27 | 28 | 29 | 30 | %Library.CacheSerialState 31 | AddressState 32 | ^Form.Test.AddressS 33 | 34 | listnode 35 | 36 | 37 | House 38 | 39 | 40 | Street 41 | 42 | 43 | City 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Form/Test/Company.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %Persistent,Form.Adaptor,%Populate 5 | 64101,39307.619051 6 | 7 | 8 | Company 9 | 10 | 11 | 12 | %String 13 | CRUD 14 | 15 | 16 | 17 | 18 | Name is a field used as a basic info about this form object 19 | %String 20 | name 21 | 22 | 23 | 24 | 25 | The main property describing this object, automatically computes 26 | on insert or update (for SQL) or on save (for object access) 27 | Property displayName As %String(DISPLAYNAME = "Компаниия") [ SqlComputeCode = {set {*} = {name}}, SqlComputed, SqlComputeOnChange = (%%INSERT, %%UPDATE) ]; 28 | %String 29 | 30 | 31 | 32 | 33 | 34 | Person objects associated with this Company.]]> 36 | Form.Test.Person 37 | many 38 | company 39 | 1 40 | 41 | 42 | 43 | 44 | %Library.CacheStorage 45 | ^Form.Test.CompanyD 46 | CompanyDefaultData 47 | ^Form.Test.CompanyD 48 | ^Form.Test.CompanyI 49 | ^Form.Test.CompanyS 50 | 51 | 52 | %%CLASSNAME 53 | 54 | 55 | displayName 56 | 57 | 58 | name 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /Form/Test/Person.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Test form: Person 6 | %Persistent,Form.Adaptor,%Populate 7 | 64101,38189.100761 8 | 9 | 10 | Person 11 | 12 | 13 | 14 | %String 15 | CRUD 16 | 17 | 18 | 19 | 20 | Name is a field used as a basic info about this form object 21 | %String 22 | name 23 | 24 | 25 | 26 | %String 27 | dob 28 | 29 | 30 | 31 | 32 | Person's name. 33 | Use only first 250 symbols for comparison. 34 | Required for all long properties used by "order by" 35 | %String 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Person's gender 44 | Person's Date of Birth. 45 | %Date 46 | 47 | 48 | 49 | 50 | 51 | %TimeStamp 52 | $ZDATETIME($ZTIMESTAMP, 3, 1, 3) 53 | 54 | 55 | 56 | 57 | %Numeric 58 | "2.15" 59 | 60 | 61 | 62 | 63 | 65 | This is a calculated field whose value is derived from DOB.]]> 66 | %Integer 67 | 1 68 | set {*}=##class(Form.Test.Person).currentAge({dob}) 69 | 1 70 | dob 71 | 72 | 73 | 74 | 75 | date.]]> 77 | 1 78 | expression 79 | date:%Date="" 80 | %Integer 81 | 83 | 84 | 85 | 86 | 87 | Person's spouse. 88 | This is a reference to another persistent object. 89 | Form.Test.Person 90 | 91 | 92 | 93 | 94 | 95 | Person's home address. This uses an embedded object. 96 | Form.Test.Address 97 | 98 | 99 | 100 | 101 | 102 | The company this person works for. 103 | Form.Test.Company 104 | one 105 | employees 106 | 1 107 | 108 | 109 | 110 | 111 | 112 | Property amount As %Double(DISPLAYNAME = "double", POPSPEC = "Float(0,100,2)"); 113 | %Library.CacheStorage 114 | ^Form.Test.PersonD 115 | PersonDefaultData 116 | ^Form.Test.PersonD 117 | ^Form.Test.PersonI 118 | ^Form.Test.PersonS 119 | 10 120 | 121 | 122 | %%CLASSNAME 123 | 124 | 125 | name 126 | 127 | 128 | gender 129 | 130 | 131 | dob 132 | 133 | 134 | company 135 | 136 | 137 | spouse 138 | 139 | 140 | Home 141 | 142 | 143 | relative 144 | 145 | 146 | ts 147 | 148 | 149 | amount 150 | 151 | 152 | num 153 | 154 | 155 | 156 | 100.0000% 157 | 1 158 | 159 | 160 | 1 161 | 1.1 162 | 163 | 164 | 10.0000% 165 | 19.1 166 | 167 | 168 | 10.0000% 169 | 5 170 | 171 | 172 | 10.0000% 173 | 17.4 174 | 175 | 176 | 100.0000% 177 | 4 178 | 179 | 180 | 50.0000% 181 | 23 182 | 183 | 184 | -4 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /Form/Test/Simple.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %Persistent,Form.Adaptor,%Populate 5 | 64090,75943.393938 6 | 7 | 8 | 9 | Form name, not a global key so it can be anything 10 | Simple form 11 | 12 | 13 | 14 | 15 | Default permissions 16 | Objects of this form can be Created, Read, Updated and Deleted 17 | Redefine this parameter to change permissions for everyone 18 | Redefine checkPermission method (see Form.Security) for this class 19 | to add custom security based on user/roles/etc. 20 | %String 21 | CRUD 22 | 23 | 24 | 25 | 26 | Property used for basic information about the object 27 | By default getObjectDisplayName method gets its value from it 28 | %String 29 | displayName 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | The main property describing this object, automatically computes 42 | on insert or update (for SQL) or on save (for object access) 43 | %String 44 | set {*} = {text} 45 | 1 46 | %%INSERT,%%UPDATE 47 | 48 | 49 | 50 | 51 | %String 52 | 53 | 54 | 55 | 56 | 57 | do ##class(Form.Test.Simple).recreate() 58 | 1 59 | count:%Integer=10,verbose:%Boolean=$$$NO 60 | 64 | 65 | 66 | 67 | 68 | do ##class(Form.Test.Simple).recreate2() 69 | 1 70 | 76 | 77 | 78 | 79 | %Library.CacheStorage 80 | ^Form.Test.SimpleD 81 | SimpleDefaultData 82 | ^Form.Test.SimpleD 83 | ^Form.Test.SimpleI 84 | ^Form.Test.SimpleS 85 | 86 | 87 | %%CLASSNAME 88 | 89 | 90 | displayName 91 | 92 | 93 | text 94 | 95 | 96 | gender 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /Form/Util/Converter.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Various datatype conversion utilities 6 | 1 7 | 64100,82956.192829 8 | 9 | 10 | 11 | Convert delimited string to dynamic array 12 | 1 13 | string:%String,delimiter:%String="," 14 | %DynamicArray 15 | 19 | 20 | 21 | 22 | 23 | Convert $list to dynamic array 24 | 1 25 | list:%List 26 | %DynamicArray 27 | 35 | 36 | 37 | 38 | 39 | TODO. Populate on install? 40 | 1 41 | type:%Integer="" 42 | %String 43 | 55 | 56 | 57 | 58 | 1 59 | datatype:%String="",value:%String 60 | %String 61 | 88 | 89 | 90 | 91 | 1 92 | datatype:%String="",value:%String 93 | %String 94 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /Form/Util/Init.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | Form 6 | 64101,40238.796897 7 | 8 | 9 | do ##class(Form.Util.Init).populateTestForms()]]> 11 | 1 12 | count:%Integer=100,verbose:%Boolean=$$$NO 13 | %Status 14 | 22 | 23 | 24 | 25 | do ##class(Form.Util.Init).deleteTestForms()]]> 27 | 1 28 | %Status 29 | 37 | 38 | 39 | 40 | do ##class(Form.Util.Init).killTestForms()]]> 42 | 1 43 | %Status 44 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Form/Util/Translate.cls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Support for metadata translation 6 | Form 7 | 64386,54065.171182 8 | 9 | 10 | 11 | Get translated value of a text 12 | w ##class(Form.Util.Translate).get() 13 | 1 14 | text:%String 15 | 30 | 31 | 32 | 33 | 34 | Add text to ^CacheMsg 35 | do ##class(Form.Util.Translate).Insert() 36 | 1 37 | text 38 | 43 | 44 | 45 | 46 | 1 47 | %DynamicArray 48 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /INTRO_RU.md: -------------------------------------------------------------------------------- 1 | # RESTForms - REST API для ваших классов InterSystems Caché 2 | В этой статье я хотел бы представить проект RESTForms - универсальный REST API бэкэнд для современных веб-приложений. 3 | Идея проекта проста - после написания нескольких REST API стало понятно, что, как правило, REST API состоит из двух частей: 4 | - Работа с хранимыми данными 5 | - Пользовательская бизнес-логика 6 | 7 | И, хотя вам придется писать свою собственную бизнес-логику, RESTForms предоставляет все необходимое для работы с хранимыми данными из коробки. 8 | 9 | ## Варианты использования: 10 | - У Вас уже есть модель данных в Caché, и Вы хотите представить некоторую (или всю) хранящуюся информацию в форме REST API. 11 | - Вы разрабатываете новое Caché приложение и хотите сразу предоставлять REST API. 12 | ## Клиент 13 | Этот проект разработан как бэкэнд для JS веб-приложений, поэтому JS сразу может начинать работу с RESTForms, не требуется преобразования форматов данных и т.п. 14 | ## Возможности 15 | Что уже можно делать с RESTForms: 16 | - [CRUD](https://ru.wikipedia.org/wiki/CRUD) по классу данных - возможно получить метаданные класса, создавать, обновлять и удалять свойства класса. 17 | - CRUD по объекту - возможно получать, создавать, обновлять и удалять объекты. 18 | - R по наборам (через SQL) - защита от SQL-инъекций. 19 | - Selfdiscovery – сначала вы получаете список доступных классов, после этого вы можете получить метаданные класса, и на основе этих метаданных выполнять CRUD запросы по объекту. 20 | 21 | ## Пути 22 | Далее представлена таблица доступных методов API, которая демонстрирует то, что Вы можете сделать через RESTForms. 23 | 24 | | URL | Описание | 25 | |-----------------------------------|------------------------------------------| 26 | | test | Тестовый метод | 27 | | form/info | Список классов | 28 | | form/info/all | Метаинформация всех классов | 29 | | form/info/:class | Метаинформация одного класса | 30 | | form/field/:class | Добавить свойство в класс | 31 | | form/field/:class | Изменить свойство | 32 | | form/field/:class/:property | Удалить свойство | 33 | | form/object/:class/:id | Получить объект | 34 | | form/object/:class/:id/:property | Получить свойство объекта | 35 | | form/object/:class | Создать объект | 36 | | form/object/:class/:id | Обновить объект из динамического объекта | 37 | | form/object/:class | Обновить объект из объекта класса | 38 | | form/object/:class/:id | Удалить объект | 39 | | form/objects/:class/:query | Выполнить SQL запрос | 40 | | form/objects/:class/custom/:query | Выполнить пользовательский SQL запрос | 41 | 42 | ## Установка 43 | 1. Загрузите и импортируйте из последнего релиза на [странице релизов](https://github.com/intersystems-ru/RESTForms/releases/tag/v1.0) 20161.xml (для Caché 2016.1) или 201162.xml (для Caché 2016.2 +) в любую область 44 | 2. Создайте новое веб-приложение `/forms` с классом брокером `Form.REST.Main` 45 | 3. Откройте http://localhost:57772/forms/test?Debug в браузере, чтобы проверить установку (должен выводиться `{"Status": "OK"}`, возможно, будет запрошен пароль). 46 | 4. Если Вы хотите сгенерировать тестовые данные, вызовите: 47 | ```xml 48 | do ##class(Form.Util.Init).populateTestForms() 49 | ``` 50 | ## Как начать использовать RESTForms? 51 | 52 | 1. Импортируйте проект из GitHub (рекомендуется: добавить его как подмодуль (submodule) в ваш собственный репозиторий или просто загрузить релиз). 53 | 2. Для каждого хранимого класса, который Вы хотите представить через RESTForms: 54 | - Унаследуйте ваш хранимый класс от класса адаптера (Form.Adaptor) 55 | - Определите полномочия (например, вы можете представить некоторые классы только для чтения) 56 | - Определите свойство, используемое в качестве "имени" объекта 57 | - Определите отображаемые имена свойств 58 | 59 | Вот как выглядит список объектов класса: 60 | ![список классов](https://habrastorage.org/web/28d/759/b86/28d759b8637b4a13889987c2a5db5a5f.png) 61 | 62 | И объект класса: 63 | ![объект класса](https://habrastorage.org/web/8a4/cad/c93/8a4cadc931bd48b5b617c541fd2e5184.png) 64 | 65 | 66 | ## Пример использования 67 | Во-первых, вы хотите знать, какие классы доступны. Чтобы получить эту информацию, вызовите: 68 | ```xml 69 | http://localhost:57772/forms/form/info 70 | ``` 71 | Вы получите в ответ что-то вроде этого: 72 | ```xml 73 | [ 74 | { "name":"Company", "class":"Form.Test.Company" }, 75 | { "name":"Person", "class":"Form.Test.Person" }, 76 | { "name":"Simple form", "class":"Form.Test.Simple" } 77 | ] 78 | ``` 79 | На данный момент с RESTForms поставляются 3 тестовых класса. Давайте посмотрим метаданные для формы Person (класс Form.Test.Person). Чтобы получить эти данные, нужно вызвать: 80 | ```xml 81 | http://localhost:57772/forms/form/info/Form.Test.Person 82 | ``` 83 | В ответ Вы получите метаданные класса: 84 | ```xml 85 | { 86 | "name":"Person", 87 | "class":"Form.Test.Person", 88 | "displayProperty":"name", 89 | "objpermissions":"CRUD", 90 | "fields":[ 91 | { "name":"name", "type":"%Library.String", "collection":"", "displayName":"Name", "required":0, "category":"datatype" }, 92 | { "name":"dob", "type":"%Library.Date", "collection":"", "displayName":"Date of Birth", "required":0, "category":"datatype" }, 93 | { "name":"ts", "type":"%Library.TimeStamp", "collection":"", "displayName":"Timestamp", "required":0, "category":"datatype" }, 94 | { "name":"num", "type":"%Library.Numeric", "collection":"", "displayName":"Number", "required":0, "category":"datatype" }, 95 | { "name":"аge", "type":"%Library.Integer", "collection":"", "displayName":"Age", "required":0, "category":"datatype" }, 96 | { "name":"relative", "type":"Form.Test.Person", "collection":"", "displayName":"Relative", "required":0, "category":"form" }, 97 | { "name":"Home", "type":"Form.Test.Address", "collection":"", "displayName":"House", "required":0, "category":"serial" }, 98 | { "name":"company", "type":"Form.Test.Company", "collection":"", "displayName":"Company", "required":0, "category":"form" } 99 | ] 100 | } 101 | ``` 102 | Что все это значит? 103 | 104 | Метаданные класса: 105 | - name – отображаемое имя класса 106 | - class – название хранимого класса 107 | - displayProperty – свойство объекта, использующееся при отображении объекта 108 | - objpermissions – что может делать пользователь с объектом. В нашем случае пользователь может читать (R), создавать (C) новые объекты, изменять (U) и удалять (D) существующие объекты. 109 | 110 | Метаданные объектов: 111 | - name – название свойства 112 | - collection – является ли класс коллекцией (списком или массивом) 113 | - displayName – отображаемое имя свойства 114 | - required – обязательное ли свойство 115 | - type – тип свойства 116 | - category – категория типа свойства. Это обычные категории класса Caché, кроме всех классов, наследующихся от адаптера RESTForms - они имеют категорию "form" 117 | 118 | Определение класса выглядит следующим образом: 119 | ```xml 120 | /// Test form: Person 121 | Class Form.Test.Person Extends (%Persistent, Form.Adaptor) 122 | { 123 | 124 | /// Отображаемое имя формы 125 | Parameter FORMNAME = "Person"; 126 | 127 | /// Разрешения 128 | /// Объекты этого класса могут быть Созданы (C), Получены (R), Изменены (U), и удалены (D) 129 | Parameter OBJPERMISSIONS As %String = "CRUD"; 130 | 131 | /// Свойство "имени" объекта 132 | Parameter DISPLAYPROPERTY As %String = "name"; 133 | 134 | /// Свойство сортировки по-умолчанию 135 | Parameter FORMORDERBY As %String = "dob"; 136 | 137 | /// Имя 138 | Property name As %String(DISPLAYNAME = "Name"); 139 | 140 | /// Дата рождения 141 | Property dob As %Date(DISPLAYNAME = "Date of Birth"); 142 | 143 | /// Число 144 | Property num As %Numeric(DISPLAYNAME = "Number") [ InitialExpression = "2.15" ]; 145 | 146 | /// Возраст, вычисляется автоматически 147 | Property аge As %Integer(DISPLAYNAME = "Age") [ Calculated, SqlComputeCode = { set {*}=##class(Form.Test.Person).currentAge({dob})}, SqlComputed, SqlComputeOnChange = dob ]; 148 | 149 | /// Вычисление возраста 150 | ClassMethod currentAge(date As %Date = "") As %Integer [ CodeMode = expression ] 151 | { 152 | $Select(date="":"",1:($ZD($H,8)-$ZD(date,8)\10000)) 153 | } 154 | 155 | /// Родственник - ссылка на другой объект класса Form.Test.Person 156 | Property relative As Form.Test.Person(DISPLAYNAME = "Relative"); 157 | 158 | /// Адрес 159 | Property Home As Form.Test.Address(DISPLAYNAME = "House"); 160 | 161 | /// ссылка на компания, в которой человек работает 162 | /// http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_relationships 163 | Relationship company As Form.Test.Company(DISPLAYNAME = "Company") [ Cardinality = one, Inverse = employees ]; 164 | } 165 | ``` 166 | ## RESTForms – добавляем класс 167 | Чтобы сделать хранимый класс доступным для RESTForms, надо: 168 | 1. Добавить наследование от Form.Adaptor 169 | 2. Добавить параметр `FORMNAME` со значением – имя класса 170 | 3. Добавить параметр `OBJPERMISSIONS` – что можно делать с объектами класса (CRUD) 171 | 4. Добавить параметр `DISPLAYPROPERTY` – имя свойства, используемое для отображения имени объекта 172 | 5. Добавить параметр `FORMORDERBY` – свойство по умолчанию для сортировки по запросам с использованием RESTForms 173 | 6. Для каждого свойства, которое хочется видеть в метаданных, надо добавить параметр свойства DISPLAYNAME 174 | 175 | После того как мы сгенерировали некоторые тестовые данные (см. Установку, шаг 4), давайте получим Person с идентификатором 1. Чтобы получить объект, вызовем: 176 | ```xml 177 | http://localhost:57772/forms/form/object/Form.Test.Person/1 178 | ``` 179 | И получим ответ: 180 | ```xml 181 | { 182 | "_class":"Form.Test.Person", 183 | "_id":1, 184 | "name":"Klingman,Rhonda H.", 185 | "dob":"1996-10-18", 186 | "ts":"2016-09-20T10:51:31.375Z", 187 | "num":2.15, 188 | "аge":20, 189 | "relative":null, 190 | "Home":{ 191 | "_class":"Form.Test.Address", 192 | "House":430, 193 | "Street":"5337 Second Place", 194 | "City":"Jackson" 195 | }, 196 | "company":{ 197 | "_class":"Form.Test.Company", 198 | "_id":60, 199 | "name":"XenaSys.com", 200 | "employees":[ 201 | null 202 | ] 203 | } 204 | } 205 | ``` 206 | Чтобы изменить объект (в частности, свойство num), вызовем: 207 | ```xml 208 | PUT http://localhost:57772/forms/form/object/Form.Test.Person 209 | ``` 210 | С телом: 211 | ```xml 212 | { 213 | "_class":"Form.Test.Person", 214 | "_id":1, 215 | "num":3.15 216 | } 217 | ``` 218 | Обратите внимание на то, что для улучшения скорости, только `_class`, `_id` и измененные свойства должны быть в теле запроса. 219 | 220 | Теперь, давайте создадим новый объект. Вызовем: 221 | ```xml 222 | POST http://localhost:57772/forms/form/object/Form.Test.Person 223 | ``` 224 | С телом: 225 | ```xml 226 | { 227 | "_class":"Form.Test.Person", 228 | "name":"Test person", 229 | "dob":"2000-01-18", 230 | "ts":"2016-09-20T10:51:31.375Z", 231 | "num":2.15, 232 | "company":{ "_class":"Form.Test.Company", "_id":1 } 233 | } 234 | ``` 235 | Если создание объекта завершилось успешно, RESTForms вернёт идентификатор: 236 | ```xml 237 | {"Id": "101"} 238 | ``` 239 | В противном случае, будет возвращена ошибка в формате JSON. Обратите внимание на то, что на персистентные свойства необходимо ссылаться через свойства `_class` и `_id`. 240 | ## Демо 241 | Вы можете попробовать RESTForms онлайн [здесь](http://176.112.210.99:57772/forms/) (пользователь: Demo, пароль: Demo). 242 | Кроме того, есть приложение RESTFormsUI - редактор для данных RESTForms. Оно доступно [здесь](http://176.112.210.99:57772/csp/restforms/index.html#/forms) (пользователь: Demo, пароль: Demo). Скриншот списка классов: 243 | ![image](https://community.intersystems.com/sites/default/files/inline/images/risunok1.png) 244 | ## Заключение 245 | RESTForms выполняет почти всю работу, требуемую от REST API в отношении хранимых классов. 246 | ## Что же дальше? 247 | В этой статье я только начал говорить о функциональных возможностях RESTForms. В следующей я бы хотел рассказать о некоторых дополнительных функциях, а именно о запросах, которые позволяют клиенту безопасно получать наборы данных без риска SQL-инъекций. 248 | ## Ссылки 249 | - [RESTForms GitHub repository](https://github.com/intersystems-ru/RESTForms/) 250 | - [RESTForms UI GitHub repository](https://github.com/intersystems-ru/RESTFormsUI/) 251 | 252 | 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 InterSystems Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RESTForms 2 | RESTForms is a generalized REST API backend for web applications built on InterSystems (Caché or Ensemble) backend. 3 | 4 | # Installation 5 | 6 | 1. Import latest [release](https://github.com/intersystems-ru/RESTForms/releases) appropriate for your Caché version (161 for 2016.1, 162 for 2016.2+). 7 | 2. Create REST web app with `Form.REST.Main` as Dispatch Class. 8 | 3. Generate test data: `do ##class(Form.Util.Init).populateTestForms()` 9 | 10 | # Usage description and examples 11 | 12 | Read this [post on Developer Community](https://community.intersystems.com/post/restforms-rest-api-your-classes). [Developer community article about queries](https://community.intersystems.com/post/restforms-rest-api-your-classes-part-2-queries). 13 | [Russian article](https://habrahabr.ru/company/intersystems/blog/330822/). 14 | 15 | # Requests 16 | 17 | 18 | | URL | Type | Description | 19 | |--------------------------------------|--------|----------------------------------------| 20 | | test | GET | Test request | 21 | | info | GET | Basic information (currently language list)| 22 | | logout | GET | End current session | 23 | | form/info | GET | List of all availible forms | 24 | | form/info/all | GET | Get metainformation for all forms | 25 | | form/info/:class | GET | Form metainformation | 26 | | form/field/:class | POST | Add field to form | 27 | | form/field/:class | PUT | Modify form field | 28 | | form/field/:class/:property | DELETE | Delete form field | 29 | | form/object/:class/:id | GET | Retrieve form object | 30 | | form/object/:class/:id/:property | GET | Retrieve one field of the form object | 31 | | form/object/:class | POST | Create form object | 32 | | form/object/:class/:id | PUT | Update form object from dynamic object | 33 | | form/object/:class | PUT | Update form object from object | 34 | | form/object/:class/:id | DELETE | Delete form object | 35 | | form/objects/:class/:query | GET | (SQL) Get all members for the form by query| 36 | | form/objects/:class/custom/:query | GET | (SQL) Get all members for the form by custom query| 37 | | form/file/:class/:id/:property | POST | Add files to this property | 38 | | form/file/:class/:id/:property | DELETE | Delete all files from this property | 39 | | form/file/:class/:id/:property/:name | DELETE | Delete one file from property | 40 | | form/file/:class/:id/:property/:name | GET | Download one file from property | 41 | 42 | For POST/PUT requests see method descriptions for request body samples 43 | 44 | Postman 45 | ----------- 46 | 47 | You can use [Postman](https://www.getpostman.com/) to query RESTForms API. [Collection](RESTForms.postman_collection.json). [Environment](CACHE.postman_environment.json). 48 | 49 | # SQL requests 50 | 51 | `GET http://localhost:57772/forms/form/objects/Form.Test.Simple/info?size=2&page=1&orderby=text` 52 | 53 | `GET http://localhost:57772/forms/form/objects/Form.Test.Simple/all?orderby=text+desc` 54 | 55 | `GET http://localhost:57772/forms/form/objects/Form.Test.Simple/all?filter=text%20eq%20A9044` 56 | 57 | `GET http://localhost:57772/forms/form/objects/Form.Test.Simple/all?filter=text%20in%20A9044~B5920` 58 | 59 | Note, that for SQL access user must have relevant SQL privileges (SELECT on form table). 60 | 61 | ## Queries 62 | 63 | There are two query types: 64 | - Basic queries work for all RESTForms classes once defined and they differ only by the field list 65 | - Custom queries work only for the classes in which they are specified and available, but the user has full access to query text 66 | 67 | ## Basic queries 68 | 69 | Execute `form/objects/:class/:query` request, to call a simple query. Second `:query` parameter determines query type - the contents of query between SELECT and FROM. Here are default query types: 70 | 71 | 72 | | Query | Description | 73 | |----------|-----------------------| 74 | | all | all information | 75 | | info | displayName and id | 76 | | infoclass| displayName, id, class| 77 | | count | number of rows | 78 | 79 | RESTForms looks for a query named `myq` in the following places (till first hit): 80 | 1. Class method `queryMYQ` in your form class 81 | 2. Parameter `MYQ` in your queries class 82 | 3. Class method `queryMYQ` in your queries class 83 | 4. Parameter `MYQ` in `Form.REST.Objects` class 84 | 5. Class method `queryMYQ` in `Form.REST.Objects` class 85 | 86 | You can define your own queries class. To define your own query named `myq` there: 87 | 1. Define a class `YourClassName` 88 | 2. Define there a `MYQ` parameter or `queryMYQ` class method. Parameter takes precedence over the method. 89 | 3. Method or param must return the part of SQL query between SELECT and FROM 90 | 4. (Once) Execute in a terminal: `Do ##class(For.Settings).setSetting("queryclass", YourClassName)` 91 | 92 | Method signature is: `ClassMethod queryMYQ(class As %String) As %String` 93 | 94 | You can define a class-specific query. To define your own class query named `myq`: 95 | 1. Define a `queryMYQ` class method in your form class 96 | 2. Method signature is: `ClassMethod queryMYQ() As %String` 97 | 3. Method must return the part of SQL query between SELECT and FROM 98 | 99 | ## Custom Queries 100 | 101 | Execute `form/objects/:class/custom/:query` request, to call a custom query. Custom query allows user code to determine the full content of the query. URL parameters besides `size` and `page` are unavailable. Your method must parse all other url parameters. 102 | 103 | To define your own custom query named `myq`: 104 | 1. Define a `customqueryMYQ` class method in your form class 105 | 2. Method signature is: `ClassMethod customqueryMYQ() As %String` 106 | 3. Method must return a valid SQL query 107 | 108 | ## URL arguments: 109 | 110 | All arguments are optional. 111 | 112 | | Argument | Sample Value | Description | 113 | |----------|--------------------|-----------------| 114 | | size | 2 | page size | 115 | | page | 1 | page number | 116 | | filter | Value+contains+W | WHERE clause | 117 | | orderby | Value+desc | ORDER BY clause | 118 | | collation| UPPER | COLLATION clause| 119 | | nocount | 1 | Remove count of rows (speeds up query)| 120 | | mode | 0 | SQL mode value. Can be 0 - Logical, 1 - ODBC, 2 - Display. Defaults to 0.| 121 | 122 | 123 | ## ORDER BY clause 124 | 125 | Value can be: `Column` or `Column+desc`. Column is a column from the sql table or a colum number. 126 | 127 | ## WHERE clause 128 | 129 | In a format: `Column+condition+Value`. 130 | 131 | Several conditions are possible: `Column+condition+Value+Column2+condition2+Value2`. 132 | 133 | If Value contains white spaces replace them with tabs before sending to the server. 134 | 135 | Conditions: 136 | 137 | | URL | SQL | 138 | |----------------|-------------| 139 | | neq | != | 140 | | eq | = | 141 | | gte | >= | 142 | | gt | > | 143 | | lte | <= | 144 | | lt | < | 145 | | startswith | %STARTSWITH | 146 | | contains | [ | 147 | | doesnotcontain | '[ | 148 | | in | IN | 149 | | like | LIKE | 150 | 151 | ## COLLATION clause 152 | 153 | In a format: `collation=UPPER` or `collation=EXACT`. 154 | Forces specified collation on WHERE clause. If omitted, default collation is used. 155 | 156 | # Settings 157 | 158 | You can setup several settings. 159 | Set them via `Write ##class(For.Settings).setSetting(Setting, Value)`. 160 | 161 | | Setting | Values | Description | 162 | |-------------|--------------------|-----------------| 163 | | queryclass | Caché class name | Class with your own queries. See Query types for details.| 164 | | fileDir | Directory path | Directory for files. Defaults to MGR\DB directory| 165 | | timezone | ignore, utc | Affects how timestamps are converted for a client. UTC has Z on end, ignore does not| 166 | 167 | 168 | # Samples 169 | 170 | See `Form.Test.Simple` and other forms in `Form.Test` package for samples. 171 | To generate test data execute: `do ##class(Form.Util.Init).populateTestForms()` 172 | To remove test forms permanently from your local repository 173 | 1. Enter `Form\Test` directory from git bash 174 | 2. `git update-index --assume-unchanged $(git ls-files | tr '\n' ' ')` 175 | 3. Delete `Form\Test` directory 176 | 177 | # Object creation 178 | 179 | For `Form.Test.Simple`. 180 | 181 | `POST http://localhost:57772/forms/form/object/Form.Test.Simple` 182 | Headers must contain `Content-Type` and (probably) authorization 183 | 184 | ``` 185 | Content-Type: application/json 186 | Authorization: Basic Base64String 187 | ``` 188 | 189 | Body: 190 | ``` 191 | { 192 | "_class":"Form.Test.Simple", 193 | "text":3 194 | } 195 | ``` 196 | 197 | # Object update 198 | 199 | For `Form.TestForm`. 200 | 201 | `PUT http://localhost:57772/forms/form/object/Form.Test.Simple` 202 | 203 | Body: 204 | ``` 205 | { 206 | "_class":"Form.Test.Simple", 207 | "_id":3, 208 | "text":4444 209 | } 210 | ``` 211 | 212 | -------------------------------------------------------------------------------- /RESTForms.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": [], 3 | "info": { 4 | "name": "RESTForms", 5 | "_postman_id": "9f8944d0-d64a-7790-7476-7f278a4999e1", 6 | "description": "", 7 | "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" 8 | }, 9 | "item": [ 10 | { 11 | "name": "test", 12 | "request": { 13 | "auth": { 14 | "type": "basic", 15 | "basic": { 16 | "username": "{{user}}", 17 | "password": "{{pass}}", 18 | "saveHelperData": true, 19 | "showPassword": false 20 | } 21 | }, 22 | "url": "http://{{host}}:{{port}}/forms/test", 23 | "method": "GET", 24 | "header": [ 25 | { 26 | "key": "Authorization", 27 | "value": "Basic X1NZU1RFTTpTWVM=", 28 | "description": "" 29 | } 30 | ], 31 | "body": {}, 32 | "description": "Test request" 33 | }, 34 | "response": [] 35 | }, 36 | { 37 | "name": "info", 38 | "request": { 39 | "auth": { 40 | "type": "basic", 41 | "basic": { 42 | "username": "{{user}}", 43 | "password": "{{pass}}", 44 | "saveHelperData": true, 45 | "showPassword": false 46 | } 47 | }, 48 | "url": "http://{{host}}:{{port}}/forms/info", 49 | "method": "GET", 50 | "header": [ 51 | { 52 | "key": "Authorization", 53 | "value": "Basic X1NZU1RFTTpTWVM=", 54 | "description": "" 55 | } 56 | ], 57 | "body": {}, 58 | "description": "Get basic information" 59 | }, 60 | "response": [] 61 | }, 62 | { 63 | "name": "form/info", 64 | "request": { 65 | "auth": { 66 | "type": "basic", 67 | "basic": { 68 | "username": "{{user}}", 69 | "password": "{{pass}}", 70 | "saveHelperData": true, 71 | "showPassword": false 72 | } 73 | }, 74 | "url": "http://{{host}}:{{port}}/forms/form/info", 75 | "method": "GET", 76 | "header": [ 77 | { 78 | "key": "Authorization", 79 | "value": "Basic X1NZU1RFTTpTWVM=", 80 | "description": "" 81 | } 82 | ], 83 | "body": {}, 84 | "description": "Test request" 85 | }, 86 | "response": [] 87 | }, 88 | { 89 | "name": "form/info/all", 90 | "request": { 91 | "auth": { 92 | "type": "basic", 93 | "basic": { 94 | "username": "{{user}}", 95 | "password": "{{pass}}", 96 | "saveHelperData": true, 97 | "showPassword": false 98 | } 99 | }, 100 | "url": "http://{{host}}:{{port}}/forms/form/info/all", 101 | "method": "GET", 102 | "header": [ 103 | { 104 | "key": "Authorization", 105 | "value": "Basic X1NZU1RFTTpTWVM=", 106 | "description": "" 107 | } 108 | ], 109 | "body": {}, 110 | "description": "Get info about all forms" 111 | }, 112 | "response": [] 113 | }, 114 | { 115 | "name": "form/info/:class", 116 | "request": { 117 | "auth": { 118 | "type": "basic", 119 | "basic": { 120 | "username": "{{user}}", 121 | "password": "{{pass}}", 122 | "saveHelperData": true, 123 | "showPassword": false 124 | } 125 | }, 126 | "url": "http://{{host}}:{{port}}/forms/form/info/Form.Test.Person", 127 | "method": "GET", 128 | "header": [ 129 | { 130 | "key": "Authorization", 131 | "value": "Basic X1NZU1RFTTpTWVM=", 132 | "description": "" 133 | } 134 | ], 135 | "body": {}, 136 | "description": "Get meta information about one class " 137 | }, 138 | "response": [] 139 | }, 140 | { 141 | "name": "form/object/:class/:id", 142 | "request": { 143 | "auth": { 144 | "type": "basic", 145 | "basic": { 146 | "username": "{{user}}", 147 | "password": "{{pass}}", 148 | "saveHelperData": true, 149 | "showPassword": false 150 | } 151 | }, 152 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person/1", 153 | "method": "GET", 154 | "header": [ 155 | { 156 | "key": "Authorization", 157 | "value": "Basic X1NZU1RFTTpTWVM=", 158 | "description": "" 159 | } 160 | ], 161 | "body": {}, 162 | "description": "Get meta information about one class " 163 | }, 164 | "response": [] 165 | }, 166 | { 167 | "name": "form/object/:class/:id/:property", 168 | "request": { 169 | "auth": { 170 | "type": "basic", 171 | "basic": { 172 | "username": "{{user}}", 173 | "password": "{{pass}}", 174 | "saveHelperData": true, 175 | "showPassword": false 176 | } 177 | }, 178 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person/1/name", 179 | "method": "GET", 180 | "header": [ 181 | { 182 | "key": "Authorization", 183 | "value": "Basic X1NZU1RFTTpTWVM=", 184 | "description": "" 185 | } 186 | ], 187 | "body": {}, 188 | "description": "Get meta information about one class " 189 | }, 190 | "response": [] 191 | }, 192 | { 193 | "name": "form/object/:class/:id", 194 | "request": { 195 | "auth": { 196 | "type": "basic", 197 | "basic": { 198 | "username": "{{user}}", 199 | "password": "{{pass}}", 200 | "saveHelperData": true, 201 | "showPassword": false 202 | } 203 | }, 204 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person/1", 205 | "method": "PUT", 206 | "header": [ 207 | { 208 | "key": "Authorization", 209 | "value": "Basic X1NZU1RFTTpTWVM=", 210 | "description": "" 211 | } 212 | ], 213 | "body": { 214 | "mode": "raw", 215 | "raw": "{\n \"name\": \"Alice\",\n}" 216 | }, 217 | "description": "Get meta information about one class " 218 | }, 219 | "response": [] 220 | }, 221 | { 222 | "name": "form/object/:class", 223 | "request": { 224 | "auth": { 225 | "type": "basic", 226 | "basic": { 227 | "username": "{{user}}", 228 | "password": "{{pass}}", 229 | "saveHelperData": true, 230 | "showPassword": false 231 | } 232 | }, 233 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person", 234 | "method": "PUT", 235 | "header": [ 236 | { 237 | "key": "Content-Type", 238 | "value": "application/json", 239 | "description": "" 240 | }, 241 | { 242 | "key": "Authorization", 243 | "value": "Basic X1NZU1RFTTpTWVM=", 244 | "description": "" 245 | } 246 | ], 247 | "body": { 248 | "mode": "raw", 249 | "raw": "{\n \"_class\": \"Form.Test.Person\",\n \"_id\": 1,\n \"name\": \"Bob\"\n}" 250 | }, 251 | "description": "Get meta information about one class " 252 | }, 253 | "response": [] 254 | }, 255 | { 256 | "name": "form/object/:class", 257 | "request": { 258 | "auth": { 259 | "type": "basic", 260 | "basic": { 261 | "username": "{{user}}", 262 | "password": "{{pass}}", 263 | "saveHelperData": true, 264 | "showPassword": false 265 | } 266 | }, 267 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person", 268 | "method": "POST", 269 | "header": [ 270 | { 271 | "key": "Authorization", 272 | "value": "Basic X1NZU1RFTTpTWVM=", 273 | "description": "" 274 | } 275 | ], 276 | "body": { 277 | "mode": "raw", 278 | "raw": "{\n \"_class\": \"Form.Test.Person\",\n \"name\": \"Charlie\"\n}" 279 | }, 280 | "description": "Get meta information about one class " 281 | }, 282 | "response": [] 283 | }, 284 | { 285 | "name": "form/object/:class/:id", 286 | "request": { 287 | "auth": { 288 | "type": "basic", 289 | "basic": { 290 | "username": "{{user}}", 291 | "password": "{{pass}}", 292 | "saveHelperData": true, 293 | "showPassword": false 294 | } 295 | }, 296 | "url": "http://{{host}}:{{port}}/forms/form/object/Form.Test.Person/101", 297 | "method": "DELETE", 298 | "header": [ 299 | { 300 | "key": "Authorization", 301 | "value": "Basic X1NZU1RFTTpTWVM=", 302 | "description": "" 303 | } 304 | ], 305 | "body": { 306 | "mode": "raw", 307 | "raw": "" 308 | }, 309 | "description": "Get meta information about one class " 310 | }, 311 | "response": [] 312 | }, 313 | { 314 | "name": "form/object/:class/:query", 315 | "request": { 316 | "auth": { 317 | "type": "basic", 318 | "basic": { 319 | "username": "{{user}}", 320 | "password": "{{pass}}", 321 | "saveHelperData": true, 322 | "showPassword": false 323 | } 324 | }, 325 | "url": "http://{{host}}:{{port}}/forms/form/objects/Form.Test.Person/info", 326 | "method": "GET", 327 | "header": [ 328 | { 329 | "key": "Authorization", 330 | "value": "Basic X1NZU1RFTTpTWVM=", 331 | "description": "" 332 | } 333 | ], 334 | "body": {}, 335 | "description": "Get meta information about one class " 336 | }, 337 | "response": [] 338 | } 339 | ] 340 | } -------------------------------------------------------------------------------- /sc-list.txt: -------------------------------------------------------------------------------- 1 | Form.inc 2 | Form.pkg 3 | --------------------------------------------------------------------------------