"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const adminjs_1 = require("adminjs"); const lodash_1 = require("lodash"); const property_1 = __importDefault(require("./property")); const convert_filter_1 = require("./utils/convert-filter"); const create_validation_error_1 = require("./utils/create-validation-error"); const create_duplicate_error_1 = require("./utils/create-duplicate-error"); const create_cast_error_1 = require("./utils/create-cast-error"); const errors_1 = __importDefault(require("./utils/errors")); const { MONGOOSE_CAST_ERROR, MONGOOSE_DUPLICATE_ERROR_CODE, MONGOOSE_VALIDATION_ERROR } = errors_1.default; /** * Adapter for mongoose resource * @private */ class Resource extends adminjs_1.BaseResource { /** * Initialize the class with the Resource name * @param {MongooseModel} MongooseModel Class which subclass mongoose.Model * @memberof Resource */ constructor(MongooseModel) { super(MongooseModel); this.dbType = 'mongodb'; this.MongooseModel = MongooseModel; } static isAdapterFor(MoongooseModel) { return lodash_1.get(MoongooseModel, 'base.constructor.name') === 'Mongoose'; } databaseName() { return this.MongooseModel.db.name; } databaseType() { return this.dbType; } name() { return this.MongooseModel.modelName; } id() { return this.MongooseModel.modelName; } properties() { return Object.entries(this.MongooseModel.schema.paths).map(([, path], position) => (new property_1.default(path, position))); } property(name) { var _a; return (_a = this.properties().find(property => property.path() === name)) !== null && _a !== void 0 ? _a : null; } async count(filters = null) { if (Object.keys(convert_filter_1.convertFilter(filters)).length > 0) { return this.MongooseModel.count(convert_filter_1.convertFilter(filters)); } return this.MongooseModel.estimatedDocumentCount(); } async find(filters = {}, { limit = 20, offset = 0, sort = {} }) { const { direction, sortBy } = sort; const sortingParam = { [sortBy]: direction, }; const mongooseObjects = await this.MongooseModel .find(convert_filter_1.convertFilter(filters), {}, { skip: offset, limit, sort: sortingParam, }); return mongooseObjects.map(mongooseObject => new adminjs_1.BaseRecord(Resource.stringifyId(mongooseObject), this)); } async findOne(id) { const mongooseObject = await this.MongooseModel.findById(id); return new adminjs_1.BaseRecord(Resource.stringifyId(mongooseObject), this); } async findMany(ids) { const mongooseObjects = await this.MongooseModel.find({ _id: ids }, {}); return mongooseObjects.map(mongooseObject => (new adminjs_1.BaseRecord(Resource.stringifyId(mongooseObject), this))); } build(params) { return new adminjs_1.BaseRecord(Resource.stringifyId(params), this); } async create(params) { const parsedParams = this.parseParams(params); let mongooseDocument = new this.MongooseModel(parsedParams); try { mongooseDocument = await mongooseDocument.save(); } catch (error) { if (error.name === MONGOOSE_VALIDATION_ERROR) { throw create_validation_error_1.createValidationError(error); } if (error.code === MONGOOSE_DUPLICATE_ERROR_CODE) { throw create_duplicate_error_1.createDuplicateError(error, mongooseDocument.toJSON()); } throw error; } return Resource.stringifyId(mongooseDocument.toObject()); } async update(id, params) { const parsedParams = this.parseParams(params); const unflattedParams = adminjs_1.flat.unflatten(parsedParams); try { const mongooseObject = await this.MongooseModel.findOneAndUpdate({ _id: id, }, { $set: unflattedParams, }, { new: true, runValidators: true, context: 'query', }); return Resource.stringifyId(mongooseObject.toObject()); } catch (error) { if (error.name === MONGOOSE_VALIDATION_ERROR) { throw create_validation_error_1.createValidationError(error); } if (error.code === MONGOOSE_DUPLICATE_ERROR_CODE) { throw create_duplicate_error_1.createDuplicateError(error, unflattedParams); } // In update cast errors are not wrapped into a validation errors (as it happens in create). // that is why we have to have a different way of handling them - check out tests to see // example error if (error.name === MONGOOSE_CAST_ERROR) { throw create_cast_error_1.createCastError(error); } throw error; } } async delete(id) { return this.MongooseModel.findOneAndRemove({ _id: id }); } static stringifyId(mongooseObj) { // By default Id field is an ObjectID and when we change entire mongoose model to // raw object it changes _id field not to a string but to an object. // stringify/parse is a path found here: https://github.com/Automattic/mongoose/issues/2790 // @todo We can somehow speed this up const strinigified = JSON.stringify(mongooseObj); return JSON.parse(strinigified); } /** * Check all params against values they hold. In case of wrong value it corrects it. * * What it does exactly: * - changes all empty strings to `null`s for the ObjectID properties. * - changes all empty strings to [] for array fields * * @param {Object} params received from AdminJS form * * @return {Object} converted params */ parseParams(params) { const parsedParams = { ...params }; // this function handles ObjectIDs and Arrays recursively const handleProperty = (prefix = '') => (property) => { const { path, schema, instance, } = property; // mongoose doesn't supply us with the same path as we're using in our data // so we need to improvise const fullPath = [prefix, path].filter(Boolean).join('.'); const value = parsedParams[fullPath]; // this handles missing ObjectIDs if (instance === 'ObjectID') { if (value === '') { parsedParams[fullPath] = null; } else if (value) { // this works similar as this.stringifyId parsedParams[fullPath] = value.toString(); } } // this handles empty Arrays or recurse into all properties of a filled Array if (instance === 'Array') { if (value === '') { parsedParams[fullPath] = []; } else if (schema && schema.paths) { // we only want arrays of objects (with sub-paths) const subProperties = Object.values(schema.paths); // eslint-disable-next-line no-plusplus, no-constant-condition for (let i = 0; true; i++) { // loop over every item const newPrefix = `${fullPath}.${i}`; if (parsedParams[newPrefix] === '') { // this means we have an empty object here parsedParams[newPrefix] = {}; } else if (!Object.keys(parsedParams).some(key => key.startsWith(newPrefix))) { // we're past the last index of this array break; } else { // recurse into the object subProperties.forEach(handleProperty(newPrefix)); } } } } // this handles all properties of an object if (instance === 'Embedded') { if (parsedParams[fullPath] === '') { parsedParams[fullPath] = {}; } else { const subProperties = Object.values(schema.paths); subProperties.forEach(handleProperty(fullPath)); } } }; this.properties().forEach(({ mongoosePath }) => handleProperty()(mongoosePath)); return parsedParams; } } exports.default = Resource; //# sourceMappingURL=resource.js.map