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 "x16": rootUrl + org + "/assets/api/api-16.png",
82 "x32": rootUrl + org + "/assets/api/api-32.png"
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. */
87 list.items.unshift(master);
95 * Return an API Discovery document for this database's ORM where isRest = true.
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/"
101 XT.Discovery.getDiscovery = function (orm, rootUrl) {
109 org = plv8.execute("select current_database()"),
114 version = "v1alpha1";
116 rootUrl = rootUrl || "{rootUrl}";
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]);
125 if (!(orms instanceof Array)) {
128 orms = orms.concat(gotOrms).unique();
132 orms = XT.Discovery.getIsRestORMs();
135 if (org.length !== 1) {
138 org = org[0].current_database;
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();
151 isDispatchable = XT.Discovery.getDispatchableObjects();
154 if (isDispatchable.length === 0) {
155 /* If there are no resource, and no services, then there's nothing to see here */
163 discovery.kind = "discovery#restDescription";
165 /* TODO - Implement etags. */
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.";
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"
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? */
188 * Parameters section.
190 discovery.parameters = {
193 "description": "OAuth 2.0 token for the current user.",
196 /* TODO: Add support for these to the REST API routes. */
200 "description": "Data format for the response.",
205 "enumDescriptions": [
206 "Responses with Content-Type of application/json"
212 "description": "Selector specifying which fields to include in a partial response.",
217 "description": "Returns response with indentations and line breaks.",
227 discovery.auth = XT.Discovery.getAuth(orm, rootUrl);
228 discovery.auth = XT.Discovery.getServicesAuth(orm, discovery.auth, rootUrl);
233 XT.Discovery.getORMSchemas(orms, schemas);
235 /* Sanitize the JSON-Schema. */
236 XT.Discovery.sanitize(schemas);
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);
247 XT.Discovery.getServicesSchema(null, schemas);
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"};
261 if (listItemOrms.length > 0) {
262 XT.Discovery.getORMSchemas(listItemOrms, schemas);
265 /* Sort schema properties alphabetically. */
266 discovery.schemas = XT.Discovery.sortObject(schemas);
271 discovery.resources = XT.Discovery.getResources(orm, rootUrl);
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]);
282 /* This should never happen. */
283 plv8.elog(ERROR, "No key found for ormType: ", ormType);
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];
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];
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];
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];
324 /* TODO - Old way. Remove if we don't need this anymore. */
325 /*discovery.services = XT.Discovery.getServices(orm, rootUrl); */
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);
334 /* There is no resource yet, so merge this service into the parent 'resources'. */
335 discovery.resources[service] = services[service];
339 /* return the results */
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.
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/"
352 XT.Discovery.getAuth = function (orm, rootUrl) {
357 org = plv8.execute("select current_database()"),
360 rootUrl = rootUrl || "{rootUrl}";
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]);
369 if (!(orms instanceof Array)) {
372 orms = orms.concat(gotOrms).unique();
376 orms = XT.Discovery.getIsRestORMs();
379 if (org.length !== 1) {
382 org = org[0].current_database;
395 /* Set base full access scope. */
396 auth.oauth2.scopes[rootUrl + org + "/auth"] = {
397 "description": "Full access to all '" + org + "' resources"
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();
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"
412 if (!thisOrm.privileges) {
413 plv8.elog(ERROR, "ORM Fail, missing privileges: " + ormNamespace + "." + ormType);
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"
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.
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/"
437 XT.Discovery.getResources = function (orm, rootUrl) {
442 org = XT.currentDb(),
445 rootUrl = rootUrl || "{rootUrl}";
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]);
454 if (!(orms instanceof Array)) {
457 orms = orms.concat(gotOrms).unique();
461 orms = XT.Discovery.getIsRestORMs();
472 /* Loop through exposed ORM models and build resources. */
473 for (var i = 0; i < orms.length; i++) {
475 ormType = orms[i].orm_type,
476 ormNamespace = orms[i].orm_namespace,
477 thisOrm = XT.Orm.fetch(ormNamespace, ormType, {"superUser": true}),
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"]);
483 resources[ormType] = {};
484 resources[ormType].methods = {};
486 if (ormListItem.length > 0) {
487 listModel = ormType + "ListItem";
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."
503 resources[ormType].methods.delete.scopes = [
504 rootUrl + org + "/auth",
505 rootUrl + org + "/auth/" + ormTypeHyphen
512 if (thisOrm.privileges.all.read) {
513 resources[ormType].methods.get = {
514 "id": ormType + ".get",
515 "path": "resources/" + ormTypeHyphen + "/{",
517 "description": "Gets a single " + ormType + " record."
520 resources[ormType].methods.get.response = {
524 resources[ormType].methods.get.scopes = [
525 rootUrl + org + "/auth",
526 rootUrl + org + "/auth/" + ormTypeHyphen,
527 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
542 resources[ormType].methods.head.scopes = [
543 rootUrl + org + "/auth",
544 rootUrl + org + "/auth/" + ormTypeHyphen,
545 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
560 resources[ormType].methods.insert.request = {
564 resources[ormType].methods.insert.response = {
568 resources[ormType].methods.insert.scopes = [
569 rootUrl + org + "/auth",
570 rootUrl + org + "/auth/" + ormTypeHyphen
577 if (thisOrm.privileges.all.read) {
578 resources[ormType].methods.list = {
579 "id": ormType + ".list",
580 "path": "resources/" + ormTypeHyphen,
582 "description": "Returns a list of " + ormType + " records."
585 resources[ormType].methods.list.parameters = {
588 "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
590 "$ref": "TODO: add this when moving to JSON-Schema draft v5"
594 "description": "Specify the order of results for a filtered list request.",
599 "description": "Maximum number of entries to return. Optional.",
606 "description": "Maximum number of entries returned on one result page. Optional.",
613 "description": "Token specifying which result page to return. Optional.",
618 "description": "Free text search terms to find events that match these terms in any field. Optional.",
623 "description": "Return the a count of the total number of results from a filtered list request.",
628 resources[ormType].methods.list.response = {
632 resources[ormType].methods.list.scopes = [
633 rootUrl + org + "/auth",
634 rootUrl + org + "/auth/" + ormTypeHyphen,
635 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
650 resources[ormType].methods.listhead.parameters = {
653 "description": "Query different resource properties based on their JSON-Schema. e.g. ?query[property1][BEGINS_WITH]=foo&query[property2][EQUALS]=bar",
659 "description": "Specify the order of results for a filtered list request.",
664 "description": "Maximum number of entries to return. Optional.",
671 "description": "Maximum number of entries returned on one result page. Optional.",
678 "description": "Token specifying which result page to return. Optional.",
683 "description": "Free text search terms to find events that match these terms in any field. Optional.",
688 "description": "Return the a count of the total number of results from a filtered list request.",
693 resources[ormType].methods.listhead.scopes = [
694 rootUrl + org + "/auth",
695 rootUrl + org + "/auth/" + ormTypeHyphen,
696 rootUrl + org + "/auth/" + ormTypeHyphen + ".readonly"
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."
711 resources[ormType].methods.patch.request = {
715 resources[ormType].methods.patch.response = {
719 resources[ormType].methods.patch.scopes = [
720 rootUrl + org + "/auth",
721 rootUrl + org + "/auth/" + ormTypeHyphen
730 XT.Discovery.getDispatchableObjects = function (orm) {
733 var dispatchableObjects = [];
735 for (var businessObjectName in XM) {
736 var businessObject = XM[businessObjectName];
737 if (businessObject.isDispatchable &&
738 (!orm || businessObjectName === orm)) {
739 dispatchableObjects.push(businessObjectName);
743 return dispatchableObjects;
748 * Return an API Discovery document's Services JSON-Schema.
750 * @param {String} Optional. An orm_type name like "Contact".
751 * @param {Object} Optional. A schema object to add schemas too.
754 XT.Discovery.getServicesSchema = function (orm, schemas) {
757 schemas = schemas || {};
759 var dispatchableObjects = [],
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();
777 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
780 for (i = 0; i < dispatchableObjects.length; i++) {
781 businessObjectName = dispatchableObjects[i];
782 businessObject = XM[businessObjectName];
784 for (methodName in businessObject) {
785 method = businessObject[methodName];
787 Report only on documented dispatch methods. We document the methods by
788 tacking description and params attributes onto the function.
790 if (typeof method === 'function' && method.description && method.schema) {
791 for (var schema in method.schema) {
792 schemas[schema] = method.schema[schema];
803 * Return an API Discovery document's Services section.
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
811 XT.Discovery.getServices = function (orm, rootUrl, includeOrder) {
815 org = XT.currentDb(),
816 dispatchableObjects = [],
827 version = "v1alpha1";
829 rootUrl = rootUrl || "{rootUrl}";
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();
840 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
847 for (i = 0; i < dispatchableObjects.length; i++) {
848 businessObjectName = dispatchableObjects[i];
849 businessObject = XM[businessObjectName];
851 for (methodName in businessObject) {
852 method = businessObject[methodName];
854 Report only on documented dispatch methods. We document the methods by
855 tacking description and params attributes onto the function.
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";
865 var businessObjectNameHyphen = businessObjectName.camelToHyphen();
867 rootUrl + org + "/auth",
868 rootUrl + org + "/auth/" + businessObjectNameHyphen
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(),
876 description: method.description,
879 if (method.request) {
880 objectServices[methodName].request = method.request;
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;
892 if (Object.keys(objectServices).length > 0) {
893 /* only return objects with >= 1 documented dispatch function */
894 allServices[businessObjectName] = {methods: objectServices};
902 * Return an API Discovery document's Services JSON-Schema.
904 * @param {String} Optional. An orm_type name like "Contact".
905 * @param {Object} Optional. A schema object to add schemas too.
908 XT.Discovery.getServicesAuth = function (orm, auth, rootUrl) {
911 auth = auth || {oauth2: {scopes: {}}};
912 rootUrl = rootUrl || "{rootUrl}";
914 var dispatchableObjects = [],
916 org = plv8.execute("select current_database()"),
924 if (org.length !== 1) {
927 org = org[0].current_database;
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();
939 dispatchableObjects = XT.Discovery.getDispatchableObjects(null);
942 for (i = 0; i < dispatchableObjects.length; i++) {
943 businessObjectName = dispatchableObjects[i];
944 businessObject = XM[businessObjectName];
946 for (methodName in businessObject) {
947 method = businessObject[methodName];
949 Report only on documented dispatch methods. We document the methods by
950 tacking description and params attributes onto the function.
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"
964 * Helper function to convert date to string in yyyyMMdd format.
968 XT.Discovery.getDate = function () {
971 var today = new Date(),
972 year = today.getUTCFullYear(),
973 month = today.getUTCMonth() + 1,
974 day = today.getUTCDate();
976 /* Convert to string and preserve leading zero. */
991 return year + month + day;
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.
1002 * @param {Object} Object of JSON-Schemas.
1004 XT.Discovery.sanitize = function (schema) {
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});
1020 for (propName in schema[resource].properties) {
1021 propery = schema[resource].properties[propName];
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});
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]) {
1033 delete schema[parentOrmProp.toMany.type].properties[inverse];
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.
1047 * @param {Object} Object of JSON-Schemas.
1050 XT.Discovery.sortObject = function (obj) {
1058 if (obj.hasOwnProperty(key)) {
1065 for (key = 0; key < arr.length; key++) {
1066 sorted[arr[key]] = obj[arr[key]];
1074 * Helper function to get a single or all isRest ORM Models.
1076 * @param {String} Optional. An orm_type name like "Contact".
1079 XT.Discovery.getIsRestORMs = function (orm) {
1082 /* TODO - Do we need to include "XM" in the propName? */
1083 var sql = "select orm_namespace, orm_type " +
1087 " and not orm_ext " +
1088 " and orm_active " +
1089 " and orm_context = 'xtuple' " +
1091 "select orm_namespace, orm_type " +
1093 "where orm_id in (" +
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 " +
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",
1113 orms = plv8.execute(sql, [XT.username]);
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.
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});
1126 /* Find the related ORMs. */
1128 for (var prop in thisOrm.properties) {
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);
1137 for (var i = 0; i < relatedORMs.length; i++) {
1138 relations.push(relatedORMs[i].orm_type);
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]);
1152 /* The limited set of ORMs. */
1165 * Helper function to get a JSON-Schema for ORM Models.
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.
1171 XT.Discovery.getORMSchemas = function (orms, schemas) {
1174 schemas = schemas || {};
1176 if (!orms || (orms instanceof Array && !orms.length)) {
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,
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});
1192 schemas[propName] = propSchema;
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],
1203 if (childProp.items && childProp.items["$ref"]) {
1204 if (childProp.items["$ref"].indexOf("/") === -1) {
1205 childOrm = childProp.items["$ref"];
1207 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1208 childOrm = childProp.items["$ref"].split("/")[0];
1210 } else if (childProp["$ref"]) {
1211 if (childProp["$ref"].indexOf("/") === -1) {
1212 childOrm = childProp["$ref"];
1214 /* This is a JSON-Path type of $ref. e.g. SalesRep/name */
1215 childOrm = childProp["$ref"].split("/")[0];
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 }]));
1234 * Helper function to find the primary key for a JSON-Schema and return it's properties.
1236 * @param {Object} A JSON-Schema object.
1239 XT.Discovery.getKeyProps = function (schema) {
1242 if (schema && schema.properties) {
1243 for (var prop in schema.properties) {
1244 if (schema.properties[prop].isKey) {
1247 /* Use extend so we can delete without affecting schema.properties[prop]. */
1248 keyProp = XT.extend(keyProp, schema.properties[prop]);
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;
1255 return {"name": prop, "props": keyProp};