diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 91408408d1..559a7ac5c0 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -12,7 +12,7 @@ export const count: Count = async function count( { collection, locale, req = {} as PayloadRequest, where }, ) { const Model = this.collections[collection] - const options: QueryOptions = withSession(this, req.transactionID) + const options: QueryOptions = await withSession(this, req) let hasNearConstraint = false diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 621b82e96d..f01a9d6af3 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -10,7 +10,7 @@ export const create: Create = async function create( { collection, data, req = {} as PayloadRequest }, ) { const Model = this.collections[collection] - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) let doc try { ;[doc] = await Model.create([data], options) diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index 9f75a68762..6b6bc9ff41 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -14,7 +14,7 @@ export const createGlobal: CreateGlobal = async function createGlobal( globalType: slug, ...data, } - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) let [result] = (await Model.create([global], options)) as any diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index 71d196d45c..3d6a004a8d 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -9,7 +9,7 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo { autosave, createdAt, globalSlug, parent, req = {} as PayloadRequest, updatedAt, versionData }, ) { const VersionModel = this.versions[globalSlug] - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) const [doc] = await VersionModel.create( [ diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 8cdb36b5ed..2093052ccb 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -17,7 +17,7 @@ export const createVersion: CreateVersion = async function createVersion( }, ) { const VersionModel = this.versions[collectionSlug] - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) const [doc] = await VersionModel.create( [ diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index eee1bb088b..cc692e9e0e 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -10,7 +10,7 @@ export const deleteMany: DeleteMany = async function deleteMany( ) { const Model = this.collections[collection] const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index f22373de2a..66c781abc5 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -10,7 +10,7 @@ export const deleteOne: DeleteOne = async function deleteOne( { collection, req = {} as PayloadRequest, where }, ) { const Model = this.collections[collection] - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) const query = await Model.buildQuery({ payload: this.payload, diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index 90486dcc65..cd36dbaeed 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -10,7 +10,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersions( ) { const VersionsModel = this.versions[collection] const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 0a668c78cd..7ae16d907e 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -15,7 +15,7 @@ export const find: Find = async function find( ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) let hasNearConstraint = false diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 3a89508b78..1cfc06253d 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -13,7 +13,7 @@ export const findGlobal: FindGlobal = async function findGlobal( ) { const Model = this.globals const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index b61c638175..e8475eef6d 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -28,7 +28,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV this.payload.globals.config.find(({ slug }) => slug === global), ) const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), limit, skip, } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index bc1e7e56d9..c17e93aea0 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -12,7 +12,7 @@ export const findOne: FindOne = async function findOne( ) { const Model = this.collections[collection] const options: MongooseQueryOptions = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 7e26fc3931..ebbf3049db 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -26,7 +26,7 @@ export const findVersions: FindVersions = async function findVersions( const Model = this.versions[collection] const collectionConfig = this.payload.collections[collection].config const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), limit, skip, } diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 30d27799b0..3bd515250a 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -15,7 +15,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( ) { const VersionModel = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const options = withSession(this, req.transactionID) + const options = await withSession(this, req) let hasNearConstraint let sort diff --git a/packages/db-mongodb/src/transactions/commitTransaction.ts b/packages/db-mongodb/src/transactions/commitTransaction.ts index dbc45cb752..e0ebd917d4 100644 --- a/packages/db-mongodb/src/transactions/commitTransaction.ts +++ b/packages/db-mongodb/src/transactions/commitTransaction.ts @@ -1,6 +1,8 @@ import type { CommitTransaction } from 'payload' export const commitTransaction: CommitTransaction = async function commitTransaction(id) { + if (id instanceof Promise) return + if (!this.sessions[id]?.inTransaction()) { return } diff --git a/packages/db-mongodb/src/transactions/rollbackTransaction.ts b/packages/db-mongodb/src/transactions/rollbackTransaction.ts index 0dba07ee87..c53410e400 100644 --- a/packages/db-mongodb/src/transactions/rollbackTransaction.ts +++ b/packages/db-mongodb/src/transactions/rollbackTransaction.ts @@ -1,27 +1,35 @@ import type { RollbackTransaction } from 'payload' export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction( - id = '', + incomingID = '', ) { + let transactionID: number | string + + if (incomingID instanceof Promise) { + transactionID = await incomingID + } else { + transactionID = incomingID + } + // if multiple operations are using the same transaction, the first will flow through and delete the session. // subsequent calls should be ignored. - if (!this.sessions[id]) { + if (!this.sessions[transactionID]) { return } // when session exists but is not inTransaction something unexpected is happening to the session - if (!this.sessions[id].inTransaction()) { + if (!this.sessions[transactionID].inTransaction()) { this.payload.logger.warn('rollbackTransaction called when no transaction exists') - delete this.sessions[id] + delete this.sessions[transactionID] return } // the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail try { - await this.sessions[id].abortTransaction() - await this.sessions[id].endSession() + await this.sessions[transactionID].abortTransaction() + await this.sessions[transactionID].endSession() } catch (error) { // ignore the error as it is likely a race condition from multiple errors } - delete this.sessions[id] + delete this.sessions[transactionID] } diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 8c1af6d771..726a2d98ed 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -11,7 +11,7 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( ) { const Model = this.globals const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, new: true, } diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index f0e98d43ce..dc575b65fb 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -18,7 +18,7 @@ export async function updateGlobalVersion( const VersionModel = this.versions[global] const whereToUse = where || { id: { equals: id } } const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, new: true, } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 4155c9fd21..0c751621c2 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -13,7 +13,7 @@ export const updateOne: UpdateOne = async function updateOne( const where = id ? { id: { equals: id } } : whereArg const Model = this.collections[collection] const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, new: true, } diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 793d111240..84757e7176 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -11,7 +11,7 @@ export const updateVersion: UpdateVersion = async function updateVersion( const VersionModel = this.versions[collection] const whereToUse = where || { id: { equals: id } } const options = { - ...withSession(this, req.transactionID), + ...(await withSession(this, req)), lean: true, new: true, } diff --git a/packages/db-mongodb/src/withSession.ts b/packages/db-mongodb/src/withSession.ts index 6a416250a4..51d7054a5c 100644 --- a/packages/db-mongodb/src/withSession.ts +++ b/packages/db-mongodb/src/withSession.ts @@ -1,4 +1,5 @@ import type { ClientSession } from 'mongoose' +import type { PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' @@ -6,9 +7,15 @@ import type { MongooseAdapter } from './index.js' * returns the session belonging to the transaction of the req.session if exists * @returns ClientSession */ -export function withSession( +export async function withSession( db: MongooseAdapter, - transactionID?: number | string, -): { session: ClientSession } | object { - return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {} + req: PayloadRequest, +): Promise<{ session: ClientSession } | object> { + let transactionID = req.transactionID + + if (transactionID instanceof Promise) { + transactionID = await req.transactionID + } + + if (req) return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {} } diff --git a/packages/db-postgres/src/count.ts b/packages/db-postgres/src/count.ts index f206f363fe..ed8b5b8a24 100644 --- a/packages/db-postgres/src/count.ts +++ b/packages/db-postgres/src/count.ts @@ -17,7 +17,7 @@ export const count: Count = async function count( const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug)) - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const table = this.tables[tableName] const { joins, where } = await buildQuery({ diff --git a/packages/db-postgres/src/create.ts b/packages/db-postgres/src/create.ts index 5e9af9f64d..9a1d062acc 100644 --- a/packages/db-postgres/src/create.ts +++ b/packages/db-postgres/src/create.ts @@ -10,7 +10,7 @@ export const create: Create = async function create( this: PostgresAdapter, { collection: collectionSlug, data, req }, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) diff --git a/packages/db-postgres/src/createGlobal.ts b/packages/db-postgres/src/createGlobal.ts index 3b0107633d..4c13d581db 100644 --- a/packages/db-postgres/src/createGlobal.ts +++ b/packages/db-postgres/src/createGlobal.ts @@ -10,7 +10,7 @@ export async function createGlobal>( this: PostgresAdapter, { slug, data, req = {} as PayloadRequest }: CreateGlobalArgs, ): Promise { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug)) diff --git a/packages/db-postgres/src/createGlobalVersion.ts b/packages/db-postgres/src/createGlobalVersion.ts index 0494f1c262..58e2dad1e2 100644 --- a/packages/db-postgres/src/createGlobalVersion.ts +++ b/packages/db-postgres/src/createGlobalVersion.ts @@ -1,7 +1,7 @@ -import type { PayloadRequest, TypeWithID, TypeWithVersion } from 'payload' +import type { CreateGlobalVersionArgs, PayloadRequest, TypeWithID, TypeWithVersion } from 'payload' import { sql } from 'drizzle-orm' -import { type CreateGlobalVersionArgs, buildVersionGlobalFields } from 'payload' +import { buildVersionGlobalFields } from 'payload' import toSnakeCase from 'to-snake-case' import type { PostgresAdapter } from './types.js' @@ -12,7 +12,7 @@ export async function createGlobalVersion( this: PostgresAdapter, { autosave, globalSlug, req = {} as PayloadRequest, versionData }: CreateGlobalVersionArgs, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const global = this.payload.globals.config.find(({ slug }) => slug === globalSlug) const tableName = this.tableNameMap.get(`_${toSnakeCase(global.slug)}${this.versionsSuffix}`) diff --git a/packages/db-postgres/src/createVersion.ts b/packages/db-postgres/src/createVersion.ts index 839615d546..c21aecb136 100644 --- a/packages/db-postgres/src/createVersion.ts +++ b/packages/db-postgres/src/createVersion.ts @@ -18,7 +18,7 @@ export async function createVersion( versionData, }: CreateVersionArgs, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const defaultTableName = toSnakeCase(collection.slug) diff --git a/packages/db-postgres/src/deleteMany.ts b/packages/db-postgres/src/deleteMany.ts index b385375e5e..81b1e9d11c 100644 --- a/packages/db-postgres/src/deleteMany.ts +++ b/packages/db-postgres/src/deleteMany.ts @@ -11,7 +11,7 @@ export const deleteMany: DeleteMany = async function deleteMany( this: PostgresAdapter, { collection, req = {} as PayloadRequest, where }, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collectionConfig = this.payload.collections[collection].config const tableName = this.tableNameMap.get(toSnakeCase(collectionConfig.slug)) diff --git a/packages/db-postgres/src/deleteOne.ts b/packages/db-postgres/src/deleteOne.ts index 87603d94bf..df2e22b789 100644 --- a/packages/db-postgres/src/deleteOne.ts +++ b/packages/db-postgres/src/deleteOne.ts @@ -14,7 +14,7 @@ export const deleteOne: DeleteOne = async function deleteOne( this: PostgresAdapter, { collection: collectionSlug, req = {} as PayloadRequest, where: whereArg }, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) diff --git a/packages/db-postgres/src/deleteVersions.ts b/packages/db-postgres/src/deleteVersions.ts index 239b9a8fa4..dd31268789 100644 --- a/packages/db-postgres/src/deleteVersions.ts +++ b/packages/db-postgres/src/deleteVersions.ts @@ -12,7 +12,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersion( this: PostgresAdapter, { collection, locale, req = {} as PayloadRequest, where: where }, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const tableName = this.tableNameMap.get( diff --git a/packages/db-postgres/src/find/findMany.ts b/packages/db-postgres/src/find/findMany.ts index a8f0fe9ce3..b47567d97f 100644 --- a/packages/db-postgres/src/find/findMany.ts +++ b/packages/db-postgres/src/find/findMany.ts @@ -30,7 +30,7 @@ export const findMany = async function find({ tableName, where: whereArg, }: Args) { - const db = adapter.sessions[req.transactionID]?.db || adapter.drizzle + const db = adapter.sessions[await req.transactionID]?.db || adapter.drizzle const table = adapter.tables[tableName] const limit = limitArg ?? 10 diff --git a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts b/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts index 3b556070f8..46836e10e9 100644 --- a/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts +++ b/packages/db-postgres/src/predefinedMigrations/v2-v3/index.ts @@ -38,7 +38,7 @@ type Args = { */ export const migratePostgresV2toV3 = async ({ debug, payload, req }: Args) => { const adapter = payload.db as PostgresAdapter - const db = adapter.sessions[req.transactionID]?.db + const db = adapter.sessions[await req.transactionID]?.db const dir = payload.db.migrationDir // get the drizzle migrateUpSQL from drizzle using the last schema diff --git a/packages/db-postgres/src/queryDrafts.ts b/packages/db-postgres/src/queryDrafts.ts index f12fc215e8..de555b6ee8 100644 --- a/packages/db-postgres/src/queryDrafts.ts +++ b/packages/db-postgres/src/queryDrafts.ts @@ -1,6 +1,6 @@ -import type { PayloadRequest, SanitizedCollectionConfig } from 'payload' +import type { PayloadRequest, QueryDrafts, SanitizedCollectionConfig } from 'payload' -import { type QueryDrafts, buildVersionCollectionFields, combineQueries } from 'payload' +import { buildVersionCollectionFields, combineQueries } from 'payload' import toSnakeCase from 'to-snake-case' import type { PostgresAdapter } from './types.js' diff --git a/packages/db-postgres/src/transactions/commitTransaction.ts b/packages/db-postgres/src/transactions/commitTransaction.ts index ecc0082e81..c985ca64f9 100644 --- a/packages/db-postgres/src/transactions/commitTransaction.ts +++ b/packages/db-postgres/src/transactions/commitTransaction.ts @@ -1,6 +1,8 @@ import type { CommitTransaction } from 'payload' export const commitTransaction: CommitTransaction = async function commitTransaction(id) { + if (id instanceof Promise) return + // if the session was deleted it has already been aborted if (!this.sessions[id]) { return diff --git a/packages/db-postgres/src/transactions/rollbackTransaction.ts b/packages/db-postgres/src/transactions/rollbackTransaction.ts index 2a70ceac71..143fbefc3f 100644 --- a/packages/db-postgres/src/transactions/rollbackTransaction.ts +++ b/packages/db-postgres/src/transactions/rollbackTransaction.ts @@ -1,17 +1,19 @@ import type { RollbackTransaction } from 'payload' export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction( - id = '', + incomingID = '', ) { + const transactionID = incomingID instanceof Promise ? await incomingID : incomingID + // if multiple operations are using the same transaction, the first will flow through and delete the session. // subsequent calls should be ignored. - if (!this.sessions[id]) { + if (!this.sessions[transactionID]) { return } // end the session promise in failure by calling reject - await this.sessions[id].reject() + await this.sessions[transactionID].reject() // delete the session causing any other operations with the same transaction to fail - delete this.sessions[id] + delete this.sessions[transactionID] } diff --git a/packages/db-postgres/src/update.ts b/packages/db-postgres/src/update.ts index 859937c04e..94377f45f3 100644 --- a/packages/db-postgres/src/update.ts +++ b/packages/db-postgres/src/update.ts @@ -12,7 +12,7 @@ export const updateOne: UpdateOne = async function updateOne( this: PostgresAdapter, { id, collection: collectionSlug, data, draft, locale, req, where: whereArg }, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collection = this.payload.collections[collectionSlug].config const tableName = this.tableNameMap.get(toSnakeCase(collection.slug)) const whereToUse = whereArg || { id: { equals: id } } diff --git a/packages/db-postgres/src/updateGlobal.ts b/packages/db-postgres/src/updateGlobal.ts index 285feee3a0..9144c6abe6 100644 --- a/packages/db-postgres/src/updateGlobal.ts +++ b/packages/db-postgres/src/updateGlobal.ts @@ -10,7 +10,7 @@ export async function updateGlobal>( this: PostgresAdapter, { slug, data, req = {} as PayloadRequest }: UpdateGlobalArgs, ): Promise { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const globalConfig = this.payload.globals.config.find((config) => config.slug === slug) const tableName = this.tableNameMap.get(toSnakeCase(globalConfig.slug)) diff --git a/packages/db-postgres/src/updateGlobalVersion.ts b/packages/db-postgres/src/updateGlobalVersion.ts index 9dacd25b49..ed7160237e 100644 --- a/packages/db-postgres/src/updateGlobalVersion.ts +++ b/packages/db-postgres/src/updateGlobalVersion.ts @@ -25,7 +25,7 @@ export async function updateGlobalVersion( where: whereArg, }: UpdateGlobalVersionArgs, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const globalConfig: SanitizedGlobalConfig = this.payload.globals.config.find( ({ slug }) => slug === global, ) diff --git a/packages/db-postgres/src/updateVersion.ts b/packages/db-postgres/src/updateVersion.ts index c2521a6be4..ddf381c680 100644 --- a/packages/db-postgres/src/updateVersion.ts +++ b/packages/db-postgres/src/updateVersion.ts @@ -25,7 +25,7 @@ export async function updateVersion( where: whereArg, }: UpdateVersionArgs, ) { - const db = this.sessions[req.transactionID]?.db || this.drizzle + const db = this.sessions[await req.transactionID]?.db || this.drizzle const collectionConfig: SanitizedCollectionConfig = this.payload.collections[collection].config const whereToUse = whereArg || { id: { equals: id } } const tableName = this.tableNameMap.get( diff --git a/packages/graphql/src/resolvers/collections/find.ts b/packages/graphql/src/resolvers/collections/find.ts index 88d715b1a7..8870f37c8f 100644 --- a/packages/graphql/src/resolvers/collections/find.ts +++ b/packages/graphql/src/resolvers/collections/find.ts @@ -27,8 +27,8 @@ export function findResolver(collection: Collection): Resolver { let { req } = context const locale = req.locale const fallbackLocale = req.fallbackLocale - req = isolateObjectProperty(req, 'locale') - req = isolateObjectProperty(req, 'fallbackLocale') + + req = isolateObjectProperty(req, ['locale', 'fallbackLocale', 'transactionID']) req.locale = args.locale || locale req.fallbackLocale = args.fallbackLocale || fallbackLocale if (!req.query) req.query = {} @@ -49,7 +49,7 @@ export function findResolver(collection: Collection): Resolver { draft: args.draft, limit: args.limit, page: args.page, - req: isolateObjectProperty(req, 'transactionID'), + req, sort: args.sort, where: args.where, } diff --git a/packages/payload/src/collections/dataloader.ts b/packages/payload/src/collections/dataloader.ts index 512b6be074..84d9092ae6 100644 --- a/packages/payload/src/collections/dataloader.ts +++ b/packages/payload/src/collections/dataloader.ts @@ -166,7 +166,7 @@ type CreateCacheKeyArgs = { locale: string overrideAccess: boolean showHiddenFields: boolean - transactionID: number | string + transactionID: Promise | number | string } export const createDataloaderCacheKey = ({ collectionSlug, diff --git a/packages/payload/src/database/types.ts b/packages/payload/src/database/types.ts index 38fc818b32..28096296f4 100644 --- a/packages/payload/src/database/types.ts +++ b/packages/payload/src/database/types.ts @@ -159,9 +159,9 @@ export type BeginTransaction = ( options?: Record, ) => Promise -export type RollbackTransaction = (id: number | string) => Promise +export type RollbackTransaction = (id: Promise | number | string) => Promise -export type CommitTransaction = (id: number | string) => Promise +export type CommitTransaction = (id: Promise | number | string) => Promise export type QueryDraftsArgs = { collection: string diff --git a/packages/payload/src/types/index.ts b/packages/payload/src/types/index.ts index 1930478fb7..92719317a4 100644 --- a/packages/payload/src/types/index.ts +++ b/packages/payload/src/types/index.ts @@ -43,8 +43,9 @@ export type CustomPayloadRequestProperties = { t: TFunction /** * Identifier for the database transaction for interactions in a single, all-or-nothing operation. + * Can also be used to ensure consistency when multiple operations try to create a transaction concurrently on the same request. */ - transactionID?: number | string + transactionID?: Promise | number | string /** * Used to ensure consistency when multiple operations try to create a transaction concurrently on the same request */ diff --git a/packages/payload/src/utilities/initTransaction.ts b/packages/payload/src/utilities/initTransaction.ts index f4aa57056d..06b5b67bd9 100644 --- a/packages/payload/src/utilities/initTransaction.ts +++ b/packages/payload/src/utilities/initTransaction.ts @@ -5,25 +5,27 @@ import type { PayloadRequest } from '../types/index.js' * @returns true if beginning a transaction and false when req already has a transaction to use */ export async function initTransaction(req: PayloadRequest): Promise { - const { payload, transactionID, transactionIDPromise } = req + const { payload, transactionID } = req + if (transactionID instanceof Promise) { + // wait for whoever else is already creating the transaction + await transactionID + return false + } + if (transactionID) { // we already have a transaction, we're not in charge of committing it return false } - if (transactionIDPromise) { - // wait for whoever else is already creating the transaction - await transactionIDPromise - return false - } if (typeof payload.db.beginTransaction === 'function') { // create a new transaction - req.transactionIDPromise = payload.db.beginTransaction().then((transactionID) => { + req.transactionID = payload.db.beginTransaction().then((transactionID) => { if (transactionID) { req.transactionID = transactionID } - delete req.transactionIDPromise + + return transactionID }) - await req.transactionIDPromise + await req.transactionID return !!req.transactionID } return false diff --git a/packages/payload/src/utilities/isolateObjectProperty.ts b/packages/payload/src/utilities/isolateObjectProperty.ts index 83b481f0c8..8a4e47b283 100644 --- a/packages/payload/src/utilities/isolateObjectProperty.ts +++ b/packages/payload/src/utilities/isolateObjectProperty.ts @@ -1,20 +1,33 @@ +/* eslint-disable no-restricted-exports */ /** * Creates a proxy for the given object that has its own property */ -export default function isolateObjectProperty(object: T, key): T { - const delegate = {} - const handler = { +export default function isolateObjectProperty( + object: T, + key: (keyof T)[] | keyof T, +): T { + const keys = Array.isArray(key) ? key : [key] + const delegate = {} as T + + // Initialize delegate with the keys, if they exist in the original object + for (const k of keys) { + if (k in object) { + delegate[k] = object[k] + } + } + + const handler: ProxyHandler = { deleteProperty(target, p): boolean { - return Reflect.deleteProperty(p === key ? delegate : target, p) + return Reflect.deleteProperty(keys.includes(p as keyof T) ? delegate : target, p) }, get(target, p, receiver) { - return Reflect.get(p === key ? delegate : target, p, receiver) + return Reflect.get(keys.includes(p as keyof T) ? delegate : target, p, receiver) }, has(target, p) { - return Reflect.has(p === key ? delegate : target, p) + return Reflect.has(keys.includes(p as keyof T) ? delegate : target, p) }, set(target, p, newValue, receiver) { - if (p === key) { + if (keys.includes(p as keyof T)) { // in case of transactionID we must ignore any receiver, because // "If provided and target does not have a setter for propertyKey, the property will be set on receiver instead." return Reflect.set(delegate, p, newValue) diff --git a/packages/payload/src/utilities/killTransaction.ts b/packages/payload/src/utilities/killTransaction.ts index 3c8fc2dd82..79e63074d4 100644 --- a/packages/payload/src/utilities/killTransaction.ts +++ b/packages/payload/src/utilities/killTransaction.ts @@ -5,7 +5,7 @@ import type { PayloadRequest } from '../types/index.js' */ export async function killTransaction(req: PayloadRequest): Promise { const { payload, transactionID } = req - if (transactionID) { + if (transactionID && !(transactionID instanceof Promise)) { await payload.db.rollbackTransaction(req.transactionID) delete req.transactionID } diff --git a/packages/richtext-lexical/src/features/link/plugins/autoLink/index.tsx b/packages/richtext-lexical/src/features/link/plugins/autoLink/index.tsx index b89a425f31..2a52f6402f 100644 --- a/packages/richtext-lexical/src/features/link/plugins/autoLink/index.tsx +++ b/packages/richtext-lexical/src/features/link/plugins/autoLink/index.tsx @@ -1,5 +1,5 @@ 'use client' -import type { ElementNode, LexicalEditor, LexicalNode } from 'lexical' +import type { ElementNode, LexicalEditor, LexicalNode, TextNode } from 'lexical' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext.js' import { mergeRegister } from '@lexical/utils' @@ -11,7 +11,6 @@ import { $isNodeSelection, $isRangeSelection, $isTextNode, - type TextNode, TextNode as TextNodeValue, } from 'lexical' import { useEffect } from 'react' diff --git a/packages/ui/src/elements/WhereBuilder/reduceFieldMap.tsx b/packages/ui/src/elements/WhereBuilder/reduceFieldMap.tsx index f2d499e7f5..2210b50ecc 100644 --- a/packages/ui/src/elements/WhereBuilder/reduceFieldMap.tsx +++ b/packages/ui/src/elements/WhereBuilder/reduceFieldMap.tsx @@ -1,7 +1,7 @@ 'use client' -import type { ClientTranslationKeys, I18nClient } from '@payloadcms/translations' +import type { ClientTranslationKeys, I18nClient } from '@payloadcms/translations' -import { getTranslation } from '@payloadcms/translations'; +import { getTranslation } from '@payloadcms/translations' import type { FieldMap } from '../../utilities/buildComponentMap.js' diff --git a/test/dataloader/config.ts b/test/dataloader/config.ts index e8af00ed97..51c7524745 100644 --- a/test/dataloader/config.ts +++ b/test/dataloader/config.ts @@ -67,6 +67,48 @@ export default buildConfigWithDefaults({ singular: 'Relation B', }, }, + { + fields: [ + { + name: 'name', + type: 'text', + }, + { + name: 'items', + type: 'relationship', + relationTo: 'items', + hasMany: true, + }, + ], + slug: 'shops', + access: { read: () => true }, + }, + { + fields: [ + { + name: 'name', + type: 'text', + }, + { + name: 'itemTags', + type: 'relationship', + relationTo: 'itemTags', + hasMany: true, + }, + ], + slug: 'items', + access: { read: () => true }, + }, + { + fields: [ + { + name: 'name', + type: 'text', + }, + ], + slug: 'itemTags', + access: { read: () => true }, + }, ], onInit: async (payload) => { const user = await payload.create({ @@ -82,6 +124,19 @@ export default buildConfigWithDefaults({ data: postDoc, user, }) + + const tag = await payload.create({ + collection: 'itemTags', + data: { name: 'tag1' }, + }) + const item = await payload.create({ + collection: 'items', + data: { name: 'item1', itemTags: [tag.id] }, + }) + const shop = await payload.create({ + collection: 'shops', + data: { name: 'shop1', items: [item.id] }, + }) }, typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), diff --git a/test/dataloader/int.spec.ts b/test/dataloader/int.spec.ts index 2d57ab042d..19414d9e8f 100644 --- a/test/dataloader/int.spec.ts +++ b/test/dataloader/int.spec.ts @@ -32,6 +32,45 @@ describe('dataloader', () => { }) describe('graphql', () => { + it('should allow multiple parallel queries', async () => { + for (let i = 0; i < 100; i++) { + const query = ` + query { + Shops { + docs { + name + items { + name + } + } + } + Items { + docs { + name + itemTags { + name + } + } + } + }` + const { data } = await restClient + .GRAPHQL_POST({ + body: JSON.stringify({ query }), + headers: { + Authorization: `JWT ${token}`, + }, + }) + .then((res) => res.json()) + + const normalizedResponse = JSON.parse(JSON.stringify(data)) + + expect(normalizedResponse).toStrictEqual({ + Shops: { docs: [{ name: 'shop1', items: [{ name: 'item1' }] }] }, + Items: { docs: [{ name: 'item1', itemTags: [{ name: 'tag1' }] }] }, + }) + } + }) + it('should allow querying via graphql', async () => { const query = `query { Posts {