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 59 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
e685d43
Add manual join option to Mongo adapter
elliott-w Jun 7, 2025
3332aa9
Disable memory server if DATABASE_URI set
elliott-w Jun 7, 2025
5655216
Prevent createIndex and dropDatabase operations if PAYLOAD_MANUAL_JOI…
elliott-w Jun 7, 2025
b452ddc
Add support for PAYLOAD_DATABASE to be 'firestore'
elliott-w Jun 8, 2025
5b64c40
Fix dropDatabase monkey patch
elliott-w Jun 8, 2025
1060438
Fix customIDType 'number'
elliott-w Jun 8, 2025
b02796a
More work
elliott-w Jun 8, 2025
6400b5b
Fix buildSortParam
elliott-w Jun 8, 2025
f29e1be
Fix buildSortParam
elliott-w Jun 10, 2025
c76c961
Delete manual joins folder
elliott-w Jun 10, 2025
6eb5e9f
Polymorphic joins
elliott-w Jun 10, 2025
04c2cd5
better polymorphic joins
elliott-w Jun 10, 2025
32834cf
More progress
elliott-w Jun 10, 2025
95a466a
Fix queryDrafts
elliott-w Jun 10, 2025
575b11d
remove __resolveJoins flag
elliott-w Jun 10, 2025
b10d3d7
Revert resolveJoins
elliott-w Jun 10, 2025
c9aaf88
Fix returning 0 documents rather than all when limit is 0
elliott-w Jun 11, 2025
1baaada
Fix queryDrafts
elliott-w Jun 11, 2025
8123768
Use similar versions/drafts detection to buildJoinAggregation
elliott-w Jun 11, 2025
fbb13ea
Fix test "should join across multiple collections"
elliott-w Jun 11, 2025
5c5075b
Sinplify polymorphic joins
elliott-w Jun 11, 2025
bc28e5f
Fix test "should not throw a path validation error when querying join…
elliott-w Jun 11, 2025
a96a6ea
Fix type and lint issues
elliott-w Jun 11, 2025
54dfcf5
Fix failing test "should rollback operations on failure"
elliott-w Jun 11, 2025
2141268
Remove transactions
elliott-w Jun 11, 2025
a2b867c
Optimize resolveJoins performance
elliott-w Jun 11, 2025
60efea6
Update mongodb documentation
elliott-w Jun 11, 2025
6b33e2d
Fix mongoose adapter args
elliott-w Jun 11, 2025
2797699
Revert files to main
elliott-w Jun 11, 2025
e5acc57
Re-order firestore adapter
elliott-w Jun 11, 2025
2308ffd
Rename `manualJoins` -> `useJoinAggregations`
elliott-w Jun 13, 2025
22c1145
Move utility functions to bottom of file
elliott-w Jun 14, 2025
f315e80
Improve resolveJoins type comments
elliott-w Jun 14, 2025
25cc5aa
Fix transform not being applied for version documents
elliott-w Jun 14, 2025
a8775ad
Fix polymorphic collection join test expectation
elliott-w Jun 14, 2025
ec5be8c
Add commonTitle field to polymorphic test collections
elliott-w Jun 14, 2025
ecf8987
Step
elliott-w Jun 14, 2025
5d911f0
Fix typescript and lint errors
elliott-w Jun 14, 2025
c2f97cb
Fix test 'should join across multiple collections'
elliott-w Jun 14, 2025
3142336
Add ability to sort by multiple properties
elliott-w Jun 14, 2025
47fa002
Add projections
elliott-w Jun 15, 2025
bdb2a3a
Extract buildJoinProjection
elliott-w Jun 15, 2025
92f9f3a
Simplify by treating all joins as polymorphic
elliott-w Jun 16, 2025
d53a384
Revert joins int.spec.ts
elliott-w Jun 16, 2025
66f207b
Fix bugs
elliott-w Jun 16, 2025
de8954b
Apply limit and page client-side
elliott-w Jun 16, 2025
8707bd5
Fix joinPath on arrays
elliott-w Jun 16, 2025
e4de5dc
Fix polymorphic relationship fields
elliott-w Jun 16, 2025
3e6096f
Fix versions
elliott-w Jun 16, 2025
5c505cf
Add compatabilityOptions utility and remove compatabilityMode arg
elliott-w Jun 16, 2025
7982056
Revert "Add commonTitle field to polymorphic test collections"
elliott-w Jun 16, 2025
042c349
Fix lint errors resolveJoins
elliott-w Jun 16, 2025
ce6dac1
Improve generateDatabaseAdapter firestore settings
elliott-w Jun 16, 2025
7275656
Add firestore to database testing matrix
elliott-w Jun 16, 2025
8c27e55
Tidy up comments
elliott-w Jun 16, 2025
f27525e
Update documentation
elliott-w Jun 17, 2025
90d34c9
Apply sort and pagination at the database level for non-polymorphic j…
elliott-w Jun 18, 2025
d5b6802
Fix int-firestore job
elliott-w Jun 26, 2025
132461b
Use normal dropDatabase for testing
elliott-w Jun 28, 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 @@ -169,6 +169,7 @@ jobs:
matrix:
database:
- mongodb
- firestore
- postgres
- postgres-custom-schema
- postgres-uuid
Expand Down
50 changes: 33 additions & 17 deletions docs/database/mongodb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,22 @@ 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. |
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
| 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. |
| `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. |
| `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 @@ -56,9 +60,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()
}
}
}

Comment on lines +39 to 58
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this also drop existing indexes and collections themselves outside of just deleting all the documents?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@r1tsuu ideally yes, but firestore doesn't support managing (CRUD) indexes through the Mongo API (Wire Protocol). It also doesn't support the dropCollection command. It all has to be done through the Google Cloud Console or CLI.

I don't think this is that big of an issue though, because the only time payload ever drops a database is during testing and it's fine to pollute a test db with empty collections.

// 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 @@ -143,6 +143,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 @@ -159,6 +182,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 @@ -194,6 +221,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 @@ -214,6 +245,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 @@ -279,6 +314,10 @@ export function mongooseAdapter({
updateOne,
updateVersion,
upsert,
useAlternativeDropDatabase,
useBigIntForNumberIDs,
useJoinAggregations,
usePipelineInSortLookup,
})
}

Expand All @@ -290,6 +329,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
Loading