diff --git a/demo/collections/Hooks.ts b/demo/collections/Hooks.ts index 67c93e0cfc..94ee285896 100644 --- a/demo/collections/Hooks.ts +++ b/demo/collections/Hooks.ts @@ -28,7 +28,7 @@ const Hooks: PayloadCollectionConfig = { beforeChange: [ (operation) => { if (operation.req.headers.hook === 'beforeChange') { - operation.req.body.description += '-beforeChangeSuffix'; + operation.data.description += '-beforeChangeSuffix'; } return operation.data; }, diff --git a/demo/collections/RelationshipA.ts b/demo/collections/RelationshipA.ts index 29929537bd..57735b3d63 100644 --- a/demo/collections/RelationshipA.ts +++ b/demo/collections/RelationshipA.ts @@ -17,14 +17,14 @@ const RelationshipA: PayloadCollectionConfig = { relationTo: 'relationship-b', localized: true, }, - // { - // name: 'LocalizedPost', - // label: 'Localized Post', - // type: 'relationship', - // relationTo: 'localized-posts', - // hasMany: true, - // localized: true, - // }, + { + name: 'LocalizedPost', + label: 'Localized Post', + type: 'relationship', + relationTo: 'localized-posts', + hasMany: true, + localized: true, + }, { name: 'postLocalizedMultiple', label: 'Localized Post Multiple', @@ -33,6 +33,14 @@ const RelationshipA: PayloadCollectionConfig = { hasMany: true, localized: true, }, + { + name: 'postManyRelationships', + label: 'Post Many Relationships', + type: 'relationship', + relationTo: ['relationship-b'], + localized: true, + hasMany: false, + }, ], timestamps: true, }; diff --git a/demo/collections/RelationshipB.ts b/demo/collections/RelationshipB.ts index 566bf883c1..27fc3a736f 100644 --- a/demo/collections/RelationshipB.ts +++ b/demo/collections/RelationshipB.ts @@ -18,6 +18,14 @@ const RelationshipB: PayloadCollectionConfig = { localized: false, hasMany: true, }, + { + name: 'postManyRelationships', + label: 'Post Many Relationships', + type: 'relationship', + relationTo: ['relationship-a'], + localized: true, + hasMany: false, + }, ], timestamps: true, }; diff --git a/package.json b/package.json index c9ae345f96..dcb2d9e898 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "jest": "26.6.3", "joi": "^17.3.0", "jsonwebtoken": "^8.5.1", - "lodash.merge": "^4.6.2", "method-override": "^3.0.0", "micro-memoize": "^4.0.9", "mime": "^2.5.0", diff --git a/src/auth/operations/login.ts b/src/auth/operations/login.ts index 1ea28ca5ad..866c69d9b2 100644 --- a/src/auth/operations/login.ts +++ b/src/auth/operations/login.ts @@ -177,7 +177,7 @@ async function login(incomingArgs: Arguments): Promise { hook: 'afterRead', operation: 'login', overrideAccess, - reduceLocales: true, + flattenLocales: true, showHiddenFields, }); diff --git a/src/collections/buildSchema.ts b/src/collections/buildSchema.ts index c8b25ad6df..6a3be5edee 100644 --- a/src/collections/buildSchema.ts +++ b/src/collections/buildSchema.ts @@ -1,6 +1,5 @@ import paginate from 'mongoose-paginate-v2'; import buildQueryPlugin from '../mongoose/buildQuery'; -import localizationPlugin from '../localization/plugin'; import buildSchema from '../mongoose/buildSchema'; const buildCollectionSchema = (collection, config, schemaOptions = {}) => { @@ -9,10 +8,6 @@ const buildCollectionSchema = (collection, config, schemaOptions = {}) => { schema.plugin(paginate) .plugin(buildQueryPlugin); - if (config.localization) { - schema.plugin(localizationPlugin, config.localization); - } - return schema; }; diff --git a/src/collections/config/types.ts b/src/collections/config/types.ts index 85df104628..07b326301d 100644 --- a/src/collections/config/types.ts +++ b/src/collections/config/types.ts @@ -3,12 +3,12 @@ import { DeepRequired } from 'ts-essentials'; import { PaginateModel, PassportLocalModel } from 'mongoose'; import { Access } from '../../config/types'; import { Field } from '../../fields/config/types'; -import { Document, PayloadMongooseDocument } from '../../types'; +import { Document } from '../../types'; import { PayloadRequest } from '../../express/types'; import { IncomingAuthType, Auth } from '../../auth/types'; import { IncomingUploadType, Upload } from '../../uploads/types'; -export interface CollectionModel extends PaginateModel, PassportLocalModel { +export interface CollectionModel extends PaginateModel, PassportLocalModel { buildQuery: (query: unknown, locale?: string) => Record } diff --git a/src/collections/operations/create.ts b/src/collections/operations/create.ts index 78aa400fa5..b43c2e76ad 100644 --- a/src/collections/operations/create.ts +++ b/src/collections/operations/create.ts @@ -31,7 +31,7 @@ export type Arguments = { } async function create(this: Payload, incomingArgs: Arguments): Promise { - const { performFieldOperations, config, emailOptions } = this; + const { config, emailOptions } = this; let args = incomingArgs; @@ -54,10 +54,6 @@ async function create(this: Payload, incomingArgs: Arguments): Promise config: collectionConfig, }, req, - req: { - locale, - fallbackLocale, - }, disableVerificationEmail, depth, overrideAccess, @@ -100,18 +96,6 @@ async function create(this: Payload, incomingArgs: Arguments): Promise })) || data; }, Promise.resolve()); - // ///////////////////////////////////// - // beforeChange - Fields - // ///////////////////////////////////// - - data = await performFieldOperations(collectionConfig, { - data, - hook: 'beforeChange', - operation: 'create', - req, - overrideAccess, - }); - // ///////////////////////////////////// // beforeChange - Collection // ///////////////////////////////////// @@ -127,9 +111,21 @@ async function create(this: Payload, incomingArgs: Arguments): Promise }, Promise.resolve()); // ///////////////////////////////////// - // Upload and resize potential files + // beforeChange - Fields // ///////////////////////////////////// + let resultWithLocales = await this.performFieldOperations(collectionConfig, { + data, + hook: 'beforeChange', + operation: 'create', + req, + overrideAccess, + unflattenLocales: true, + }); + + // ///////////////////////////////////// + // Upload and resize potential files + // ///////////////////////////////////// if (collectionConfig.upload) { const fileData: Partial = {}; @@ -174,8 +170,8 @@ async function create(this: Payload, incomingArgs: Arguments): Promise fileData.filesize = file.size; fileData.mimeType = file.mimetype; - data = { - ...data, + resultWithLocales = { + ...resultWithLocales, ...fileData, }; } @@ -184,28 +180,22 @@ async function create(this: Payload, incomingArgs: Arguments): Promise // Create // ///////////////////////////////////// - let doc = new Model(); - - if (locale && doc.setLocale) { - doc.setLocale(locale, fallbackLocale); - } - if (collectionConfig.auth) { if (data.email) { - data.email = (data.email as string).toLowerCase(); + resultWithLocales.email = (data.email as string).toLowerCase(); } if (collectionConfig.auth.verify) { - data._verified = false; - data._verificationToken = crypto.randomBytes(20).toString('hex'); + resultWithLocales._verified = false; + resultWithLocales._verificationToken = crypto.randomBytes(20).toString('hex'); } } - Object.assign(doc, data); + let doc; if (collectionConfig.auth) { - doc = await Model.register(doc, data.password as string); + doc = await Model.register(resultWithLocales, data.password as string); } else { - await doc.save(); + doc = await Model.create(resultWithLocales); } let result: Document = doc.toJSON({ virtuals: true }); @@ -218,7 +208,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise // afterChange - Fields // ///////////////////////////////////// - result = await performFieldOperations(collectionConfig, { + result = await this.performFieldOperations(collectionConfig, { data: result, hook: 'afterChange', operation: 'create', @@ -270,7 +260,7 @@ async function create(this: Payload, incomingArgs: Arguments): Promise hook: 'afterRead', operation: 'create', overrideAccess, - reduceLocales: false, + flattenLocales: true, showHiddenFields, }); diff --git a/src/collections/operations/delete.ts b/src/collections/operations/delete.ts index b5aef5631e..f62d19b526 100644 --- a/src/collections/operations/delete.ts +++ b/src/collections/operations/delete.ts @@ -94,10 +94,6 @@ async function deleteQuery(incomingArgs: Arguments): Promise { if (!docToDelete && !hasWhereAccess) throw new NotFound(); if (!docToDelete && hasWhereAccess) throw new Forbidden(); - if (locale && docToDelete.setLocale) { - docToDelete.setLocale(locale, fallbackLocale); - } - const resultToDelete = docToDelete.toJSON({ virtuals: true }); // ///////////////////////////////////// @@ -137,10 +133,6 @@ async function deleteQuery(incomingArgs: Arguments): Promise { const doc = await Model.findOneAndDelete({ _id: id }); - if (locale && doc.setLocale) { - doc.setLocale(locale, fallbackLocale); - } - let result: Document = doc.toJSON({ virtuals: true }); result = removeInternalFields(result); @@ -169,7 +161,7 @@ async function deleteQuery(incomingArgs: Arguments): Promise { hook: 'afterRead', operation: 'delete', overrideAccess, - reduceLocales: false, + flattenLocales: true, showHiddenFields, }); diff --git a/src/collections/operations/find.ts b/src/collections/operations/find.ts index 8762057cdb..617a3eac8c 100644 --- a/src/collections/operations/find.ts +++ b/src/collections/operations/find.ts @@ -148,7 +148,7 @@ async function find(incomingArgs: Arguments): Promise { hook: 'afterRead', operation: 'read', overrideAccess, - reduceLocales: true, + flattenLocales: true, showHiddenFields, }, find, diff --git a/src/collections/operations/findByID.ts b/src/collections/operations/findByID.ts index d482c823ad..ec3203378a 100644 --- a/src/collections/operations/findByID.ts +++ b/src/collections/operations/findByID.ts @@ -141,7 +141,7 @@ async function findByID(incomingArgs: Arguments): Promise { operation: 'read', currentDepth, overrideAccess, - reduceLocales: true, + flattenLocales: true, showHiddenFields, }); diff --git a/src/collections/operations/local/create.ts b/src/collections/operations/local/create.ts index 95e86e25af..3979301e03 100644 --- a/src/collections/operations/local/create.ts +++ b/src/collections/operations/local/create.ts @@ -17,8 +17,8 @@ export default async function create(options: Options): Promise { const { collection: collectionSlug, depth, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, data, user, overrideAccess = true, diff --git a/src/collections/operations/local/delete.ts b/src/collections/operations/local/delete.ts index 07c967d4d0..337a74caa1 100644 --- a/src/collections/operations/local/delete.ts +++ b/src/collections/operations/local/delete.ts @@ -16,8 +16,8 @@ export default async function localDelete(options: Options): Promise { collection: collectionSlug, depth, id, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, user, overrideAccess = true, showHiddenFields, diff --git a/src/collections/operations/local/find.ts b/src/collections/operations/local/find.ts index 357833464a..cef864e294 100644 --- a/src/collections/operations/local/find.ts +++ b/src/collections/operations/local/find.ts @@ -22,8 +22,8 @@ export default async function find(options: Options): Promise { page, limit, where, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, user, overrideAccess = true, showHiddenFields, diff --git a/src/collections/operations/local/findByID.ts b/src/collections/operations/local/findByID.ts index 8d6537d0a3..f03b846edb 100644 --- a/src/collections/operations/local/findByID.ts +++ b/src/collections/operations/local/findByID.ts @@ -17,8 +17,8 @@ export default async function findByID(options: Options): Promise { collection: collectionSlug, depth, id, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, user, overrideAccess = true, disableErrors = false, diff --git a/src/collections/operations/local/update.ts b/src/collections/operations/local/update.ts index a86164494c..0fca854022 100644 --- a/src/collections/operations/local/update.ts +++ b/src/collections/operations/local/update.ts @@ -18,8 +18,8 @@ export default async function update(options: Options): Promise { const { collection: collectionSlug, depth, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, data, id, user, diff --git a/src/collections/operations/update.ts b/src/collections/operations/update.ts index db6f7dda2e..1f64b2344b 100644 --- a/src/collections/operations/update.ts +++ b/src/collections/operations/update.ts @@ -1,13 +1,12 @@ -import httpStatus from 'http-status'; import deepmerge from 'deepmerge'; -import merge from 'lodash.merge'; +import httpStatus from 'http-status'; import path from 'path'; import { UploadedFile } from 'express-fileupload'; import { Where, Document } from '../../types'; import { Collection } from '../config/types'; -import removeInternalFields from '../../utilities/removeInternalFields'; import overwriteMerge from '../../utilities/overwriteMerge'; +import removeInternalFields from '../../utilities/removeInternalFields'; import executeAccess from '../../auth/executeAccess'; import { NotFound, Forbidden, APIError, FileUploadError } from '../../errors'; import isImage from '../../uploads/isImage'; @@ -60,7 +59,6 @@ async function update(incomingArgs: Arguments): Promise { req, req: { locale, - fallbackLocale, }, overrideAccess, showHiddenFields, @@ -104,14 +102,20 @@ async function update(incomingArgs: Arguments): Promise { if (!doc && !hasWherePolicy) throw new NotFound(); if (!doc && hasWherePolicy) throw new Forbidden(); - if (locale && doc.setLocale) { - doc.setLocale(locale, null); - } + let docWithLocales: Document = doc.toJSON({ virtuals: true }); + docWithLocales = JSON.stringify(docWithLocales); + docWithLocales = JSON.parse(docWithLocales); - let originalDoc: Document = doc.toJSON({ virtuals: true }); - - originalDoc = JSON.stringify(originalDoc); - originalDoc = JSON.parse(originalDoc); + const originalDoc = await performFieldOperations(collectionConfig, { + depth, + req, + data: docWithLocales, + hook: 'afterRead', + operation: 'update', + overrideAccess, + flattenLocales: true, + showHiddenFields, + }); let { data } = args; @@ -144,20 +148,6 @@ async function update(incomingArgs: Arguments): Promise { })) || data; }, Promise.resolve()); - // ///////////////////////////////////// - // beforeChange - Fields - // ///////////////////////////////////// - - data = await performFieldOperations(collectionConfig, { - data, - req, - id, - originalDoc, - hook: 'beforeChange', - operation: 'update', - overrideAccess, - }); - // ///////////////////////////////////// // beforeChange - Collection // ///////////////////////////////////// @@ -179,6 +169,22 @@ async function update(incomingArgs: Arguments): Promise { data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge }); + // ///////////////////////////////////// + // beforeChange - Fields + // ///////////////////////////////////// + + let result = await performFieldOperations(collectionConfig, { + data, + req, + id, + originalDoc, + hook: 'beforeChange', + operation: 'update', + overrideAccess, + unflattenLocales: true, + docWithLocales, + }); + // ///////////////////////////////////// // Upload and resize potential files // ///////////////////////////////////// @@ -219,13 +225,13 @@ async function update(incomingArgs: Arguments): Promise { throw new FileUploadError(); } - data = { - ...data, + result = { + ...result, ...fileData, }; - } else if (data.file === null) { - data = { - ...data, + } else if (result.file === null) { + result = { + ...result, filename: null, sizes: null, }; @@ -241,26 +247,52 @@ async function update(incomingArgs: Arguments): Promise { if (password) { await doc.setPassword(password as string); delete data.password; + delete result.password; } // ///////////////////////////////////// // Update // ///////////////////////////////////// - merge(doc, data); - - await doc.save(); - - if (locale && doc.setLocale) { - doc.setLocale(locale, fallbackLocale); - } - - let result: Document = doc.toJSON({ virtuals: true }); + result = await Model.findByIdAndUpdate( + { _id: id }, + result, + { overwrite: true, new: true }, + ); + result = result.toJSON({ virtuals: true }); result = removeInternalFields(result); result = JSON.stringify(result); result = JSON.parse(result); + // ///////////////////////////////////// + // afterRead - Fields + // ///////////////////////////////////// + + result = await performFieldOperations(collectionConfig, { + depth, + req, + data: result, + hook: 'afterRead', + operation: 'update', + overrideAccess, + flattenLocales: true, + showHiddenFields, + }); + + // ///////////////////////////////////// + // afterRead - Collection + // ///////////////////////////////////// + + await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { + await priorHook; + + result = await hook({ + req, + doc: result, + }) || result; + }, Promise.resolve()); + // ///////////////////////////////////// // afterChange - Fields // ///////////////////////////////////// @@ -290,34 +322,6 @@ async function update(incomingArgs: Arguments): Promise { }) || result; }, Promise.resolve()); - // ///////////////////////////////////// - // afterRead - Fields - // ///////////////////////////////////// - - result = await performFieldOperations(collectionConfig, { - depth, - req, - data: result, - hook: 'afterRead', - operation: 'update', - overrideAccess, - reduceLocales: false, - showHiddenFields, - }); - - // ///////////////////////////////////// - // afterRead - Collection - // ///////////////////////////////////// - - await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => { - await priorHook; - - result = await hook({ - req, - doc: result, - }) || result; - }, Promise.resolve()); - // ///////////////////////////////////// // Return results // ///////////////////////////////////// diff --git a/src/fields/performFieldOperations.ts b/src/fields/performFieldOperations.ts index 146137d82c..4197fdaba9 100644 --- a/src/fields/performFieldOperations.ts +++ b/src/fields/performFieldOperations.ts @@ -1,3 +1,4 @@ +import { Payload } from '..'; import { ValidationError } from '../errors'; import sanitizeFallbackLocale from '../localization/sanitizeFallbackLocale'; import traverseFields from './traverseFields'; @@ -6,25 +7,29 @@ import { GlobalConfig } from '../globals/config/types'; import { Operation } from '../types'; import { PayloadRequest } from '../express/types'; import { HookName } from './config/types'; +import deepCopyObject from '../utilities/deepCopyObject'; type Arguments = { data: Record operation: Operation - hook: HookName + hook?: HookName req: PayloadRequest overrideAccess: boolean - reduceLocales?: boolean + flattenLocales?: boolean + unflattenLocales?: boolean originalDoc?: Record + docWithLocales?: Record id?: string showHiddenFields?: boolean depth?: number currentDepth?: number } -export default async function performFieldOperations(entityConfig: CollectionConfig | GlobalConfig, args: Arguments): Promise<{ [key: string]: unknown }> { +export default async function performFieldOperations(this: Payload, entityConfig: CollectionConfig | GlobalConfig, args: Arguments): Promise<{ [key: string]: unknown }> { const { - data: fullData, + data, originalDoc: fullOriginalDoc, + docWithLocales, operation, hook, req, @@ -34,10 +39,13 @@ export default async function performFieldOperations(entityConfig: CollectionCon locale, }, overrideAccess, - reduceLocales, + flattenLocales, + unflattenLocales = false, showHiddenFields = false, } = args; + const fullData = deepCopyObject(data); + const fallbackLocale = sanitizeFallbackLocale(req.fallbackLocale); let depth = 0; @@ -57,6 +65,7 @@ export default async function performFieldOperations(entityConfig: CollectionCon const accessPromises = []; const relationshipPopulations = []; const hookPromises = []; + const unflattenLocaleActions = []; const errors: { message: string, field: string }[] = []; // ////////////////////////////////////////// @@ -64,11 +73,11 @@ export default async function performFieldOperations(entityConfig: CollectionCon // ////////////////////////////////////////// traverseFields({ - fields: entityConfig.fields, // TODO: Bad typing, this exists + fields: entityConfig.fields, data: fullData, originalDoc: fullOriginalDoc, path: '', - reduceLocales, + flattenLocales, locale, fallbackLocale, accessPromises, @@ -87,6 +96,9 @@ export default async function performFieldOperations(entityConfig: CollectionCon errors, payload: this, showHiddenFields, + unflattenLocales, + unflattenLocaleActions, + docWithLocales, }); await Promise.all(hookPromises); @@ -99,6 +111,8 @@ export default async function performFieldOperations(entityConfig: CollectionCon throw new ValidationError(errors); } + unflattenLocaleActions.forEach((action) => action()); + await Promise.all(accessPromises); const relationshipPopulationPromises = relationshipPopulations.map((population) => population()); diff --git a/src/fields/traverseFields.ts b/src/fields/traverseFields.ts index e3e191ecaf..eaf03d9e60 100644 --- a/src/fields/traverseFields.ts +++ b/src/fields/traverseFields.ts @@ -11,7 +11,7 @@ type Arguments = { data: Record originalDoc: Record path: string - reduceLocales: boolean + flattenLocales: boolean locale: string fallbackLocale: string accessPromises: Promise[] @@ -30,6 +30,9 @@ type Arguments = { errors: {message: string, field: string}[] payload: Payload showHiddenFields: boolean + unflattenLocales: boolean + unflattenLocaleActions: (() => void)[] + docWithLocales?: Record } const traverseFields = (args: Arguments): void => { @@ -38,7 +41,7 @@ const traverseFields = (args: Arguments): void => { data = {}, originalDoc = {}, path, - reduceLocales, + flattenLocales, locale, fallbackLocale, accessPromises, @@ -57,6 +60,9 @@ const traverseFields = (args: Arguments): void => { errors, payload, showHiddenFields, + unflattenLocaleActions, + unflattenLocales, + docWithLocales = {}, } = args; fields.forEach((field) => { @@ -85,7 +91,7 @@ const traverseFields = (args: Arguments): void => { && field.name && field.localized && locale !== 'all' - && reduceLocales; + && flattenLocales; if (hasLocalizedValue) { let localizedValue = data[field.name][locale]; @@ -94,6 +100,15 @@ const traverseFields = (args: Arguments): void => { dataCopy[field.name] = localizedValue; } + if (field.localized && unflattenLocales) { + unflattenLocaleActions.push(() => { + data[field.name] = payload.config.localization.locales.reduce((locales, localeID) => ({ + ...locales, + [localeID]: localeID === locale ? data[field.name] : docWithLocales?.[field.name]?.[localeID], + }), {}); + }); + } + accessPromises.push(accessPromise({ data, originalDoc, @@ -128,12 +143,12 @@ const traverseFields = (args: Arguments): void => { } else if (fieldIsArrayType(field)) { if (Array.isArray(data[field.name])) { (data[field.name] as Record[]).forEach((rowData, i) => { - const originalDocRow = originalDoc && originalDoc[field.name] && originalDoc[field.name][i]; traverseFields({ ...args, fields: field.fields, data: rowData, - originalDoc: originalDocRow || undefined, + originalDoc: originalDoc?.[field.name]?.[i], + docWithLocales: docWithLocales?.[field.name]?.[i], path: `${path}${field.name}.${i}.`, }); }); @@ -144,6 +159,7 @@ const traverseFields = (args: Arguments): void => { fields: field.fields, data: data[field.name] as Record, originalDoc: originalDoc[field.name], + docWithLocales: docWithLocales?.[field.name], path: `${path}${field.name}.`, }); } @@ -153,14 +169,14 @@ const traverseFields = (args: Arguments): void => { if (Array.isArray(data[field.name])) { (data[field.name] as Record[]).forEach((rowData, i) => { const block = field.blocks.find((blockType) => blockType.slug === rowData.blockType); - const originalDocRow = originalDoc && originalDoc[field.name] && originalDoc[field.name][i]; if (block) { traverseFields({ ...args, fields: block.fields, data: rowData, - originalDoc: originalDocRow || undefined, + originalDoc: originalDoc?.[field.name]?.[i], + docWithLocales: docWithLocales?.[field.name]?.[i], path: `${path}${field.name}.${i}.`, }); } @@ -168,7 +184,7 @@ const traverseFields = (args: Arguments): void => { } } - if ((operation === 'create' || operation === 'update') && field.name) { + if (hook === 'beforeChange' && field.name) { const updatedData = data; if (data?.[field.name] === undefined && originalDoc?.[field.name] === undefined && field.defaultValue) { diff --git a/src/globals/buildModel.ts b/src/globals/buildModel.ts index 850a9794d7..8a8491353a 100644 --- a/src/globals/buildModel.ts +++ b/src/globals/buildModel.ts @@ -1,25 +1,15 @@ import mongoose from 'mongoose'; import buildSchema from '../mongoose/buildSchema'; -import localizationPlugin from '../localization/plugin'; import { Config } from '../config/types'; const buildModel = (config: Config): mongoose.PaginateModel | null => { if (config.globals && config.globals.length > 0) { const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true }); - if (config.localization) { - globalsSchema.plugin(localizationPlugin, config.localization); - } - const Globals = mongoose.model('globals', globalsSchema); Object.values(config.globals).forEach((globalConfig) => { const globalSchema = buildSchema(config, globalConfig.fields, {}); - - if (config.localization) { - globalSchema.plugin(localizationPlugin, config.localization); - } - Globals.discriminator(globalConfig.slug, globalSchema); }); diff --git a/src/globals/operations/findOne.ts b/src/globals/operations/findOne.ts index 05a44742ee..ecc17e354b 100644 --- a/src/globals/operations/findOne.ts +++ b/src/globals/operations/findOne.ts @@ -31,6 +31,10 @@ async function findOne(args) { delete doc._id; } + doc = removeInternalFields(doc); + doc = JSON.stringify(doc); + doc = JSON.parse(doc); + // ///////////////////////////////////// // 3. Execute before collection hook // ///////////////////////////////////// @@ -54,7 +58,7 @@ async function findOne(args) { operation: 'read', req, depth, - reduceLocales: true, + flattenLocales: true, showHiddenFields, }); @@ -75,10 +79,6 @@ async function findOne(args) { // 6. Return results // ///////////////////////////////////// - doc = removeInternalFields(doc); - doc = JSON.stringify(doc); - doc = JSON.parse(doc); - return doc; } diff --git a/src/globals/operations/local/findOne.ts b/src/globals/operations/local/findOne.ts index 94aa262386..07e848a2f8 100644 --- a/src/globals/operations/local/findOne.ts +++ b/src/globals/operations/local/findOne.ts @@ -2,8 +2,8 @@ async function findOne(options) { const { global: globalSlug, depth, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, user, overrideAccess = true, showHiddenFields, diff --git a/src/globals/operations/local/update.ts b/src/globals/operations/local/update.ts index e430f7b009..b7c9cbd23d 100644 --- a/src/globals/operations/local/update.ts +++ b/src/globals/operations/local/update.ts @@ -2,8 +2,8 @@ async function update(options) { const { global: globalSlug, depth, - locale, - fallbackLocale, + locale = this?.config?.localization?.defaultLocale, + fallbackLocale = null, data, user, overrideAccess = true, diff --git a/src/globals/operations/update.ts b/src/globals/operations/update.ts index bbeeb43644..9e69bbe013 100644 --- a/src/globals/operations/update.ts +++ b/src/globals/operations/update.ts @@ -1,5 +1,4 @@ import deepmerge from 'deepmerge'; -import merge from 'lodash.merge'; import overwriteMerge from '../../utilities/overwriteMerge'; import executeAccess from '../../auth/executeAccess'; import removeInternalFields from '../../utilities/removeInternalFields'; @@ -11,10 +10,6 @@ async function update(args) { globalConfig, slug, req, - req: { - locale, - fallbackLocale, - }, depth, overrideAccess, showHiddenFields, @@ -33,25 +28,48 @@ async function update(args) { // ///////////////////////////////////// let global = await Model.findOne({ globalType: slug }); + let globalJSON; - if (!global) { - global = new Model({ globalType: slug }); + if (global) { + globalJSON = global.toJSON({ virtuals: true }); + globalJSON = JSON.stringify(globalJSON); + globalJSON = JSON.parse(globalJSON); + + if (globalJSON._id) { + delete globalJSON._id; + } + } else { + globalJSON = { globalType: slug }; } - if (locale && global.setLocale) { - global.setLocale(locale, null); - } - - const globalJSON = global.toJSON({ virtuals: true }); - - if (globalJSON._id) { - delete globalJSON._id; - } + const originalDoc = await this.performFieldOperations(globalConfig, { + depth, + req, + data: globalJSON, + hook: 'afterRead', + operation: 'update', + overrideAccess, + flattenLocales: true, + showHiddenFields, + }); let { data } = args; // ///////////////////////////////////// - // 3. Execute before validate collection hooks + // beforeValidate - Fields + // ///////////////////////////////////// + + data = await this.performFieldOperations(globalConfig, { + data, + req, + originalDoc, + hook: 'beforeValidate', + operation: 'update', + overrideAccess, + }); + + // ///////////////////////////////////// + // beforeValidate - Global // ///////////////////////////////////// await globalConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => { @@ -60,24 +78,12 @@ async function update(args) { data = (await hook({ data, req, - originalDoc: globalJSON, + originalDoc, })) || data; }, Promise.resolve()); // ///////////////////////////////////// - // 4. Execute field-level hooks, access, and validation - // ///////////////////////////////////// - - data = await this.performFieldOperations(globalConfig, { - data, - req, - hook: 'beforeChange', - operation: 'update', - originalDoc: global, - }); - - // ///////////////////////////////////// - // 5. Execute before global hook + // beforeChange - Global // ///////////////////////////////////// await globalConfig.hooks.beforeChange.reduce(async (priorHook, hook) => { @@ -86,32 +92,51 @@ async function update(args) { data = (await hook({ data, req, - originalDoc: global, + originalDoc, })) || data; }, Promise.resolve()); // ///////////////////////////////////// - // 6. Merge updates into existing data + // Merge updates into existing data // ///////////////////////////////////// - data = deepmerge(globalJSON, data, { arrayMerge: overwriteMerge }); + data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge }); // ///////////////////////////////////// - // 7. Perform database operation + // beforeChange - Fields // ///////////////////////////////////// - merge(global, data); + const result = await this.performFieldOperations(globalConfig, { + data, + req, + hook: 'beforeChange', + operation: 'update', + unflattenLocales: true, + originalDoc, + docWithLocales: globalJSON, + }); - await global.save(); + // ///////////////////////////////////// + // Update + // ///////////////////////////////////// - if (locale && global.setLocale) { - global.setLocale(locale, fallbackLocale); + if (global) { + global = await Model.findOneAndUpdate( + { globalType: slug }, + result, + { overwrite: true, new: true }, + ); + } else { + global = await Model.create(result); } global = global.toJSON({ virtuals: true }); + global = removeInternalFields(global); + global = JSON.stringify(global); + global = JSON.parse(global); // ///////////////////////////////////// - // 8. Execute field-level hooks and access + // afterRead - Fields // ///////////////////////////////////// global = await this.performFieldOperations(globalConfig, { @@ -121,10 +146,11 @@ async function update(args) { req, depth, showHiddenFields, + flattenLocales: true, }); // ///////////////////////////////////// - // 9. Execute after global hook + // afterRead - Global // ///////////////////////////////////// await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => { @@ -137,11 +163,9 @@ async function update(args) { }, Promise.resolve()); // ///////////////////////////////////// - // 10. Return global + // Return results // ///////////////////////////////////// - global = removeInternalFields(global); - return global; } diff --git a/src/localization/formatRefPathLocales.ts b/src/localization/formatRefPathLocales.ts deleted file mode 100644 index eeb6fb8154..0000000000 --- a/src/localization/formatRefPathLocales.ts +++ /dev/null @@ -1,50 +0,0 @@ -export default function formatRefPathLocales(schema, parentSchema?: any, parentPath?: string): void { - // Loop through all refPaths within schema - schema.eachPath((pathname, schemaType) => { - // If a dynamic refPath is found - if (schemaType.options.refPath && schemaType.options.refPath.includes('{{LOCALE}}') && parentSchema) { - // Create a clone of the schema for each locale - const newSchema = schema.clone(); - - // Remove the old pathname in order to rebuild it after it's formatted - newSchema.remove(pathname); - - // Get the locale from the parent path - let locale = parentPath; - - // Split the parent path and take only the last segment as locale - if (parentPath && parentPath.includes('.')) { - locale = parentPath.split('.').pop(); - } - - // Replace {{LOCALE}} appropriately - const refPath = schemaType.options.refPath.replace('{{LOCALE}}', locale); - - // Add new schemaType back to newly cloned schema - newSchema.add({ - [pathname]: { - ...schemaType.options, - refPath, - }, - }); - - // Removing and adding a path to a schema does not update tree, so do it manually - newSchema.tree[pathname].refPath = refPath; - - const parentSchemaType = parentSchema.path(parentPath).instance; - - // Remove old schema from parent - parentSchema.remove(parentPath); - - // Replace newly cloned and updated schema on parent - parentSchema.add({ - [parentPath]: parentSchemaType === 'Array' ? [newSchema] : newSchema, - }); - } - - // If nested schema found, continue recursively - if (schemaType.schema) { - formatRefPathLocales(schemaType.schema, schema, pathname); - } - }); -} diff --git a/src/localization/plugin.ts b/src/localization/plugin.ts deleted file mode 100644 index 32d9a1c626..0000000000 --- a/src/localization/plugin.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable func-names */ -/* eslint-disable no-param-reassign */ -/* eslint-disable no-restricted-syntax */ -import mongoose from 'mongoose'; -import sanitizeFallbackLocale from './sanitizeFallbackLocale'; -import formatRefPathLocales from './formatRefPathLocales'; - -export default function localizationPlugin(schema: any, options): void { - if (!options || !options.locales || !Array.isArray(options.locales) || !options.locales.length) { - throw new mongoose.Error('Required locales array is missing'); - } - - schema.eachPath((path, schemaType) => { - if (schemaType.schema) { // propagate plugin initialization for sub-documents schemas - schemaType.schema.plugin(localizationPlugin, options); - } - - if (!schemaType.options.localized && !(schemaType.schema && schemaType.schema.options.localized)) { - return; - } - - if (schemaType.options.unique) { - schemaType.options.sparse = true; - } - - const pathArray = path.split('.'); - const key = pathArray.pop(); - let 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 - const tree = pathArray.reduce((mem, part) => 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 - const owner = this.ownerDocument ? this.ownerDocument() : this; - const locale = owner.getLocale(); - const localeSubDoc = this.$__getValue(path); - - if (localeSubDoc === null || localeSubDoc === undefined) { - return localeSubDoc; - } - - const value = localeSubDoc[locale] || null; - - if (locale === 'all') { - return localeSubDoc; - } - - // If there is no value to return, AKA no translation in locale, handle fallbacks - if (!value) { - // If user specified fallback code as null, send back null - if (this.fallbackLocale === null || (this.fallbackLocale && !localeSubDoc[this.fallbackLocale])) { - return null; - - // If user specified fallback code AND record exists, return that - } if (localeSubDoc[this.fallbackLocale]) { - return localeSubDoc[this.fallbackLocale]; - - // Otherwise, check if there is a default fallback value and if so, send that - } if (options.fallback && localeSubDoc[options.defaultLocale]) { - return localeSubDoc[options.defaultLocale]; - } - } - - return value; - }) - .set(function (value) { - // embedded and sub-documents will use locale methods from the top level document - const owner = this.ownerDocument ? this.ownerDocument() : this; - const locale = owner.getLocale(); - - this.set(`${path}.${locale}`, value); - }); - - // localized option is not needed for the current path any more, - // and is unwanted for all child locale-properties - // delete schemaType.options.localized; // This was removed to allow viewing inside query parser - - const localizedObject = { - [key]: {}, - }; - - options.locales.forEach(function (locale) { - const localeOptions = { ...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; - } - - this[locale] = localeOptions; - }, localizedObject[key]); - - schema.add(localizedObject, prefix); - }); - - schema.eachPath((path, schemaType) => { - if (schemaType.schema && schemaType.options.localized && schemaType.schema.discriminators) { - Object.keys(schemaType.schema.discriminators).forEach((key) => { - if (schema.path(path)) { - schema.path(path).discriminator(key, schemaType.schema.discriminators[key]); - } - }); - } - }); - - // document methods to set the locale for each model instance (document) - schema.method({ - getLocales() { - return options.locales; - }, - getLocale() { - return this.docLocale || options.defaultLocale; - }, - setLocale(locale, fallbackLocale) { - const locales = [...this.getLocales(), 'all']; - if (locale && locales.indexOf(locale) !== -1) { - this.docLocale = locale; - } - - this.fallbackLocale = sanitizeFallbackLocale(fallbackLocale); - this.schema.eachPath((path, schemaType) => { - if (schemaType.options.type instanceof Array) { - if (this[path]) this[path].forEach((doc) => doc.setLocale && doc.setLocale(locale, this.fallbackLocale)); - } - - if (schemaType.options.ref && this[path]) { - if (this[path] && this[path].setLocale) this[path].setLocale(locale, this.fallbackLocale); - } - }); - }, - }); - - // Find any dynamic {{LOCALE}} in refPaths and modify schemas appropriately - formatRefPathLocales(schema); -} diff --git a/src/mongoose/buildSchema.ts b/src/mongoose/buildSchema.ts index 15659ea381..1b0dfc9508 100644 --- a/src/mongoose/buildSchema.ts +++ b/src/mongoose/buildSchema.ts @@ -1,9 +1,7 @@ /* eslint-disable no-use-before-define */ import { Schema, SchemaDefinition } from 'mongoose'; import { Config } from '../config/types'; -import { MissingFieldInputOptions } from '../errors'; import { ArrayField, Block, BlockField, Field, GroupField, RadioField, RelationshipField, RowField, SelectField, UploadField } from '../fields/config/types'; -import localizationPlugin from '../localization/plugin'; type FieldSchemaGenerator = (field: Field, fields: SchemaDefinition, config: Config) => SchemaDefinition; @@ -23,33 +21,30 @@ const setBlockDiscriminators = (fields: Field[], schema: Schema, config: Config) const blockSchema = new Schema(blockSchemaFields, { _id: false }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Possible incorrect typing in mongoose types, this works - schema.path(field.name).discriminator(blockItem.slug, blockSchema); - - if (config.localization) { - blockSchema.plugin(localizationPlugin, config.localization); + if (field.localized) { + config.localization.locales.forEach((locale) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Possible incorrect typing in mongoose types, this works + schema.path(`${field.name}.${locale}`).discriminator(blockItem.slug, blockSchema); + setBlockDiscriminators(blockItem.fields, blockSchema, config); + }); + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Possible incorrect typing in mongoose types, this works + schema.path(field.name).discriminator(blockItem.slug, blockSchema); + setBlockDiscriminators(blockItem.fields, blockSchema, config); } - - setBlockDiscriminators(blockItem.fields, blockSchema, config); }); } }); }; -const formatBaseSchema = (field: Field) => { - const createAccess = field.access && field.access.create; - - const condition = field.admin && field.admin.condition; - - return { - localized: field.localized || false, - unique: field.unique || false, - required: (field.required && !field.localized && !condition && !createAccess) || false, - default: field.defaultValue || undefined, - index: field.index || field.unique || false, - }; -}; +const formatBaseSchema = (field: Field) => ({ + unique: field.unique || false, + required: (field.required && !field.localized && !field?.admin?.condition && !field?.access?.create) || false, + default: field.defaultValue || undefined, + index: field.index || field.unique || false, +}); const buildSchema = (config: Config, configFields: Field[], options = {}): Schema => { let fields = {}; @@ -70,36 +65,134 @@ const buildSchema = (config: Config, configFields: Field[], options = {}): Schem }; const fieldToSchemaMap = { - number: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: Number }, - }), - text: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: String }, - }), - email: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: String }, - }), - textarea: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: String }, - }), - richText: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: Schema.Types.Mixed }, - }), - code: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: String }, - }), - radio: (field: RadioField, fields: SchemaDefinition) => { - if (!field.options || field.options.length === 0) { - throw new MissingFieldInputOptions(field); + number: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: Number }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; } - const schema = { + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + text: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + email: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + textarea: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + richText: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: Schema.Types.Mixed }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + code: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + radio: (field: RadioField, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String, enum: field.options.map((option) => { @@ -107,57 +200,146 @@ const fieldToSchemaMap = { return option; }), }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } return { ...fields, - [field.name]: schema, + [field.name]: schemaToReturn, }; }, - checkbox: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: Boolean }, - }), - date: (field: Field, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { ...formatBaseSchema(field), type: Date }, - }), - upload: (field: UploadField, fields: SchemaDefinition): SchemaDefinition => ({ - ...fields, - [field.name]: { + checkbox: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: Boolean }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + date: (field: Field, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: Date }; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, + }; + }, + upload: (field: UploadField, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: Schema.Types.ObjectId, ref: field.relationTo, - }, - }), - relationship: (field: RelationshipField, fields: SchemaDefinition) => { - let schema: { [key: string]: any } = {}; + }; - if (Array.isArray(field.relationTo)) { - schema._id = false; - schema.value = { - type: Schema.Types.ObjectId, - refPath: `${field.name}${field.localized ? '.{{LOCALE}}' : ''}.relationTo`, + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, }; - schema.relationTo = { type: String, enum: field.relationTo }; } else { - schema = { - ...formatBaseSchema(field), - }; - - schema.type = Schema.Types.ObjectId; - schema.ref = field.relationTo; - } - - if (field.hasMany) { - schema = { - type: [schema], - localized: field.localized || false, - }; + schemaToReturn = baseSchema; } return { ...fields, - [field.name]: schema, + [field.name]: schemaToReturn, + }; + }, + relationship: (field: RelationshipField, fields: SchemaDefinition, config: Config) => { + const hasManyRelations = Array.isArray(field.relationTo); + let schemaToReturn: { [key: string]: any } = {}; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((locales, locale) => { + let localeSchema: { [key: string]: any } = {}; + + if (hasManyRelations) { + localeSchema._id = false; + localeSchema.value = { + type: Schema.Types.ObjectId, + refPath: `${field.name}.${locale}.relationTo`, + }; + localeSchema.relationTo = { type: String, enum: field.relationTo }; + } else { + localeSchema = { + ...formatBaseSchema(field), + type: Schema.Types.ObjectId, + ref: field.relationTo, + }; + } + + return { + ...locales, + [locale]: field.hasMany ? [localeSchema] : localeSchema, + }; + }, {}), + localized: true, + }; + } else if (hasManyRelations) { + schemaToReturn._id = false; + schemaToReturn.value = { + type: Schema.Types.ObjectId, + refPath: `${field.name}.relationTo`, + }; + schemaToReturn.relationTo = { type: String, enum: field.relationTo }; + + if (field.hasMany) schemaToReturn = [schemaToReturn]; + } else { + schemaToReturn = { + ...formatBaseSchema(field), + type: Schema.Types.ObjectId, + ref: field.relationTo, + }; + + if (field.hasMany) schemaToReturn = [schemaToReturn]; + } + + return { + ...fields, + [field.name]: schemaToReturn, }; }, row: (field: RowField, fields: SchemaDefinition, config: Config): SchemaDefinition => { @@ -175,34 +357,58 @@ const fieldToSchemaMap = { return newFields; }, array: (field: ArrayField, fields: SchemaDefinition, config: Config) => { - const schema = buildSchema(config, field.fields, { _id: false, id: false }); + const baseSchema = { + ...formatBaseSchema(field), + type: [buildSchema(config, field.fields, { _id: false, id: false })], + }; + + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } return { ...fields, - [field.name]: { - ...formatBaseSchema(field), - type: [schema], - }, + [field.name]: schemaToReturn, }; }, group: (field: GroupField, fields: SchemaDefinition, config: Config): SchemaDefinition => { - const schema = buildSchema(config, field.fields, { _id: false, id: false }); + const baseSchema = { + ...formatBaseSchema(field), + required: field.fields.some((subField) => subField.required === true), + type: buildSchema(config, field.fields, { _id: false, id: false }), + }; + + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } return { ...fields, - [field.name]: { - ...formatBaseSchema(field), - required: field.fields.some((subField) => subField.required === true), - type: schema, - }, + [field.name]: schemaToReturn, }; }, - select: (field: SelectField, fields: SchemaDefinition) => { - if (!field.options || field.options.length === 0) { - throw new MissingFieldInputOptions(field); - } - - const schema = { + select: (field: SelectField, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = { ...formatBaseSchema(field), type: String, enum: field.options.map((option) => { @@ -211,20 +417,43 @@ const fieldToSchemaMap = { }), }; - return { - ...fields, - [field.name]: field.hasMany ? [schema] : schema, - }; - }, - blocks: (field: BlockField, fields: SchemaDefinition) => { - const blocksSchema = new Schema({ blockName: String }, { discriminatorKey: 'blockType', _id: false, id: false }); + let schemaToReturn; + + if (field.localized) { + schemaToReturn = { + type: config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}), + localized: true, + }; + } else { + schemaToReturn = baseSchema; + } + + if (field.hasMany) schemaToReturn = [schemaToReturn]; return { ...fields, - [field.name]: { - type: [blocksSchema], - localized: field.localized || false, - }, + [field.name]: schemaToReturn, + }; + }, + blocks: (field: BlockField, fields: SchemaDefinition, config: Config): SchemaDefinition => { + const baseSchema = [new Schema({ blockName: String }, { discriminatorKey: 'blockType', _id: false, id: false })]; + let schemaToReturn; + + if (field.localized) { + schemaToReturn = config.localization.locales.reduce((localeSchema, locale) => ({ + ...localeSchema, + [locale]: baseSchema, + }), {}); + } else { + schemaToReturn = baseSchema; + } + + return { + ...fields, + [field.name]: schemaToReturn, }; }, }; diff --git a/src/mongoose/connect.ts b/src/mongoose/connect.ts index 0cb83dea5f..531a2d1960 100644 --- a/src/mongoose/connect.ts +++ b/src/mongoose/connect.ts @@ -13,6 +13,7 @@ const connectMongoose = async (url: string, options: ConnectionOptions): Promise useUnifiedTopology: true, useCreateIndex: true, autoIndex: false, + useFindAndModify: false, }; if (process.env.NODE_ENV === 'test') { diff --git a/yarn.lock b/yarn.lock index c0566d9a4c..ee6bb962ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8222,11 +8222,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"