gpii-universal
Version:
Cross platform, core components of the GPII personalization infrastructure.
563 lines (516 loc) • 22.5 kB
JavaScript
/*!
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
*/
;
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;
};