feat: add upsert to database interface and adapters (#8397)

- Adds the upsert method to the database interface
- Adds a mongodb specific option to extend the updateOne to accept
mongoDB Query Options (to pass `upsert: true`)
- Added upsert method to all database adapters
- Uses db.upsert in the payload preferences update operation

Includes a test using payload-preferences
This commit is contained in:
Dan Ribbens
2024-09-25 09:23:54 -04:00
committed by GitHub
parent 06ea67a184
commit 82ba1930e5
10 changed files with 101 additions and 24 deletions

View File

@@ -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<number | string, ClientSession>
transactionOptions: TransactionOptions
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
versions: {
[slug: string]: CollectionModel
}
@@ -191,6 +193,7 @@ export function mongooseAdapter({
updateGlobalVersion,
updateOne,
updateVersion,
upsert,
})
}

View File

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

View File

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

View File

@@ -150,6 +150,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -151,6 +151,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -150,6 +150,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
updateGlobalVersion,
updateOne,
updateVersion,
upsert: updateOne,
})
}

View File

@@ -135,6 +135,8 @@ export interface BaseDatabaseAdapter {
updateOne: UpdateOne
updateVersion: UpdateVersion
upsert: Upsert
}
export type Init = () => Promise<void> | 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<string, unknown>
req: PayloadRequest
} & (
| {
@@ -394,6 +400,17 @@ export type UpdateOneArgs = {
export type UpdateOne = (args: UpdateOneArgs) => Promise<Document>
export type UpsertArgs = {
collection: string
data: Record<string, unknown>
joins?: JoinQuery
locale?: string
req: PayloadRequest
where: Where
}
export type Upsert = (args: UpsertArgs) => Promise<Document>
export type DeleteOneArgs = {
collection: string
joins?: JoinQuery

View File

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

View File

@@ -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({
return await payload.db.upsert({
collection,
data: preference,
req,
where,
})
} catch (err: unknown) {
result = await payload.db.create({
collection,
data: preference,
req,
})
}
return result
}

View File

@@ -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: {