diff --git a/demo/Page/Page.model.js b/demo/Page/Page.model.js index 8c5a7997d2..601f2752ae 100644 --- a/demo/Page/Page.model.js +++ b/demo/Page/Page.model.js @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import buildQuery from '../../src/plugins/buildQuery'; import paginate from '../../src/plugins/paginate'; -import mongooseIntl from 'mongoose-intl'; +import internationalization from '../../src/plugins/internationalization'; import payloadConfig from '.././payload.config'; import { schemaBaseFields } from '../../src/helpers/mongoose/schemaBaseFields'; @@ -9,6 +9,7 @@ const PageSchema = new mongoose.Schema({ ...schemaBaseFields, title: { type: String, intl: true, unique: true }, content: { type: String, intl: true }, + //slug: { type: String, intl: true, unique: true, required: true }, metaTitle: String, metaDesc: String }, @@ -17,12 +18,6 @@ const PageSchema = new mongoose.Schema({ PageSchema.plugin(paginate); PageSchema.plugin(buildQuery); - -const formattedIntl = { - defaultLanguage: payloadConfig.localization.defaultLocale, - languages: payloadConfig.localization.locales -}; - -PageSchema.plugin(mongooseIntl, formattedIntl); +PageSchema.plugin(internationalization, payloadConfig.localization); module.exports = mongoose.model('Page', PageSchema); diff --git a/demo/shared/requestHandlers/update.js b/demo/shared/requestHandlers/update.js index 4ccdd18d48..b923616cc3 100644 --- a/demo/shared/requestHandlers/update.js +++ b/demo/shared/requestHandlers/update.js @@ -2,7 +2,7 @@ import httpStatus from '../../../src/requestHandlers'; export default (req, res) => { // do something custom specific to this app - req.model.setDefaultLanguage(req.locale); + req.model.setDefaultLocale(req.locale); req.model.findOne({_id: req.params.id}, '', {}, (err, doc) => { if (!doc) return res.status(httpStatus.NOT_FOUND).send('Not Found'); diff --git a/package.json b/package.json index f06a8413ce..fb2108b18a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "joi": "^13.6.0", "jsonwebtoken": "^8.4.0", "method-override": "^3.0.0", - "mongoose-intl": "^3.1.0", "passport": "^0.4.0", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", diff --git a/src/plugins/internationalization.js b/src/plugins/internationalization.js new file mode 100644 index 0000000000..eacd446e60 --- /dev/null +++ b/src/plugins/internationalization.js @@ -0,0 +1,200 @@ +import mongoose from 'mongoose'; + +export default function internationalization(schema, options) { + if (!options || !options.locales || !Array.isArray(options.locales) || !options.locales.length) { + throw new mongoose.Error('Required locales array is missing'); + } + + // plugin options to be set under schema options + schema.options.mongooseIntl = {}; + const pluginOptions = schema.options.mongooseIntl; + + pluginOptions.locales = options.locales.slice(0); + + // the first available locale will be used as default if it's not set or unknown value passed + if (!options.defaultLocale || pluginOptions.locales.indexOf(options.defaultLocale) === -1) { + pluginOptions.defaultLocale = pluginOptions.locales[0]; + } else { + pluginOptions.defaultLocale = options.defaultLocale.slice(0); + } + + schema.eachPath((path, schemaType) => { + if (schemaType.schema) { // propagate plugin initialization for sub-documents schemas + schemaType.schema.plugin(internationalization, pluginOptions); + return; + } + + if (!schemaType.options.intl) { + return; + } + + if (!(schemaType instanceof mongoose.Schema.Types.String)) { + throw new mongoose.Error('intl can only be used on type String'); + } + + let pathArray = path.split('.'), + key = pathArray.pop(), + prefix = pathArray.join('.'); + + if (prefix) prefix += '.'; + + // removing real path, it will be changed to virtual later + schema.remove(path); + + // schema.remove removes path from paths object only, but doesn't update tree + // sounds like a bug, removing item from the tree manually + let tree = pathArray.reduce((mem, part) => { + return mem[part]; + }, schema.tree); + delete tree[key]; + + + schema.virtual(path) + .get(function () { + // embedded and sub-documents will use locale methods from the top level document + let owner = this.ownerDocument ? this.ownerDocument() : this, + locale = owner.getLocale(), + localeSubDoc = this.getValue(path); + + if (localeSubDoc === null || localeSubDoc === void 0) { + return localeSubDoc; + } + + if (localeSubDoc.hasOwnProperty(locale)) { + return localeSubDoc[locale]; + } + + // are there any other locales defined? + for (let prop in localeSubDoc) { + if (localeSubDoc.hasOwnProperty(prop)) { + // TODO: find a different lang to return + return null; // some other locales exist, but the required is not - return null value + } + } + return void 0; // no locales defined - the entire field is undefined + }) + .set(function (value) { + // multiple locales are set as an object + if (typeof value === 'object') { + let locales = this.schema.options.mongooseIntl.locales; + locales.forEach(locale => { + if (!value[locale]) { + return; + } + this.set(path + '.' + locale, value[locale]); + }, this); + return; + } + + // embedded and sub-documents will use locale methods from the top level document + let owner = this.ownerDocument ? this.ownerDocument() : this; + this.set(path + '.' + owner.getLocale(), value); + }); + + + // intl option is not needed for the current path any more, + // and is unwanted for all child locale-properties + delete schemaType.options.intl; + + let intlObject = {}; + intlObject[key] = {}; + pluginOptions.locales.forEach(function (locale) { + let localeOptions = Object.assign({}, schemaType.options); + if (locale !== options.defaultLocale) { + delete localeOptions.default; + delete localeOptions.required; + } + + if (schemaType.options.defaultAll) { + localeOptions.default = schemaType.options.defaultAll; + } + + if (schemaType.options.requiredAll) { + localeOptions.required = schemaType.options.requiredAll; + } + // the use of this inside of a function caused an issue when changed to an arrow function + // this object will not be scoped inside an arrow function but then I don't understand what is set here + // if not lost at the end + this[locale] = localeOptions; + }, intlObject[key]); + + // intlObject example: + // { fieldName: { + // en: '', + // de: '' + // }} + schema.add(intlObject, prefix); + }); + + // document methods to set the locale for each model instance (document) + schema.method({ + getLocales: function () { + return this.schema.options.mongooseIntl.locales; + }, + getLocale: function () { + return this.docLocale || this.schema.options.mongooseIntl.defaultLocale; + }, + setLocale: function (locale) { + if (locale && this.getLocales().indexOf(locale) !== -1) { + this.docLocale = locale; + } + }, + unsetLocale: function () { + delete this.docLocale; + } + }); + + // model methods to set the locale for the current schema + schema.static({ + getLocales: function () { + return this.schema.options.mongooseIntl.locales; + }, + getDefaultLocale: function () { + return this.schema.options.mongooseIntl.defaultLocale; + }, + setDefaultLocale: function (locale) { + + let updateLocale = function (schema, locale) { + schema.options.mongooseIntl.defaultLocale = locale.slice(0); + + // default locale change for sub-documents schemas + schema.eachPath((path, schemaType) => { + if (schemaType.schema) { + updateLocale(schemaType.schema, locale); + } + }); + + }; + + if (locale && this.getLocales().indexOf(locale) !== -1) { + updateLocale(this.schema, locale); + } + } + }); + + // Mongoose will emit 'init' event once the schema will be attached to the model + schema.on('init', (model) => { + // no actions are required in the global method is already defined + if (model.db.setDefaultLocale) { + return; + } + + // define a global method to change the locale for all models (and their schemas) + // created for the current mongo connection + model.db.setDefaultLocale = function (locale) { + let model, modelName; + for (modelName in this.models) { + if (this.models.hasOwnProperty(modelName)) { + model = this.models[modelName]; + model.setDefaultLocale && model.setDefaultLocale(locale); + } + } + }; + + // create an alias for the global change locale method attached to the default connection + if (!mongoose.setDefaultLocale) { + mongoose.setDefaultLocale = mongoose.connection.setDefaultLocale; + } + }); +} + diff --git a/src/requestHandlers/create.js b/src/requestHandlers/create.js index 85142ef664..a5dea123ce 100644 --- a/src/requestHandlers/create.js +++ b/src/requestHandlers/create.js @@ -1,7 +1,7 @@ import httpStatus from 'http-status'; const create = (req, res) => { - req.model.setDefaultLanguage(req.locale); + req.model.setDefaultLocale(req.locale); req.model.create(req.body, (err, result) => { if (err) return res.send(httpStatus.INTERNAL_SERVER_ERROR, { error: err }); diff --git a/src/requestHandlers/destroy.js b/src/requestHandlers/destroy.js index 4189d5cb78..0c93c31a0d 100644 --- a/src/requestHandlers/destroy.js +++ b/src/requestHandlers/destroy.js @@ -1,7 +1,6 @@ import httpStatus from 'http-status'; const destroy = (req, res) => { - req.model.setDefaultLanguage(req.locale); req.model.findOneAndDelete({ _id: req.params._id }, (err, doc) => { if (err) return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ error: err }); diff --git a/src/requestHandlers/findOne.js b/src/requestHandlers/findOne.js index 319507d62e..46adeeb8ae 100644 --- a/src/requestHandlers/findOne.js +++ b/src/requestHandlers/findOne.js @@ -1,8 +1,6 @@ import httpStatus from 'http-status'; const findOne = (req, res) => { - // req.model.setDefaultLanguage(req.locale); - req.model.findOne({ _id: req.params._id }, (err, doc) => { if (err) return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ error: err }); @@ -11,7 +9,7 @@ const findOne = (req, res) => { return res.status(httpStatus.NOT_FOUND).send('Not Found'); if (req.locale) { - doc.setLanguage(req.locale); + doc.setLocale(req.locale); return res.json(doc.toJSON({ virtuals: true })); } diff --git a/src/requestHandlers/query.js b/src/requestHandlers/query.js index 802f73af6e..24a2ebde39 100644 --- a/src/requestHandlers/query.js +++ b/src/requestHandlers/query.js @@ -2,7 +2,7 @@ import httpStatus from 'http-status'; const query = (req, res) => { if (req.query.locale) { - req.model.setDefaultLanguage(req.query.locale); + req.model.setDefaultLocale(req.query.locale); } req.model.paginate(req.model.apiQuery(req.query), req.query, (err, result) => { diff --git a/src/requestHandlers/update.js b/src/requestHandlers/update.js index 6b54967319..ec9f51e703 100644 --- a/src/requestHandlers/update.js +++ b/src/requestHandlers/update.js @@ -1,7 +1,6 @@ import httpStatus from 'http-status'; const update = (req, res) => { - req.model.setDefaultLanguage(req.locale); req.model.findOne({ _id: req.params._id }, '', {}, (err, doc) => { if (!doc) return res.status(httpStatus.NOT_FOUND).send('Not Found'); diff --git a/src/tests/middleware.spec.js b/src/tests/middleware.spec.js index e6cbf2398b..80118432f9 100644 --- a/src/tests/middleware.spec.js +++ b/src/tests/middleware.spec.js @@ -52,8 +52,8 @@ describe('Payload Middleware', () => { body: {} }; localization = { - languages: ['en', 'es'], - defaultLanguage: 'en' + locales: ['en', 'es'], + defaultLocale: 'en' }; }); @@ -73,7 +73,7 @@ describe('Payload Middleware', () => { locale(localization)(req, res, next); expect(next).toHaveBeenCalledTimes(1); - expect(req.locale).toEqual(localization.defaultLanguage); + expect(req.locale).toEqual(localization.defaultLocale); }); it('Supports accept-language header', () => { @@ -92,7 +92,7 @@ describe('Payload Middleware', () => { locale(localization)(req, res, next); expect(next).toHaveBeenCalledTimes(1); - expect(req.locale).toEqual(localization.defaultLanguage); + expect(req.locale).toEqual(localization.defaultLocale); }); it('Query param takes precedence over header', () => { @@ -105,14 +105,14 @@ describe('Payload Middleware', () => { expect(req.locale).toEqual('es'); }); - it('Supports default language', () => { + it('Supports default locale', () => { locale(localization)(req, res, next); expect(next).toHaveBeenCalledTimes(1); - expect(req.locale).toEqual(localization.defaultLanguage); + expect(req.locale).toEqual(localization.defaultLocale); }); - it('Supports language all', () => { + it('Supports locale all', () => { req.query.locale = '*'; locale(localization)(req, res, next);