1 select xt.install_js('XT','Discovery','xtuple', $$
8 * The XT.Discovery class includes all functions necessary to return an
9 * API Discovery document: (https://developers.google.com/discovery/v1/using)
14 XT.Discovery.isDispatchable = true;
17 * Return an API Discovery List for this database's ORM where isRest = true.
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/"
23 XT.Discovery.getList = function (orm, rootUrl) {
29 orms = XT.Discovery.getIsRestORMs(orm),
32 rootUrl = rootUrl || "{rootUrl}";
42 list.kind = "discovery#directoryList";
43 list.discoveryVersion = version;
46 /* Loop through exposed ORM models and build list items. */
47 for (var i = 0; i < orms.length; i++) {
49 ormType = orms[i].orm_type,
50 ormTypeHyphen = ormType.camelToHyphen();
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";
61 "x16": rootUrl + org + "/assets/api/" + ormTypeHyphen + "-16.png",
62 "x32": rootUrl + org + "/assets/api/" + ormTypeHyphen + "-32.png"
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. */
71 /* Add master item that includes all ORM models in one Discovery Document. */
72 master.kind = "discovery#directoryItem";
73 master.id = org + ":" + version;
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";
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",
86 "x16": rootUrl + org + "/assets/api/api-16.png",
87 "x32": rootUrl + org + "/assets/api/api-32.png"
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. */
92 list.items.unshift(master);
100 * Return an API Discovery document for this database's ORM where isRest = true.
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/"
106 XT.Discovery.getDiscovery = function (orm, rootUrl) {
114 org = plv8.execute("select current_database()"),
119 version = "v1alpha1";
121 rootUrl = rootUrl || "{rootUrl}";
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]);
130 if (!(orms instanceof Array)) {
133 orms = orms.concat(gotOrms).unique();
137 orms = XT.Discovery.getIsRestORMs();
140 if (org.length !== 1) {
143 org = org[0].current_database;
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();
156 isDispatchable = XT.Discovery.getDispatchableObjects();
159 if (isDispatchable.length === 0) {
160 /* If there are no resource, and no services, then there's nothing to see here */
168 discovery.kind = "discovery#restDescription";
170 /* TODO - Implement etags. */
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.";
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"
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? */
193 * Parameters section.
195 discovery.parameters = {
198 "description": "OAuth 2.0 token for the current user.",
201 /* TODO: Add support for these to the REST API routes. */
205 "description": "Data format for the response.",
210 "enumDescriptions": [
211 "Responses with Content-Type of application/json"
217 "description": "Selector specifying which fields to include in a partial response.",
222 "description": "Returns response with indentations and line breaks.",
232 discovery.auth = XT.Discovery.getAuth(orm, rootUrl);
233 discovery.auth = XT.Discovery.getServicesAuth(orm, discovery.auth, rootUrl);
238 XT.Discovery.getORMSchemas(orms, schemas);
240 /* Sanitize the JSON-Schema. */
241 XT.Discovery.sanitize(schemas);
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);
252 XT.Discovery.getServicesSchema(null, schemas);
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"};
266 if (listItemOrms.length > 0) {
267 XT.Discovery.getORMSchemas(listItemOrms, schemas);
270 /* Sort schema properties alphabetically. */
271 discovery.schemas = XT.Discovery.sortObject(schemas);
276 discovery.resources = XT.Discovery.getResources(orm, rootUrl);
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]);
287 /* This should never happen. */
288 plv8.elog(ERROR, "No key found for ormType: ", ormType);
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];
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];
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];
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];
329 /* TODO - Old way. Remove if we don't need this anymore. */
330 /*discovery.services = XT.Discovery.getServices(orm, rootUrl); */
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);
339 /* There is no resource yet, so merge this service into the parent 'resources'. */
340 discovery.resources[service] = services[service];
344 /* return the results */
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.
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/"
357 XT.Discovery.getAuth = function (orm, rootUrl) {
362 org = plv8.execute("select current_database()"),
365 rootUrl = rootUrl || "{rootUrl}";
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]);
374 if (!(orms instanceof Array)) {
377 orms = orms.concat(gotOrms).unique();
381 orms = XT.Discovery.getIsRestORMs();
384 if (org.length !== 1) {
387 org = org[0].current_database;
400 /* Set base full access scope. */
401 auth.oauth2.scopes[rootUrl + org + "/auth"] = {
402 "description": "Full access to all '" + org + "' resources"
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();
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"
417 if (!thisOrm.privileges) {
418 plv8.elog(ERROR, "ORM Fail, missing privileges: " + ormNamespace + "." + ormType);
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"
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.
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/"
442 XT.Discovery.getResources = function (orm, rootUrl) {
447 org = XT.currentDb(),
450 rootUrl = rootUrl || "{rootUrl}";
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]);
459 if (!(orms instanceof Array)) {
462 orms = orms.concat(gotOrms).unique();
466 orms = XT.Discovery.getIsRestORMs();
477 /* Loop through exposed ORM models and build resources. */
478 for (var i = 0; i < orms.length; i++) {
480 ormType = orms[i].orm_type,
481 ormNamespace = orms[i].orm_namespace,
482 thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
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"]);
488 resources[ormType] = {};
489 resources[ormType].methods = {};
491 if (ormListItem.length > 0) {
492 listModel = ormType + "ListItem";
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."
508 resources[ormType].methods.delete.scopes = [
509 rootUrl + org + "/auth",
510 rootUrl + org + "/auth/" + ormTypeHyphen
517 if (thisOrm.privileges.all.read) {
518 resources[ormType].methods.get = {
519 "id": ormType + ".get",
520 "path": "resources/" + ormTypeHyphen + "/{",
522 "description": "Gets a single " + ormType + " record."
525 resources[ormType].methods.get.response = {
529 resources[ormType].methods.get.scopes = [
530 rootUrl + org + "/auth",
531 rootUrl + org + "/auth/" + ormTypeHyphen,
532 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
547 resources[ormType].methods.head.scopes = [
548 rootUrl + org + "/auth",
549 rootUrl + org + "/auth/" + ormTypeHyphen,
550 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
565 resources[ormType].methods.insert.request = {
569 resources[ormType].methods.insert.response = {
573 resources[ormType].methods.insert.scopes = [
574 rootUrl + org + "/auth",
575 rootUrl + org + "/auth/" + ormTypeHyphen
582 if (thisOrm.privileges.all.read) {
583 resources[ormType].methods.list = {
584 "id": ormType + ".list",
585 "path": "resources/" + ormTypeHyphen,
587 "description": "Returns a list of " + ormType + " records."
590 resources[ormType].methods.list.parameters = {
593 "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
595 //"$ref": "TODO: add this when moving to JSON-Schema draft v5"
599 "description": "Specify the order of results for a filtered list request.",
604 "description": "Maximum number of entries to return. Optional.",
611 "description": "Maximum number of entries returned on one result page. Optional.",
618 "description": "Token specifying which result page to return. Optional.",
623 "description": "Free text search terms to find events that match these terms in any field. Optional.",
628 "description": "Return the a count of the total number of results from a filtered list request.",
633 resources[ormType].methods.list.response = {
637 resources[ormType].methods.list.scopes = [
638 rootUrl + org + "/auth",
639 rootUrl + org + "/auth/" + ormTypeHyphen,
640 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
655 resources[ormType].methods.listhead.parameters = {
658 "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
664 "description": "Specify the order of results for a filtered list request.",
669 "description": "Maximum number of entries to return. Optional.",
676 "description": "Maximum number of entries returned on one result page. Optional.",
683 "description": "Token specifying which result page to return. Optional.",
688 "description": "Free text search terms to find events that match these terms in any field. Optional.",
693 "description": "Return the a count of the total number of results from a filtered list request.",
698 resources[ormType].methods.listhead.scopes = [
699 rootUrl + org + "/auth",
700 rootUrl + org + "/auth/" + ormTypeHyphen,
701 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
716 resources[ormType].methods.patch.request = {
720 resources[ormType].methods.patch.response = {
724 resources[ormType].methods.patch.scopes = [
725 rootUrl + org + "/auth",
726 rootUrl + org + "/auth/" + ormTypeHyphen
735 XT.Discovery.getDispatchableObjects = function (orm) {
738 var dispatchableObjects = [];
740 for (var businessObjectName in XM) {
741 var businessObject = XM[businessObjectName];
742 if (businessObject.isDispatchable &&
743 (!orm || businessObjectName === orm)) {
744 dispatchableObjects.push(businessObjectName);
748 return dispatchableObjects;
753 * Return an API Discovery document's Services JSON-Schema.
755 * @param {String} Optional. An orm_type name like "Contact".
756 * @param {Object} Optional. A schema object to add schemas too.
759 XT.Discovery.getServicesSchema = function (orm, schemas) {
762 schemas = schemas || {};
764 var dispatchableObjects = [],
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();
782 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
785 for (i = 0; i < dispatchableObjects.length; i++) {
786 businessObjectName = dispatchableObjects[i];
787 businessObject = XM[businessObjectName];
789 for (methodName in businessObject) {
790 method = businessObject[methodName];
792 Report only on documented dispatch methods. We document the methods by
793 tacking description and params attributes onto the function.
795 if (typeof method === 'function' && method.description && method.schema) {
796 for (var schema in method.schema) {
797 schemas[schema] = method.schema[schema];
808 * Return an API Discovery document's Services section.
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
816 XT.Discovery.getServices = function (orm, rootUrl, includeOrder) {
820 org = XT.currentDb(),
821 dispatchableObjects = [],
832 version = "v1alpha1";
834 rootUrl = rootUrl || "{rootUrl}";
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();
845 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
852 for (i = 0; i < dispatchableObjects.length; i++) {
853 businessObjectName = dispatchableObjects[i];
854 businessObject = XM[businessObjectName];
856 for (methodName in businessObject) {
857 method = businessObject[methodName];
859 Report only on documented dispatch methods. We document the methods by
860 tacking description and params attributes onto the function.
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";
870 var businessObjectNameHyphen = businessObjectName.camelToHyphen();
872 rootUrl + org + "/auth",
873 rootUrl + org + "/auth/" + businessObjectNameHyphen
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(),
881 description: method.description,
884 if (method.request) {
885 objectServices[methodName].request = method.request;
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;
897 if (Object.keys(objectServices).length > 0) {
898 /* only return objects with >= 1 documented dispatch function */
899 allServices[businessObjectName] = {methods: objectServices};
907 * Return an API Discovery document's Services JSON-Schema.
909 * @param {String} Optional. An orm_type name like "Contact".
910 * @param {Object} Optional. A schema object to add schemas too.
913 XT.Discovery.getServicesAuth = function (orm, auth, rootUrl) {
916 auth = auth || {oauth2: {scopes: {}}};
917 rootUrl = rootUrl || "{rootUrl}";
919 var dispatchableObjects = [],
921 org = plv8.execute("select current_database()"),
929 if (org.length !== 1) {
932 org = org[0].current_database;
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();
944 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
947 for (i = 0; i < dispatchableObjects.length; i++) {
948 businessObjectName = dispatchableObjects[i];
949 businessObject = XM[businessObjectName];
951 for (methodName in businessObject) {
952 method = businessObject[methodName];
954 Report only on documented dispatch methods. We document the methods by
955 tacking description and params attributes onto the function.
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"
969 * Helper function to convert date to string in yyyyMMdd format.
973 XT.Discovery.getDate = function () {
976 var today = new Date(),
977 year = today.getUTCFullYear(),
978 month = today.getUTCMonth() + 1,
979 day = today.getUTCDate();
981 /* Convert to string and preserve leading zero. */
996 return year + month + day;
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.
1007 * @param {Object} Object of JSON-Schemas.
1009 XT.Discovery.sanitize = function (schema) {
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});
1025 for (propName in schema[resource].properties) {
1026 propery = schema[resource].properties[propName];
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});
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]) {
1038 delete schema[parentOrmProp.toMany.type].properties[inverse];
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.
1052 * @param {Object} Object of JSON-Schemas.
1055 XT.Discovery.sortObject = function (obj) {
1063 if (obj.hasOwnProperty(key)) {
1070 for (key = 0; key < arr.length; key++) {
1071 sorted[arr[key]] = obj[arr[key]];
1079 * Helper function to get a single or all isRest ORM Models.
1081 * @param {String} Optional. An orm_type name like "Contact".
1084 XT.Discovery.getIsRestORMs = function (orm) {
1087 /* TODO - Do we need to include "XM" in the propName? */
1088 var sql = "select orm_namespace, orm_type " +
1092 " and not orm_ext " +
1093 " and orm_active " +
1094 " and orm_context = 'xtuple' " +
1096 "select orm_namespace, orm_type " +
1098 "where orm_id in (" +
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 " +
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",
1118 orms = plv8.execute(sql, [XT.username]);
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.
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});
1131 /* Find the related ORMs. */
1133 for (var prop in thisOrm.properties) {
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);
1142 for (var i = 0; i < relatedORMs.length; i++) {
1143 relations.push(relatedORMs[i].orm_type);
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]);
1157 /* The limited set of ORMs. */
1170 * Helper function to get a JSON-Schema for ORM Models.
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.
1176 XT.Discovery.getORMSchemas = function (orms, schemas) {
1179 schemas = schemas || {};
1181 if (!orms || (orms instanceof Array && !orms.length)) {
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,
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});
1197 schemas[propName] = propSchema;
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],
1208 if (childProp.items && childProp.items["$ref"]) {
1209 if (childProp.items["$ref"].indexOf("/") === -1) {
1210 childOrm = childProp.items["$ref"];
1212 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1213 childOrm = childProp.items["$ref"].split("/")[0];
1215 } else if (childProp["$ref"]) {
1216 if (childProp["$ref"].indexOf("/") === -1) {
1217 childOrm = childProp["$ref"];
1219 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1220 childOrm = childProp["$ref"].split("/")[0];
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 }]));
1239 * Helper function to find the primary key for a JSON-Schema and return it's properties.
1241 * @param {Object} A JSON-Schema object.
1244 XT.Discovery.getKeyProps = function (schema) {
1247 if (schema && schema.properties) {
1248 for (var prop in schema.properties) {
1249 if (schema.properties[prop].isKey) {
1252 /* Use extend so we can delete without affecting schema.properties[prop]. */
1253 keyProp = XT.extend(keyProp, schema.properties[prop]);
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;
1260 return {"name": prop, "props": keyProp};