This commit is contained in:
James
2020-11-23 19:37:44 -05:00
33 changed files with 638 additions and 139 deletions

View File

@@ -1,11 +1,12 @@
import crypto from 'crypto';
import { AfterForgotPasswordHook, BeforeOperationHook } from '../../collections/config/types';
import { APIError } from '../../errors';
async function forgotPassword(incomingArgs) {
const { config, sendEmail: email } = this;
if (!Object.prototype.hasOwnProperty.call(incomingArgs.data, 'email')) {
throw new APIError('Missing email.');
throw new APIError('Missing email.', 400);
}
let args = incomingArgs;
@@ -14,7 +15,7 @@ async function forgotPassword(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({
@@ -38,7 +39,7 @@ async function forgotPassword(incomingArgs) {
// Forget password
// /////////////////////////////////////
let token = crypto.randomBytes(20);
let token: string | Buffer = crypto.randomBytes(20);
token = token.toString('hex');
const user = await Model.findOne({ email: data.email.toLowerCase() });
@@ -90,7 +91,7 @@ async function forgotPassword(incomingArgs) {
// afterForgotPassword - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterForgotPassword.reduce(async (priorHook, hook) => {
await collectionConfig.hooks.afterForgotPassword.reduce(async (priorHook: AfterForgotPasswordHook, hook: AfterForgotPasswordHook) => {
await priorHook;
await hook({ args });
}, Promise.resolve());

View File

@@ -3,6 +3,7 @@ import { AuthenticationError, LockedAuth } from '../../errors';
import getCookieExpiration from '../../utilities/getCookieExpiration';
import isLocked from '../isLocked';
import removeInternalFields from '../../utilities/removeInternalFields';
import { BeforeLoginHook, BeforeOperationHook } from '../../collections/config/types';
async function login(incomingArgs) {
const { config, operations, secret } = this;
@@ -13,7 +14,7 @@ async function login(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({

View File

@@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken';
import { BeforeOperationHook } from '../../collections/config/types';
import { Forbidden } from '../../errors';
import getCookieExpiration from '../../utilities/getCookieExpiration';
@@ -9,7 +10,7 @@ async function refresh(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({

View File

@@ -1,14 +1,9 @@
import { Access, Hook } from '../../config/types';
/* eslint-disable no-use-before-define */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Access } from '../../config/types';
import { Field } from '../../fields/config/types';
import { PayloadRequest } from '../../express/types/payloadRequest';
export type ImageSize = {
name: string,
width: number,
height: number,
crop: string, // comes from sharp package
};
export type Collection = {
slug: string;
labels: {
@@ -22,18 +17,17 @@ export type Collection = {
components: any;
};
hooks: {
beforeOperation: Hook[];
beforeValidate: Hook[];
beforeChange: Hook[];
afterChange: Hook[];
beforeRead: Hook[];
afterRead: Hook[];
beforeDelete: Hook[];
afterDelete?: Hook[];
beforeLogin?: Hook[];
afterLogin?: Hook[];
afterForgotPassword?: Hook[];
forgotPassword?: Hook[];
beforeOperation?: BeforeOperationHook[];
beforeValidate?: BeforeValidateHook[];
beforeChange?: BeforeChangeHook[];
afterChange?: AfterChangeHook[];
beforeRead?: BeforeReadHook[];
afterRead?: AfterReadHook[];
beforeDelete?: BeforeDeleteHook[];
afterDelete?: AfterDeleteHook[];
beforeLogin?: BeforeLoginHook[];
afterLogin?: AfterLoginHook[];
afterForgotPassword?: AfterForgotPasswordHook[];
};
access: {
create: Access;
@@ -70,3 +64,83 @@ export type Collection = {
adminThumbnail?: string;
};
};
export type ImageSize = {
name: string,
width: number,
height: number,
crop: string, // comes from sharp package
};
// Hooks
export type HookOperationType =
| 'create'
| 'read'
| 'update'
| 'delete'
| 'refresh'
| 'login'
| 'forgotPassword';
export type BeforeOperationHook = (args?: {
args?: any;
operation: HookOperationType;
}) => any;
export type BeforeValidateHook = (args?: {
data?: any;
req?: PayloadRequest;
operation: 'create' | 'update';
originalDoc?: any; // undefined on 'create' operation
}) => any;
export type BeforeChangeHook = (args?: {
data: any;
req: PayloadRequest;
operation: 'create' | 'update'
originalDoc?: any; // undefined on 'create' operation
}) => any;
export type AfterChangeHook = (args?: {
doc: any;
req: PayloadRequest;
operation: 'create' | 'update';
}) => any;
export type BeforeReadHook = (args?: {
doc: any;
req: PayloadRequest;
query: { [key: string]: any };
}) => any;
export type AfterReadHook = (args?: {
doc: any;
req: PayloadRequest;
query: { [key: string]: any };
}) => any;
export type BeforeDeleteHook = (args?: {
req: PayloadRequest;
id: string;
}) => any;
export type AfterDeleteHook = (args?: {
req: PayloadRequest;
id: string;
doc: any;
}) => any;
export type BeforeLoginHook = (args?: {
req: PayloadRequest;
}) => any;
export type AfterLoginHook = (args?: {
req: PayloadRequest;
user: any;
token: string;
}) => any;
export type AfterForgotPasswordHook = (args?: {
args?: any;
}) => any;

View File

@@ -2,13 +2,17 @@ import mongoose from 'mongoose';
import express from 'express';
import passport from 'passport';
import passportLocalMongoose from 'passport-local-mongoose';
const LocalStrategy = require('passport-local').Strategy;
import Passport from 'passport-local';
import { UpdateQuery } from 'mongodb';
import apiKeyStrategy from '../auth/strategies/apiKey';
import buildSchema from './buildSchema';
import bindCollectionMiddleware from './bindCollection';
import { Collection } from './config/types';
function registerCollections() {
this.config.collections = this.config.collections.map((collection) => {
const LocalStrategy = Passport.Strategy;
export default function registerCollections(): void {
this.config.collections = this.config.collections.map((collection: Collection) => {
const formattedCollection = collection;
const schema = buildSchema(formattedCollection, this.config);
@@ -32,7 +36,8 @@ function registerCollections() {
}, cb);
}
const updates = { $inc: { loginAttempts: 1 } };
type LoginSchema = { loginAttempts: number; };
const updates: UpdateQuery<LoginSchema> = { $inc: { loginAttempts: 1 } };
// Lock the account if at max attempts and not already locked
if (this.loginAttempts + 1 >= maxLoginAttempts && !this.isLocked) {
updates.$set = { lockUntil: Date.now() + lockTime };
@@ -151,5 +156,3 @@ function registerCollections() {
return formattedCollection;
});
}
export default registerCollections;

View File

@@ -9,9 +9,11 @@ import { MissingFile, FileUploadError } from '../../errors';
import resizeAndSave from '../../uploads/imageResizer';
import getSafeFilename from '../../uploads/getSafeFilename';
import getImageSize from '../../uploads/getImageSize';
import imageMIMETypes from '../../uploads/imageMIMETypes';
import isImage from '../../uploads/isImage';
import { FileData } from '../../uploads/types';
import sendVerificationEmail from '../../auth/sendVerificationEmail';
import { AfterChangeHook, BeforeOperationHook, BeforeValidateHook } from '../config/types';
async function create(incomingArgs) {
const { performFieldOperations, config } = this;
@@ -22,7 +24,7 @@ async function create(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({
@@ -73,7 +75,7 @@ async function create(incomingArgs) {
// beforeValidate - Collections
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook: BeforeValidateHook, hook: BeforeValidateHook) => {
await priorHook;
data = (await hook({
@@ -113,8 +115,9 @@ async function create(incomingArgs) {
// Upload and resize potential files
// /////////////////////////////////////
if (collectionConfig.upload) {
const fileData = {};
const fileData: Partial<FileData> = {};
const { staticDir, imageSizes } = collectionConfig.upload;
@@ -137,7 +140,7 @@ async function create(incomingArgs) {
try {
await file.mv(`${staticPath}/${fsSafeName}`);
if (imageMIMETypes.indexOf(file.mimetype) > -1) {
if (isImage(file.mimetype)) {
const dimensions = await getImageSize(`${staticPath}/${fsSafeName}`);
fileData.width = dimensions.width;
fileData.height = dimensions.height;
@@ -148,7 +151,7 @@ async function create(incomingArgs) {
}
} catch (err) {
console.error(err);
throw new FileUploadError(err);
throw new FileUploadError();
}
@@ -214,7 +217,7 @@ async function create(incomingArgs) {
// afterChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await collectionConfig.hooks.afterChange.reduce(async (priorHook: AfterChangeHook, hook: AfterChangeHook) => {
await priorHook;
result = await hook({

View File

@@ -5,6 +5,7 @@ import removeInternalFields from '../../utilities/removeInternalFields';
import { NotFound, Forbidden, ErrorDeletingFile } from '../../errors';
import executeAccess from '../../auth/executeAccess';
import fileExists from '../../uploads/fileExists';
import { BeforeOperationHook } from '../config/types';
async function deleteQuery(incomingArgs) {
let args = incomingArgs;
@@ -13,7 +14,7 @@ async function deleteQuery(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({

View File

@@ -1,5 +1,6 @@
import executeAccess from '../../auth/executeAccess';
import removeInternalFields from '../../utilities/removeInternalFields';
import { BeforeOperationHook, BeforeReadHook } from '../config/types';
async function find(incomingArgs) {
let args = incomingArgs;
@@ -8,7 +9,7 @@ async function find(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-underscore-dangle */
import memoize from 'micro-memoize';
import { BeforeOperationHook } from '../config/types';
/* eslint-disable no-underscore-dangle */
import removeInternalFields from '../../utilities/removeInternalFields';
import { Forbidden, NotFound } from '../../errors';
import executeAccess from '../../auth/executeAccess';
@@ -11,7 +12,7 @@ async function findByID(incomingArgs) {
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({

View File

@@ -1,25 +1,30 @@
import httpStatus from 'http-status';
import deepmerge from 'deepmerge';
import path from 'path';
import { BeforeOperationHook, BeforeChangeHook, BeforeValidateHook } from '../config/types';
import removeInternalFields from '../../utilities/removeInternalFields';
import overwriteMerge from '../../utilities/overwriteMerge';
import executeAccess from '../../auth/executeAccess';
import { NotFound, Forbidden, APIError, FileUploadError } from '../../errors';
import imageMIMETypes from '../../uploads/imageMIMETypes';
import isImage from '../../uploads/isImage';
import getImageSize from '../../uploads/getImageSize';
import getSafeFilename from '../../uploads/getSafeFilename';
import resizeAndSave from '../../uploads/imageResizer';
import { FileData } from '../../uploads/types';
async function update(incomingArgs) {
const { performFieldOperations, config } = this;
let args = incomingArgs;
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook: BeforeOperationHook, hook: BeforeOperationHook) => {
await priorHook;
args = (await hook({
@@ -59,7 +64,7 @@ async function update(incomingArgs) {
// Retrieve document
// /////////////////////////////////////
const queryToBuild = {
const queryToBuild: { [key: string]: any } = {
where: {
and: [
{
@@ -97,7 +102,7 @@ async function update(incomingArgs) {
// beforeValidate - Fields
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data = await performFieldOperations(collectionConfig, {
data,
req,
id,
@@ -111,7 +116,7 @@ async function update(incomingArgs) {
// beforeValidate - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook: BeforeValidateHook, hook: BeforeValidateHook) => {
await priorHook;
data = (await hook({
@@ -126,7 +131,7 @@ async function update(incomingArgs) {
// beforeChange - Fields
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data = await performFieldOperations(collectionConfig, {
data,
req,
id,
@@ -140,7 +145,7 @@ async function update(incomingArgs) {
// beforeChange - Collection
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await collectionConfig.hooks.beforeChange.reduce(async (priorHook: BeforeChangeHook, hook: BeforeChangeHook) => {
await priorHook;
data = (await hook({
@@ -162,14 +167,14 @@ async function update(incomingArgs) {
// /////////////////////////////////////
if (collectionConfig.upload) {
const fileData = {};
const fileData: Partial<FileData> = {};
const { staticDir, imageSizes } = collectionConfig.upload;
let staticPath = staticDir;
if (staticDir.indexOf('/') !== 0) {
staticPath = path.join(this.config.paths.configDir, staticDir);
staticPath = path.join(config.paths.configDir, staticDir);
}
const file = (req.files && req.files.file) ? req.files.file : req.fileData;
@@ -184,7 +189,7 @@ async function update(incomingArgs) {
fileData.filesize = file.size;
fileData.mimeType = file.mimetype;
if (imageMIMETypes.indexOf(file.mimetype) > -1) {
if (isImage(file.mimetype)) {
const dimensions = await getImageSize(`${staticPath}/${fsSafeName}`);
fileData.width = dimensions.width;
fileData.height = dimensions.height;
@@ -194,10 +199,9 @@ async function update(incomingArgs) {
}
}
} catch (err) {
throw new FileUploadError(err);
throw new FileUploadError();
}
data = {
...data,
...fileData,
@@ -240,7 +244,7 @@ async function update(incomingArgs) {
// afterChange - Fields
// /////////////////////////////////////
doc = await this.performFieldOperations(collectionConfig, {
doc = await performFieldOperations(collectionConfig, {
data: doc,
hook: 'afterChange',
operation: 'update',
@@ -269,7 +273,7 @@ async function update(incomingArgs) {
// afterRead - Fields
// /////////////////////////////////////
doc = await this.performFieldOperations(collectionConfig, {
doc = await performFieldOperations(collectionConfig, {
depth,
req,
data: doc,

View File

@@ -2,8 +2,14 @@ import { PayloadConfig, Config } from './types';
import sanitize from './sanitize';
import validate from './validate';
/**
* @description Builds and validates Payload configuration
* @param config Payload Config
* @returns Built and sanitized Payload Config
*/
export function buildConfig(config: PayloadConfig): Config {
const validatedConfig = validate(config);
const sanitizedConfig = sanitize(validatedConfig);
return sanitizedConfig;
const validated = validate(config);
const sanitized = sanitize(validated);
return sanitized;
}

View File

@@ -43,18 +43,18 @@ export type MockEmailCredentials = {
web: string;
};
export type Hook = (...args: any[]) => any | void;
export type Access = (args?: any) => boolean;
export type PayloadConfig = {
admin?: {
user?: string
user?: string;
meta?: {
titleSuffix?: string
ogImage?: string
favicon?: string
titleSuffix?: string;
ogImage?: string;
favicon?: string;
}
disable?: boolean
disable?: boolean;
indexHTML?: string;
};
collections?: Collection[];
globals?: Global[];
@@ -69,7 +69,7 @@ export type PayloadConfig = {
graphQL?: string;
graphQLPlayground?: string;
};
express: {
express?: {
json: {
limit?: number
}

View File

@@ -29,7 +29,7 @@ class APIError extends ExtendableError {
* @param {object} data - response data to be returned.
* @param {boolean} isPublic - Whether the message should be visible to user or not.
*/
constructor(message: string, status: number = httpStatus.INTERNAL_SERVER_ERROR, data: any, isPublic = false) {
constructor(message: string, status: number = httpStatus.INTERNAL_SERVER_ERROR, data: any = null, isPublic = false) {
super(message, status, data, isPublic);
}
}

View File

@@ -1,7 +1,8 @@
import { Field } from '../fields/config/types';
import APIError from './APIError';
class InvalidFieldRelationship extends APIError {
constructor(field, relationship) {
constructor(field: Field, relationship: string) {
super(`Field ${field.label} has invalid relationship '${relationship}'.`);
}
}

View File

@@ -1,7 +1,9 @@
import httpStatus from 'http-status';
import { Response, NextFunction } from 'express';
import formatErrorResponse from '../responses/formatError';
import { PayloadRequest } from '../types/payloadRequest';
const errorHandler = (config, logger) => async (err, req, res, next) => {
const errorHandler = (config, logger) => async (err, req: PayloadRequest, res: Response): Promise<void> => {
const data = formatErrorResponse(err);
let response;
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR;

View File

@@ -9,8 +9,9 @@ import rateLimit from 'express-rate-limit';
import localizationMiddleware from '../../localization/middleware';
import authenticate from './authenticate';
import identifyAPI from './identifyAPI';
import { Payload } from '../..';
const middleware = (payload) => {
const middleware = (payload: Payload) => {
const rateLimitOptions = {
windowMs: payload.config.rateLimit.window,
max: payload.config.rateLimit.max,

View File

@@ -9,7 +9,8 @@ export type FieldHook = (args: {
originalDoc?: any,
data?: any,
operation?: 'create' | 'update',
req?: PayloadRequest}) => Promise<any> | any;
req?: PayloadRequest
}) => Promise<any> | any;
type FieldBase = {
name: string;
@@ -136,4 +137,20 @@ export type BlockField = FieldBase & {
blocks?: Block[];
};
export type Field = NumberField | TextField | EmailField | TextareaField | CodeField | CheckboxField | DateField | BlockField | RadioField | RelationshipField | ArrayField | RichTextField | GroupField | RowField | SelectField | SelectManyField | UploadField;
export type Field = NumberField
| TextField
| EmailField
| TextareaField
| CodeField
| CheckboxField
| DateField
| BlockField
| RadioField
| RelationshipField
| ArrayField
| RichTextField
| GroupField
| RowField
| SelectField
| SelectManyField
| UploadField;

View File

@@ -4,8 +4,7 @@ import traverseFields from './traverseFields';
import { Collection } from '../collections/config/types';
import { OperationArguments } from '../types';
export default async function performFieldOperations(entityConfig: Collection, args: OperationArguments) {
export default async function performFieldOperations(entityConfig: Collection, args: OperationArguments): any {
const {
data: fullData,
originalDoc: fullOriginalDoc,
@@ -29,7 +28,7 @@ export default async function performFieldOperations(entityConfig: Collection, a
let depth = 0;
if (payloadAPI === 'REST' || payloadAPI === 'local') {
depth = (args.depth || args.depth === 0) ? parseInt(args.depth, 10) : this.config.defaultDepth;
depth = (args.depth || args.depth === 0) ? parseInt(String(args.depth), 10) : this.config.defaultDepth;
if (depth > this.config.maxDepth) depth = this.config.maxDepth;
}

View File

@@ -2,7 +2,40 @@ import defaultRichTextValue from './richText/defaultValue';
const defaultMessage = 'This field is required.';
export const number = (value, options = {}) => {
type NumberOptions = {
required?: boolean;
min?: number;
max?: number;
}
type FieldOptions = {
required?: boolean;
minLength?: number;
maxLength?: number;
}
type RowOptions = {
required?: boolean;
minRows?: number;
maxRows?: number;
}
type SelectAndRadioOptions = {
required?: boolean;
options?: {
value: string
}[];
}
type RequiredOption = { required?: boolean };
type Validator = (value?: any, options?: NumberOptions |
FieldOptions |
RequiredOption |
RowOptions |
SelectAndRadioOptions) => string | boolean;
export const number: Validator = (value: string, options: NumberOptions = {}) => {
const parsedValue = parseInt(value, 10);
if ((value && typeof parsedValue !== 'number') || (options.required && Number.isNaN(parsedValue))) {
@@ -24,7 +57,7 @@ export const number = (value, options = {}) => {
return true;
};
export const text = (value, options = {}) => {
export const text: Validator = (value, options: FieldOptions = {}) => {
if (options.maxLength && (value && value.length > options.maxLength)) {
return `This value must be shorter than the max length of ${options.max} characters.`;
}
@@ -42,13 +75,13 @@ export const text = (value, options = {}) => {
return true;
};
export const password = (value, options = {}) => {
export const password: Validator = (value, options: FieldOptions = {}) => {
if (options.maxLength && value.length > options.maxLength) {
return `This value must be shorter than the max length of ${options.max} characters.`;
return `This value must be shorter than the max length of ${options.maxLength} characters.`;
}
if (options.minLength && value.length < options.minLength) {
return `This value must be longer than the minimum length of ${options.max} characters.`;
return `This value must be longer than the minimum length of ${options.maxLength} characters.`;
}
if (options.required && !value) {
@@ -58,7 +91,7 @@ export const password = (value, options = {}) => {
return true;
};
export const email = (value, options = {}) => {
export const email: Validator = (value, options: FieldOptions = {}) => {
if ((value && !/\S+@\S+\.\S+/.test(value))
|| (!value && options.required)) {
return 'Please enter a valid email address.';
@@ -67,13 +100,13 @@ export const email = (value, options = {}) => {
return true;
};
export const textarea = (value, options = {}) => {
export const textarea: Validator = (value, options: FieldOptions = {}) => {
if (options.maxLength && value.length > options.maxLength) {
return `This value must be shorter than the max length of ${options.max} characters.`;
return `This value must be shorter than the max length of ${options.maxLength} characters.`;
}
if (options.minLength && value.length < options.minLength) {
return `This value must be longer than the minimum length of ${options.max} characters.`;
return `This value must be longer than the minimum length of ${options.maxLength} characters.`;
}
if (options.required && !value) {
@@ -83,7 +116,7 @@ export const textarea = (value, options = {}) => {
return true;
};
export const wysiwyg = (value, options = {}) => {
export const wysiwyg: Validator = (value, options: RequiredOption = {}) => {
if (options.required && !value) {
return defaultMessage;
}
@@ -91,7 +124,7 @@ export const wysiwyg = (value, options = {}) => {
return true;
};
export const code = (value, options = {}) => {
export const code: Validator = (value, options: RequiredOption = {}) => {
if (options.required && value === undefined) {
return defaultMessage;
}
@@ -99,7 +132,7 @@ export const code = (value, options = {}) => {
return true;
};
export const richText = (value, options) => {
export const richText: Validator = (value, options: RequiredOption = {}) => {
if (options.required) {
const stringifiedDefaultValue = JSON.stringify(defaultRichTextValue);
if (value && JSON.stringify(value) !== stringifiedDefaultValue) return true;
@@ -109,7 +142,7 @@ export const richText = (value, options) => {
return true;
};
export const checkbox = (value, options = {}) => {
export const checkbox: Validator = (value, options: RequiredOption = {}) => {
if ((value && typeof value !== 'boolean')
|| (options.required && typeof value !== 'boolean')) {
return 'This field can only be equal to true or false.';
@@ -118,7 +151,7 @@ export const checkbox = (value, options = {}) => {
return true;
};
export const date = (value, options = {}) => {
export const date: Validator = (value, options: RequiredOption = {}) => {
if (value && !isNaN(Date.parse(value.toString()))) { /* eslint-disable-line */
return true;
}
@@ -134,17 +167,17 @@ export const date = (value, options = {}) => {
return true;
};
export const upload = (value, options = {}) => {
export const upload: Validator = (value, options: RequiredOption = {}) => {
if (value || !options.required) return true;
return defaultMessage;
};
export const relationship = (value, options = {}) => {
export const relationship: Validator = (value, options = {}) => {
if (value || !options.required) return true;
return defaultMessage;
};
export const array = (value, options = {}) => {
export const array: Validator = (value, options: RowOptions = {}) => {
if (options.minRows && value < options.minRows) {
return `This field requires at least ${options.minRows} row(s).`;
}
@@ -160,7 +193,7 @@ export const array = (value, options = {}) => {
return true;
};
export const select = (value, options = {}) => {
export const select: Validator = (value, options: SelectAndRadioOptions = {}) => {
if (Array.isArray(value) && value.find((input) => !options.options.find((option) => (option === input || option.value === input)))) {
return 'This field has an invalid selection';
}
@@ -176,13 +209,13 @@ export const select = (value, options = {}) => {
return true;
};
export const radio = (value, options = {}) => {
export const radio: Validator = (value, options: SelectAndRadioOptions = {}) => {
const stringValue = String(value);
if ((typeof value !== 'undefined' || !options.required) && (options.options.find((option) => String(option.value) === stringValue))) return true;
return defaultMessage;
};
export const blocks = (value, options) => {
export const blocks: Validator = (value, options: RowOptions = {}) => {
if (options.minRows && value < options.minRows) {
return `This field requires at least ${options.minRows} row(s).`;
}

View File

@@ -1,8 +1,9 @@
import mongoose from 'mongoose';
import buildSchema from '../mongoose/buildSchema';
import localizationPlugin from '../localization/plugin';
import { Config } from '../config/types';
const buildModel = (config) => {
const buildModel = (config: Config): mongoose.PaginateModel<any> | null => {
if (config.globals && config.globals.length > 0) {
const globalsSchema = new mongoose.Schema({}, { discriminatorKey: 'globalType', timestamps: true });

View File

@@ -1,7 +1,7 @@
import express from 'express';
import buildModel from './buildModel';
function initGlobals() {
export default function initGlobals(): void {
if (this.config.globals) {
this.globals = {
Model: buildModel(this.config),
@@ -23,5 +23,3 @@ function initGlobals() {
}
}
}
export default initGlobals;

View File

@@ -2,6 +2,7 @@ import deepmerge from 'deepmerge';
import overwriteMerge from '../../utilities/overwriteMerge';
import executeAccess from '../../auth/executeAccess';
import removeInternalFields from '../../utilities/removeInternalFields';
import { AfterChangeHook, BeforeValidateHook } from '../../collections/config/types';
async function update(args) {
const { globals: { Model } } = this;
@@ -53,7 +54,7 @@ async function update(args) {
// 3. Execute before validate collection hooks
// /////////////////////////////////////
await globalConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await globalConfig.hooks.beforeValidate.reduce(async (priorHook: BeforeValidateHook, hook: BeforeValidateHook) => {
await priorHook;
data = (await hook({
@@ -123,7 +124,7 @@ async function update(args) {
// 9. Execute after global hook
// /////////////////////////////////////
await globalConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await globalConfig.hooks.afterChange.reduce(async (priorHook: AfterChangeHook, hook: AfterChangeHook) => {
await priorHook;
global = await hook({

View File

@@ -18,6 +18,7 @@ import {
FindByIDOptions,
UpdateOptions,
DeleteOptions,
FindResponse,
} from './types';
import Logger, { PayloadLogger } from './utilities/logger';
import bindOperations from './init/bindOperations';
@@ -46,6 +47,9 @@ import { PayloadRequest } from './express/types/payloadRequest';
require('isomorphic-fetch');
/**
* @description Payload
*/
export class Payload {
config: Config;
@@ -90,7 +94,11 @@ export class Payload {
performFieldOperations: typeof performFieldOperations;
// requestHandlers: { collections: { create: any; find: any; findByID: any; update: any; delete: any; auth: { access: any; forgotPassword: any; init: any; login: any; logout: any; me: any; refresh: any; registerFirstUser: any; resetPassword: any; verifyEmail: any; unlock: any; }; }; globals: { ...; }; };
init(options: InitOptions) {
/**
* @description Initializes Payload
* @param options
*/
init(options: InitOptions): void {
this.logger = Logger();
this.logger.info('Starting Payload...');
@@ -217,13 +225,23 @@ export class Payload {
return email.account;
}
/**
* @description Performs create operation
* @param options
* @returns created document
*/
async create(options: CreateOptions): Promise<any> {
let { create } = localOperations;
create = create.bind(this);
return create(options);
}
async find(options: FindOptions): Promise<any> {
/**
* @description Find documents with criteria
* @param options
* @returns documents satisfying query
*/
async find(options: FindOptions): Promise<FindResponse> {
let { find } = localOperations;
find = find.bind(this);
return find(options);
@@ -241,12 +259,22 @@ export class Payload {
return update(options);
}
/**
* @description Find document by ID
* @param options
* @returns document with specified ID
*/
async findByID(options: FindByIDOptions): Promise<any> {
let { findByID } = localOperations;
findByID = findByID.bind(this);
return findByID(options);
}
/**
* @description Update document
* @param options
* @returns Updated document
*/
async update(options: UpdateOptions): Promise<any> {
let { update } = localOperations;
update = update.bind(this);

View File

@@ -0,0 +1,263 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { BeforeOperationHook, BeforeValidateHook, HookOperationType } from './collections/config/types';
import { buildConfig } from './config/build';
import { Field, Block, BlockField, RadioField, ArrayField, RichTextField, GroupField, SelectField, SelectManyField, UploadField, RelationshipField } from './fields/config/types';
const cfg = buildConfig({
serverURL: 'localhost:3000',
admin: {
disable: true,
},
});
const beforeOpHook: BeforeOperationHook = ({ args, operation }) => {
if (operation === 'create' && args.req.query && typeof args.req.query.checkout !== 'undefined') {
return {
...args,
disableVerificationEmail: true,
};
}
return args;
};
const beforeOpHookResult = beforeOpHook({ args: {}, operation: 'create' });
const beforeValidate: BeforeValidateHook = ({ data, req, operation, originalDoc }) => {
if (operation === 'create') {
const formattedData = { ...data };
const { user } = req;
}
};
const TextField: Field = {
name: 'text',
type: 'text',
label: 'Text',
required: true,
defaultValue: 'Default Value',
unique: true,
access: {
read: ({ req: { user } }) => Boolean(user),
},
};
const NumbersBlock: Block = {
slug: 'number',
labels: {
singular: 'Number',
plural: 'Numbers',
},
fields: [
{
name: 'testNumber',
label: 'Test Number Field',
type: 'number',
maxLength: 100,
required: true,
},
],
};
const CTA: Block = {
slug: 'cta',
labels: {
singular: 'Call to Action',
plural: 'Calls to Action',
},
fields: [
{
name: 'label',
label: 'Label',
type: 'text',
maxLength: 100,
required: true,
},
{
name: 'url',
label: 'URL',
type: 'text',
height: 100,
required: true,
},
],
};
const blockField: BlockField = {
name: 'blocks',
type: 'blocks',
label: 'Blocks Content',
minRows: 2,
blocks: [NumbersBlock, CTA],
localized: true,
required: true,
};
const radioGroup: RadioField = {
name: 'radioGroupExample',
label: 'Radio Group Example',
type: 'radio',
options: [{
value: 'option-1',
label: 'Options 1 Label',
}, {
value: 'option-2',
label: 'Option 2 Label',
}, {
value: 'option-3',
label: 'Option 3 Label',
}],
defaultValue: 'option-2',
required: true,
admin: {
readOnly: true,
},
};
const arrayField: ArrayField = {
type: 'array',
label: 'Array',
name: 'array',
minRows: 2,
maxRows: 4,
fields: [
// {
// type: 'row',
// fields: [
// {
// name: 'arrayText1',
// label: 'Array Text 1',
// type: 'text',
// },
// {
// name: 'arrayText2',
// label: 'Array Text 2',
// type: 'text',
// required: true,
// },
// ],
// },
{
type: 'text',
name: 'arrayText3',
label: 'Array Text 3',
admin: {
readOnly: true,
},
},
{
name: 'checkbox',
label: 'Checkbox',
type: 'checkbox',
},
],
};
const richTextField: RichTextField = {
name: 'richText',
type: 'richText',
label: 'Rich Text',
required: true,
admin: {
elements: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'blockquote',
'ul',
'ol',
'link',
],
leaves: [
'bold',
'italic',
'underline',
'strikethrough',
],
},
};
const groupField: GroupField = {
name: 'group',
label: 'Group',
type: 'group',
fields: [
{
type: 'text',
name: 'nestedGroupCustomField',
label: 'Nested Group Custom Field',
},
],
};
console.log(groupField);
const selectField: SelectField = {
name: 'select',
label: 'Select',
type: 'select',
options: [{
value: 'option-1',
label: 'Option 1 Label',
}, {
value: 'option-2',
label: 'Option 2 Label',
}, {
value: 'option-3',
label: 'Option 3 Label',
}, {
value: 'option-4',
label: 'Option 4 Label',
}],
defaultValue: 'option-1',
required: true,
};
const selectMany: SelectManyField = {
name: 'selectMany',
label: 'Select w/ hasMany',
type: 'select',
options: [{
value: 'option-1',
label: 'Option 1 Label',
}, {
value: 'option-2',
label: 'Option 2 Label',
}, {
value: 'option-3',
label: 'Option 3 Label',
}, {
value: 'option-4',
label: 'Option 4 Label',
}],
defaultValue: 'option-1',
required: true,
hasMany: true,
};
const upload: UploadField = {
name: 'image',
type: 'upload',
label: 'Image',
relationTo: 'media',
};
const rel1: RelationshipField = {
type: 'relationship',
label: 'Relationship to One Collection',
name: 'relationship',
relationTo: 'conditions',
hasMany: false,
};
const rel2: RelationshipField = {
type: 'relationship',
label: 'Relationship hasMany',
name: 'relationshipHasMany',
relationTo: ['localized-posts', 'sdf'],
hasMany: true,
};

View File

@@ -15,6 +15,8 @@ export type CreateOptions = {
export type FindOptions = {
collection: string;
where?: { [key: string]: any };
depth?: number;
limit?: number;
};
export type FindResponse = {

View File

@@ -1,9 +1,14 @@
import fs from 'fs';
import probeImageSize from 'probe-image-size';
const getImageSize = async (path) => {
type ProbedImageSize = {
width: number,
height: number,
type: string,
mime: string,
}
export default async function (path: string): Promise<ProbedImageSize> {
const image = fs.createReadStream(path);
return probeImageSize(image);
};
export default getImageSize;
}

View File

@@ -1,7 +1,7 @@
import imageMIMETypes from './imageMIMETypes';
import isImage from './isImage';
const getThumbnail = (mimeType, staticURL, filename, sizes, adminThumbnail) => {
if (imageMIMETypes.indexOf(mimeType) > -1) {
const getThumbnail = (mimeType, staticURL, filename, sizes, adminThumbnail): string | boolean => {
if (isImage(mimeType)) {
if (sizes?.[adminThumbnail]?.filename) {
return `${staticURL}/${sizes[adminThumbnail].filename}`;
}

View File

@@ -1 +0,0 @@
export default ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'];

View File

@@ -3,6 +3,8 @@ import sharp from 'sharp';
import sanitize from 'sanitize-filename';
import getImageSize from './getImageSize';
import fileExists from './fileExists';
import { Collection } from '../collections/config/types';
import { FileSizes } from './types';
function getOutputImage(sourceImage, size) {
const extension = sourceImage.split('.').pop();
@@ -16,15 +18,20 @@ function getOutputImage(sourceImage, size) {
};
}
export default async function resizeAndSave(staticPath, config, savedFilename, mimeType) {
/**
* Resize images according to image desired width and height and return sizes
* @param config Object
* @param uploadConfig Object
* @param savedFilename String
* @returns String[]
*/
/**
* @description
* @param staticPath Path to save images
* @param config Payload config
* @param savedFilename
* @param mimeType
* @returns image sizes keyed to strings
*/
export default async function resizeAndSave(
staticPath: string,
config: Collection,
savedFilename: string,
mimeType: string,
): Promise<FileSizes> {
const { imageSizes } = config.upload;
const sourceImage = `${staticPath}/${savedFilename}`;
@@ -69,4 +76,4 @@ export default async function resizeAndSave(staticPath, config, savedFilename, m
filesize: size.filesize,
},
}), {});
};
}

3
src/uploads/isImage.ts Normal file
View File

@@ -0,0 +1,3 @@
export default function isImage(mimeType: string): boolean {
return ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].indexOf(mimeType) > -1;
}

20
src/uploads/types.ts Normal file
View File

@@ -0,0 +1,20 @@
export type FileSizes = {
[size: string]: {
filename: string;
filesize: number;
mimeType: string;
name: string;
width: number;
height: number;
crop: string;
}
}
export type FileData = {
filename: string;
filesize: number;
mimeType: string;
width: number;
height: number;
sizes: FileSizes;
};