apollo-datasource-mongodb
Version:
Apollo data source for MongoDB
240 lines (199 loc) • 7.37 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.prepFields = prepFields;
exports.getNestedValue = getNestedValue;
exports.createCachingMethods = exports.stringToId = exports.isValidObjectIdString = exports.idToString = void 0;
var _dataloader = _interopRequireDefault(require("dataloader"));
var _mongodb = require("mongodb");
var _bson = require("bson");
var _helpers = require("./helpers");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const idToString = id => {
if (id instanceof _mongodb.ObjectId) {
return id.toHexString();
} else {
return id && id.toString ? id.toString() : id;
}
}; // https://www.geeksforgeeks.org/how-to-check-if-a-string-is-valid-mongodb-objectid-in-nodejs/
exports.idToString = idToString;
const isValidObjectIdString = string => _mongodb.ObjectId.isValid(string) && String(new _mongodb.ObjectId(string)) === string;
exports.isValidObjectIdString = isValidObjectIdString;
const stringToId = string => {
if (string instanceof _mongodb.ObjectId) {
return string;
}
if (isValidObjectIdString(string)) {
return new _mongodb.ObjectId(string);
}
return string;
};
exports.stringToId = stringToId;
function prepFields(fields) {
const cleanedFields = {};
Object.keys(fields).sort().forEach(key => {
if (typeof key !== 'undefined') {
cleanedFields[key] = Array.isArray(fields[key]) ? fields[key] : [fields[key]];
}
});
return {
loaderKey: _bson.EJSON.stringify(cleanedFields),
cleanedFields
};
} // getNestedValue({ nested: { foo: 'bar' } }, 'nested.foo')
// => 'bar'
function getNestedValue(object, string) {
string = string.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
string = string.replace(/^\./, ''); // strip a leading dot
var a = string.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in object) {
object = object[k];
} else {
return;
}
}
return object;
} // https://github.com/graphql/dataloader#batch-function
// "The Array of values must be the same length as the Array of keys."
// "Each index in the Array of values must correspond to the same index in the Array of keys."
const orderDocs = (fieldsArray, docs) => fieldsArray.map(fields => docs.filter(doc => {
for (let fieldName of Object.keys(fields)) {
const fieldValue = getNestedValue(fields, fieldName);
if (typeof fieldValue === 'undefined') continue;
const filterValuesArr = Array.isArray(fieldValue) ? fieldValue.map(val => idToString(val)) : [idToString(fieldValue)];
const docValue = doc[fieldName];
const docValuesArr = Array.isArray(docValue) ? docValue.map(val => idToString(val)) : [idToString(docValue)];
let isMatch = false;
for (const filterVal of filterValuesArr) {
if (docValuesArr.includes(filterVal)) {
isMatch = true;
}
}
if (!isMatch) return false;
}
return true;
}));
const createCachingMethods = ({
collection,
model,
cache
}) => {
const loader = new _dataloader.default(async ejsonArray => {
const fieldsArray = ejsonArray.map(_bson.EJSON.parse);
(0, _helpers.log)('fieldsArray', fieldsArray);
const filterArray = fieldsArray.reduce((filterArray, fields) => {
const existingFieldsFilter = filterArray.find(filter => [...Object.keys(filter)].sort().join() === [...Object.keys(fields)].sort().join());
const filter = existingFieldsFilter || {};
for (const fieldName in fields) {
if (typeof fields[fieldName] === 'undefined') continue;
if (!filter[fieldName]) filter[fieldName] = {
$in: []
};
let newVals = Array.isArray(fields[fieldName]) ? fields[fieldName] : [fields[fieldName]];
filter[fieldName].$in = [...filter[fieldName].$in, ...newVals.map(stringToId).filter(val => !filter[fieldName].$in.includes(val))];
}
if (existingFieldsFilter) return filterArray;
return [...filterArray, filter];
}, []);
(0, _helpers.log)('filterArray: ', filterArray);
const filter = filterArray.length === 1 ? filterArray[0] : {
$or: filterArray
};
(0, _helpers.log)('filter: ', filter);
const findPromise = model ? model.find(filter).exec() : collection.find(filter).toArray();
const results = await findPromise;
(0, _helpers.log)('results: ', results);
const orderedDocs = orderDocs(fieldsArray, results);
(0, _helpers.log)('orderedDocs: ', orderedDocs);
return orderedDocs;
});
const cachePrefix = `mongo-${(0, _helpers.getCollection)(collection).collectionName}-`;
const methods = {
findOneById: async (_id, {
ttl
} = {}) => {
const cacheKey = cachePrefix + idToString(_id);
const cacheDoc = await cache.get(cacheKey);
(0, _helpers.log)('findOneById found in cache:', cacheDoc);
if (cacheDoc) {
return _bson.EJSON.parse(cacheDoc);
}
(0, _helpers.log)(`Dataloader.load: ${_bson.EJSON.stringify({
_id
})}`);
const docs = await loader.load(_bson.EJSON.stringify({
_id
}));
(0, _helpers.log)('Dataloader.load returned: ', docs);
if (Number.isInteger(ttl)) {
// https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-caching#apollo-server-caching
cache.set(cacheKey, _bson.EJSON.stringify(docs[0]), {
ttl
});
}
return docs[0];
},
findManyByIds: (ids, {
ttl
} = {}) => {
return Promise.all(ids.map(id => methods.findOneById(id, {
ttl
})));
},
findByFields: async (fields, {
ttl
} = {}) => {
const {
cleanedFields,
loaderKey
} = prepFields(fields);
const cacheKey = cachePrefix + loaderKey;
const cacheDoc = await cache.get(cacheKey);
if (cacheDoc) {
return _bson.EJSON.parse(cacheDoc);
}
const fieldNames = Object.keys(cleanedFields);
let docs;
if (fieldNames.length === 1) {
const field = cleanedFields[fieldNames[0]];
const fieldArray = Array.isArray(field) ? field : [field];
const docsArray = await Promise.all(fieldArray.map(value => {
const filter = {};
filter[fieldNames[0]] = value;
return loader.load(_bson.EJSON.stringify(filter));
}));
docs = [].concat(...docsArray);
} else {
docs = await loader.load(loaderKey);
}
if (Number.isInteger(ttl)) {
// https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-caching#apollo-server-caching
cache.set(cacheKey, _bson.EJSON.stringify(docs), {
ttl
});
}
return docs;
},
deleteFromCacheById: async _id => {
loader.clear(_bson.EJSON.stringify({
_id
}));
const cacheKey = cachePrefix + idToString(_id);
(0, _helpers.log)('Deleting cache key: ', cacheKey);
await cache.delete(cacheKey);
},
deleteFromCacheByFields: async fields => {
const {
loaderKey
} = prepFields(fields);
const cacheKey = cachePrefix + loaderKey;
loader.clear(loaderKey);
await cache.delete(cacheKey);
}
};
return methods;
};
exports.createCachingMethods = createCachingMethods;