UNPKG

gpii-universal

Version:

Cross platform, core components of the GPII personalization infrastructure.

563 lines (516 loc) 22.5 kB
/*! GPII CouchDB Data Store Utilities Copyright 2016-2019 OCAD university Licensed under the New BSD license. You may not use this file except in compliance with this License. You may obtain a copy of the License at https://github.com/GPII/universal/blob/master/LICENSE.txt */ "use strict"; var fluid = fluid || require("infusion"), gpii = fluid.registerNamespace("gpii"), $ = fluid.registerNamespace("jQuery"), uuid = uuid || require("node-uuid"); fluid.registerNamespace("gpii.dbOperation.dbDataStore"); /** * Use the kettle dataSource `get` method to retrieve one record. This function provides extra * verification on input required fields. It returns an empty object if the record is not found. * This requires further processing besides using the kettle dataSource `notFoundIsEmpty` option because * when retrieving CouchDB using views , an empty `rows` array rather than 404 (not found) http * response code will be received when the record is not found. * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource. * @param {Object} directModel - The direct model expressing the "coordinates" of the model to be fetched. * @param {String|Array} valueNotEmpty - One or more required field(s). * @param {Function} dataProcessFunc - The function to further process the retrieved record when the returned * record is not empty. * @return {Promise} A promise for the retrieved record. */ gpii.dbOperation.dbDataStore.findRecord = function (dataSource, directModel, valueNotEmpty, dataProcessFunc) { // Remove or rename CouchDB specific fields such as _id, _rev, type dataProcessFunc = dataProcessFunc || gpii.dbOperation.dbDataStore.cleanUpDoc; var promiseTogo = fluid.promise(); // Verify required field values. Make sure they are not undefined. var emptyFields = gpii.dbOperation.dbDataStore.filterEmptyFields(directModel, valueNotEmpty); if (emptyFields.length > 0) { var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: emptyFields.join(" & ")}); fluid.log("gpii-db-operation, findRecord(), error: ", error); promiseTogo.reject(error); // reject #1 of 3. } else { var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); var promise = dataSource.get(finalDirectModel); promise.then(function (data) { // TODO: The line below that converts an empty object to undefined is to work around an issue with using the // kettle notFoundIsEmpty option with fetching couchDB documents by views. The way that notFoundIsEmpty is // implemented in kettle is that, it returns undefined when encountering a 404 response. However, when querying // couchdb by views, the returned value would not be a 404 http status code even when the doc is not found. // The response would still be an object but with an empty "rows" array. An example response is: // { total_rows: 1, offset: 0, rows: [] } // This response is then further transformed using kettle readPayload option: // readPayload: { "": "rows.0.value" } // Due to an issue with the infusion model transformation described at https://issues.fluidproject.org/browse/FLUID-5969, // after the transformation, an empty object is eventually received here and then converted into undefined. // Note that this issue only occurs when querying CouchDB by a view(map) function. when querying CouchDB directly // by a document id, 404 status is returned and this conversion is not needed. var result = $.isEmptyObject(data) ? undefined : dataProcessFunc(data); if (result !== undefined && result.isError) { promiseTogo.reject(result); // reject #2 of 3. } else { promiseTogo.resolve(result); // result can be undefined. } }, function (error) { fluid.log("gpii-db-operation, findRecord(), error: ", error); promiseTogo.reject(error); // reject #3 of 3. }); } return promiseTogo; }; /** * Filter the given array valueNotEmpty to return elements that satisfy: * 1. the element isn't used as a path name in the object; * 2. the element matches a path name in the object but the corresponding value is undefined. * Note the given object can NOT be a nested object. * @param {Object} obj - The object used for path name check. * @param {String|Array<String>} valueNotEmpty - One or a set of path name(s) to look up in the give obj. * @return {Array} An subset array of valueNotEmpty. * For example, gpii.dbOperation.dbDataStore.filterEmptyFields({"a": 1, "c": undefined}, ["a", "b", "c"]) returns ["b", "c"]. */ gpii.dbOperation.dbDataStore.filterEmptyFields = function (obj, valueNotEmpty) { var emptyFields = []; valueNotEmpty = fluid.makeArray(valueNotEmpty); fluid.each(valueNotEmpty, function (fieldName) { if (obj[fieldName] === undefined) { emptyFields.push(fieldName); } }); return emptyFields; }; /** * Remove the CouchDB internal fields: _id and _rev. Also save "_id" field value into "id" field. * The use of "id" instead of "_id" field name is to maintain the API backward compatibility as data store * API is expected to output the record identifier in "id" field instead of a couchdb specific name * of "_id". * @param {Object} data - An object to transform. * @return {Object} An object with CouchDB-specific internal fields transformed. */ gpii.dbOperation.dbDataStore.cleanUpDoc = function (data) { if (data) { data.id = data._id; delete data._id; delete data._rev; } return data; }; /** Use the kettle dataSource `set` method to create a new record. Before sending the input data to * CouchDB, it is modified by adding an unique _id field and a proper document type. * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record creation. * @param {String} docType - The document type. See gpii.dbOperation.docTypes defined in * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js. * @param {String} idName - The name for the unique id field. Usually "id". * @param {Object} data - The data to be saved in the new record. * @param {String} [idValue] - [optional] The id value. If not provided, an UUID will be generated. * @return {Promise} A promise for the save response. */ gpii.dbOperation.dbDataStore.addRecord = function (dataSource, docType, idName, data, idValue) { var promise = fluid.promise(); if (data !== undefined) { var directModel = {}; directModel[idName] = idValue || uuid.v4(); fluid.extend(data, {type: docType}); var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); promise = dataSource.set(finalDirectModel, data); } else { fluid.log("gpii-db-operation, addRecord(), error occurs: ", error); var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType}); promise.reject(error); } return promise; }; /** Use the kettle dataSource `set` method to udpate a record by its id. * @param {Component} dataSource - An instance of gpii.dbOperation.dbDataSource that handles the record update. * @param {String} docType - The expected document type in the updated record. See gpii.dbOperation.docTypes defined in * %gpii-universal/gpii/node_modules/gpii-db-operation/src/DbConst.js. * @param {String} docId - The document id. * @param {Object} data - The data to be updated. * @return {Promise} A promise for the update response. */ gpii.dbOperation.dbDataStore.updateRecord = function (dataSource, docType, docId, data) { var promise = fluid.promise(); var error; if (!data) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: docType}); promise.reject(error); } else if (data.type !== docType) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.mismatchedDocType, {docType: docType, selectedDocType: data.type}); promise.reject(error); } else { var directModel = { id: docId }; var finalDirectModel = fluid.extend(true, {}, dataSource.options.directModel, directModel); promise = dataSource.set(finalDirectModel, data); } return promise; }; // General GPII keys Functions // --------------------------- /** * Add a GPII key * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. * @param {Object} gpiiKeyData - The GPII key data. An example of the data: * { * gpiiKey: {String}, // Optional * prefsSafeId: {String}, * prefsSetId: {String} * } * If this parameter is not provided, a new GPII key is still generated but does not associate with a prefs safe. * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the * GPII key record. When `gpiiKeyData` is not provided, returns the object {prefsSafeId: null, prefsSetId: null}. */ gpii.dbOperation.dbDataStore.addGpiiKey = function (saveDataSource, gpiiKeyData) { var promiseTogo = fluid.promise(); var data; if (!gpiiKeyData) { gpiiKeyData = { prefsSafeId: null, prefsSetId: null }; } data = { schemaVersion: gpii.dbOperation.schemaVersion, prefsSafeId: gpiiKeyData.prefsSafeId || null, prefsSetId: gpiiKeyData.prefsSetId || null, revoked: false, revokedReason: null, timestampCreated: gpii.dbOperation.getCurrentTimestamp(), timestampUpdated: null }; promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, "id", data, gpiiKeyData.gpiiKey); return promiseTogo; }; /** * Update a GPII key record * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. * @param {String} gpiiKey - The GPII key. * @param {Object} gpiiKeyData - The GPII key data. An example of the data: * { * type: {String}, * schemaVersion: {String}, * prefsSafeId: {String}, * prefsSetId: {String}, * revoked: {String}, * revokedReason: {String}, * timestampCreated: {Date}, * timestampRevoked: {Date} * } * * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the * GPII key record, or an error if `gpiiKey` or `gpiiKeyData` is not provided or `gpiiKeyData.type` is not * the doc type for gpiiKey. */ gpii.dbOperation.dbDataStore.updateGpiiKey = function (saveDataSource, gpiiKey, gpiiKeyData) { var promiseTogo = fluid.promise(); var data, error; if (!gpiiKey) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "gpiiKey"}); promiseTogo.reject(error); } else if (!gpiiKeyData) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiKey}); promiseTogo.reject(error); } else { data = { type: gpiiKeyData.type, schemaVersion: gpiiKeyData.schemaVersion, prefsSafeId: gpiiKeyData.prefsSafeId, prefsSetId: gpiiKeyData.prefsSetId, revoked: gpiiKeyData.revoked, revokedReason: gpiiKeyData.revokedReason, timestampCreated: gpiiKeyData.timestampCreated, timestampUpdated: gpii.dbOperation.getCurrentTimestamp(), timestampRevoked: gpiiKeyData.timestampRevoked }; promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiKey, gpiiKey, data); } return promiseTogo; }; // General Preferences Safes Functions // ----------------------------------- /** * Transform the data in CouchDB form to a more understandable structure * @param {Object} data - Contains GPII key and preferences safe information associated with a GPII key. * An input example: * { * key: {String}, // GPII key * id: {String}, // GPII key * value: { * _id: {String}, // prefs Safe id * gpiiKey: { * type: {String}, * schemaVersion: {String}, * prefsSafeId: {String}, * prefsSetId: {String}, * revoked: {Boolean}, * revokedReason: {String}, * timestampCreated: {Date}, * timestampUpdated: {Date}, * _id: {String}, * _rev: {String} * } * }, * doc: { * type: {String}, * schemaVersion: {String}, * name: {String}, * password: {String}, * email: {String}, * preferences: {Object}, * timestampCreated: {Date}, * timestampUpdated: {Date}, * _id: {String}, * _rev: {String} * } * } * @return {Object} An object in the structure: * { * gpiiKey: {String}, * gpiiKeyDetails: {Object}, * prefsSafe: {Object} * } */ gpii.dbOperation.dbDataStore.findPrefsSafeByGpiiKeyPostProcess = function (data) { var result; if (data && data.doc && data.value) { result = { gpiiKey: data.key, gpiiKeyDetails: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.gpiiKey), prefsSafe: data.doc.type === gpii.dbOperation.docTypes.prefsSafe ? gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc) : null }; } return result; }; /** * Add a prefs safe * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. * @param {Object} prefsSafeData - The prefs safe data. An example of the data: * { * prefsSafeType: {String}, * name: {String}, * password: {String}, * email: {String}, * preferences: {Object} * } * * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the * prefs safe record, or an error if `prefsSafeData` parameter is not provided. */ gpii.dbOperation.dbDataStore.addPrefsSafe = function (saveDataSource, prefsSafeData) { var promiseTogo = fluid.promise(); var data; if (!prefsSafeData) { var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe}); promiseTogo.reject(error); } else { data = { schemaVersion: gpii.dbOperation.schemaVersion, prefsSafeType: prefsSafeData.prefsSafeType, name: prefsSafeData.name, password: prefsSafeData.password, email: prefsSafeData.email, preferences: prefsSafeData.preferences, timestampCreated: gpii.dbOperation.getCurrentTimestamp(), timestampUpdated: null }; promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, "id", data); } return promiseTogo; }; /** * Update a prefs safe * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. * @param {String} prefsSafeId - The prefs safe id. * @param {Object} prefsSafeData - The prefs safe data. An example of the data: * { * type: {String}, * schemaVersion: {String}, * prefsSafeType: {String}, * name: {String}, * password: {String}, * email: {String}, * preferences: {Object}, * timestampCreated: {Date} * } * @return {Promise} A promise object that carries either a response returned from CouchDB for updating the * prefs safe record, or an error if `prefsSafeId` or `prefsSafeData` is not provided or `prefsSafeData.type` is not * the doc type for prefsSafe. */ gpii.dbOperation.dbDataStore.updatePrefsSafe = function (saveDataSource, prefsSafeId, prefsSafeData) { var promiseTogo = fluid.promise(); var data, error; if (!prefsSafeId) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingInput, {fieldName: "prefsSafeId"}); promiseTogo.reject(error); } else if (!prefsSafeData) { error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.prefsSafe}); promiseTogo.reject(error); } else { data = { type: prefsSafeData.type, schemaVersion: prefsSafeData.schemaVersion, prefsSafeType: prefsSafeData.prefsSafeType, name: prefsSafeData.name || null, password: prefsSafeData.password || null, email: prefsSafeData.email || null, preferences: prefsSafeData.preferences, timestampCreated: prefsSafeData.timestampCreated, timestampUpdated: gpii.dbOperation.getCurrentTimestamp() }; promiseTogo = gpii.dbOperation.dbDataStore.updateRecord(saveDataSource, gpii.dbOperation.docTypes.prefsSafe, prefsSafeId, data); } return promiseTogo; }; // General Client Functions // ------------------------ /** * Transform the data in CouchDB form to a more understandable structure * @param {Object} data - Contains both client and client credential information associated with an oauth2 client id. * An input example: * { * key: {String}, // access token * id: {String}, // authorization id * value: { * _id: {String}, // client id * clientCredential: { * type: {String}, * schemaVersion: {String}, * clientId: {String}, * oauth2ClientId: {String}, * oauth2ClientSecret: {String}, * revoked: {Boolean}, * revokedReason: {String}, * timestampCreated: {Date}, * timestampRevoked: {Date}, * _id: {String}, * _rev: {String} * } * }, * doc: { * type: {String}, // client type * schemaVersion: {String}, * name: {String}, * computerType: {String}, * timestampCreated: {Date}, * timestampUpdated: {Date}, * _id: {String}, * _rev: {String} * } * } * @return {Object} An object in the structure: * { * oauth2ClientId: {String}, * client: {Object}, * clientCredential: {Object} * } */ gpii.dbOperation.dbDataStore.findClientByOauth2ClientIdPostProcess = function (data) { var result; if (data && data.doc && data.value) { result = { oauth2ClientId: data.key, client: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc), clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.clientCredential) }; } return result; }; // General Authorization Functions // ------------------------------------ /** * Transform the data in CouchDB form to a more understandable structure * @param {Object} data - Contains both client and authorization information associated with an access token. * An input example: * { * key: {String}, // access token * id: {String}, // authorization id * value: { * _id: {String}, // client id * authorization: { * type: {String}, * schemaVersion: {String}, * clientId: {String}, * gpiiKey: {String}, * accessToken: {String}, * revoked: {Boolean}, * revokedReason: {String}, * timestampCreated: {Date}, * timestampRevoked: {Date}, * timestampExpires: {Date}, * _id: {String}, * _rev: {String} * } * }, * doc: { * type: {String}, // client type * name: {String}, * computerType: {String}, * timestampCreated: {Date}, * timestampUpdated: {Date}, * _id: {String}, * _rev: {String} * } * } * @return {Object} An object in the structure: * { * accessToken: {String}, * authorization: {Object} * } */ gpii.dbOperation.dbDataStore.findInfoByAccessTokenPostProcess = function (data) { var result; if (data && data.doc && data.value) { result = { accessToken: data.key, clientCredential: gpii.dbOperation.dbDataStore.cleanUpDoc(data.doc), authorization: gpii.dbOperation.dbDataStore.cleanUpDoc(data.value.authorization) }; } return result; }; /** * Add an authorization * @param {Component} saveDataSource - The saveDataSource component provided by gpii.dbOperation.dbDataStore. * @param {Object} authorizationData - The authorization data. An example of gpiiAppInstallationAuthorization data: * * gpiiAppInstallationAuthorization: * { * clientId: {String}, * gpiiKey: {String}, * clientCredentialId: {String}, * accessToken: {String}, * timestampExpires: {String} * } * * @return {Promise} A promise object that carries either a response returned from CouchDB for adding the * authorization record, or an error if `authorizationData` parameter is not provided. */ gpii.dbOperation.dbDataStore.addAuthorization = function (saveDataSource, authorizationData) { var promiseTogo = fluid.promise(); var data; if (!authorizationData) { var error = gpii.dbOperation.composeError(gpii.dbOperation.errors.missingDoc, {docType: gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization}); promiseTogo.reject(error); } else { data = { schemaVersion: gpii.dbOperation.schemaVersion, clientId: authorizationData.clientId, gpiiKey: authorizationData.gpiiKey, clientCredentialId: authorizationData.clientCredentialId, accessToken: authorizationData.accessToken, revoked: false, revokedReason: null, timestampCreated: gpii.dbOperation.getCurrentTimestamp(), timestampRevoked: null, timestampExpires: authorizationData.timestampExpires }; promiseTogo = gpii.dbOperation.dbDataStore.addRecord(saveDataSource, gpii.dbOperation.docTypes.gpiiAppInstallationAuthorization, "id", data); } return promiseTogo; };