Group schema + auto populate for mongoose

This commit is contained in:
Alexandre Bodin 2019-07-04 15:27:27 +02:00
parent 2753176d89
commit 5f29e81556
4 changed files with 578 additions and 560 deletions

View File

@ -119,7 +119,8 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
// Add every relationships to the loaded model for Bookshelf. // Add every relationships to the loaded model for Bookshelf.
// Basic attributes don't need this-- only relations. // Basic attributes don't need this-- only relations.
_.forEach(definition.attributes, (details, name) => { Object.keys(definition.attributes).forEach(name => {
const details = definition.attributes[name];
if (details.type !== undefined) { if (details.type !== undefined) {
return; return;
} }
@ -433,7 +434,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
attrs[key] = attrs[key] =
definition.attributes[key].repeatable === true definition.attributes[key].repeatable === true
? groups ? groups
: _.first(groups); : _.first(groups) || null;
} }
}); });

View File

@ -43,6 +43,7 @@ module.exports = function(strapi) {
.map(async connectionName => { .map(async connectionName => {
const connection = connections[connectionName]; const connection = connections[connectionName];
const instance = new Mongoose(); const instance = new Mongoose();
_.defaults(connection.settings, strapi.config.hook.settings.mongoose); _.defaults(connection.settings, strapi.config.hook.settings.mongoose);
const { const {
@ -65,9 +66,6 @@ module.exports = function(strapi) {
// Connect to mongo database // Connect to mongo database
const connectOptions = {}; const connectOptions = {};
const options = {
useFindAndModify: false,
};
if (!_.isEmpty(username)) { if (!_.isEmpty(username)) {
connectOptions.user = username; connectOptions.user = username;
@ -86,8 +84,6 @@ module.exports = function(strapi) {
connectOptions.dbName = database; connectOptions.dbName = database;
connectOptions.useCreateIndex = true; connectOptions.useCreateIndex = true;
options.debug = debug === true || debug === 'true';
try { try {
/* FIXME: for now, mongoose doesn't support srv auth except the way including user/pass in URI. /* FIXME: for now, mongoose doesn't support srv auth except the way including user/pass in URI.
* https://github.com/Automattic/mongoose/issues/6881 */ * https://github.com/Automattic/mongoose/issues/6881 */
@ -117,7 +113,8 @@ module.exports = function(strapi) {
require(initFunctionPath)(instance, connection); require(initFunctionPath)(instance, connection);
} }
Object.keys(options, key => instance.set(key, options[key])); instance.set('debug', debug === true || debug === 'true');
instance.set('useFindAndModify', false);
const ctx = { const ctx = {
instance, instance,

View File

@ -2,41 +2,107 @@
const _ = require('lodash'); const _ = require('lodash');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const mongooseUtils = require('mongoose/lib/utils');
const utilsModels = require('strapi-utils').models; const utilsModels = require('strapi-utils').models;
const utils = require('./utils/'); const utils = require('./utils');
const relations = require('./relations'); const relations = require('./relations');
module.exports = ({ models, target, plugin = false }, ctx) => { module.exports = ({ models, target, plugin = false }, ctx) => {
const { instance } = ctx; const { instance } = ctx;
const loadedAttributes = _.after(_.size(models), () => { // Parse every authenticated model.
_.forEach(models, (definition, model) => { Object.keys(models).map(model => {
try { const definition = models[model];
let collection = definition.orm = 'mongoose';
strapi.config.hook.settings.mongoose.collections[ definition.associations = [];
mongooseUtils.toCollectionName(definition.globalName) definition.globalName = _.upperFirst(_.camelCase(definition.globalId));
]; definition.loadedModel = {};
// Set the default values to model settings. // Set the default values to model settings.
_.defaults(definition, { _.defaults(definition, {
primaryKey: '_id', primaryKey: '_id',
}); });
if (!plugin) {
global[definition.globalName] = {};
}
const groupAttributes = Object.keys(definition.attributes).filter(
key => definition.attributes[key].type === 'group'
);
const scalarAttributes = Object.keys(definition.attributes).filter(key => {
const { type } = definition.attributes[key];
return type !== undefined && type !== null && type !== 'group';
});
const relationalAttributes = Object.keys(definition.attributes).filter(
key => {
const { type } = definition.attributes[key];
return type === undefined;
}
);
// handle gorup attrs
if (groupAttributes.length > 0) {
// create join morph collection thingy
groupAttributes.forEach(name => {
definition.loadedModel[name] = [
{
kind: String,
ref: {
type: mongoose.Schema.Types.ObjectId,
refPath: `${name}.kind`,
},
},
];
});
}
// handle scalar attrs
scalarAttributes.forEach(name => {
const attr = definition.attributes[name];
definition.loadedModel[name] = {
...attr,
type: utils(instance).convertType(attr.type),
};
});
// handle relational attrs
relationalAttributes.forEach(name => {
buildRelation({
definition,
model,
instance,
name,
attribute: definition.attributes[name],
});
});
const schema = new instance.Schema(
_.omitBy(definition.loadedModel, ({ type }) => type === 'virtual')
);
// Initialize lifecycle callbacks. // Initialize lifecycle callbacks.
const preLifecycle = { const preLifecycle = {
validate: 'beforeCreate', validate: 'beforeCreate',
find: 'beforeFetchAll',
findOne: 'beforeFetch',
findOneAndUpdate: 'beforeUpdate', findOneAndUpdate: 'beforeUpdate',
findOneAndRemove: 'beforeDestroy', findOneAndRemove: 'beforeDestroy',
remove: 'beforeDestroy', remove: 'beforeDestroy',
update: 'beforeUpdate', update: 'beforeUpdate',
updateOne: 'beforeUpdate', updateOne: 'beforeUpdate',
find: 'beforeFetchAll',
findOne: 'beforeFetch',
save: 'beforeSave', save: 'beforeSave',
}; };
const findLifecycles = [
'find',
'findOne',
'findOneAndUpdate',
'findOneAndRemove',
];
/* /*
Override populate path for polymorphic association. Override populate path for polymorphic association.
It allows us to make Upload.find().populate('related') It allows us to make Upload.find().populate('related')
@ -44,77 +110,24 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
*/ */
const morphAssociations = definition.associations.filter( const morphAssociations = definition.associations.filter(
association => association => association.nature.toLowerCase().indexOf('morph') !== -1
association.nature.toLowerCase().indexOf('morph') !== -1
); );
if (morphAssociations.length > 0) { const populateFn = createOnFetchPopulateFn({
morphAssociations.forEach(association => { groupAttributes,
Object.keys(preLifecycle) morphAssociations,
.filter(key => key.indexOf('find') !== -1) definition,
.forEach(key => {
collection.schema.pre(key, function(next) {
if (
this._mongooseOptions.populate &&
this._mongooseOptions.populate[association.alias]
) {
if (
association.nature === 'oneToManyMorph' ||
association.nature === 'manyToManyMorph'
) {
this._mongooseOptions.populate[
association.alias
].match = {
[`${association.via}.${association.filter}`]: association.alias,
[`${association.via}.kind`]: definition.globalId,
};
// Select last related to an entity.
this._mongooseOptions.populate[
association.alias
].options = {
sort: '-createdAt',
};
} else {
this._mongooseOptions.populate[
association.alias
].path = `${association.alias}.ref`;
}
} else {
if (!this._mongooseOptions.populate) {
this._mongooseOptions.populate = {};
}
// Images are not displayed in populated data.
// We automatically populate morph relations.
if (
association.nature === 'oneToManyMorph' ||
association.nature === 'manyToManyMorph'
) {
this._mongooseOptions.populate[association.alias] = {
path: association.alias,
match: {
[`${association.via}.${association.filter}`]: association.alias,
[`${association.via}.kind`]: definition.globalId,
},
options: {
sort: '-createdAt',
},
select: undefined,
model: undefined,
_docs: {},
};
}
}
next();
}); });
});
});
}
_.forEach(preLifecycle, (fn, key) => { findLifecycles.forEach(key => {
schema.pre(key, populateFn);
});
Object.keys(preLifecycle).forEach(key => {
const fn = preLifecycle[key];
if (_.isFunction(target[model.toLowerCase()][fn])) { if (_.isFunction(target[model.toLowerCase()][fn])) {
collection.schema.pre(key, function(next) { schema.pre(key, function(next) {
target[model.toLowerCase()] target[model.toLowerCase()]
[fn](this) [fn](this)
.then(next) .then(next)
@ -136,9 +149,11 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
// Mongoose doesn't allow post 'remove' event on model. // Mongoose doesn't allow post 'remove' event on model.
// See https://github.com/Automattic/mongoose/issues/3054 // See https://github.com/Automattic/mongoose/issues/3054
_.forEach(postLifecycle, (fn, key) => { Object.keys(postLifecycle).forEach(key => {
const fn = postLifecycle[key];
if (_.isFunction(target[model.toLowerCase()][fn])) { if (_.isFunction(target[model.toLowerCase()][fn])) {
collection.schema.post(key, function(doc, next) { schema.post(key, function(doc, next) {
target[model.toLowerCase()] target[model.toLowerCase()]
[fn](this, doc) [fn](this, doc)
.then(next) .then(next)
@ -156,7 +171,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
return model.type === 'virtual'; return model.type === 'virtual';
}), }),
(value, key) => { (value, key) => {
collection.schema.virtual(key.replace('_v', ''), { schema.virtual(key.replace('_v', ''), {
ref: value.ref, ref: value.ref,
localField: '_id', localField: '_id',
foreignField: value.via, foreignField: value.via,
@ -175,9 +190,9 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
? _.get(definition, 'options.timestamps[1]') ? _.get(definition, 'options.timestamps[1]')
: 'updatedAt', : 'updatedAt',
}; };
collection.schema.set('timestamps', timestamps); schema.set('timestamps', timestamps);
} else { } else {
collection.schema.set( schema.set(
'timestamps', 'timestamps',
_.get(definition, 'options.timestamps') === true _.get(definition, 'options.timestamps') === true
); );
@ -189,7 +204,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
: false : false
); );
} }
collection.schema.set( schema.set(
'minimize', 'minimize',
_.get(definition, 'options.minimize', false) === true _.get(definition, 'options.minimize', false) === true
); );
@ -197,7 +212,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
// Save all attributes (with timestamps) // Save all attributes (with timestamps)
target[model].allAttributes = _.clone(definition.attributes); target[model].allAttributes = _.clone(definition.attributes);
collection.schema.options.toObject = collection.schema.options.toJSON = { schema.options.toObject = schema.options.toJSON = {
virtuals: true, virtuals: true,
transform: function(doc, returned) { transform: function(doc, returned) {
// Remover $numberDecimal nested property. // Remover $numberDecimal nested property.
@ -221,21 +236,32 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
break; break;
case 'manyMorphToMany': case 'manyMorphToMany':
case 'manyMorphToOne': case 'manyMorphToOne':
returned[association.alias] = returned[ returned[association.alias] = returned[association.alias].map(
association.alias obj => obj.ref
].map(obj => obj.ref); );
break; break;
default: default:
} }
} }
}); });
groupAttributes.forEach(name => {
const attribute = definition.attributes[name];
if (Array.isArray(returned[name])) {
const groups = returned[name].map(el => el.ref);
// Reformat data by bypassing the many-to-many relationship.
returned[name] =
attribute.repeatable === true ? groups : _.first(groups) || null;
}
});
}, },
}; };
// Instantiate model. // Instantiate model.
const Model = instance.model( const Model = instance.model(
definition.globalId, definition.globalId,
collection.schema, schema,
definition.collectionName definition.collectionName
); );
@ -249,94 +275,87 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
// Push attributes to be aware of model schema. // Push attributes to be aware of model schema.
target[model]._attributes = definition.attributes; target[model]._attributes = definition.attributes;
target[model].updateRelations = relations.update; target[model].updateRelations = relations.update;
} catch (err) { });
strapi.log.error('Impossible to register the `' + model + '` model.'); };
strapi.log.error(err);
strapi.stop(); const createOnFetchPopulateFn = ({
morphAssociations,
groupAttributes,
definition,
}) => {
return function(next) {
morphAssociations.forEach(association => {
if (
this._mongooseOptions.populate &&
this._mongooseOptions.populate[association.alias]
) {
if (
association.nature === 'oneToManyMorph' ||
association.nature === 'manyToManyMorph'
) {
this._mongooseOptions.populate[association.alias].match = {
[`${association.via}.${association.filter}`]: association.alias,
[`${association.via}.kind`]: definition.globalId,
};
// Select last related to an entity.
this._mongooseOptions.populate[association.alias].options = {
sort: '-createdAt',
};
} else {
this._mongooseOptions.populate[
association.alias
].path = `${association.alias}.ref`;
}
} else {
if (!this._mongooseOptions.populate) {
this._mongooseOptions.populate = {};
}
// Images are not displayed in populated data.
// We automatically populate morph relations.
if (
association.nature === 'oneToManyMorph' ||
association.nature === 'manyToManyMorph'
) {
this._mongooseOptions.populate[association.alias] = {
path: association.alias,
match: {
[`${association.via}.${association.filter}`]: association.alias,
[`${association.via}.kind`]: definition.globalId,
},
options: {
sort: '-createdAt',
},
select: undefined,
model: undefined,
_docs: {},
};
}
} }
}); });
groupAttributes.forEach(name => {
if (
this._mongooseOptions.populate &&
this._mongooseOptions.populate[name]
) {
this._mongooseOptions.populate[name].path = `${name}.ref`;
} else {
this._mongooseOptions.populate[name] = {
path: `${name}.ref`,
_docs: {},
};
}
}); });
// Parse every authenticated model. next();
_.forEach(models, (definition, model) => { };
definition.globalName = _.upperFirst(_.camelCase(definition.globalId)); };
// Make sure the model has a connection. const buildRelation = ({ definition, model, instance, attribute, name }) => {
// If not, use the default connection.
if (_.isEmpty(definition.connection)) {
definition.connection =
strapi.config.currentEnvironment.database.defaultConnection;
}
// Make sure this connection exists.
if (!_.has(strapi.config.connections, definition.connection)) {
strapi.log.error(
'The connection `' +
definition.connection +
'` specified in the `' +
model +
'` model does not exist.'
);
strapi.stop();
}
// Add some informations about ORM & client connection
definition.orm = 'mongoose';
definition.client = _.get(
strapi.config.connections[definition.connection],
'client'
);
definition.associations = [];
// Register the final model for Mongoose.
definition.loadedModel = _.cloneDeep(definition.attributes);
// Initialize the global variable with the
// capitalized model name.
if (!plugin) {
global[definition.globalName] = {};
}
if (_.isEmpty(definition.attributes)) {
// Generate empty schema
_.set(
strapi.config.hook.settings.mongoose,
'collections.' +
mongooseUtils.toCollectionName(definition.globalName) +
'.schema',
new instance.Schema({})
);
return loadedAttributes();
}
// Call this callback function after we are done parsing
// all attributes for relationships-- see below.
const done = _.after(_.size(definition.attributes), () => {
// Generate schema without virtual populate
const schema = new instance.Schema(
_.omitBy(definition.loadedModel, model => {
return model.type === 'virtual';
})
);
_.set(
strapi.config.hook.settings.mongoose,
'collections.' +
mongooseUtils.toCollectionName(definition.globalName) +
'.schema',
schema
);
loadedAttributes();
});
// Add every relationships to the loaded model for Bookshelf.
// Basic attributes don't need this-- only relations.
_.forEach(definition.attributes, (details, name) => {
const verbose = const verbose =
_.get( _.get(
utilsModels.getNature(details, name, undefined, model.toLowerCase()), utilsModels.getNature(attribute, name, undefined, model.toLowerCase()),
'verbose' 'verbose'
) || ''; ) || '';
@ -344,21 +363,15 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
utilsModels.defineAssociations( utilsModels.defineAssociations(
model.toLowerCase(), model.toLowerCase(),
definition, definition,
details, attribute,
name name
); );
if (_.isEmpty(verbose)) {
definition.loadedModel[name].type = utils(instance).convertType(
details.type
);
}
switch (verbose) { switch (verbose) {
case 'hasOne': { case 'hasOne': {
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.model].globalId ? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[details.model].globalId; : strapi.models[attribute.model].globalId;
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: instance.Schema.Types.ObjectId, type: instance.Schema.Types.ObjectId,
@ -370,9 +383,9 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
const FK = _.find(definition.associations, { const FK = _.find(definition.associations, {
alias: name, alias: name,
}); });
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.collection].globalId ? strapi.plugins[attribute.plugin].models[attribute.collection].globalId
: strapi.models[details.collection].globalId; : strapi.models[attribute.collection].globalId;
if (FK) { if (FK) {
definition.loadedModel[name] = { definition.loadedModel[name] = {
@ -383,7 +396,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}; };
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; attribute.isVirtual = true;
} else { } else {
definition.loadedModel[name] = [ definition.loadedModel[name] = [
{ {
@ -398,9 +411,9 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
const FK = _.find(definition.associations, { const FK = _.find(definition.associations, {
alias: name, alias: name,
}); });
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.model].globalId ? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[details.model].globalId; : strapi.models[attribute.model].globalId;
if ( if (
FK && FK &&
@ -417,7 +430,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}; };
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; attribute.isVirtual = true;
} else { } else {
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: instance.Schema.Types.ObjectId, type: instance.Schema.Types.ObjectId,
@ -431,12 +444,12 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
const FK = _.find(definition.associations, { const FK = _.find(definition.associations, {
alias: name, alias: name,
}); });
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.collection].globalId ? strapi.plugins[attribute.plugin].models[attribute.collection].globalId
: strapi.models[details.collection].globalId; : strapi.models[attribute.collection].globalId;
// One-side of the relationship has to be a virtual field to be bidirectional. // One-side of the relationship has to be a virtual field to be bidirectional.
if ((FK && _.isUndefined(FK.via)) || details.dominant !== true) { if ((FK && _.isUndefined(FK.via)) || attribute.dominant !== true) {
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'virtual', type: 'virtual',
ref, ref,
@ -444,7 +457,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}; };
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; attribute.isVirtual = true;
} else { } else {
definition.loadedModel[name] = [ definition.loadedModel[name] = [
{ {
@ -459,9 +472,9 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
const FK = _.find(definition.associations, { const FK = _.find(definition.associations, {
alias: name, alias: name,
}); });
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.model].globalId ? strapi.plugins[attribute.plugin].models[attribute.model].globalId
: strapi.models[details.model].globalId; : strapi.models[attribute.model].globalId;
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'virtual', type: 'virtual',
@ -471,16 +484,16 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}; };
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; attribute.isVirtual = true;
break; break;
} }
case 'morphMany': { case 'morphMany': {
const FK = _.find(definition.associations, { const FK = _.find(definition.associations, {
alias: name, alias: name,
}); });
const ref = details.plugin const ref = attribute.plugin
? strapi.plugins[details.plugin].models[details.collection].globalId ? strapi.plugins[attribute.plugin].models[attribute.collection].globalId
: strapi.models[details.collection].globalId; : strapi.models[attribute.collection].globalId;
definition.loadedModel[name] = { definition.loadedModel[name] = {
type: 'virtual', type: 'virtual',
@ -489,13 +502,13 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
}; };
// Set this info to be able to see if this field is a real database's field. // Set this info to be able to see if this field is a real database's field.
details.isVirtual = true; attribute.isVirtual = true;
break; break;
} }
case 'belongsToMorph': { case 'belongsToMorph': {
definition.loadedModel[name] = { definition.loadedModel[name] = {
kind: String, kind: String,
[details.filter]: String, [attribute.filter]: String,
ref: { ref: {
type: instance.Schema.Types.ObjectId, type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`, refPath: `${name}.kind`,
@ -507,7 +520,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
definition.loadedModel[name] = [ definition.loadedModel[name] = [
{ {
kind: String, kind: String,
[details.filter]: String, [attribute.filter]: String,
ref: { ref: {
type: instance.Schema.Types.ObjectId, type: instance.Schema.Types.ObjectId,
refPath: `${name}.kind`, refPath: `${name}.kind`,
@ -519,8 +532,4 @@ module.exports = ({ models, target, plugin = false }, ctx) => {
default: default:
break; break;
} }
done();
});
});
}; };

View File

@ -8,8 +8,14 @@ const Mongoose = require('mongoose');
*/ */
module.exports = (mongoose = Mongoose) => { module.exports = (mongoose = Mongoose) => {
mongoose.Schema.Types.Decimal = require('mongoose-float').loadType(mongoose, 2); mongoose.Schema.Types.Decimal = require('mongoose-float').loadType(
mongoose.Schema.Types.Float = require('mongoose-float').loadType(mongoose, 20); mongoose,
2
);
mongoose.Schema.Types.Float = require('mongoose-float').loadType(
mongoose,
20
);
/** /**
* Convert MongoDB ID to the stringify version as GraphQL throws an error if not. * Convert MongoDB ID to the stringify version as GraphQL throws an error if not.
@ -20,8 +26,7 @@ module.exports = (mongoose = Mongoose) => {
return this.toString(); return this.toString();
}; };
const utils = { const convertType = mongooseType => {
convertType: mongooseType => {
switch (mongooseType.toLowerCase()) { switch (mongooseType.toLowerCase()) {
case 'array': case 'array':
return Array; return Array;
@ -52,16 +57,11 @@ module.exports = (mongoose = Mongoose) => {
case 'text': case 'text':
return 'String'; return 'String';
default: default:
return undefined;
} }
}, };
valueToId: value => {
if (utils.isMongoId(value)) {
return mongoose.Types.ObjectId(value);
}
return value; const isMongoId = value => {
},
isMongoId: value => {
if (value instanceof mongoose.Types.ObjectId) { if (value instanceof mongoose.Types.ObjectId) {
return true; return true;
} }
@ -74,8 +74,19 @@ module.exports = (mongoose = Mongoose) => {
// it returns for instance true for any integer value // it returns for instance true for any integer value
const hexadecimal = /^[0-9A-F]+$/i; const hexadecimal = /^[0-9A-F]+$/i;
return hexadecimal.test(value) && value.length === 24; return hexadecimal.test(value) && value.length === 24;
},
}; };
return utils; const valueToId = value => {
if (isMongoId(value)) {
return mongoose.Types.ObjectId(value);
}
return value;
};
return {
convertType,
valueToId,
isMongoId,
};
}; };