feat: detaches localization from mongoose entirely

This commit is contained in:
James
2021-02-17 22:31:53 -05:00
parent 8e022cd48c
commit 162ec74445
29 changed files with 603 additions and 539 deletions

View File

@@ -177,7 +177,7 @@ async function login(incomingArgs: Arguments): Promise<Result> {
hook: 'afterRead',
operation: 'login',
overrideAccess,
reduceLocales: true,
flattenLocales: true,
showHiddenFields,
});

View File

@@ -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;
};

View File

@@ -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<PayloadMongooseDocument>, PassportLocalModel<PayloadMongooseDocument> {
export interface CollectionModel extends PaginateModel<any>, PassportLocalModel<any> {
buildQuery: (query: unknown, locale?: string) => Record<string, unknown>
}

View File

@@ -31,7 +31,7 @@ export type Arguments = {
}
async function create(this: Payload, incomingArgs: Arguments): Promise<Document> {
const { performFieldOperations, config, emailOptions } = this;
const { config, emailOptions } = this;
let args = incomingArgs;
@@ -54,10 +54,6 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
config: collectionConfig,
},
req,
req: {
locale,
fallbackLocale,
},
disableVerificationEmail,
depth,
overrideAccess,
@@ -100,18 +96,6 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
})) || 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<Document>
}, 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<FileData> = {};
@@ -174,8 +170,8 @@ async function create(this: Payload, incomingArgs: Arguments): Promise<Document>
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<Document>
// 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<Document>
// 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<Document>
hook: 'afterRead',
operation: 'create',
overrideAccess,
reduceLocales: false,
flattenLocales: true,
showHiddenFields,
});

View File

@@ -94,10 +94,6 @@ async function deleteQuery(incomingArgs: Arguments): Promise<Document> {
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<Document> {
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<Document> {
hook: 'afterRead',
operation: 'delete',
overrideAccess,
reduceLocales: false,
flattenLocales: true,
showHiddenFields,
});

View File

@@ -148,7 +148,7 @@ async function find(incomingArgs: Arguments): Promise<PaginatedDocs> {
hook: 'afterRead',
operation: 'read',
overrideAccess,
reduceLocales: true,
flattenLocales: true,
showHiddenFields,
},
find,

View File

@@ -141,7 +141,7 @@ async function findByID(incomingArgs: Arguments): Promise<Document> {
operation: 'read',
currentDepth,
overrideAccess,
reduceLocales: true,
flattenLocales: true,
showHiddenFields,
});

View File

@@ -17,8 +17,8 @@ export default async function create(options: Options): Promise<Document> {
const {
collection: collectionSlug,
depth,
locale,
fallbackLocale,
locale = this?.config?.localization?.defaultLocale,
fallbackLocale = null,
data,
user,
overrideAccess = true,

View File

@@ -16,8 +16,8 @@ export default async function localDelete(options: Options): Promise<Document> {
collection: collectionSlug,
depth,
id,
locale,
fallbackLocale,
locale = this?.config?.localization?.defaultLocale,
fallbackLocale = null,
user,
overrideAccess = true,
showHiddenFields,

View File

@@ -22,8 +22,8 @@ export default async function find(options: Options): Promise<PaginatedDocs> {
page,
limit,
where,
locale,
fallbackLocale,
locale = this?.config?.localization?.defaultLocale,
fallbackLocale = null,
user,
overrideAccess = true,
showHiddenFields,

View File

@@ -17,8 +17,8 @@ export default async function findByID(options: Options): Promise<Document> {
collection: collectionSlug,
depth,
id,
locale,
fallbackLocale,
locale = this?.config?.localization?.defaultLocale,
fallbackLocale = null,
user,
overrideAccess = true,
disableErrors = false,

View File

@@ -18,8 +18,8 @@ export default async function update(options: Options): Promise<Document> {
const {
collection: collectionSlug,
depth,
locale,
fallbackLocale,
locale = this?.config?.localization?.defaultLocale,
fallbackLocale = null,
data,
id,
user,

View File

@@ -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<Document> {
req,
req: {
locale,
fallbackLocale,
},
overrideAccess,
showHiddenFields,
@@ -104,14 +102,20 @@ async function update(incomingArgs: Arguments): Promise<Document> {
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<Document> {
})) || 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<Document> {
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<Document> {
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<Document> {
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<Document> {
}) || 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
// /////////////////////////////////////

View File

@@ -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<string, unknown>
operation: Operation
hook: HookName
hook?: HookName
req: PayloadRequest
overrideAccess: boolean
reduceLocales?: boolean
flattenLocales?: boolean
unflattenLocales?: boolean
originalDoc?: Record<string, unknown>
docWithLocales?: Record<string, unknown>
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());

View File

@@ -11,7 +11,7 @@ type Arguments = {
data: Record<string, any>
originalDoc: Record<string, any>
path: string
reduceLocales: boolean
flattenLocales: boolean
locale: string
fallbackLocale: string
accessPromises: Promise<void>[]
@@ -30,6 +30,9 @@ type Arguments = {
errors: {message: string, field: string}[]
payload: Payload
showHiddenFields: boolean
unflattenLocales: boolean
unflattenLocaleActions: (() => void)[]
docWithLocales?: Record<string, any>
}
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<string, unknown>[]).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<string, unknown>,
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<string, unknown>[]).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) {

View File

@@ -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<any> | 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);
});

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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);
}
});
}

View File

@@ -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);
}

View File

@@ -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,
};
},
};

View File

@@ -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') {