Compare commits

...

2 Commits

Author SHA1 Message Date
Dan Ribbens
acf78d3665 chore: improve transaction errors 2023-12-12 10:41:38 -05:00
Dan Ribbens
2e5f76c3e5 feat(db-mongodb): add transactionOptions 2023-12-07 16:38:12 -05:00
6 changed files with 67 additions and 35 deletions

View File

@@ -23,7 +23,7 @@
"bson-objectid": "2.0.4",
"deepmerge": "4.3.1",
"get-port": "5.1.1",
"mongoose": "6.12.0",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
@@ -32,6 +32,7 @@
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"mongodb": "4.17.1",
"mongodb-memory-server": "8.13.0",
"payload": "workspace:*"
},

View File

@@ -48,6 +48,13 @@ export const connect: Connect = async function connect(this: MongooseAdapter, pa
try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
const client = this.connection.getClient()
if (!client.options.replicaSet || this.transactionOptions === false) {
this.transactionOptions = false
this.beginTransaction = undefined
}
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
this.payload.logger.info('---- DROPPING DATABASE ----')
await mongoose.connection.dropDatabase()

View File

@@ -1,3 +1,4 @@
import type { TransactionOptions } from 'mongodb'
import type { ClientSession, ConnectOptions, Connection } from 'mongoose'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
@@ -7,8 +8,6 @@ import mongoose from 'mongoose'
import path from 'path'
import { createDatabaseAdapter } from 'payload/database'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
import type { CollectionModel, GlobalModel } from './types'
import { connect } from './connect'
@@ -39,6 +38,8 @@ import { updateGlobalVersion } from './updateGlobalVersion'
import { updateOne } from './updateOne'
import { updateVersion } from './updateVersion'
export type { MigrateDownArgs, MigrateUpArgs } from './types'
export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean
@@ -50,6 +51,7 @@ export interface Args {
/** Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false */
disableIndexHints?: boolean
migrationDir?: string
transactionOptions?: TransactionOptions | false
/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string
}
@@ -81,6 +83,7 @@ declare module 'payload' {
globals: GlobalModel
mongoMemoryServer: any
sessions: Record<number | string, ClientSession>
transactionOptions: TransactionOptions
versions: {
[slug: string]: CollectionModel
}
@@ -92,15 +95,21 @@ export function mongooseAdapter({
connectOptions,
disableIndexHints = false,
migrationDir: migrationDirArg,
transactionOptions,
url,
}: Args): MongooseAdapterResult {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg)
let beginTransactionFunction = beginTransaction
mongoose.set('strictQuery', false)
extendWebpackConfig(payload.config)
extendViteConfig(payload.config)
if (transactionOptions === false) {
beginTransactionFunction = undefined
}
return createDatabaseAdapter<MongooseAdapter>({
name: 'mongoose',
@@ -113,11 +122,12 @@ export function mongooseAdapter({
globals: undefined,
mongoMemoryServer: undefined,
sessions: {},
transactionOptions: transactionOptions === false ? undefined : transactionOptions,
url,
versions: {},
// DatabaseAdapter
beginTransaction,
beginTransaction: beginTransactionFunction,
commitTransaction,
connect,
create,

View File

@@ -1,34 +1,30 @@
// @ts-expect-error // TODO: Fix this import
import type { TransactionOptions } from 'mongodb'
import type { BeginTransaction } from 'payload/database'
import { APIError } from 'payload/errors'
import { v4 as uuid } from 'uuid'
let transactionsNotAvailable: boolean
import type { MongooseAdapter } from '../index'
export const beginTransaction: BeginTransaction = async function beginTransaction(
options: TransactionOptions = {},
this: MongooseAdapter,
options: TransactionOptions,
) {
let id = null
if (!this.connection) {
throw new APIError('beginTransaction called while no connection to the database exists')
}
if (transactionsNotAvailable) return id
const client = this.connection.getClient()
if (!client.options.replicaSet) {
transactionsNotAvailable = true
} else {
id = uuid()
if (!this.sessions[id]) {
this.sessions[id] = await client.startSession()
}
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
await this.sessions[id].startTransaction(options)
}
const id = uuid()
if (!this.sessions[id]) {
this.sessions[id] = client.startSession()
}
if (this.sessions[id].inTransaction()) {
this.payload.logger.warn('beginTransaction called while transaction already exists')
} else {
this.sessions[id].startTransaction(options || (this.transactionOptions as TransactionOptions))
}
return id
}

View File

@@ -1,11 +1,26 @@
import type { ClientSession } from 'mongoose'
import type { CommitTransaction } from 'payload/database'
import { APIError } from 'payload/errors'
// eslint-disable-next-line @typescript-eslint/require-await
export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
if (!this.sessions[id]?.inTransaction()) {
if (!this.sessions[id]) {
// either transactions aren't being used (no replicaSet) or it was already aborted from an error elsewhere
return
}
await this.sessions[id].commitTransaction()
await this.sessions[id].endSession()
const session = this.sessions[id] as ClientSession
if (!session.inTransaction()) {
// session was expecting to be in a healthy state but is not
throw new APIError('commitTransaction called when no transaction exists')
}
await session.commitTransaction().catch((reason) => {
throw new APIError(`transaction could not be committed: ${reason}`)
})
await session.endSession()
delete this.sessions[id]
}

23
pnpm-lock.yaml generated
View File

@@ -439,8 +439,8 @@ importers:
specifier: 5.1.1
version: 5.1.1
mongoose:
specifier: 6.12.0
version: 6.12.0
specifier: 6.12.3
version: 6.12.3
mongoose-aggregate-paginate-v2:
specifier: 1.0.6
version: 1.0.6
@@ -460,6 +460,9 @@ importers:
'@types/mongoose-aggregate-paginate-v2':
specifier: 1.0.9
version: 1.0.9
mongodb:
specifier: 4.17.1
version: 4.17.1
mongodb-memory-server:
specifier: 8.13.0
version: 8.13.0
@@ -1889,10 +1892,10 @@ packages:
'@aws-sdk/credential-provider-sso': 3.414.0
'@aws-sdk/credential-provider-web-identity': 3.413.0
'@aws-sdk/types': 3.413.0
'@smithy/credential-provider-imds': 2.0.11
'@smithy/property-provider': 2.0.9
'@smithy/shared-ini-file-loader': 2.0.10
'@smithy/types': 2.3.2
'@smithy/credential-provider-imds': 2.0.16
'@smithy/property-provider': 2.0.12
'@smithy/shared-ini-file-loader': 2.2.0
'@smithy/types': 2.3.5
tslib: 2.6.2
transitivePeerDependencies:
- aws-crt
@@ -2260,7 +2263,7 @@ packages:
'@smithy/middleware-stack': 2.0.5
'@smithy/node-config-provider': 2.1.1
'@smithy/node-http-handler': 2.1.7
'@smithy/property-provider': 2.0.9
'@smithy/property-provider': 2.0.12
'@smithy/protocol-http': 3.0.7
'@smithy/shared-ini-file-loader': 2.0.10
'@smithy/smithy-client': 2.1.11
@@ -6078,7 +6081,7 @@ packages:
/@types/mongoose-aggregate-paginate-v2@1.0.9:
resolution: {integrity: sha512-YKDKtSuE1vzMY/SAtlDTWJr52UhTYdrOypCqyx7T2xFYEWfybLnV98m4ZoVgYJH0XowVl7Y2Gnn6p1sF+3NbLA==}
dependencies:
mongoose: 6.12.0
mongoose: 6.12.3
transitivePeerDependencies:
- aws-crt
- supports-color
@@ -13449,8 +13452,8 @@ packages:
- supports-color
dev: true
/mongoose@6.12.0:
resolution: {integrity: sha512-sd/q83C6TBRPBrrD2A/POSbA/exbCFM2WOuY7Lf2JuIJFlHFG39zYSDTTAEiYlzIfahNOLmXPxBGFxdAch41Mw==}
/mongoose@6.12.3:
resolution: {integrity: sha512-MNJymaaXali7w7rHBxVUoQ3HzHHMk/7I/+yeeoSa4rUzdjZwIWQznBNvVgc0A8ghuJwsuIkb5LyLV6gSjGjWyQ==}
engines: {node: '>=12.0.0'}
dependencies:
bson: 4.7.2