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