diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index 2b5244589c..587d6e122f 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -1,7 +1,7 @@ import type { CollationOptions, TransactionOptions } from 'mongodb' import type { MongoMemoryReplSet } from 'mongodb-memory-server' -import type { ClientSession, Connection, ConnectOptions } from 'mongoose' -import type { BaseDatabaseAdapter, DatabaseAdapterObj, Payload } from 'payload' +import type { ClientSession, Connection, ConnectOptions, QueryOptions } from 'mongoose' +import type { BaseDatabaseAdapter, DatabaseAdapterObj, Payload, UpdateOneArgs } from 'payload' import fs from 'fs' import mongoose from 'mongoose' @@ -36,6 +36,7 @@ import { updateGlobal } from './updateGlobal.js' import { updateGlobalVersion } from './updateGlobalVersion.js' import { updateOne } from './updateOne.js' import { updateVersion } from './updateVersion.js' +import { upsert } from './upsert.js' export type { MigrateDownArgs, MigrateUpArgs } from './types.js' @@ -124,6 +125,7 @@ declare module 'payload' { }[] sessions: Record transactionOptions: TransactionOptions + updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise versions: { [slug: string]: CollectionModel } @@ -191,6 +193,7 @@ export function mongooseAdapter({ updateGlobalVersion, updateOne, updateVersion, + upsert, }) } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 0265f47701..c456b57d26 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -1,3 +1,4 @@ +import type { QueryOptions } from 'mongoose' import type { PayloadRequest, UpdateOne } from 'payload' import type { MongooseAdapter } from './index.js' @@ -9,11 +10,20 @@ import { withSession } from './withSession.js' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, - { id, collection, data, locale, req = {} as PayloadRequest, where: whereArg }, + { + id, + collection, + data, + locale, + options: optionsArgs = {}, + req = {} as PayloadRequest, + where: whereArg, + }, ) { const where = id ? { id: { equals: id } } : whereArg const Model = this.collections[collection] - const options = { + const options: QueryOptions = { + ...optionsArgs, ...(await withSession(this, req)), lean: true, new: true, diff --git a/packages/db-mongodb/src/upsert.ts b/packages/db-mongodb/src/upsert.ts new file mode 100644 index 0000000000..c4d4376e19 --- /dev/null +++ b/packages/db-mongodb/src/upsert.ts @@ -0,0 +1,10 @@ +import type { PayloadRequest, Upsert } from 'payload' + +import type { MongooseAdapter } from './index.js' + +export const upsert: Upsert = async function upsert( + this: MongooseAdapter, + { collection, data, locale, req = {} as PayloadRequest, where }, +) { + return this.updateOne({ collection, data, locale, options: { upsert: true }, req, where }) +} diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts index e44f578a28..e60dcda1f6 100644 --- a/packages/db-postgres/src/index.ts +++ b/packages/db-postgres/src/index.ts @@ -150,6 +150,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj updateGlobalVersion, updateOne, updateVersion, + upsert: updateOne, }) } diff --git a/packages/db-sqlite/src/index.ts b/packages/db-sqlite/src/index.ts index 8cd8ec5391..8db109d43a 100644 --- a/packages/db-sqlite/src/index.ts +++ b/packages/db-sqlite/src/index.ts @@ -151,6 +151,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj { updateGlobalVersion, updateOne, updateVersion, + upsert: updateOne, }) } diff --git a/packages/db-vercel-postgres/src/index.ts b/packages/db-vercel-postgres/src/index.ts index 8ab7fd0cd9..f25b007cd1 100644 --- a/packages/db-vercel-postgres/src/index.ts +++ b/packages/db-vercel-postgres/src/index.ts @@ -150,6 +150,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj Promise | void @@ -380,6 +382,10 @@ export type UpdateOneArgs = { draft?: boolean joins?: JoinQuery locale?: string + /** + * Additional database adapter specific options to pass to the query + */ + options?: Record req: PayloadRequest } & ( | { @@ -394,6 +400,17 @@ export type UpdateOneArgs = { export type UpdateOne = (args: UpdateOneArgs) => Promise +export type UpsertArgs = { + collection: string + data: Record + joins?: JoinQuery + locale?: string + req: PayloadRequest + where: Where +} + +export type Upsert = (args: UpsertArgs) => Promise + export type DeleteOneArgs = { collection: string joins?: JoinQuery diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index c8caf1f997..0c322a7068 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -821,6 +821,7 @@ export type { UpdateOneArgs, UpdateVersion, UpdateVersionArgs, + Upsert, } from './database/types.js' export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from './email/types.js' export { diff --git a/packages/payload/src/preferences/operations/update.ts b/packages/payload/src/preferences/operations/update.ts index 4baaf61ce8..b1b0e37f48 100644 --- a/packages/payload/src/preferences/operations/update.ts +++ b/packages/payload/src/preferences/operations/update.ts @@ -35,23 +35,10 @@ export async function update(args: PreferenceUpdateRequest) { value, } - let result - - try { - // try/catch because we attempt to update without first reading to check if it exists first to save on db calls - result = await payload.db.updateOne({ - collection, - data: preference, - req, - where, - }) - } catch (err: unknown) { - result = await payload.db.create({ - collection, - data: preference, - req, - }) - } - - return result + return await payload.db.upsert({ + collection, + data: preference, + req, + where, + }) } diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index 932e9920fe..5144969f28 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -101,7 +101,7 @@ describe('Auth', () => { describe('logged in', () => { let token: string | undefined - let loggedInUser: User | undefined + let loggedInUser: undefined | User beforeAll(async () => { const response = await restClient.POST(`/${slug}/login`, { @@ -396,6 +396,52 @@ describe('Auth', () => { expect(result.docs).toHaveLength(1) }) + it('should only have one preference per user per key', async () => { + await restClient.POST(`/payload-preferences/${key}`, { + body: JSON.stringify({ + value: { property: 'test', property2: 'test' }, + }), + headers: { + Authorization: `JWT ${token}`, + }, + }) + await restClient.POST(`/payload-preferences/${key}`, { + body: JSON.stringify({ + value: { property: 'updated', property2: 'updated' }, + }), + headers: { + Authorization: `JWT ${token}`, + }, + }) + + const result = await payload.find({ + collection: 'payload-preferences', + depth: 0, + where: { + and: [ + { + key: { equals: key }, + }, + { + 'user.relationTo': { + equals: 'users', + }, + }, + { + 'user.value': { + equals: loggedInUser.id, + }, + }, + ], + }, + }) + + expect(result.docs[0].value.property).toStrictEqual('updated') + expect(result.docs[0].value.property2).toStrictEqual('updated') + + expect(result.docs).toHaveLength(1) + }) + it('should delete', async () => { const response = await restClient.DELETE(`/payload-preferences/${key}`, { headers: {