Merge remote-tracking branch 'origin/main' into chore/bump-next-15.4.1

This commit is contained in:
Alessio Gravili
2025-07-17 09:35:43 -07:00
58 changed files with 2474 additions and 572 deletions

View File

@@ -1,7 +1,13 @@
import type { MongooseAdapter } from '@payloadcms/db-mongodb'
import type { PostgresAdapter } from '@payloadcms/db-postgres/types'
import type { NextRESTClient } from 'helpers/NextRESTClient.js'
import type { Payload, PayloadRequest, TypeWithID, ValidationError } from 'payload'
import type {
DataFromCollectionSlug,
Payload,
PayloadRequest,
TypeWithID,
ValidationError,
} from 'payload'
import {
migrateRelationshipsV2_V3,
@@ -379,6 +385,118 @@ describe('database', () => {
})
})
it('should find distinct field values of the collection', async () => {
await payload.delete({ collection: 'posts', where: {} })
const titles = [
'title-1',
'title-2',
'title-3',
'title-4',
'title-5',
'title-6',
'title-7',
'title-8',
'title-9',
].map((title) => ({ title }))
for (const { title } of titles) {
// eslint-disable-next-line jest/no-conditional-in-test
const docsCount = Math.random() > 0.5 ? 3 : Math.random() > 0.5 ? 2 : 1
for (let i = 0; i < docsCount; i++) {
await payload.create({ collection: 'posts', data: { title } })
}
}
const res = await payload.findDistinct({
collection: 'posts',
field: 'title',
})
expect(res.values).toStrictEqual(titles)
// const resREST = await restClient
// .GET('/posts/distinct', {
// headers: {
// Authorization: `Bearer ${token}`,
// },
// query: { sortOrder: 'asc', field: 'title' },
// })
// .then((res) => res.json())
// expect(resREST.values).toEqual(titles)
const resLimit = await payload.findDistinct({
collection: 'posts',
field: 'title',
limit: 3,
})
expect(resLimit.values).toStrictEqual(
['title-1', 'title-2', 'title-3'].map((title) => ({ title })),
)
// count is still 9
expect(resLimit.totalDocs).toBe(9)
const resDesc = await payload.findDistinct({
collection: 'posts',
sort: '-title',
field: 'title',
})
expect(resDesc.values).toStrictEqual(titles.toReversed())
const resAscDefault = await payload.findDistinct({
collection: 'posts',
field: 'title',
})
expect(resAscDefault.values).toStrictEqual(titles)
})
it('should populate distinct relationships when depth>0', async () => {
await payload.delete({ collection: 'posts', where: {} })
const categories = ['category-1', 'category-2', 'category-3', 'category-4'].map((title) => ({
title,
}))
const categoriesIDS: { category: string }[] = []
for (const { title } of categories) {
const doc = await payload.create({ collection: 'categories', data: { title } })
categoriesIDS.push({ category: doc.id })
}
for (const { category } of categoriesIDS) {
// eslint-disable-next-line jest/no-conditional-in-test
const docsCount = Math.random() > 0.5 ? 3 : Math.random() > 0.5 ? 2 : 1
for (let i = 0; i < docsCount; i++) {
await payload.create({ collection: 'posts', data: { title: randomUUID(), category } })
}
}
const resultDepth0 = await payload.findDistinct({
collection: 'posts',
sort: 'category.title',
field: 'category',
})
expect(resultDepth0.values).toStrictEqual(categoriesIDS)
const resultDepth1 = await payload.findDistinct({
depth: 1,
collection: 'posts',
field: 'category',
sort: 'category.title',
})
for (let i = 0; i < resultDepth1.values.length; i++) {
const fromRes = resultDepth1.values[i] as any
const id = categoriesIDS[i].category as any
const title = categories[i]?.title
expect(fromRes.category.title).toBe(title)
expect(fromRes.category.id).toBe(id)
}
})
describe('Compound Indexes', () => {
beforeEach(async () => {
await payload.delete({ collection: 'compound-indexes', where: {} })
@@ -2807,7 +2925,7 @@ describe('database', () => {
}
})
it('should update simple', async () => {
it('should use optimized updateOne', async () => {
const post = await payload.create({
collection: 'posts',
data: {
@@ -2818,7 +2936,7 @@ describe('database', () => {
arrayWithIDs: [{ text: 'some text' }],
},
})
const res = await payload.db.updateOne({
const res = (await payload.db.updateOne({
where: { id: { equals: post.id } },
data: {
title: 'hello updated',
@@ -2826,14 +2944,89 @@ describe('database', () => {
tab: { text: 'in tab updated' },
},
collection: 'posts',
})
})) as unknown as DataFromCollectionSlug<'posts'>
expect(res.title).toBe('hello updated')
expect(res.text).toBe('other text (should not be nuked)')
expect(res.group.text).toBe('in group updated')
expect(res.tab.text).toBe('in tab updated')
expect(res.group?.text).toBe('in group updated')
expect(res.tab?.text).toBe('in tab updated')
expect(res.arrayWithIDs).toHaveLength(1)
expect(res.arrayWithIDs[0].text).toBe('some text')
expect(res.arrayWithIDs?.[0]?.text).toBe('some text')
})
it('should use optimized updateMany', async () => {
const post1 = await payload.create({
collection: 'posts',
data: {
text: 'other text (should not be nuked)',
title: 'hello',
group: { text: 'in group' },
tab: { text: 'in tab' },
arrayWithIDs: [{ text: 'some text' }],
},
})
const post2 = await payload.create({
collection: 'posts',
data: {
text: 'other text 2 (should not be nuked)',
title: 'hello',
group: { text: 'in group' },
tab: { text: 'in tab' },
arrayWithIDs: [{ text: 'some text' }],
},
})
const res = (await payload.db.updateMany({
where: { id: { in: [post1.id, post2.id] } },
data: {
title: 'hello updated',
group: { text: 'in group updated' },
tab: { text: 'in tab updated' },
},
collection: 'posts',
})) as unknown as Array<DataFromCollectionSlug<'posts'>>
expect(res).toHaveLength(2)
const resPost1 = res?.find((r) => r.id === post1.id)
const resPost2 = res?.find((r) => r.id === post2.id)
expect(resPost1?.text).toBe('other text (should not be nuked)')
expect(resPost2?.text).toBe('other text 2 (should not be nuked)')
for (const post of res) {
expect(post.title).toBe('hello updated')
expect(post.group?.text).toBe('in group updated')
expect(post.tab?.text).toBe('in tab updated')
expect(post.arrayWithIDs).toHaveLength(1)
expect(post.arrayWithIDs?.[0]?.text).toBe('some text')
}
})
it('should allow incremental number update', async () => {
const post = await payload.create({ collection: 'posts', data: { number: 1, title: 'post' } })
const res = await payload.db.updateOne({
data: {
number: {
$inc: 10,
},
},
collection: 'posts',
where: { id: { equals: post.id } },
})
expect(res.number).toBe(11)
const res2 = await payload.db.updateOne({
data: {
number: {
$inc: -3,
},
},
collection: 'posts',
where: { id: { equals: post.id } },
})
expect(res2.number).toBe(8)
})
it('should support x3 nesting blocks', async () => {

View File

@@ -1,5 +1,5 @@
{
"id": "bf183b76-944c-4e83-bd58-4aa993885106",
"id": "80e7a0d2-ffb3-4f22-8597-0442b3ab8102",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",

View File

@@ -1,9 +1,9 @@
import * as migration_20250707_123508 from './20250707_123508.js'
import * as migration_20250714_201659 from './20250714_201659.js';
export const migrations = [
{
up: migration_20250707_123508.up,
down: migration_20250707_123508.down,
name: '20250707_123508',
up: migration_20250714_201659.up,
down: migration_20250714_201659.down,
name: '20250714_201659'
},
]
];

View File

@@ -21,6 +21,25 @@ export const allDatabaseAdapters = {
strength: 1,
},
})`,
firestore: `
import { mongooseAdapter, compatabilityOptions } from '@payloadcms/db-mongodb'
export const databaseAdapter = mongooseAdapter({
...compatabilityOptions.firestore,
url:
process.env.DATABASE_URI ||
process.env.MONGODB_MEMORY_SERVER_URI ||
'mongodb://127.0.0.1/payloadtests',
collation: {
strength: 1,
},
// The following options prevent some tests from failing.
// More work needed to get tests succeeding without these options.
ensureIndexes: true,
transactionOptions: {},
disableIndexHints: false,
useAlternativeDropDatabase: false,
})`,
postgres: `
import { postgresAdapter } from '@payloadcms/db-postgres'

View File

@@ -13,7 +13,7 @@ const dirname = path.dirname(filename)
const writeDBAdapter = process.env.WRITE_DB_ADAPTER !== 'false'
process.env.PAYLOAD_DROP_DATABASE = process.env.PAYLOAD_DROP_DATABASE || 'true'
if (process.env.PAYLOAD_DATABASE === 'mongodb') {
if (process.env.PAYLOAD_DATABASE === 'mongodb' || process.env.PAYLOAD_DATABASE === 'firestore') {
throw new Error('Not supported')
}

View File

@@ -16,7 +16,7 @@ import { devUser } from '../credentials.js'
type ValidPath = `/${string}`
type RequestOptions = {
auth?: boolean
query?: {
query?: { [key: string]: unknown } & {
depth?: number
fallbackLocale?: string
joins?: JoinQuery

View File

@@ -1,5 +1,8 @@
import type { Payload } from 'payload'
export function isMongoose(_payload?: Payload) {
return _payload?.db?.name === 'mongoose' || ['mongodb'].includes(process.env.PAYLOAD_DATABASE)
return (
_payload?.db?.name === 'mongoose' ||
['firestore', 'mongodb'].includes(process.env.PAYLOAD_DATABASE)
)
}

View File

@@ -14,13 +14,17 @@ declare global {
*/
// eslint-disable-next-line no-restricted-exports
export default async () => {
if (process.env.DATABASE_URI) {
return
}
process.env.NODE_ENV = 'test'
process.env.PAYLOAD_DROP_DATABASE = 'true'
process.env.NODE_OPTIONS = '--no-deprecation'
process.env.DISABLE_PAYLOAD_HMR = 'true'
if (
(!process.env.PAYLOAD_DATABASE || process.env.PAYLOAD_DATABASE === 'mongodb') &&
(!process.env.PAYLOAD_DATABASE ||
['firestore', 'mongodb'].includes(process.env.PAYLOAD_DATABASE)) &&
!global._mongoMemoryServer
) {
console.log('Starting memory db...')

View File

@@ -38,7 +38,7 @@ const dirname = path.dirname(filename)
type EasierChained = { id: string; relation: EasierChained }
const mongoIt = process.env.PAYLOAD_DATABASE === 'mongodb' ? it : it.skip
const mongoIt = ['firestore', 'mongodb'].includes(process.env.PAYLOAD_DATABASE || '') ? it : it.skip
describe('Relationships', () => {
beforeAll(async () => {
@@ -791,6 +791,47 @@ describe('Relationships', () => {
expect(localized_res_2.docs).toStrictEqual([movie_1, movie_2])
})
it('should sort by multiple properties of a relationship', async () => {
await payload.delete({ collection: 'directors', where: {} })
await payload.delete({ collection: 'movies', where: {} })
const createDirector = {
collection: 'directors',
data: {
name: 'Dan',
},
} as const
const director_1 = await payload.create(createDirector)
const director_2 = await payload.create(createDirector)
const movie_1 = await payload.create({
collection: 'movies',
depth: 0,
data: { director: director_1.id, name: 'Some Movie 1' },
})
const movie_2 = await payload.create({
collection: 'movies',
depth: 0,
data: { director: director_2.id, name: 'Some Movie 2' },
})
const res_1 = await payload.find({
collection: 'movies',
sort: ['director.name', 'director.createdAt'],
depth: 0,
})
const res_2 = await payload.find({
collection: 'movies',
sort: ['director.name', '-director.createdAt'],
depth: 0,
})
expect(res_1.docs).toStrictEqual([movie_1, movie_2])
expect(res_2.docs).toStrictEqual([movie_2, movie_1])
})
it('should sort by a property of a hasMany relationship', async () => {
const movie1 = await payload.create({
collection: 'movies',