diff --git a/packages/payload/src/auth/strategies/local/register.ts b/packages/payload/src/auth/strategies/local/register.ts index ef381ce5f8..edf769b20f 100644 --- a/packages/payload/src/auth/strategies/local/register.ts +++ b/packages/payload/src/auth/strategies/local/register.ts @@ -41,7 +41,7 @@ export const registerLocalStrategy = async ({ const sanitizedDoc = { ...doc } if (sanitizedDoc.password) delete sanitizedDoc.password - return payload.db.create({ + const dbArgs = { collection: collection.slug, data: { ...sanitizedDoc, @@ -49,5 +49,10 @@ export const registerLocalStrategy = async ({ salt, }, req, - }) + } + if (collection?.db?.create) { + return collection.db.create(dbArgs) + } else { + return payload.db.create(dbArgs) + } } diff --git a/packages/payload/src/collections/config/schema.ts b/packages/payload/src/collections/config/schema.ts index dccd688038..719e1df7ba 100644 --- a/packages/payload/src/collections/config/schema.ts +++ b/packages/payload/src/collections/config/schema.ts @@ -118,6 +118,7 @@ const collectionSchema = joi.object().keys({ joi.boolean(), ), custom: joi.object().pattern(joi.string(), joi.any()), + db: joi.object(), dbName: joi.alternatives().try(joi.string(), joi.func()), defaultSort: joi.string(), endpoints: endpointsSchema, diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index abe12bef05..d3ce40a0eb 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -3,7 +3,7 @@ import type { Response } from 'express' import type { GraphQLInputObjectType, GraphQLNonNull, GraphQLObjectType } from 'graphql' import type { DeepRequired } from 'ts-essentials' -import type { GeneratedTypes } from '../../' +import type { DatabaseAdapter, GeneratedTypes } from '../../' import type { CustomPreviewButtonProps, CustomPublishButtonType, @@ -383,6 +383,14 @@ export type CollectionConfig = { auth?: IncomingAuthType | boolean /** Extension point to add your custom data. */ custom?: Record + + /** + * Add a custom database adapter to this collection. + */ + db?: Pick< + DatabaseAdapter, + 'create' | 'deleteMany' | 'deleteOne' | 'find' | 'findOne' | 'updateOne' + > /** * Used to override the default naming of the database table or collection with your using a function or string * @WARNING: If you change this property with existing data, you will need to handle the renaming of the table in your database or by using migrations diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index 83d98d7a7e..6c324b5268 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -242,11 +242,16 @@ async function create( req, }) } else { - doc = await payload.db.create({ + const dbArgs = { collection: collectionConfig.slug, data: resultWithLocales, req, - }) + } + if (collectionConfig?.db?.create) { + doc = await collectionConfig.db.create(dbArgs) + } else { + doc = await payload.db.create(dbArgs) + } } const verificationToken = doc._verificationToken diff --git a/packages/payload/src/collections/operations/delete.ts b/packages/payload/src/collections/operations/delete.ts index 24b0a38df7..2f4c026f5d 100644 --- a/packages/payload/src/collections/operations/delete.ts +++ b/packages/payload/src/collections/operations/delete.ts @@ -104,12 +104,20 @@ async function deleteOperation({ + const dbArgs = { collection: collectionConfig.slug, locale, req, where: fullWhere, - }) + } + let docs + if (collectionConfig?.db?.find) { + const result = await collectionConfig.db.find(dbArgs) + docs = result.docs + } else { + const result = await payload.db.find(dbArgs) + docs = result.docs + } const errors = [] @@ -160,7 +168,7 @@ async function deleteOperation( // ///////////////////////////////////// // Retrieve document // ///////////////////////////////////// - - const docToDelete = await req.payload.db.findOne({ + let docToDelete: Document + const dbArgs = { collection: collectionConfig.slug, locale: req.locale, req, where: combineQueries({ id: { equals: id } }, accessResults), - }) + } + + if (collectionConfig?.db?.findOne) { + docToDelete = await collectionConfig.db.findOne(dbArgs) + } else { + docToDelete = await req.payload.db.findOne(dbArgs) + } if (!docToDelete && !hasWhereAccess) throw new NotFound(t) if (!docToDelete && hasWhereAccess) throw new Forbidden(t) @@ -132,11 +138,17 @@ async function deleteByID( // Delete document // ///////////////////////////////////// - let result = await req.payload.db.deleteOne({ + let result + const deleteOneArgs = { collection: collectionConfig.slug, req, where: { id: { equals: id } }, - }) + } + if (collectionConfig?.db?.deleteOne) { + result = await collectionConfig?.db.deleteOne(deleteOneArgs) + } else { + result = await payload.db.deleteOne(deleteOneArgs) + } // ///////////////////////////////////// // Delete Preferences diff --git a/packages/payload/src/collections/operations/find.ts b/packages/payload/src/collections/operations/find.ts index 44497cac6b..185d830cf0 100644 --- a/packages/payload/src/collections/operations/find.ts +++ b/packages/payload/src/collections/operations/find.ts @@ -142,7 +142,7 @@ async function find>( where, }) - result = await payload.db.find({ + const dbArgs = { collection: collectionConfig.slug, limit: sanitizedLimit, locale, @@ -151,7 +151,13 @@ async function find>( req, sort, where: fullWhere, - }) + } + + if (collectionConfig?.db?.find) { + result = await collectionConfig.db.find(dbArgs) + } else { + result = await payload.db.find(dbArgs) + } } // ///////////////////////////////////// diff --git a/packages/payload/src/collections/operations/findByID.ts b/packages/payload/src/collections/operations/findByID.ts index 764cfb636e..adf53e3e69 100644 --- a/packages/payload/src/collections/operations/findByID.ts +++ b/packages/payload/src/collections/operations/findByID.ts @@ -87,7 +87,12 @@ async function findByID(incomingArgs: Arguments): Promise< if (!findOneArgs.where.and[0].id) throw new NotFound(t) - let result: T = await req.payload.db.findOne(findOneArgs) + let result: T + if (collectionConfig?.db?.findOne) { + result = await collectionConfig.db.findOne(findOneArgs) + } else { + result = await req.payload.db.findOne(findOneArgs) + } if (!result) { if (!disableErrors) { diff --git a/packages/payload/src/collections/operations/restoreVersion.ts b/packages/payload/src/collections/operations/restoreVersion.ts index 16e218261f..cb355a4ac7 100644 --- a/packages/payload/src/collections/operations/restoreVersion.ts +++ b/packages/payload/src/collections/operations/restoreVersion.ts @@ -85,7 +85,12 @@ async function restoreVersion(args: Arguments): Prom where: combineQueries({ id: { equals: parentDocID } }, accessResults), } - const doc = await req.payload.db.findOne(findOneArgs) + let doc: T + if (collectionConfig?.db?.findOne) { + doc = await collectionConfig.db.findOne(findOneArgs) + } else { + doc = await req.payload.db.findOne(findOneArgs) + } if (!doc && !hasWherePolicy) throw new NotFound(t) if (!doc && hasWherePolicy) throw new Forbidden(t) @@ -106,12 +111,18 @@ async function restoreVersion(args: Arguments): Prom // Update // ///////////////////////////////////// - let result = await req.payload.db.updateOne({ + const restoreVersionArgs = { id: parentDocID, collection: collectionConfig.slug, data: rawVersion.version, req, - }) + } + let result + if (collectionConfig?.db?.updateOne) { + result = await collectionConfig.db.updateOne(restoreVersionArgs) + } else { + result = await req.payload.db.updateOne(restoreVersionArgs) + } // ///////////////////////////////////// // Save `previousDoc` as a version after restoring diff --git a/packages/payload/src/collections/operations/update.ts b/packages/payload/src/collections/operations/update.ts index 3520442a70..ca067bbfb7 100644 --- a/packages/payload/src/collections/operations/update.ts +++ b/packages/payload/src/collections/operations/update.ts @@ -137,14 +137,21 @@ async function update( docs = query.docs } else { - const query = await payload.db.find({ + const dbArgs = { collection: collectionConfig.slug, limit: 0, locale, pagination: false, req, where: fullWhere, - }) + } + + let query + if (collectionConfig?.db?.find) { + query = await collectionConfig.db.find(dbArgs) + } else { + query = await payload.db.find(dbArgs) + } docs = query.docs } @@ -282,13 +289,18 @@ async function update( // ///////////////////////////////////// if (!shouldSaveDraft || data._status === 'published') { - result = await req.payload.db.updateOne({ + const dbArgs = { id, collection: collectionConfig.slug, data: result, locale, req, - }) + } + if (collectionConfig?.db?.updateOne) { + result = await collectionConfig.db.updateOne(dbArgs) + } else { + result = await req.payload.db.updateOne(dbArgs) + } } // ///////////////////////////////////// diff --git a/packages/payload/src/collections/operations/updateByID.ts b/packages/payload/src/collections/operations/updateByID.ts index 9502098bca..c8739bb24b 100644 --- a/packages/payload/src/collections/operations/updateByID.ts +++ b/packages/payload/src/collections/operations/updateByID.ts @@ -270,13 +270,18 @@ async function updateByID( // ///////////////////////////////////// if (!shouldSaveDraft || data._status === 'published') { - result = await req.payload.db.updateOne({ + const dbArgs = { id, collection: collectionConfig.slug, data: dataToUpdate, locale, req, - }) + } + if (collectionConfig?.db?.updateOne) { + result = await collectionConfig.db.updateOne(dbArgs) + } else { + result = await req.payload.db.updateOne(dbArgs) + } } // ///////////////////////////////////// diff --git a/packages/payload/src/versions/getLatestCollectionVersion.ts b/packages/payload/src/versions/getLatestCollectionVersion.ts index d0e883f1d8..90a8fcd1ae 100644 --- a/packages/payload/src/versions/getLatestCollectionVersion.ts +++ b/packages/payload/src/versions/getLatestCollectionVersion.ts @@ -23,7 +23,9 @@ export const getLatestCollectionVersion = async ({ }: Args): Promise => { let latestVersion: TypeWithVersion - if (config.versions?.drafts) { + const hasConfigDb = Object.keys(config?.db ? config?.db : {}).length > 0 + + if (config.versions?.drafts && !hasConfigDb) { const { docs } = await payload.db.findVersions({ collection: config.slug, limit: 1, @@ -35,7 +37,12 @@ export const getLatestCollectionVersion = async ({ ;[latestVersion] = docs } - const doc = await payload.db.findOne({ ...query, req }) + let doc + if (config?.db?.findOne) { + doc = await config.db.findOne({ ...query, req }) + } else { + doc = await payload.db.findOne({ ...query, req }) + } if (!latestVersion || (docHasTimestamps(doc) && latestVersion.updatedAt < doc.updatedAt)) { return doc diff --git a/test/collections-db/config.ts b/test/collections-db/config.ts new file mode 100644 index 0000000000..f635941e4c --- /dev/null +++ b/test/collections-db/config.ts @@ -0,0 +1,68 @@ +import type { CollectionConfig } from '../../packages/payload/types' + +import { buildConfigWithDefaults } from '../buildConfigWithDefaults' +import { devUser } from '../credentials' + +export const doc = { + id: -1, + customData: true, +} +export const docs = [doc] + +const collectionWithDb = (collectionSlug: string): CollectionConfig => { + return { + slug: collectionSlug, + db: { + // @ts-expect-error + create: () => { + return doc + }, + // @ts-expect-error + deleteOne: () => { + return docs + }, + // Only used in deleteUserPreferences on user collections + // @ts-expect-error + deleteMany: () => { + return docs + }, + // @ts-expect-error + find: () => { + return { docs } + }, + // @ts-expect-error + findOne: () => { + return doc + }, + // @ts-expect-error + updateOne: () => { + return { ...doc, updated: true } + }, + }, + fields: [ + { + name: 'name', + type: 'text', + }, + ], + } +} + +export const collectionSlug = 'collection-db' +export default buildConfigWithDefaults({ + // @ts-expect-error + collections: [collectionWithDb(collectionSlug)], + graphQL: { + schemaOutputFile: './test/collections-db/schema.graphql', + }, + + onInit: async (payload) => { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }) + }, +}) diff --git a/test/collections-db/int.spec.ts b/test/collections-db/int.spec.ts new file mode 100644 index 0000000000..3d334c5d9c --- /dev/null +++ b/test/collections-db/int.spec.ts @@ -0,0 +1,110 @@ +import payload from '../../packages/payload/src' +import { devUser } from '../credentials' +import { initPayloadTest } from '../helpers/configHelpers' +import { collectionSlug } from './config' +import { doc } from './config' + +require('isomorphic-fetch') + +let apiUrl +let jwt + +const headers = { + 'Content-Type': 'application/json', +} +const { email, password } = devUser + +describe('Collection Database Operations', () => { + // --__--__--__--__--__--__--__--__--__ + // Boilerplate test setup/teardown + // --__--__--__--__--__--__--__--__--__ + beforeAll(async () => { + const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }) + apiUrl = `${serverURL}/api` + + const response = await fetch(`${apiUrl}/users/login`, { + body: JSON.stringify({ + email, + password, + }), + headers, + method: 'POST', + }) + + const data = await response.json() + jwt = data.token + }) + + afterAll(async () => { + if (typeof payload.db.destroy === 'function') { + await payload.db.destroy(payload) + } + }) + + // --__--__--__--__--__--__--__--__--__ + // Local API + // --__--__--__--__--__--__--__--__--__ + + it('collection DB Create', async () => { + const result = await payload.create({ + collection: collectionSlug, + data: { + id: doc.id, + }, + }) + + expect(result.id).toEqual(doc.id) + expect(result.customData).toEqual(doc.customData) + }) + + it('collection DB Update', async () => { + const where = { id: { equals: doc.id } } + const result = await payload.update({ + collection: collectionSlug, + where, + data: { + id: doc.id, + }, + }) + + expect(result.docs[0].id).toEqual(doc.id) + expect(result.docs[0].customData).toEqual(doc.customData) + expect(result.docs[0].updated).toEqual(true) + }) + + it('collection DB Find', async () => { + const where = { id: { equals: doc.id } } + const result = await payload.find({ + collection: collectionSlug, + where, + }) + + expect(result.docs[0].id).toEqual(doc.id) + expect(result.docs[0].customData).toEqual(doc.customData) + }) + + it('collection DB Find One', async () => { + const result = await payload.findByID({ + collection: collectionSlug, + id: doc.id, + }) + + expect(result.id).toEqual(doc.id) + expect(result.customData).toEqual(doc.customData) + }) + + it('collection DB Delete', async () => { + const where = { id: { equals: doc.id } } + + const result = await payload.delete({ + collection: collectionSlug, + depth: 0, + user: devUser, + where, + }) + + expect(result.docs[0].id).toEqual(doc.id) + expect(result.docs[0].customData).toEqual(doc.customData) + expect(result.errors).toHaveLength(0) + }) +})