diff --git a/docs/database/mongodb.mdx b/docs/database/mongodb.mdx index 28453d5fd4..9092a3c177 100644 --- a/docs/database/mongodb.mdx +++ b/docs/database/mongodb.mdx @@ -34,6 +34,7 @@ export default buildConfig({ |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. | | `schemaOptions` | Customize schema options for all Mongoose schemas created internally. | +| `jsonParse` | Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. | | `collections` | Options on a collection-by-collection basis. [More](#collections-options) | | `globals` | Options for the Globals collection created by Payload. [More](#globals-options) | | `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. | @@ -74,4 +75,28 @@ const db = mongooseAdapter({ ### Global Options -Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future. \ No newline at end of file +Payload automatically creates a single `globals` collection that correspond with any Payload globals that you define. When you initialize the `mongooseAdapter`, you can specify settings here for your globals in a similar manner to how you can for collections above. Right now, the only property available is `schemaOptions` but more may be added in the future. + +### Preserving externally managed data + +You can use Payload in conjunction with an existing MongoDB database, where you might have some fields "tracked" in Payload via corresponding field configs, and other fields completely unknown to Payload. + +If you have external field data in existing MongoDB collections which you'd like to use in combination with Payload, and you don't want to lose those external fields, you can configure Payload to "preserve" that data while it makes updates to your existing documents. + +To do this, the first step is to configure Mongoose's `strict` property, which tells Mongoose to write all data that it receives (and not disregard any data that it does not know about). + +The second step is to disable Payload's automatic JSON parsing of documents it receives from MongoDB. + +Here's an example for how to configure your Mongoose adapter to preserve external collection fields that are not tracked by Payload: + +```ts +mongooseAdapter({ + url: process.env.DATABASE_URI, + // Disable the JSON parsing that Payload performs + jsonParse: false, + // Disable strict mode for Mongoose + schemaOptions: { + strict: false, + }, +}) +``` \ No newline at end of file diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index c0f42863b5..1e5b12f66e 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -1,9 +1,10 @@ import type { Create } from 'payload/database' -import type { Document, PayloadRequest } from 'payload/types' +import type { PayloadRequest } from 'payload/types' import type { MongooseAdapter } from '.' import handleError from './utilities/handleError' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' import { withSession } from './withSession' export const create: Create = async function create( @@ -19,15 +20,13 @@ export const create: Create = async function create( handleError(error, req) } - // doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here - const result: Document = JSON.parse(JSON.stringify(doc)) + const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject() + const verificationToken = doc._verificationToken - // custom id type reset - result.id = result._id if (verificationToken) { result._verificationToken = verificationToken } - return result + return sanitizeInternalFields(result) } diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index 691598b2ac..4e36841970 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -19,10 +19,8 @@ export const createGlobal: CreateGlobal = async function createGlobal( let [result] = (await Model.create([global], options)) as any - result = JSON.parse(JSON.stringify(result)) + result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result.toObject() - // custom id type reset - result.id = result._id result = sanitizeInternalFields(result) return result diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index 32a03d38fa..9e8147b1be 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -1,9 +1,9 @@ import type { CreateGlobalVersion } from 'payload/database' import type { PayloadRequest } from 'payload/types' -import type { Document } from 'payload/types' import type { MongooseAdapter } from '.' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' import { withSession } from './withSession' export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( @@ -52,13 +52,12 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo options, ) - const result: Document = JSON.parse(JSON.stringify(doc)) + const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject() + const verificationToken = doc._verificationToken - // custom id type reset - result.id = result._id if (verificationToken) { result._verificationToken = verificationToken } - return result + return sanitizeInternalFields(result) } diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 163068f228..c48991f2ae 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,9 +1,9 @@ import type { CreateVersion } from 'payload/database' import type { PayloadRequest } from 'payload/types' -import type { Document } from 'payload/types' import type { MongooseAdapter } from '.' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' import { withSession } from './withSession' export const createVersion: CreateVersion = async function createVersion( @@ -60,13 +60,13 @@ export const createVersion: CreateVersion = async function createVersion( options, ) - const result: Document = JSON.parse(JSON.stringify(doc)) + const result = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc.toObject() + const verificationToken = doc._verificationToken - // custom id type reset - result.id = result._id if (verificationToken) { result._verificationToken = verificationToken } - return result + + return sanitizeInternalFields(result) } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 25845364f2..243c92d6d5 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,6 +1,5 @@ import type { DeleteOne } from 'payload/database' import type { PayloadRequest } from 'payload/types' -import type { Document } from 'payload/types' import type { MongooseAdapter } from '.' @@ -19,13 +18,9 @@ export const deleteOne: DeleteOne = async function deleteOne( where, }) - const doc = await Model.findOneAndDelete(query, options).lean() + let doc = await Model.findOneAndDelete(query, options).lean() - let result: Document = JSON.parse(JSON.stringify(doc)) + doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) - - return result + return sanitizeInternalFields(doc) } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 05e0410ee3..f76451fb9f 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -86,13 +86,12 @@ export const find: Find = async function find( } const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + + const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs return { ...result, docs: docs.map((doc) => { - // eslint-disable-next-line no-param-reassign - doc.id = doc._id return sanitizeInternalFields(doc) }), } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 9fe72d6d60..17cedd0b33 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -30,12 +30,16 @@ export const findGlobal: FindGlobal = async function findGlobal( if (!doc) { return null } + + if (this.jsonParse) { + doc = JSON.parse(JSON.stringify(doc)) + } + if (doc._id) { - doc.id = doc._id + doc.id = JSON.parse(JSON.stringify(doc._id)) delete doc._id } - doc = JSON.parse(JSON.stringify(doc)) doc = sanitizeInternalFields(doc) return doc diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index b8de110994..0ef2d9fc81 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -105,13 +105,12 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV } const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + + const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs return { ...result, docs: docs.map((doc) => { - // eslint-disable-next-line no-param-reassign - doc.id = doc._id return sanitizeInternalFields(doc) }), } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index a5235196c3..48aadbc959 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,7 +1,6 @@ import type { MongooseQueryOptions } from 'mongoose' import type { FindOne } from 'payload/database' import type { PayloadRequest } from 'payload/types' -import type { Document } from 'payload/types' import type { MongooseAdapter } from '.' @@ -24,17 +23,15 @@ export const findOne: FindOne = async function findOne( where, }) - const doc = await Model.findOne(query, {}, options) + let doc = await Model.findOne(query, {}, options) if (!doc) { return null } - let result: Document = JSON.parse(JSON.stringify(doc)) + doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + doc = sanitizeInternalFields(doc) - return result + return doc } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 8986f9c561..d424f6ea0b 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -101,13 +101,12 @@ export const findVersions: FindVersions = async function findVersions( } const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + + const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs return { ...result, docs: docs.map((doc) => { - // eslint-disable-next-line no-param-reassign - doc.id = doc._id return sanitizeInternalFields(doc) }), } diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index cde1bcbd28..a354309e88 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -56,6 +56,7 @@ export interface Args { /** Set false to disable $facet aggregation in non-supporting databases, Defaults to true */ useFacet?: boolean } + /** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */ disableIndexHints?: boolean /** Define Mongoose options for the globals collection. @@ -63,6 +64,8 @@ export interface Args { globals?: { schemaOptions?: SchemaOptions } + /** Set to false to disable the automatic JSON stringify/parse of data queried by MongoDB. For example, if you have data not tracked by Payload such as `Date` fields and similar, you can use this option to ensure that existing `Date` properties remain as `Date` and not strings. */ + jsonParse?: boolean migrationDir?: string /** Define default Mongoose schema options for all schemas created. */ @@ -87,6 +90,7 @@ export type MongooseAdapter = BaseDatabaseAdapter & globalsOptions: { schemaOptions?: SchemaOptions } + jsonParse: boolean mongoMemoryServer: any schemaOptions?: SchemaOptions sessions: Record @@ -114,6 +118,7 @@ declare module 'payload' { globalsOptions: { schemaOptions?: SchemaOptions } + jsonParse: boolean mongoMemoryServer: any schemaOptions?: SchemaOptions @@ -131,6 +136,7 @@ export function mongooseAdapter({ connectOptions, disableIndexHints = false, globals, + jsonParse = true, migrationDir: migrationDirArg, schemaOptions, transactionOptions = {}, @@ -153,6 +159,7 @@ export function mongooseAdapter({ disableIndexHints, globals: undefined, globalsOptions: globals || {}, + jsonParse, mongoMemoryServer: undefined, schemaOptions: schemaOptions || {}, sessions: {}, diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index bac8c91f4f..a324792109 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -87,12 +87,14 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( } const result = await VersionModel.paginate(versionQuery, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + + const docs = this.jsonParse ? JSON.parse(JSON.stringify(result.docs)) : result.docs return { ...result, docs: docs.map((doc) => { // eslint-disable-next-line no-param-reassign + doc = { _id: doc.parent, id: doc.parent, diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index d21d8d0d1d..361483e034 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -21,10 +21,8 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( result = await Model.findOneAndUpdate({ globalType: slug }, data, options) - result = JSON.parse(JSON.stringify(result)) + result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result - // custom id type reset - result.id = result._id result = sanitizeInternalFields(result) return result diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index ee421c5937..64fd7ccf63 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -3,6 +3,7 @@ import type { PayloadRequest, TypeWithID } from 'payload/types' import type { MongooseAdapter } from '.' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' import { withSession } from './withSession' export async function updateGlobalVersion( @@ -30,16 +31,14 @@ export async function updateGlobalVersion( where: whereToUse, }) - const doc = await VersionModel.findOneAndUpdate(query, versionData, options) + let doc = await VersionModel.findOneAndUpdate(query, versionData, options) - const result = JSON.parse(JSON.stringify(doc)) + doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc const verificationToken = doc._verificationToken - // custom id type reset - result.id = result._id if (verificationToken) { - result._verificationToken = verificationToken + doc._verificationToken = verificationToken } - return result + return sanitizeInternalFields(doc) } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 370ad11dc6..e4efe0a45d 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -32,9 +32,7 @@ export const updateOne: UpdateOne = async function updateOne( handleError(error, req) } - result = JSON.parse(JSON.stringify(result)) - result.id = result._id - result = sanitizeInternalFields(result) + result = this.jsonParse ? JSON.parse(JSON.stringify(result)) : result - return result + return sanitizeInternalFields(result) } diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index f1b8377c10..4225576b5b 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -3,6 +3,7 @@ import type { PayloadRequest } from 'payload/types' import type { MongooseAdapter } from '.' +import sanitizeInternalFields from './utilities/sanitizeInternalFields' import { withSession } from './withSession' export const updateVersion: UpdateVersion = async function updateVersion( @@ -23,16 +24,14 @@ export const updateVersion: UpdateVersion = async function updateVersion( where: whereToUse, }) - const doc = await VersionModel.findOneAndUpdate(query, versionData, options) - - const result = JSON.parse(JSON.stringify(doc)) + let doc = await VersionModel.findOneAndUpdate(query, versionData, options) const verificationToken = doc._verificationToken - // custom id type reset - result.id = result._id + doc = this.jsonParse ? JSON.parse(JSON.stringify(doc)) : doc + if (verificationToken) { - result._verificationToken = verificationToken + doc._verificationToken = verificationToken } - return result + return sanitizeInternalFields(doc) } diff --git a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts index 6877cde862..9c97c40dd5 100644 --- a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts +++ b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts @@ -1,11 +1,13 @@ const internalFields = ['__v'] -const sanitizeInternalFields = >(incomingDoc: T): T => - Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => { - if (key === '_id') { +const sanitizeInternalFields = >(incomingDoc: T): T => { + const id = incomingDoc._id ? JSON.parse(JSON.stringify(incomingDoc._id)) : incomingDoc.id + + return Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => { + if (key === '_id' || key === 'id') { return { ...newDoc, - id: val, + id, } } @@ -18,5 +20,6 @@ const sanitizeInternalFields = >(incomingDoc: [key]: val, } }, {} as T) +} export default sanitizeInternalFields diff --git a/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts b/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts index bed84ec034..6461e9eca7 100644 --- a/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts +++ b/packages/payload/src/fields/hooks/beforeChange/cloneDataFromOriginalDoc.ts @@ -11,6 +11,10 @@ export const cloneDataFromOriginalDoc = (originalDocData: unknown): unknown => { }) } + if (originalDocData instanceof Date) { + return originalDocData + } + if (typeof originalDocData === 'object' && originalDocData !== null) { return { ...originalDocData } } diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index f7e1dc24e2..b3de04323c 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -209,6 +209,10 @@ export const checkbox: Validate = ( } export const date: Validate = (value, { required, t }) => { + if (value instanceof Date) { + return true + } + if (value && !isNaN(Date.parse(value.toString()))) { /* eslint-disable-line */ return true diff --git a/test/collections-graphql/media/test-image-1.jpg b/test/collections-graphql/media/test-image-1.jpg new file mode 100644 index 0000000000..3a0303dcf8 Binary files /dev/null and b/test/collections-graphql/media/test-image-1.jpg differ diff --git a/test/collections-graphql/media/test-image-2.jpg b/test/collections-graphql/media/test-image-2.jpg new file mode 100644 index 0000000000..3a0303dcf8 Binary files /dev/null and b/test/collections-graphql/media/test-image-2.jpg differ diff --git a/test/collections-graphql/media/test-image-3.jpg b/test/collections-graphql/media/test-image-3.jpg new file mode 100644 index 0000000000..3a0303dcf8 Binary files /dev/null and b/test/collections-graphql/media/test-image-3.jpg differ diff --git a/test/database/config.ts b/test/database/config.ts index 2f438154ea..ac572fbc3e 100644 --- a/test/database/config.ts +++ b/test/database/config.ts @@ -187,6 +187,8 @@ const createDatabaseTestConfig = async () => { configWithDefaults.db = mongooseAdapter({ migrationDir, url: 'mongodb://127.0.0.1/payloadtests', + // Disable JSON parsing to retain existing data shape(s) + jsonParse: false, // Disable strict mode for Mongoose schemaOptions: { strict: false, diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts index d3437287cd..a9b77b94cc 100644 --- a/test/database/int.spec.ts +++ b/test/database/int.spec.ts @@ -353,39 +353,45 @@ describe('database', () => { if (payload.db.name === 'mongoose') { const Model = payload.db.collections['custom-schema'] - const [doc] = await Model.create([ - { - text: 'hello', - localizedText: { - en: 'goodbye', + const [doc] = await Model.create( + [ + { + text: 'hello', + localizedText: { + en: 'goodbye', + }, + noFieldDefined: 'hi', + isoDate: new Date('2024-07-24T12:00:00Z'), + array: [ + { + id: uuid(), + noFieldDefined: 'hi', + isoDate: new Date('2024-07-24T12:00:00Z'), + text: 'hello', + localizedText: { + en: 'goodbye', + }, + }, + ], + blocks: [ + { + id: uuid(), + blockType: 'block', + noFieldDefined: 'hi', + isoDate: new Date('2024-07-24T12:00:00Z'), + text: 'hello', + localizedText: { + en: 'goodbye', + }, + }, + ], }, - noFieldDefined: 'hi', - array: [ - { - id: uuid(), - noFieldDefined: 'hi', - text: 'hello', - localizedText: { - en: 'goodbye', - }, - }, - ], - blocks: [ - { - id: uuid(), - blockType: 'block', - noFieldDefined: 'hi', - text: 'hello', - localizedText: { - en: 'goodbye', - }, - }, - ], - }, - ]) + ], + { lean: true }, + ) - const result = JSON.parse(JSON.stringify(doc)) - result.id = result._id + const result = doc.toObject() + result.id = result._id.toString() existingDataDoc = result } }) @@ -406,6 +412,10 @@ describe('database', () => { expect(docWithExistingData.noFieldDefined).toStrictEqual('hi') expect(docWithExistingData.array[0].noFieldDefined).toStrictEqual('hi') expect(docWithExistingData.blocks[0].noFieldDefined).toStrictEqual('hi') + + expect(docWithExistingData.isoDate instanceof Date).toBeTruthy() + expect(docWithExistingData.array[0].isoDate instanceof Date).toBeTruthy() + expect(docWithExistingData.blocks[0].isoDate instanceof Date).toBeTruthy() } }) @@ -447,6 +457,10 @@ describe('database', () => { expect(result.noFieldDefined).toStrictEqual('hi') expect(result.array[0].noFieldDefined).toStrictEqual('hi') expect(result.blocks[0].noFieldDefined).toStrictEqual('hi') + + expect(result.isoDate instanceof Date).toBeTruthy() + expect(result.array[0].isoDate instanceof Date).toBeTruthy() + expect(result.blocks[0].isoDate instanceof Date).toBeTruthy() } }) }) diff --git a/test/uploads/focal-only/focal-2-400x300.png b/test/uploads/focal-only/focal-2-400x300.png new file mode 100644 index 0000000000..96ad250801 Binary files /dev/null and b/test/uploads/focal-only/focal-2-400x300.png differ diff --git a/test/uploads/focal-only/focal-2-600x300.png b/test/uploads/focal-only/focal-2-600x300.png new file mode 100644 index 0000000000..386f9946a6 Binary files /dev/null and b/test/uploads/focal-only/focal-2-600x300.png differ diff --git a/test/uploads/focal-only/focal-2-900x300.png b/test/uploads/focal-only/focal-2-900x300.png new file mode 100644 index 0000000000..f0d5e648f4 Binary files /dev/null and b/test/uploads/focal-only/focal-2-900x300.png differ diff --git a/test/uploads/focal-only/focal-2.png b/test/uploads/focal-only/focal-2.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-2.png differ diff --git a/test/uploads/focal-only/focal-3-400x300.png b/test/uploads/focal-only/focal-3-400x300.png new file mode 100644 index 0000000000..749d3a83f6 Binary files /dev/null and b/test/uploads/focal-only/focal-3-400x300.png differ diff --git a/test/uploads/focal-only/focal-3-600x300.png b/test/uploads/focal-only/focal-3-600x300.png new file mode 100644 index 0000000000..f215c9cca8 Binary files /dev/null and b/test/uploads/focal-only/focal-3-600x300.png differ diff --git a/test/uploads/focal-only/focal-3-900x300.png b/test/uploads/focal-only/focal-3-900x300.png new file mode 100644 index 0000000000..4c8d2fe642 Binary files /dev/null and b/test/uploads/focal-only/focal-3-900x300.png differ diff --git a/test/uploads/focal-only/focal-3.png b/test/uploads/focal-only/focal-3.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-3.png differ diff --git a/test/uploads/focal-only/focal-4-400x300.png b/test/uploads/focal-only/focal-4-400x300.png new file mode 100644 index 0000000000..96ad250801 Binary files /dev/null and b/test/uploads/focal-only/focal-4-400x300.png differ diff --git a/test/uploads/focal-only/focal-4-600x300.png b/test/uploads/focal-only/focal-4-600x300.png new file mode 100644 index 0000000000..386f9946a6 Binary files /dev/null and b/test/uploads/focal-only/focal-4-600x300.png differ diff --git a/test/uploads/focal-only/focal-4-900x300.png b/test/uploads/focal-only/focal-4-900x300.png new file mode 100644 index 0000000000..f0d5e648f4 Binary files /dev/null and b/test/uploads/focal-only/focal-4-900x300.png differ diff --git a/test/uploads/focal-only/focal-4.png b/test/uploads/focal-only/focal-4.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-4.png differ diff --git a/test/uploads/focal-only/focal-5-400x300.png b/test/uploads/focal-only/focal-5-400x300.png new file mode 100644 index 0000000000..749d3a83f6 Binary files /dev/null and b/test/uploads/focal-only/focal-5-400x300.png differ diff --git a/test/uploads/focal-only/focal-5-600x300.png b/test/uploads/focal-only/focal-5-600x300.png new file mode 100644 index 0000000000..f215c9cca8 Binary files /dev/null and b/test/uploads/focal-only/focal-5-600x300.png differ diff --git a/test/uploads/focal-only/focal-5-900x300.png b/test/uploads/focal-only/focal-5-900x300.png new file mode 100644 index 0000000000..4c8d2fe642 Binary files /dev/null and b/test/uploads/focal-only/focal-5-900x300.png differ diff --git a/test/uploads/focal-only/focal-5.png b/test/uploads/focal-only/focal-5.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-5.png differ diff --git a/test/uploads/focal-only/focal-6-400x300.png b/test/uploads/focal-only/focal-6-400x300.png new file mode 100644 index 0000000000..96ad250801 Binary files /dev/null and b/test/uploads/focal-only/focal-6-400x300.png differ diff --git a/test/uploads/focal-only/focal-6-600x300.png b/test/uploads/focal-only/focal-6-600x300.png new file mode 100644 index 0000000000..386f9946a6 Binary files /dev/null and b/test/uploads/focal-only/focal-6-600x300.png differ diff --git a/test/uploads/focal-only/focal-6-900x300.png b/test/uploads/focal-only/focal-6-900x300.png new file mode 100644 index 0000000000..f0d5e648f4 Binary files /dev/null and b/test/uploads/focal-only/focal-6-900x300.png differ diff --git a/test/uploads/focal-only/focal-6.png b/test/uploads/focal-only/focal-6.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-6.png differ diff --git a/test/uploads/focal-only/focal-7-400x300.png b/test/uploads/focal-only/focal-7-400x300.png new file mode 100644 index 0000000000..749d3a83f6 Binary files /dev/null and b/test/uploads/focal-only/focal-7-400x300.png differ diff --git a/test/uploads/focal-only/focal-7-600x300.png b/test/uploads/focal-only/focal-7-600x300.png new file mode 100644 index 0000000000..f215c9cca8 Binary files /dev/null and b/test/uploads/focal-only/focal-7-600x300.png differ diff --git a/test/uploads/focal-only/focal-7-900x300.png b/test/uploads/focal-only/focal-7-900x300.png new file mode 100644 index 0000000000..4c8d2fe642 Binary files /dev/null and b/test/uploads/focal-only/focal-7-900x300.png differ diff --git a/test/uploads/focal-only/focal-7.png b/test/uploads/focal-only/focal-7.png new file mode 100644 index 0000000000..1949a7c4ea Binary files /dev/null and b/test/uploads/focal-only/focal-7.png differ