a9b6694a4e30331ff71c26f49c889bbbb022c979
[xtuple] / lib / orm / source / xt / javascript / discovery.sql
1 select xt.install_js('XT','Discovery','xtuple', $$
2
3 (function () {
4
5   /**
6    * @class
7    *
8    * The XT.Discovery class includes all functions necessary to return an
9    * API Discovery document: (https://developers.google.com/discovery/v1/using)
10    */
11
12   XT.Discovery = {};
13
14   XT.Discovery.isDispatchable = true;
15
16   /**
17    * Return an API Discovery List for this database's ORM where isRest = true.
18    *
19    * @param {String} Optional. An orm_type name like "Contact". If null you get all of them.
20    * @param {String} Optional. The rootUrl path of the API. e.g. "https://www.example.com/"
21    * @returns {Object}
22    */
23   XT.Discovery.getList = function (orm, rootUrl) {
24     "use strict";
25
26     var list = {},
27         master = {},
28         org = XT.currentDb(),
29         orms = XT.Discovery.getIsRestORMs(orm),
30         version = "v1alpha1";
31
32     rootUrl = rootUrl || "{rootUrl}";
33
34     if (!org) {
35       return false;
36     }
37
38     if (!orms) {
39       return false;
40     }
41
42     list.kind = "discovery#directoryList";
43     list.discoveryVersion = version;
44     list.items = [];
45
46     /* Loop through exposed ORM models and build list items. */
47     for (var i = 0; i < orms.length; i++) {
48       var item = {},
49           ormType = orms[i].orm_type,
50           ormTypeHyphen = ormType.camelToHyphen();
51
52       item.kind = "discovery#directoryItem";
53       item.id = org + "." + ormTypeHyphen + ":" + version;
54       item.name = org + "." + ormTypeHyphen;
55       item.version = version;
56       item.title = "xTuple ERP REST API for " + ormType + " business objects.";
57       item.description = "Lets you get and manipulate xTuple ERP " + ormType + " business objects.";
58       item.discoveryRestUrl = rootUrl + org + "/discovery/" + version + "/apis/" + ormTypeHyphen + "/" + version + "/rest";
59       item.discoveryLink = "./apis/" + ormTypeHyphen + "/" + version + "/rest";
60       item.icons = {
61         "x16": rootUrl + org + "/assets/api/" + ormTypeHyphen + "-16.png",
62         "x32": rootUrl + org + "/assets/api/" + ormTypeHyphen + "-32.png"
63       };
64       item.documentationLink = "https://dev.xtuple.com/api/" + ormTypeHyphen; /* TODO - What should this be? */
65       item.preferred = true; /* TODO - Change this as we add new versions. */
66
67       list.items[i] = item;
68     }
69
70     if (!orm) {
71       /* Add master item that includes all ORM models in one Discovery Document. */
72       master.kind = "discovery#directoryItem";
73       master.id = org + ":" + version;
74       master.name = org;
75       master.version = version;
76       master.title = "xTuple ERP REST API all business objects.";
77       master.description = "Lets you get and manipulate all xTuple ERP business objects.";
78       master.discoveryRestUrl = rootUrl + org + "/discovery/" + version + "/apis/" + version + "/rest";
79       master.discoveryLink = "./apis/" + version + "/rest";
80       master.icons = {
81         "x16": rootUrl + org + "/assets/api/api-16.png",
82         "x32": rootUrl + org + "/assets/api/api-32.png"
83       };
84       master.documentationLink = "https://dev.xtuple.com/api/"; /* TODO - What should this be? */
85       master.preferred = true; /* TODO - Change this as we add new versions. */
86
87       list.items.unshift(master);
88     }
89
90     return list;
91   };
92
93
94   /**
95    * Return an API Discovery document for this database's ORM where isRest = true.
96    *
97    * @param {String} or {Array} Optional. An orm_type name like "Contact".
98    * @param {String} Optional. The rootUrl path of the API. e.g. "https://www.example.com/"
99    * @returns {Object}
100    */
101   XT.Discovery.getDiscovery = function (orm, rootUrl) {
102     "use strict";
103
104     var discovery = {},
105         gotDispatchable,
106         gotOrms,
107         isDispatchable = [],
108         listItemOrms = [],
109         org = plv8.execute("select current_database()"),
110         ormAuth = {},
111         orms,
112         schemas = {},
113         services,
114         version = "v1alpha1";
115
116     rootUrl = rootUrl || "{rootUrl}";
117
118     if (orm && typeof orm === 'string') {
119       orms = XT.Discovery.getIsRestORMs(orm);
120     } else if (orm instanceof Array && orm.length) {
121       /* Build up ORMs from array. */
122       for (var i = 0; i < orm.length; i++) {
123         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
124         if (gotOrms) {
125           if (!(orms instanceof Array)) {
126             orms = [];
127           }
128           orms = orms.concat(gotOrms).unique();
129         }
130       }
131     } else {
132       orms = XT.Discovery.getIsRestORMs();
133     }
134
135     if (org.length !== 1) {
136       return false;
137     } else {
138       org = org[0].current_database;
139     }
140
141     if (!orms) {
142       if (orm && typeof orm === 'string') {
143         isDispatchable = XT.Discovery.getDispatchableObjects(orm);
144       } else if (orm instanceof Array && orm.length) {
145         /* Check Dispatchable for orm array. */
146         for (var i = 0; i < orm.length; i++) {
147           gotDispatchable = XT.Discovery.getDispatchableObjects(orm[i]);
148           isDispatchable = isDispatchable.concat(gotDispatchable).unique();
149         }
150       } else {
151         isDispatchable = XT.Discovery.getDispatchableObjects();
152       }
153
154       if (isDispatchable.length === 0) {
155         /* If there are no resource, and no services, then there's nothing to see here */
156         return false;
157       }
158     }
159
160     /*
161      * Header section.
162      */
163     discovery.kind = "discovery#restDescription";
164
165     /* TODO - Implement etags. */
166     discovery.etag = "";
167
168     discovery.discoveryVersion = version; /* TODO - Move this to v1 for release. */
169     discovery.id = org + (typeof orm === 'string' ? "." + orm.camelToHyphen() : "") + ":" + version;
170     discovery.name = org + (typeof orm === 'string' ? "." + orm.camelToHyphen() : "");
171     discovery.version = version;
172     discovery.revision = XT.Discovery.getDate();
173     discovery.title = "xTuple ERP REST API for " + (typeof orm === 'string' ? orm : "all") + " business objects.";
174     discovery.description = "Lets you get and manipulate xTuple ERP " + (orm ? orm + " " : "") + "business objects.";
175     discovery.icons = {
176       "x16": rootUrl + org + "/assets/api/" + (typeof orm === 'string' ? orm.camelToHyphen() : "api") + "-16.png",
177       "x32": rootUrl + org + "/assets/api/" + (typeof orm === 'string' ? orm.camelToHyphen() : "api") + "-32.png"
178     };
179     discovery.documentationLink = "https://dev.xtuple.com/" + (typeof orm === 'string' ? orm.camelToHyphen() : ""); /* TODO - What should this be? */
180     discovery.protocol = "rest";
181     discovery.baseUrl = rootUrl + org + "/api/" + version + "/";
182     discovery.basePath = "/" + org + "/api/" + version + "/";
183     discovery.rootUrl = rootUrl;
184     discovery.servicePath = org + "/api/" + version + "/";
185     discovery.batchPath = "batch"; /* TODO - Support batch requests? */
186
187     /*
188      * Parameters section.
189      */
190     discovery.parameters = {
191       "oauth_token": {
192         "type": "string",
193         "description": "OAuth 2.0 token for the current user.",
194         "location": "query"
195       }
196 /* TODO: Add support for these to the REST API routes. */
197 /*
198       "alt": {
199         "type": "string",
200         "description": "Data format for the response.",
201         "default": "json",
202         "enum": [
203           "json"
204         ],
205         "enumDescriptions": [
206           "Responses with Content-Type of application/json"
207         ],
208         "location": "query"
209       },
210       "fields": {
211         "type": "string",
212         "description": "Selector specifying which fields to include in a partial response.",
213         "location": "query"
214       },
215       "prettyPrint": {
216         "type": "boolean",
217         "description": "Returns response with indentations and line breaks.",
218         "default": "true",
219         "location": "query"
220       }
221 */
222     };
223
224     /*
225      * Auth section.
226      */
227     discovery.auth = XT.Discovery.getAuth(orm, rootUrl);
228     discovery.auth = XT.Discovery.getServicesAuth(orm, discovery.auth, rootUrl);
229
230     /*
231      * Schema section.
232      */
233     XT.Discovery.getORMSchemas(orms, schemas);
234
235     /* Sanitize the JSON-Schema. */
236     XT.Discovery.sanitize(schemas);
237
238     /* Get services JSON-Schema. */
239     if (orm && typeof orm === 'string') {
240       XT.Discovery.getServicesSchema(orm, schemas);
241     } else if (orm instanceof Array && orm.length) {
242       /* Build up schemas from array. */
243       for (var i = 0; i < orm.length; i++) {
244         XT.Discovery.getServicesSchema(orm[i], schemas);
245       }
246     } else {
247       XT.Discovery.getServicesSchema(null, schemas);
248     }
249
250     if (!schemas) {
251       return false;
252     }
253
254     /* Get parent ListItem ORMs */
255     if (orms && orms instanceof Array && orms.length) {
256       for (var i = 0; i < orms.length; i++) {
257         listItemOrms[i] = {"orm_namespace": orms[i].orm_namespace, "orm_type": orms[i].orm_type + "ListItem"};
258       }
259     }
260
261     if (listItemOrms.length > 0) {
262       XT.Discovery.getORMSchemas(listItemOrms, schemas);
263     }
264
265     /* Sort schema properties alphabetically. */
266     discovery.schemas = XT.Discovery.sortObject(schemas);
267
268     /*
269      * Resources section.
270      */
271     discovery.resources = XT.Discovery.getResources(orm, rootUrl);
272
273     /* Loop through resources and add JSON-Schema primKeyProp for methods that need it. */
274     if (orms && orms instanceof Array && orms.length) {
275       for (var i = 0; i < orms.length; i++) {
276         var ormType = orms[i].orm_type,
277             ormNamespace = orms[i].orm_namespace,
278             thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
279             key = XT.Discovery.getKeyProps(discovery.schemas[ormType]);
280
281         if (!key) {
282           /* This should never happen. */
283           plv8.elog(ERROR, "No key found for ormType: ", ormType);
284         }
285
286
287         if (thisOrm.privileges.all.delete) {
288           discovery.resources[ormType].methods.delete.path = discovery.resources[ormType].methods.delete.path + key.name + "}";
289           discovery.resources[ormType].methods.delete.parameters = {};
290           discovery.resources[ormType].methods.delete.parameters[key.name] = key.props;
291           discovery.resources[ormType].methods.delete.parameters[key.name].location = 'path';
292           discovery.resources[ormType].methods.delete.parameterOrder = [key.name];
293         }
294
295         if (thisOrm.privileges.all.read) {
296           discovery.resources[ormType].methods.get.path = discovery.resources[ormType].methods.get.path + key.name + "}";
297           discovery.resources[ormType].methods.get.parameters = {};
298           discovery.resources[ormType].methods.get.parameters[key.name] = key.props;
299           discovery.resources[ormType].methods.get.parameters[key.name].location = 'path';
300           discovery.resources[ormType].methods.get.parameterOrder = [key.name];
301         }
302
303         if (thisOrm.privileges.all.read) {
304           discovery.resources[ormType].methods.head.path = discovery.resources[ormType].methods.head.path + key.name + "}";
305           discovery.resources[ormType].methods.head.parameters = {};
306           discovery.resources[ormType].methods.head.parameters[key.name] = key.props;
307           discovery.resources[ormType].methods.head.parameters[key.name].location = 'path';
308           discovery.resources[ormType].methods.head.parameterOrder = [key.name];
309         }
310
311         if (thisOrm.privileges.all.update) {
312           discovery.resources[ormType].methods.patch.path = discovery.resources[ormType].methods.patch.path + key.name + "}";
313           discovery.resources[ormType].methods.patch.parameters = {};
314           discovery.resources[ormType].methods.patch.parameters[key.name] = key.props;
315           discovery.resources[ormType].methods.patch.parameters[key.name].location = 'path';
316           discovery.resources[ormType].methods.patch.parameterOrder = [key.name];
317         }
318       }
319     }
320
321     /*
322      * Services section.
323      */
324     /* TODO - Old way. Remove if we don't need this anymore. */
325     /*discovery.services = XT.Discovery.getServices(orm, rootUrl); */
326
327     /* Merge our services into the discovery.resources object. */
328     services = XT.Discovery.getServices(orm, rootUrl);
329     for (var service in services) {
330       if (discovery.resources[service] && discovery.resources[service].methods) {
331         /* Resource exists, so merge with existing methods. */
332         discovery.resources[service].methods = XT.extend(discovery.resources[service].methods, services[service].methods);
333       } else {
334         /* There is no resource yet, so merge this service into the parent 'resources'. */
335         discovery.resources[service] = services[service];
336       }
337     }
338
339     /* return the results */
340     return discovery;
341   };
342
343
344   /**
345    * Return an API Discovery document's Auth section for this database's ORM where isRest = true.
346    * This function allows you get the Auth section much faster than the full getDiscovery() above.
347    *
348    * @param {String} Optional. An orm_type name like "Contact".
349    * @param {String} Optional. The rootUrl path of the API. e.g. "https://www.example.com/"
350    * @returns {Object}
351    */
352   XT.Discovery.getAuth = function (orm, rootUrl) {
353     "use strict";
354
355     var auth = {},
356       gotOrms,
357       org = plv8.execute("select current_database()"),
358       orms;
359
360     rootUrl = rootUrl || "{rootUrl}";
361
362     if (orm && typeof orm === 'string') {
363       orms = XT.Discovery.getIsRestORMs(orm);
364     } else if (orm instanceof Array && orm.length) {
365       /* Build up ORMs from array. */
366       for (var i = 0; i < orm.length; i++) {
367         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
368         if (gotOrms) {
369           if (!(orms instanceof Array)) {
370             orms = [];
371           }
372           orms = orms.concat(gotOrms).unique();
373         }
374       }
375     } else {
376       orms = XT.Discovery.getIsRestORMs();
377     }
378
379     if (org.length !== 1) {
380       return false;
381     } else {
382       org = org[0].current_database;
383     }
384
385     if (!orms) {
386       return false;
387     }
388
389     auth = {
390       "oauth2": {
391         "scopes": {}
392       }
393     };
394
395     /* Set base full access scope. */
396     auth.oauth2.scopes[rootUrl + org + "/auth"] = {
397       "description": "Full access to all '" + org + "' resources"
398     };
399
400     /* Loop through exposed ORM models and build scopes. */
401     for (var i = 0; i < orms.length; i++) {
402       var ormType = orms[i].orm_type,
403           ormNamespace = orms[i].orm_namespace,
404           thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
405           ormTypeHyphen = ormType.camelToHyphen();
406
407       /* TODO - Do we need to include "XM" in the name? */
408       auth.oauth2.scopes[rootUrl + org + "/auth/" + ormTypeHyphen] = {
409         "description": "Manage " + orms[i].orm_type + " resources"
410       };
411
412       if (!thisOrm.privileges) {
413         plv8.elog(ERROR, "ORM Fail, missing privileges: " + ormNamespace + "." + ormType);
414       }
415
416       /* Only include readonly if privileges are read only. */
417       if (!thisOrm.privileges.all.create && !thisOrm.privileges.all.update && !thisOrm.privileges.all.delete) {
418         auth.oauth2.scopes[rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"] = {
419           "description": "View " + orms[i].orm_type + " resources"
420         };
421       }
422     }
423
424     return auth;
425   };
426
427
428   /**
429    * Return an API Discovery document's Resources section for this database's ORM where isRest = true.
430    * This function allows you get the Resources section much faster than the full getDiscovery() above.
431    * To make it faster, the JSON-Schema is skipped, so method's parameter's primKeyProp will be blank.
432    *
433    * @param {String} Optional. An orm_type name like "Contact".
434    * @param {String} Optional. The rootUrl path of the API. e.g. "https://www.example.com/"
435    * @returns {Object}
436    */
437   XT.Discovery.getResources = function (orm, rootUrl) {
438     "use strict";
439
440     var gotOrms,
441         resources = {},
442         org = XT.currentDb(),
443         orms = [];
444
445     rootUrl = rootUrl || "{rootUrl}";
446
447     if (orm && typeof orm === 'string') {
448       orms = XT.Discovery.getIsRestORMs(orm);
449     } else if (orm instanceof Array && orm.length) {
450       /* Build up ORMs from array. */
451       for (var i = 0; i < orm.length; i++) {
452         gotOrms = XT.Discovery.getIsRestORMs(orm[i]);
453         if (gotOrms) {
454           if (!(orms instanceof Array)) {
455             orms = [];
456           }
457           orms = orms.concat(gotOrms).unique();
458         }
459       }
460     } else {
461       orms = XT.Discovery.getIsRestORMs();
462     }
463
464     if (!org) {
465       return false;
466     }
467
468     if (!orms) {
469       return false;
470     }
471
472     /* Loop through exposed ORM models and build resources. */
473     for (var i = 0; i < orms.length; i++) {
474       var listModel,
475           ormType = orms[i].orm_type,
476           ormNamespace = orms[i].orm_namespace,
477           thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
478           thisListOrm,
479           ormTypeHyphen = ormType.camelToHyphen(),
480           sql = 'select orm_type from xt.orm where orm_type=$1 and orm_active;',
481           ormListItem = plv8.execute(sql, [ormType + "ListItem"]);
482
483       resources[ormType] = {};
484       resources[ormType].methods = {};
485
486       if (ormListItem.length > 0) {
487         listModel = ormType + "ListItem";
488       } else {
489         listModel = ormType;
490       }
491
492       /*
493        * delete
494        */
495       if (thisOrm.privileges.all.delete) {
496         resources[ormType].methods.delete = {
497           "id": ormType + ".delete",
498           "path": "resources/" + ormTypeHyphen + "/{",
499           "httpMethod": "DELETE",
500           "description": "Deletes a single " + ormType + " record."
501         };
502
503         resources[ormType].methods.delete.scopes = [
504           rootUrl + org + "/auth",
505           rootUrl + org + "/auth/" + ormTypeHyphen
506         ];
507       }
508
509       /*
510        * get
511        */
512       if (thisOrm.privileges.all.read) {
513         resources[ormType].methods.get = {
514           "id": ormType + ".get",
515           "path": "resources/" + ormTypeHyphen + "/{",
516           "httpMethod": "GET",
517           "description": "Gets a single " + ormType + " record."
518         };
519
520         resources[ormType].methods.get.response = {
521           "$ref": ormType
522         };
523
524         resources[ormType].methods.get.scopes = [
525           rootUrl + org + "/auth",
526           rootUrl + org + "/auth/" + ormTypeHyphen,
527           rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
528         ];
529       }
530
531       /*
532        * head
533        */
534       if (thisOrm.privileges.all.read) {
535         resources[ormType].methods.head = {
536           "id": ormType + ".head",
537           "path": "resources/" + ormTypeHyphen + "/{",
538           "httpMethod": "HEAD",
539           "description": "Returns the HTTP Header as if you made a GET request for a single " + ormType + " record, but will not return any response body."
540         };
541
542         resources[ormType].methods.head.scopes = [
543           rootUrl + org + "/auth",
544           rootUrl + org + "/auth/" + ormTypeHyphen,
545           rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
546         ];
547       }
548
549       /*
550        * insert
551        */
552       if (thisOrm.privileges.all.create) {
553         resources[ormType].methods.insert = {
554           "id": ormType + ".insert",
555           "path": "resources/" + ormTypeHyphen,
556           "httpMethod": "POST",
557           "description": "Add a single " + ormType + " record."
558         };
559
560         resources[ormType].methods.insert.request = {
561           "$ref": ormType
562         };
563
564         resources[ormType].methods.insert.response = {
565           "$ref": ormType
566         };
567
568         resources[ormType].methods.insert.scopes = [
569           rootUrl + org + "/auth",
570           rootUrl + org + "/auth/" + ormTypeHyphen
571         ];
572       }
573
574       /*
575        * list
576        */
577       if (thisOrm.privileges.all.read) {
578         resources[ormType].methods.list = {
579           "id": ormType + ".list",
580           "path": "resources/" + ormTypeHyphen,
581           "httpMethod": "GET",
582           "description": "Returns a list of " + ormType + " records."
583         };
584
585         resources[ormType].methods.list.parameters = {
586           "query": {
587             "type": "object",
588             "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
589             "location": "query",
590             "$ref": "TODO: add this when moving to JSON-Schema draft v5"
591           },
592           "orderby": {
593             "type": "object",
594             "description": "Specify the order of results for a filtered list request.",
595             "location": "query"
596           },
597           "rowLimit": {
598             "type": "integer",
599             "description": "Maximum number of entries to return. Optional.",
600             "format": "int32",
601             "minimum": "1",
602             "location": "query"
603           },
604           "maxResults": {
605             "type": "integer",
606             "description": "Maximum number of entries returned on one result page. Optional.",
607             "format": "int32",
608             "minimum": "1",
609             "location": "query"
610           },
611           "pageToken": {
612             "type": "string",
613             "description": "Token specifying which result page to return. Optional.",
614             "location": "query"
615           },
616           "q": {
617             "type": "string",
618             "description": "Free text search terms to find events that match these terms in any field. Optional.",
619             "location": "query"
620           },
621           "count": {
622             "type": "boolean",
623             "description": "Return the a count of the total number of results from a filtered list request.",
624             "location": "query"
625           }
626         };
627
628         resources[ormType].methods.list.response = {
629           "$ref": listModel
630         };
631
632         resources[ormType].methods.list.scopes = [
633           rootUrl + org + "/auth",
634           rootUrl + org + "/auth/" + ormTypeHyphen,
635           rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
636         ];
637       }
638
639       /*
640        * listhead
641        */
642       if (thisOrm.privileges.all.read) {
643         resources[ormType].methods.listhead = {
644           "id": ormType + ".listhead",
645           "path": "resources/" + ormTypeHyphen,
646           "httpMethod": "HEAD",
647           "description": "Returns the HTTP Header as if you made a GET request for a list of " + ormType + " records, but will not return any response body."
648         };
649
650         resources[ormType].methods.listhead.parameters = {
651           "query": {
652             "type": "object",
653             "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
654             "location": "query",
655             "$ref": "TODO"
656           },
657           "orderby": {
658             "type": "object",
659             "description": "Specify the order of results for a filtered list request.",
660             "location": "query"
661           },
662           "rowLimit": {
663             "type": "integer",
664             "description": "Maximum number of entries to return. Optional.",
665             "format": "int32",
666             "minimum": "1",
667             "location": "query"
668           },
669           "maxResults": {
670             "type": "integer",
671             "description": "Maximum number of entries returned on one result page. Optional.",
672             "format": "int32",
673             "minimum": "1",
674             "location": "query"
675           },
676           "pageToken": {
677             "type": "string",
678             "description": "Token specifying which result page to return. Optional.",
679             "location": "query"
680           },
681           "q": {
682             "type": "string",
683             "description": "Free text search terms to find events that match these terms in any field. Optional.",
684             "location": "query"
685           },
686           "count": {
687             "type": "boolean",
688             "description": "Return the a count of the total number of results from a filtered list request.",
689             "location": "query"
690           }
691         };
692
693         resources[ormType].methods.listhead.scopes = [
694           rootUrl + org + "/auth",
695           rootUrl + org + "/auth/" + ormTypeHyphen,
696           rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
697         ];
698       }
699
700       /*
701        * patch
702        */
703       if (thisOrm.privileges.all.update) {
704         resources[ormType].methods.patch = {
705           "id": ormType + ".patch",
706           "path": "resources/" + ormTypeHyphen + "/{",
707           "httpMethod": "PATCH",
708           "description": "Modifies a single " + ormType + " record. This method supports JSON-Patch semantics."
709         };
710
711         resources[ormType].methods.patch.request = {
712           "$ref": ormType
713         };
714
715         resources[ormType].methods.patch.response = {
716           "$ref": ormType
717         };
718
719         resources[ormType].methods.patch.scopes = [
720           rootUrl + org + "/auth",
721           rootUrl + org + "/auth/" + ormTypeHyphen
722         ];
723       }
724     }
725
726     return resources;
727   };
728
729
730   XT.Discovery.getDispatchableObjects = function (orm) {
731     "use strict";
732
733     var dispatchableObjects = [];
734
735     for (var businessObjectName in XM) {
736       var businessObject = XM[businessObjectName];
737       if (businessObject.isDispatchable &&
738           (!orm || businessObjectName === orm)) {
739         dispatchableObjects.push(businessObjectName);
740       }
741     }
742
743     return dispatchableObjects;
744   };
745
746
747   /**
748    * Return an API Discovery document's Services JSON-Schema.
749    *
750    * @param {String} Optional. An orm_type name like "Contact".
751    * @param {Object} Optional. A schema object to add schemas too.
752    * @returns {Object}
753    */
754   XT.Discovery.getServicesSchema = function (orm, schemas) {
755     "use strict";
756
757     schemas = schemas || {};
758
759     var dispatchableObjects = [],
760       gotOrms,
761       i,
762       businessObject,
763       businessObjectName,
764       method,
765       methodName,
766       objectServices;
767
768     if (orm && typeof orm === 'string') {
769       dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
770     } else if (orm instanceof Array && orm.length) {
771       /* Build up ORMs from array. */
772       for (var i = 0; i < orm.length; i++) {
773         gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
774         dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
775       }
776     } else {
777       dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
778     }
779
780     for (i = 0; i < dispatchableObjects.length; i++) {
781       businessObjectName = dispatchableObjects[i];
782       businessObject = XM[businessObjectName];
783       objectServices = {};
784       for (methodName in businessObject) {
785         method = businessObject[methodName];
786         /*
787         Report only on documented dispatch methods. We document the methods by
788         tacking description and params attributes onto the function.
789         */
790         if (typeof method === 'function' && method.description && method.schema) {
791           for (var schema in method.schema) {
792             schemas[schema] = method.schema[schema];
793           }
794         }
795       }
796     }
797
798     return schemas;
799   };
800
801
802   /**
803    * Return an API Discovery document's Services section.
804    *
805    * @param {String} Optional. An orm_type name like "Contact".
806    * @param {String} Optional. The rootUrl path of the API. e.g. "https://www.example.com/"
807    * @param {Boolean} Optional. Some services have no query parameters, but their function
808    *  does and we need to know what order to put them in. @See restRouter.js
809    * @returns {Object}
810    */
811   XT.Discovery.getServices = function (orm, rootUrl, includeOrder) {
812     "use strict";
813
814     var resources = {},
815       org = XT.currentDb(),
816       dispatchableObjects = [],
817       gotOrms,
818       i,
819       businessObject,
820       businessObjectName,
821       method,
822       methodName,
823       methodParam,
824       methodParamName,
825       objectServices,
826       allServices = {},
827       version = "v1alpha1";
828
829     rootUrl = rootUrl || "{rootUrl}";
830
831     if (orm && typeof orm === 'string') {
832       dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
833     } else if (orm instanceof Array && orm.length) {
834       /* Build up ORMs from array. */
835       for (var i = 0; i < orm.length; i++) {
836         gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
837         dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
838       }
839     } else {
840       dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
841     }
842
843     if (!org) {
844       return false;
845     }
846
847     for (i = 0; i < dispatchableObjects.length; i++) {
848       businessObjectName = dispatchableObjects[i];
849       businessObject = XM[businessObjectName];
850       objectServices = {};
851       for (methodName in businessObject) {
852         method = businessObject[methodName];
853         /*
854         Report only on documented dispatch methods. We document the methods by
855         tacking description and params attributes onto the function.
856         */
857         if (typeof method === 'function' && method.description && (method.params || method.schema)) {
858           for (methodParamName in method.params) {
859             /* The parameter location is query unless otherwise specified */
860             methodParam = method.params[methodParamName];
861             if (!methodParam.location) {
862               methodParam.location = "query";
863             }
864           }
865           var businessObjectNameHyphen = businessObjectName.camelToHyphen();
866           var scopes = [
867             rootUrl + org + "/auth",
868             rootUrl + org + "/auth/" + businessObjectNameHyphen
869           ];
870           objectServices[methodName] = {
871             id: businessObjectName + "." + methodName,
872             /* TODO: decide the path we want to put these under in restRouter, and reflect that here */
873             path:  "services/" +  businessObjectNameHyphen + "/" + methodName.camelToHyphen(),
874             httpMethod: "POST",
875             scopes: scopes,
876             description: method.description,
877           };
878
879           if (method.request) {
880             objectServices[methodName].request = method.request;
881           }
882
883           if (method.params) {
884             objectServices[methodName].parameters = method.params;
885             objectServices[methodName].parameterOrder = Object.keys(method.params);
886           } else if (method.parameterOrder && includeOrder) {
887             /* This isn't included in the Discovery Doc, just when called from restRouter.js */
888             objectServices[methodName].parameterOrder = method.parameterOrder;
889           }
890         }
891       }
892       if (Object.keys(objectServices).length > 0) {
893         /* only return objects with >= 1 documented dispatch function */
894         allServices[businessObjectName] = {methods: objectServices};
895       }
896     }
897
898     return allServices;
899   };
900
901   /**
902    * Return an API Discovery document's Services JSON-Schema.
903    *
904    * @param {String} Optional. An orm_type name like "Contact".
905    * @param {Object} Optional. A schema object to add schemas too.
906    * @returns {Object}
907    */
908   XT.Discovery.getServicesAuth = function (orm, auth, rootUrl) {
909     "use strict";
910
911     auth = auth || {oauth2: {scopes: {}}};
912     rootUrl = rootUrl || "{rootUrl}";
913
914     var dispatchableObjects = [],
915       gotOrms,
916       org = plv8.execute("select current_database()"),
917       i,
918       businessObject,
919       businessObjectName,
920       method,
921       methodName,
922       objectServices;
923
924     if (org.length !== 1) {
925       return false;
926     } else {
927       org = org[0].current_database;
928     }
929
930     if (orm && typeof orm === 'string') {
931       dispatchableObjects = XT.Discovery.getDispatchableObjects(orm);
932     } else if (orm instanceof Array && orm.length) {
933       /* Build up ORMs from array. */
934       for (var i = 0; i < orm.length; i++) {
935         gotOrms = XT.Discovery.getDispatchableObjects(orm[i]);
936         dispatchableObjects = dispatchableObjects.concat(gotOrms).unique();
937       }
938     } else {
939       dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
940     }
941
942     for (i = 0; i < dispatchableObjects.length; i++) {
943       businessObjectName = dispatchableObjects[i];
944       businessObject = XM[businessObjectName];
945       objectServices = {};
946       for (methodName in businessObject) {
947         method = businessObject[methodName];
948         /*
949         Report only on documented dispatch methods. We document the methods by
950         tacking description and params attributes onto the function.
951         */
952         if (typeof method === 'function' && method.description && method.scope) {
953           auth.oauth2.scopes[rootUrl + org + "/auth/" + method.scope.camelToHyphen()] = {
954             description: "Use " + method.scope + " services"
955           }
956         }
957       }
958     }
959
960     return auth;
961   };
962
963   /*
964    * Helper function to convert date to string in yyyyMMdd format.
965    *
966    * @returns {String}
967    */
968   XT.Discovery.getDate = function () {
969     "use strict";
970
971     var today = new Date(),
972         year = today.getUTCFullYear(),
973         month = today.getUTCMonth() + 1,
974         day = today.getUTCDate();
975
976     /* Convert to string and preserve leading zero. */
977     if (day < 10) {
978       day = "0" + day;
979     } else {
980       day = "" + day;
981     }
982
983     if (month < 10) {
984       month = "0" + month;
985     } else {
986       month = "" + month;
987     }
988
989     year = "" + year;
990
991     return year + month + day;
992   };
993
994
995   /*
996    * Helper function to sanitize the schemas relations.
997    * Right now, this just removes the "inverse" property from a child schema.
998    * TODO: Consider using this to remove primary keys instead of the other
999    * logic above. May also need to remove unprivileged properties.
1000    * @See: XT.Data.sanitize() function that is similar.
1001    *
1002    * @param {Object} Object of JSON-Schemas.
1003    */
1004   XT.Discovery.sanitize = function (schema) {
1005     "use strict";
1006
1007     var childOrm,
1008         inverse,
1009         parentOrm,
1010         parentOrmProp,
1011         propName,
1012         propery,
1013         resource;
1014
1015     for (resource in schema) {
1016       /* Find the inverse value from the original ORM. */
1017       /* TODO: Assuming "XM" here... */
1018       parentOrm = XT.Orm.fetch("XM", resource, {"silentError": true});
1019
1020       for (propName in schema[resource].properties) {
1021         propery = schema[resource].properties[propName];
1022
1023         if (propery.items && propery.items.$ref) {
1024           parentOrmProp = XT.Orm.getProperty(parentOrm, propName);
1025           if (parentOrmProp.toMany && parentOrmProp.toMany.type && parentOrmProp.toMany.inverse) {
1026             inverse = parentOrmProp.toMany.inverse;
1027             childOrm = XT.Orm.fetch("XM", parentOrmProp.toMany.type, {"silentError": true});
1028
1029             /* Delete the inverse property from the Child JSON-Schema. */
1030             if (childOrm && childOrm.isNestedOnly && schema[parentOrmProp.toMany.type] &&
1031               schema[parentOrmProp.toMany.type].properties[inverse]) {
1032
1033               delete schema[parentOrmProp.toMany.type].properties[inverse];
1034             }
1035           }
1036         }
1037       }
1038     }
1039   };
1040
1041
1042   /*
1043    * Helper function to sort the schemas properties alphabetically.
1044    * Note: ECMA-262 does not specify enumeration order. This is just for
1045    * human readability in outputted JSON.
1046    *
1047    * @param {Object} Object of JSON-Schemas.
1048    * @returns {Object}
1049    */
1050   XT.Discovery.sortObject = function (obj) {
1051     "use strict";
1052
1053     var arr = [],
1054         key,
1055         sorted = {};
1056
1057     for (key in obj) {
1058       if (obj.hasOwnProperty(key)) {
1059         arr.push(key);
1060       }
1061     }
1062
1063     arr.sort();
1064
1065     for (key = 0; key < arr.length; key++) {
1066       sorted[arr[key]] = obj[arr[key]];
1067     }
1068
1069     return sorted;
1070   };
1071
1072
1073   /*
1074    * Helper function to get a single or all isRest ORM Models.
1075    *
1076    * @param {String} Optional. An orm_type name like "Contact".
1077    * @returns {Object}
1078    */
1079   XT.Discovery.getIsRestORMs = function (orm) {
1080     "use strict";
1081
1082     /* TODO - Do we need to include "XM" in the propName? */
1083     var sql = "select orm_namespace, orm_type " +
1084               "from xt.orm " +
1085               "where 1=1 " +
1086               " and orm_rest " +
1087               " and not orm_ext " +
1088               " and orm_active " +
1089               " and orm_context = 'xtuple' " +
1090               "union all " +
1091               "select orm_namespace, orm_type " +
1092               "from xt.orm " +
1093               "where orm_id in (" +
1094               " select orm_id " +
1095               " from xt.orm " +
1096               " left join xt.ext on ext_name=orm_context " +
1097               " left join xt.usrext on ext_id=usrext_ext_id " +
1098               " left join xt.grpext on ext_id=grpext_ext_id " +
1099               " left join usrgrp on usrgrp_grp_id=grpext_grp_id " +
1100               " where 1=1 " +
1101               "  and orm_rest " +
1102               "  and not orm_ext " +
1103               "  and orm_active " +
1104               "  and orm_context != 'xtuple' " +
1105               "  and (usrext_usr_username=$1 or usrgrp_username=$1)) " +
1106               "group by orm_namespace, orm_type order by orm_namespace, orm_type",
1107         orms = [],
1108         relatedORMs,
1109         relations = [],
1110         singleOrms = [],
1111         thisOrm;
1112
1113     orms = plv8.execute(sql, [XT.username]);
1114
1115     /* If this is a single ORM request, find all the related ORMs that are
1116      * exposed to REST and return only the single and related ORMs.
1117      */
1118     if (orm) {
1119       /* Fetch the single ORM. Only need this loop to get the namespace. */
1120       for (var i = 0; i < orms.length; i++) {
1121         if (orm === orms[i].orm_type) {
1122           thisOrm = XT.Orm.fetch(orms[i].orm_namespace, orm, {"superUser": true});
1123         }
1124       }
1125
1126       /* Find the related ORMs. */
1127       if (thisOrm) {
1128         for (var prop in thisOrm.properties) {
1129           var relation;
1130
1131           if (thisOrm.properties[prop].toOne || thisOrm.properties[prop].toMany) {
1132             relation = thisOrm.properties[prop].toOne || thisOrm.properties[prop].toMany;
1133             if (relation.type) {
1134               /* Recurse into the relation's ORM and add it's related ORMs. */
1135               relatedORMs = XT.Discovery.getIsRestORMs(relation.type);
1136
1137               for (var i = 0; i < relatedORMs.length; i++) {
1138                 relations.push(relatedORMs[i].orm_type);
1139               }
1140             }
1141           }
1142         }
1143       }
1144
1145       /* Return only ORM that are the single requested or a REST exposed relation. */
1146       for (var i = 0; i < orms.length; i++) {
1147         if (orms[i].orm_type === orm || relations.indexOf(orms[i].orm_type) !== -1) {
1148           singleOrms.push(orms[i]);
1149         }
1150       }
1151
1152       /* The limited set of ORMs. */
1153       orms = singleOrms;
1154     }
1155
1156     if (!orms.length) {
1157       return false;
1158     }
1159
1160     return orms;
1161   };
1162
1163
1164   /*
1165    * Helper function to get a JSON-Schema for ORM Models.
1166    *
1167    * @param {Array} An array of orm objects name like [{"orm_namespace": "XM", "orm_type":"Contact"}].
1168    * @param {Object} Optional. A schema object to add schemas too.
1169    * @returns {Object}
1170    */
1171   XT.Discovery.getORMSchemas = function (orms, schemas) {
1172     "use strict";
1173
1174     schemas = schemas || {};
1175
1176     if (!orms || (orms instanceof Array && !orms.length)) {
1177       return false;
1178     }
1179
1180     /* Loop through the returned ORMs and get their JSON-Schema. */
1181     for (var i = 0; i < orms.length; i++) {
1182       /* TODO - Do we need to include "XM" in the propName? */
1183       var propName = orms[i].orm_type,
1184           propSchema;
1185
1186       /* Only get this parent schema if we don't already have it. */
1187       if (!schemas[propName]) {
1188         /* Get parent ORM */
1189         propSchema = XT.Schema.getProperties({"nameSpace": orms[i].orm_namespace, "type": orms[i].orm_type});
1190
1191         if (propSchema) {
1192           schemas[propName] = propSchema;
1193         }
1194       }
1195
1196       if (schemas[propName] && schemas[propName].properties) {
1197         /* Drill down through schemas and get all $ref schemas. */
1198         for (var prop in schemas[propName].properties) {
1199           var childProp = schemas[propName].properties[prop],
1200               childOrm;
1201
1202           if (childProp) {
1203             if (childProp.items && childProp.items["$ref"]) {
1204               if (childProp.items["$ref"].indexOf("/") === -1) {
1205                 childOrm = childProp.items["$ref"];
1206               } else {
1207                 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1208                 childOrm = childProp.items["$ref"].split("/")[0];
1209               }
1210             } else if (childProp["$ref"]) {
1211               if (childProp["$ref"].indexOf("/") === -1) {
1212                 childOrm = childProp["$ref"];
1213               } else {
1214                 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1215                 childOrm = childProp["$ref"].split("/")[0];
1216               }
1217             }
1218
1219             /* Only get this child schema if we don't already have it. */
1220             if (childOrm && !schemas[childOrm]) {
1221               /* Recursing into children. */
1222               schemas = XT.extend(schemas, XT.Discovery.getORMSchemas([{ "orm_namespace": "XM", "orm_type": childOrm }]));
1223             }
1224           }
1225         }
1226       }
1227     }
1228
1229     return schemas;
1230   };
1231
1232
1233   /*
1234    * Helper function to find the primary key for a JSON-Schema and return it's properties.
1235    *
1236    * @param {Object} A JSON-Schema object.
1237    * @returns {Object}
1238    */
1239   XT.Discovery.getKeyProps = function (schema) {
1240     "use strict";
1241
1242     if (schema && schema.properties) {
1243       for (var prop in schema.properties) {
1244         if (schema.properties[prop].isKey) {
1245           var keyProp = {};
1246
1247           /* Use extend so we can delete without affecting schema.properties[prop]. */
1248           keyProp = XT.extend(keyProp, schema.properties[prop]);
1249
1250           /* Delete these properties which are not needed for a resource's parameters. */
1251           delete keyProp.isKey;
1252           delete keyProp.title;
1253           delete keyProp.required;
1254
1255           return {"name": prop, "props": keyProp};
1256         }
1257       }
1258     } else {
1259       return false;
1260     }
1261   };
1262
1263 }());
1264
1265 $$ );