Skip to content

fix(db-mongodb): improve compatability with Firestore database #12763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c182997
Add manual join option to Mongo adapter
elliott-w Jun 7, 2025
42d4ca5
Disable memory server if DATABASE_URI set
elliott-w Jun 7, 2025
620d053
Prevent createIndex and dropDatabase operations if PAYLOAD_MANUAL_JOI…
elliott-w Jun 7, 2025
7b9b256
Add support for PAYLOAD_DATABASE to be 'firestore'
elliott-w Jun 8, 2025
824ab15
Fix dropDatabase monkey patch
elliott-w Jun 8, 2025
a78ba19
Fix customIDType 'number'
elliott-w Jun 8, 2025
eebaa46
More work
elliott-w Jun 8, 2025
5f7a8c7
Fix buildSortParam
elliott-w Jun 8, 2025
6004bf7
Fix buildSortParam
elliott-w Jun 10, 2025
75ec812
Delete manual joins folder
elliott-w Jun 10, 2025
e2230e0
Polymorphic joins
elliott-w Jun 10, 2025
2d09272
better polymorphic joins
elliott-w Jun 10, 2025
ef3fe35
More progress
elliott-w Jun 10, 2025
320050d
Fix queryDrafts
elliott-w Jun 10, 2025
e533186
remove __resolveJoins flag
elliott-w Jun 10, 2025
67113e4
Revert resolveJoins
elliott-w Jun 10, 2025
e8ec31d
Fix returning 0 documents rather than all when limit is 0
elliott-w Jun 11, 2025
6db79b7
Fix queryDrafts
elliott-w Jun 11, 2025
912af71
Use similar versions/drafts detection to buildJoinAggregation
elliott-w Jun 11, 2025
99f3586
Fix test "should join across multiple collections"
elliott-w Jun 11, 2025
dae7a80
Sinplify polymorphic joins
elliott-w Jun 11, 2025
c40062c
Fix test "should not throw a path validation error when querying join…
elliott-w Jun 11, 2025
b9b0542
Fix type and lint issues
elliott-w Jun 11, 2025
d348921
Fix failing test "should rollback operations on failure"
elliott-w Jun 11, 2025
09a2996
Remove transactions
elliott-w Jun 11, 2025
dad811c
Optimize resolveJoins performance
elliott-w Jun 11, 2025
a5e9178
Update mongodb documentation
elliott-w Jun 11, 2025
717b311
Fix mongoose adapter args
elliott-w Jun 11, 2025
a364549
Revert files to main
elliott-w Jun 11, 2025
da01bfb
Re-order firestore adapter
elliott-w Jun 11, 2025
e62a1cd
Rename `manualJoins` -> `useJoinAggregations`
elliott-w Jun 13, 2025
485a51c
Move utility functions to bottom of file
elliott-w Jun 14, 2025
b23c2b2
Improve resolveJoins type comments
elliott-w Jun 14, 2025
acdb24b
Fix transform not being applied for version documents
elliott-w Jun 14, 2025
c9b2d83
Fix polymorphic collection join test expectation
elliott-w Jun 14, 2025
853e0c6
Add commonTitle field to polymorphic test collections
elliott-w Jun 14, 2025
75546af
Step
elliott-w Jun 14, 2025
dfcedfe
Fix typescript and lint errors
elliott-w Jun 14, 2025
d8be930
Fix test 'should join across multiple collections'
elliott-w Jun 14, 2025
34d4d20
Add ability to sort by multiple properties
elliott-w Jun 14, 2025
3437f4b
Add projections
elliott-w Jun 15, 2025
7aa4476
Extract buildJoinProjection
elliott-w Jun 15, 2025
d9c87ee
Simplify by treating all joins as polymorphic
elliott-w Jun 16, 2025
f8e8322
Revert joins int.spec.ts
elliott-w Jun 16, 2025
a10a552
Fix bugs
elliott-w Jun 16, 2025
0253b27
Apply limit and page client-side
elliott-w Jun 16, 2025
0dae7f2
Fix joinPath on arrays
elliott-w Jun 16, 2025
0662b95
Fix polymorphic relationship fields
elliott-w Jun 16, 2025
30334c5
Fix versions
elliott-w Jun 16, 2025
7420449
Add compatabilityOptions utility and remove compatabilityMode arg
elliott-w Jun 16, 2025
d49213a
Revert "Add commonTitle field to polymorphic test collections"
elliott-w Jun 16, 2025
a8482fd
Fix lint errors resolveJoins
elliott-w Jun 16, 2025
833c504
Improve generateDatabaseAdapter firestore settings
elliott-w Jun 16, 2025
f092e8d
Add firestore to database testing matrix
elliott-w Jun 16, 2025
235af62
Tidy up comments
elliott-w Jun 16, 2025
5d14cdc
Update documentation
elliott-w Jun 17, 2025
90ee5a7
Apply sort and pagination at the database level for non-polymorphic j…
elliott-w Jun 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ jobs:
matrix:
database:
- mongodb
- firestore
- postgres
- postgres-custom-schema
- postgres-uuid
Expand Down
48 changes: 32 additions & 16 deletions docs/database/mongodb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,21 @@ export default buildConfig({

## Options

| Option | Description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| Option | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. |
| `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. |
| `collectionsSchemaOptions` | Customize Mongoose schema options for collections. |
| `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false |
| `migrationDir` | Customize the directory that migrations are stored. |
| `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. |
| `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). |
| `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. |
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
| `useAlternativeDropDatabase` | Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command. Payload only uses `dropDatabase` for testing purposes. Defaults to `false`. |
| `useBigIntForNumberIDs` | Set to `true` to use `BigInt` for custom ID fields of type `'number'`. Useful for databases that don't support `double` or `int32` IDs. Defaults to `false`. |
| `useJoinAggregations` | Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries. Defaults to `true`. |
| `usePipelineInSortLookup` | Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting. Defaults to `true`. |

## Access to Mongoose models

Expand All @@ -55,9 +59,21 @@ You can access Mongoose models as follows:

## Using other MongoDB implementations

Limitations with [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db):
You can import the `compatabilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated):

