feat(db-*): add updateMany method to database adapter (#11441)
This PR adds a new `payload.db.updateMany` method, which is a more performant way to update multiple documents compared to using `payload.update`.
This commit is contained in:
@@ -16,6 +16,7 @@ import type {
|
||||
TypeWithVersion,
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateManyArgs,
|
||||
UpdateOneArgs,
|
||||
UpdateVersionArgs,
|
||||
} from 'payload'
|
||||
@@ -53,6 +54,7 @@ import { commitTransaction } from './transactions/commitTransaction.js'
|
||||
import { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
import { updateGlobal } from './updateGlobal.js'
|
||||
import { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
import { updateMany } from './updateMany.js'
|
||||
import { updateOne } from './updateOne.js'
|
||||
import { updateVersion } from './updateVersion.js'
|
||||
import { upsert } from './upsert.js'
|
||||
@@ -160,6 +162,7 @@ declare module 'payload' {
|
||||
updateGlobalVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateGlobalVersionArgs<T>,
|
||||
) => Promise<TypeWithVersion<T>>
|
||||
|
||||
updateOne: (args: { options?: QueryOptions } & UpdateOneArgs) => Promise<Document>
|
||||
updateVersion: <T extends TypeWithID = TypeWithID>(
|
||||
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
|
||||
@@ -200,6 +203,7 @@ export function mongooseAdapter({
|
||||
mongoMemoryServer,
|
||||
sessions: {},
|
||||
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
|
||||
updateMany,
|
||||
url,
|
||||
versions: {},
|
||||
// DatabaseAdapter
|
||||
|
||||
61
packages/db-mongodb/src/updateMany.ts
Normal file
61
packages/db-mongodb/src/updateMany.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { MongooseUpdateQueryOptions } from 'mongoose'
|
||||
import type { UpdateMany } from 'payload'
|
||||
|
||||
import type { MongooseAdapter } from './index.js'
|
||||
|
||||
import { buildQuery } from './queries/buildQuery.js'
|
||||
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
|
||||
import { getSession } from './utilities/getSession.js'
|
||||
import { handleError } from './utilities/handleError.js'
|
||||
import { transform } from './utilities/transform.js'
|
||||
|
||||
export const updateMany: UpdateMany = async function updateMany(
|
||||
this: MongooseAdapter,
|
||||
{ collection, data, locale, options: optionsArgs = {}, req, returning, select, where },
|
||||
) {
|
||||
const Model = this.collections[collection]
|
||||
const fields = this.payload.collections[collection].config.fields
|
||||
|
||||
const options: MongooseUpdateQueryOptions = {
|
||||
...optionsArgs,
|
||||
lean: true,
|
||||
new: true,
|
||||
projection: buildProjectionFromSelect({
|
||||
adapter: this,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
select,
|
||||
}),
|
||||
session: await getSession(this, req),
|
||||
}
|
||||
|
||||
const query = await buildQuery({
|
||||
adapter: this,
|
||||
collectionSlug: collection,
|
||||
fields: this.payload.collections[collection].config.flattenedFields,
|
||||
locale,
|
||||
where,
|
||||
})
|
||||
|
||||
transform({ adapter: this, data, fields, operation: 'write' })
|
||||
|
||||
try {
|
||||
await Model.updateMany(query, data, options)
|
||||
} catch (error) {
|
||||
handleError({ collection, error, req })
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
const result = await Model.find(query, {}, options)
|
||||
|
||||
transform({
|
||||
adapter: this,
|
||||
data: result,
|
||||
fields,
|
||||
operation: 'read',
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
@@ -185,6 +186,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert: updateOne,
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
@@ -120,6 +121,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
||||
tableNameMap: new Map<string, string>(),
|
||||
tables: {},
|
||||
transactionOptions: args.transactionOptions || undefined,
|
||||
updateMany,
|
||||
versionsSuffix: args.versionsSuffix || '_v',
|
||||
|
||||
// DatabaseAdapter
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
} from '@payloadcms/drizzle'
|
||||
@@ -186,6 +187,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
||||
rollbackTransaction,
|
||||
updateGlobal,
|
||||
updateGlobalVersion,
|
||||
updateMany,
|
||||
updateOne,
|
||||
updateVersion,
|
||||
upsert: updateOne,
|
||||
|
||||
@@ -33,6 +33,7 @@ export { commitTransaction } from './transactions/commitTransaction.js'
|
||||
export { rollbackTransaction } from './transactions/rollbackTransaction.js'
|
||||
export { updateGlobal } from './updateGlobal.js'
|
||||
export { updateGlobalVersion } from './updateGlobalVersion.js'
|
||||
export { updateMany } from './updateMany.js'
|
||||
export { updateOne } from './updateOne.js'
|
||||
export { updateVersion } from './updateVersion.js'
|
||||
export { upsertRow } from './upsertRow/index.js'
|
||||
|
||||
97
packages/drizzle/src/updateMany.ts
Normal file
97
packages/drizzle/src/updateMany.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||
import type { UpdateMany } from 'payload'
|
||||
|
||||
import toSnakeCase from 'to-snake-case'
|
||||
|
||||
import type { DrizzleAdapter } from './types.js'
|
||||
|
||||
import buildQuery from './queries/buildQuery.js'
|
||||
import { selectDistinct } from './queries/selectDistinct.js'
|
||||
import { upsertRow } from './upsertRow/index.js'
|
||||
import { getTransaction } from './utilities/getTransaction.js'
|
||||
|
||||
export const updateMany: UpdateMany = async function updateMany(
|
||||
this: DrizzleAdapter,
|
||||
{
|
||||
collection: collectionSlug,
|
||||
data,
|
||||
joins: joinQuery,
|
||||
locale,
|
||||
req,
|
||||
returning,
|
||||
select,
|
||||
where: whereToUse,
|
||||
},
|
||||
) {
|
||||
const db = await getTransaction(this, req)
|
||||
const collection = this.payload.collections[collectionSlug].config
|
||||
const tableName = this.tableNameMap.get(toSnakeCase(collection.slug))
|
||||
|
||||
const { joins, selectFields, where } = buildQuery({
|
||||
adapter: this,
|
||||
fields: collection.flattenedFields,
|
||||
locale,
|
||||
tableName,
|
||||
where: whereToUse,
|
||||
})
|
||||
|
||||
let idsToUpdate: (number | string)[] = []
|
||||
|
||||
const selectDistinctResult = await selectDistinct({
|
||||
adapter: this,
|
||||
db,
|
||||
joins,
|
||||
selectFields,
|
||||
tableName,
|
||||
where,
|
||||
})
|
||||
|
||||
if (selectDistinctResult?.[0]?.id) {
|
||||
idsToUpdate = selectDistinctResult?.map((doc) => doc.id)
|
||||
|
||||
// If id wasn't passed but `where` without any joins, retrieve it with findFirst
|
||||
} else if (whereToUse && !joins.length) {
|
||||
const _db = db as LibSQLDatabase
|
||||
|
||||
const table = this.tables[tableName]
|
||||
|
||||
const docsToUpdate = await _db
|
||||
.select({
|
||||
id: table.id,
|
||||
})
|
||||
.from(table)
|
||||
.where(where)
|
||||
|
||||
idsToUpdate = docsToUpdate?.map((doc) => doc.id)
|
||||
}
|
||||
|
||||
if (!idsToUpdate.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
||||
// TODO: We need to batch this to reduce the amount of db calls. This can get very slow if we are updating a lot of rows.
|
||||
for (const idToUpdate of idsToUpdate) {
|
||||
const result = await upsertRow({
|
||||
id: idToUpdate,
|
||||
adapter: this,
|
||||
data,
|
||||
db,
|
||||
fields: collection.flattenedFields,
|
||||
ignoreResult: returning === false,
|
||||
joinQuery,
|
||||
operation: 'update',
|
||||
req,
|
||||
select,
|
||||
tableName,
|
||||
})
|
||||
results.push(result)
|
||||
}
|
||||
|
||||
if (returning === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -145,6 +145,8 @@ export interface BaseDatabaseAdapter {
|
||||
|
||||
updateGlobalVersion: UpdateGlobalVersion
|
||||
|
||||
updateMany: UpdateMany
|
||||
|
||||
updateOne: UpdateOne
|
||||
|
||||
updateVersion: UpdateVersion
|
||||
@@ -510,6 +512,29 @@ export type UpdateOneArgs = {
|
||||
*/
|
||||
export type UpdateOne = (args: UpdateOneArgs) => Promise<Document>
|
||||
|
||||
export type UpdateManyArgs = {
|
||||
collection: CollectionSlug
|
||||
data: Record<string, unknown>
|
||||
draft?: boolean
|
||||
joins?: JoinQuery
|
||||
locale?: string
|
||||
/**
|
||||
* Additional database adapter specific options to pass to the query
|
||||
*/
|
||||
options?: Record<string, unknown>
|
||||
req?: Partial<PayloadRequest>
|
||||
/**
|
||||
* If true, returns the updated documents
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
returning?: boolean
|
||||
select?: SelectType
|
||||
where: Where
|
||||
}
|
||||
|
||||
export type UpdateMany = (args: UpdateManyArgs) => Promise<Document[] | null>
|
||||
|
||||
export type UpsertArgs = {
|
||||
collection: CollectionSlug
|
||||
data: Record<string, unknown>
|
||||
|
||||
@@ -1112,6 +1112,7 @@ export type {
|
||||
Connect,
|
||||
Count,
|
||||
CountArgs,
|
||||
CountGlobalVersionArgs,
|
||||
CountGlobalVersions,
|
||||
CountVersions,
|
||||
Create,
|
||||
@@ -1156,6 +1157,8 @@ export type {
|
||||
UpdateGlobalArgs,
|
||||
UpdateGlobalVersion,
|
||||
UpdateGlobalVersionArgs,
|
||||
UpdateMany,
|
||||
UpdateManyArgs,
|
||||
UpdateOne,
|
||||
UpdateOneArgs,
|
||||
UpdateVersion,
|
||||
|
||||
@@ -8,7 +8,21 @@ import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js'
|
||||
import { seed } from './seed.js'
|
||||
import {
|
||||
customIDsSlug,
|
||||
customSchemaSlug,
|
||||
defaultValuesSlug,
|
||||
errorOnUnnamedFieldsSlug,
|
||||
fakeCustomIDsSlug,
|
||||
fieldsPersistanceSlug,
|
||||
pgMigrationSlug,
|
||||
placesSlug,
|
||||
postsSlug,
|
||||
relationASlug,
|
||||
relationBSlug,
|
||||
relationshipsMigrationSlug,
|
||||
} from './shared.js'
|
||||
|
||||
const defaultValueField: TextField = {
|
||||
name: 'defaultValue',
|
||||
@@ -183,7 +197,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'default-values',
|
||||
slug: defaultValuesSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -222,7 +236,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'relation-a',
|
||||
slug: relationASlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -239,7 +253,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'relation-b',
|
||||
slug: relationBSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -261,7 +275,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'pg-migrations',
|
||||
slug: pgMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'relation1',
|
||||
@@ -329,7 +343,7 @@ export default buildConfigWithDefaults({
|
||||
versions: true,
|
||||
},
|
||||
{
|
||||
slug: 'custom-schema',
|
||||
slug: customSchemaSlug,
|
||||
dbName: 'customs',
|
||||
fields: [
|
||||
{
|
||||
@@ -404,7 +418,7 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'places',
|
||||
slug: placesSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'country',
|
||||
@@ -417,7 +431,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'fields-persistance',
|
||||
slug: fieldsPersistanceSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
@@ -475,7 +489,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'custom-ids',
|
||||
slug: customIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
@@ -502,7 +516,7 @@ export default buildConfigWithDefaults({
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: 'fake-custom-ids',
|
||||
slug: fakeCustomIDsSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -535,7 +549,7 @@ export default buildConfigWithDefaults({
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'relationships-migration',
|
||||
slug: relationshipsMigrationSlug,
|
||||
fields: [
|
||||
{
|
||||
type: 'relationship',
|
||||
@@ -587,13 +601,9 @@ export default buildConfigWithDefaults({
|
||||
locales: ['en', 'es'],
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
|
||||
await seed(payload)
|
||||
}
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
|
||||
@@ -28,6 +28,7 @@ import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { isMongoose } from '../helpers/isMongoose.js'
|
||||
import removeFiles from '../helpers/removeFiles.js'
|
||||
import { seed } from './seed.js'
|
||||
import { errorOnUnnamedFieldsSlug, postsSlug } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
@@ -43,9 +44,17 @@ process.env.PAYLOAD_CONFIG_PATH = path.join(dirname, 'config.ts')
|
||||
|
||||
describe('database', () => {
|
||||
beforeAll(async () => {
|
||||
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
payload.db.migrationDir = path.join(dirname, './migrations')
|
||||
|
||||
await seed(payload)
|
||||
|
||||
await restClient.login({
|
||||
slug: 'users',
|
||||
credentials: devUser,
|
||||
})
|
||||
|
||||
const loginResult = await payload.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
@@ -794,6 +803,7 @@ describe('database', () => {
|
||||
data: {
|
||||
title,
|
||||
},
|
||||
depth: 0,
|
||||
disableTransaction: true,
|
||||
})
|
||||
})
|
||||
@@ -876,6 +886,80 @@ describe('database', () => {
|
||||
|
||||
expect(result.point).toEqual([5, 10])
|
||||
})
|
||||
|
||||
it('ensure updateMany updates all docs and respects where query', async () => {
|
||||
await payload.db.deleteMany({
|
||||
collection: postsSlug,
|
||||
where: {
|
||||
id: {
|
||||
exists: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'notupdated',
|
||||
},
|
||||
})
|
||||
|
||||
// Create 5 posts
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: `v1 ${i}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const result = await payload.db.updateMany({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
title: 'updated',
|
||||
},
|
||||
where: {
|
||||
title: {
|
||||
not_equals: 'notupdated',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result?.length).toBe(5)
|
||||
expect(result?.[0]?.title).toBe('updated')
|
||||
expect(result?.[4]?.title).toBe('updated')
|
||||
|
||||
// Ensure all posts minus the one we don't want updated are updated
|
||||
const { docs } = await payload.find({
|
||||
collection: postsSlug,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
where: {
|
||||
title: {
|
||||
equals: 'updated',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(docs).toHaveLength(5)
|
||||
expect(docs?.[0]?.title).toBe('updated')
|
||||
expect(docs?.[4]?.title).toBe('updated')
|
||||
|
||||
const { docs: notUpdatedDocs } = await payload.find({
|
||||
collection: postsSlug,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
where: {
|
||||
title: {
|
||||
not_equals: 'updated',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(notUpdatedDocs).toHaveLength(1)
|
||||
expect(notUpdatedDocs?.[0]?.title).toBe('notupdated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handler', () => {
|
||||
|
||||
26
test/database/seed.ts
Normal file
26
test/database/seed.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { seedDB } from '../helpers/seed.js'
|
||||
import { collectionSlugs } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export const _seed = async (_payload: Payload) => {
|
||||
await _payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function seed(_payload: Payload) {
|
||||
return await _seed(_payload)
|
||||
}
|
||||
@@ -1,2 +1,37 @@
|
||||
export const postsSlug = 'posts'
|
||||
export const errorOnUnnamedFieldsSlug = 'error-on-unnamed-fields'
|
||||
|
||||
export const defaultValuesSlug = 'default-values'
|
||||
|
||||
export const relationASlug = 'relation-a'
|
||||
|
||||
export const relationBSlug = 'relation-b'
|
||||
|
||||
export const pgMigrationSlug = 'pg-migrations'
|
||||
|
||||
export const customSchemaSlug = 'custom-schema'
|
||||
|
||||
export const placesSlug = 'places'
|
||||
|
||||
export const fieldsPersistanceSlug = 'fields-persistance'
|
||||
|
||||
export const customIDsSlug = 'custom-ids'
|
||||
|
||||
export const fakeCustomIDsSlug = 'fake-custom-ids'
|
||||
|
||||
export const relationshipsMigrationSlug = 'relationships-migration'
|
||||
|
||||
export const collectionSlugs = [
|
||||
postsSlug,
|
||||
errorOnUnnamedFieldsSlug,
|
||||
defaultValuesSlug,
|
||||
relationASlug,
|
||||
relationBSlug,
|
||||
pgMigrationSlug,
|
||||
customSchemaSlug,
|
||||
placesSlug,
|
||||
fieldsPersistanceSlug,
|
||||
customIDsSlug,
|
||||
fakeCustomIDsSlug,
|
||||
relationshipsMigrationSlug,
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user