- For Azure Cosmos DB you must pass `transactionOptions: false` to the adapter options. Azure Cosmos DB does not support transactions that update two and more documents in different collections, which is a common case when using Payload (via hooks).
- For Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
- The [Join Field](../fields/join) is not supported in DocumentDB and Azure Cosmos DB, as we internally use MongoDB aggregations to query data for that field, which are limited there. This can be changed in the future.
- For DocumentDB pass `disableIndexHints: true` to disable hinting to the DB to use `id` as index which can cause problems with DocumentDB.
```ts
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'

export default buildConfig({
db: mongooseAdapter({
url: process.env.DATABASE_URI,
// For example, if you're using firestore:
...compatabilityOptions.firestore,
}),
})
```

We export compatability options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations:

- Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks).
- Azure Cosmos DB the root config property `indexSortableFields` must be set to `true`.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"test:e2e:prod:ci": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod",
"test:e2e:prod:ci:noturbo": "pnpm prepare-run-test-against-prod:ci && pnpm runts ./test/runE2E.ts --prod --no-turbo",
"test:int": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:firestore": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=firestore DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:postgres": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:int:sqlite": "cross-env NODE_OPTIONS=\"--no-deprecation --no-experimental-strip-types\" NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=sqlite DISABLE_LOGGING=true jest --forceExit --detectOpenHandles --config=test/jest.config.js --runInBand",
"test:types": "tstyche",
Expand Down
19 changes: 19 additions & 0 deletions packages/db-mongodb/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ export const connect: Connect = async function connect(

try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
if (this.useAlternativeDropDatabase) {
if (this.connection.db) {
// Firestore doesn't support dropDatabase, so we monkey patch
// dropDatabase to delete all documents from all collections instead
this.connection.db.dropDatabase = async function (): Promise<boolean> {
const existingCollections = await this.listCollections().toArray()
await Promise.all(
existingCollections.map(async (collectionInfo) => {
const collection = this.collection(collectionInfo.name)
await collection.deleteMany({})
}),
)
return true
}
this.connection.dropDatabase = async function () {
await this.db?.dropDatabase()
}
}
}

// If we are running a replica set with MongoDB Memory Server,
// wait until the replica set elects a primary before proceeding
Expand Down
11 changes: 11 additions & 0 deletions packages/db-mongodb/src/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { resolveJoins } from './utilities/resolveJoins.js'
import { transform } from './utilities/transform.js'

export const find: Find = async function find(
Expand Down Expand Up @@ -155,6 +156,16 @@ export const find: Find = async function find(
result = await Model.paginate(query, paginationOptions)
}

if (!this.useJoinAggregations) {
await resolveJoins({
adapter: this,
collectionSlug,
docs: result.docs as Record<string, unknown>[],
joins,
locale,
})
}

transform({
adapter: this,
data: result.docs,
Expand Down
11 changes: 11 additions & 0 deletions packages/db-mongodb/src/findOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getCollection } from './utilities/getEntity.js'
import { getSession } from './utilities/getSession.js'
import { resolveJoins } from './utilities/resolveJoins.js'
import { transform } from './utilities/transform.js'

export const findOne: FindOne = async function findOne(
Expand Down Expand Up @@ -67,6 +68,16 @@ export const findOne: FindOne = async function findOne(
doc = await Model.findOne(query, {}, options)
}

if (doc && !this.useJoinAggregations) {
await resolveJoins({
adapter: this,
collectionSlug,
docs: [doc] as Record<string, unknown>[],
joins,
locale,
})
}

if (!doc) {
return null
}
Expand Down
41 changes: 41 additions & 0 deletions packages/db-mongodb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,29 @@ export interface Args {

/** The URL to connect to MongoDB or false to start payload and prevent connecting */
url: false | string

/**
* Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command.
* Payload only uses `dropDatabase` for testing purposes.
* @default false
*/
useAlternativeDropDatabase?: boolean
/**
* Set to `true` to use `BigInt` for custom ID fields of type `'number'`.
* Useful for databases that don't support `double` or `int32` IDs.
* @default false
*/
useBigIntForNumberIDs?: boolean
/**
* Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries.
* @default true
*/
useJoinAggregations?: boolean
/**
* Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting.
* @default true
*/
usePipelineInSortLookup?: boolean
}

export type MongooseAdapter = {
Expand All @@ -151,6 +174,10 @@ export type MongooseAdapter = {
up: (args: MigrateUpArgs) => Promise<void>
}[]
sessions: Record<number | string, ClientSession>
useAlternativeDropDatabase: boolean
useBigIntForNumberIDs: boolean
useJoinAggregations: boolean
usePipelineInSortLookup: boolean
versions: {
[slug: string]: CollectionModel
}
Expand Down Expand Up @@ -186,6 +213,10 @@ declare module 'payload' {
updateVersion: <T extends TypeWithID = TypeWithID>(
args: { options?: QueryOptions } & UpdateVersionArgs<T>,
) => Promise<TypeWithVersion<T>>
useAlternativeDropDatabase: boolean
useBigIntForNumberIDs: boolean
useJoinAggregations: boolean
usePipelineInSortLookup: boolean
versions: {
[slug: string]: CollectionModel
}
Expand All @@ -205,6 +236,10 @@ export function mongooseAdapter({
prodMigrations,
transactionOptions = {},
url,
useAlternativeDropDatabase = false,
useBigIntForNumberIDs = false,
useJoinAggregations = true,
usePipelineInSortLookup = true,
}: Args): DatabaseAdapterObj {
function adapter({ payload }: { payload: Payload }) {
const migrationDir = findMigrationDir(migrationDirArg)
Expand Down Expand Up @@ -269,6 +304,10 @@ export function mongooseAdapter({
updateOne,
updateVersion,
upsert,
useAlternativeDropDatabase,
useBigIntForNumberIDs,
useJoinAggregations,
usePipelineInSortLookup,
})
}

Expand All @@ -280,6 +319,8 @@ export function mongooseAdapter({
}
}

export { compatabilityOptions } from './utilities/compatabilityOptions.js'

/**
* Attempt to find migrations directory.
*
Expand Down
13 changes: 11 additions & 2 deletions packages/db-mongodb/src/models/buildSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,12 @@ export const buildSchema = (args: {
const idField = schemaFields.find((field) => fieldAffectsData(field) && field.name === 'id')
if (idField) {
fields = {
_id: idField.type === 'number' ? Number : String,
_id:
idField.type === 'number'
? payload.db.useBigIntForNumberIDs
? mongoose.Schema.Types.BigInt
: Number
: String,
}
schemaFields = schemaFields.filter(
(field) => !(fieldAffectsData(field) && field.name === 'id'),
Expand Down Expand Up @@ -900,7 +905,11 @@ const getRelationshipValueType = (field: RelationshipField | UploadField, payloa
}

if (customIDType === 'number') {
return mongoose.Schema.Types.Number
if (payload.db.useBigIntForNumberIDs) {
return mongoose.Schema.Types.BigInt
} else {
return mongoose.Schema.Types.Number
}
}

return mongoose.Schema.Types.String
Expand Down
Loading