Files
payloadcms/test/select/postgreslogs.int.spec.ts
Alessio Gravili aeee0704dd chore: add new int test verifying that select *improves* performance of new optimization (#13254)
https://github.com/payloadcms/payload/pull/13186 actually made the
select API _more powerful_, as it can reduce the amount of db calls even
for complex collections with blocks down to 1.

This PR adds a test that verifies this.

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210871676349303
2025-07-23 16:48:25 -07:00

252 lines
6.6 KiB
TypeScript

/* eslint-disable jest/require-top-level-describe */
import type { Payload } from 'payload'
import path from 'path'
import { assert } from 'ts-essentials'
import { fileURLToPath } from 'url'
import type { Point, Post } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
let payload: Payload
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const describePostgres = process.env.PAYLOAD_DATABASE === 'postgres' ? describe : describe.skip
describePostgres('Select - with postgres logs', () => {
// --__--__--__--__--__--__--__--__--__
// Boilerplate test setup/teardown
// --__--__--__--__--__--__--__--__--__
beforeAll(async () => {
const initialized = await initPayloadInt(
dirname,
undefined,
undefined,
'config.postgreslogs.ts',
)
assert(initialized.payload)
assert(initialized.restClient)
;({ payload } = initialized)
})
afterAll(async () => {
await payload.destroy()
})
describe('Local API - Base', () => {
let post: Post
let postId: number | string
let point: Point
let pointId: number | string
beforeEach(async () => {
post = await createPost()
postId = post.id
point = await createPoint()
pointId = point.id
})
// Clean up to safely mutate in each test
afterEach(async () => {
await payload.delete({ id: postId, collection: 'posts' })
await payload.delete({ id: pointId, collection: 'points' })
})
it('ensure optimized db update is still used when using select', async () => {
const post = await createPost()
// Count every console log
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
const res = removeEmptyAndUndefined(
(await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: {
text: 'new text',
},
select: { text: true, number: true },
})) as any,
)
expect(consoleCount).toHaveBeenCalledTimes(1) // Should be 1 single sql call if the optimization is used. If not, this would be 2 calls
consoleCount.mockRestore()
expect(res.number).toEqual(1)
expect(res.text).toEqual('new text')
expect(res.id).toEqual(post.id)
expect(Object.keys(res)).toHaveLength(3)
})
// This verifies that select actually improves performance of simple updates for complex collections.
// This is possible as no `with` is returned by buildFindManyArgs for the blocks field, only if we have a select that does not select that blocks field.
it('ensure simple update of complex collection uses optimized upsertRow with optimized returning() if only simple fields are selected', async () => {
const page = await payload.create({
collection: 'pages',
data: {
slug: 'test-page',
additional: 'value',
blocks: [
{
id: '123',
blockType: 'some',
other: 'value',
title: 'Test Block',
},
],
},
})
// Count every console log
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
const res = removeEmptyAndUndefined(
(await payload.db.updateOne({
collection: 'pages',
id: page.id,
select: {
slug: true,
additional: true,
},
data: {
slug: 'new-slug',
},
})) as any,
)
expect(consoleCount).toHaveBeenCalledTimes(1) // Should be 1 single sql call if the optimization is used. If not, this would be 2 calls
consoleCount.mockRestore()
expect(res.slug).toEqual('new-slug')
expect(res.additional).toEqual('value')
expect(res.id).toEqual(page.id)
expect(Object.keys(res)).toHaveLength(3)
// Do full find without select just to ensure that the update worked
const fullPage: any = await payload.findByID({
collection: 'pages',
id: page.id,
})
delete fullPage.createdAt
delete fullPage.updatedAt
delete fullPage.array
delete fullPage.content
expect(fullPage).toEqual({
id: page.id,
slug: 'new-slug',
additional: 'value',
relatedPage: null,
blocks: [
{
id: '123',
blockType: 'some',
blockName: null,
other: 'value',
title: 'Test Block',
},
],
})
})
})
})
function removeEmptyAndUndefined(obj: any): any {
if (Array.isArray(obj)) {
const cleanedArray = obj
.map(removeEmptyAndUndefined)
.filter(
(item) =>
item !== undefined && !(typeof item === 'object' && Object.keys(item).length === 0),
)
return cleanedArray.length > 0 ? cleanedArray : undefined
}
if (obj !== null && typeof obj === 'object') {
const cleanedEntries = Object.entries(obj)
.map(([key, value]) => [key, removeEmptyAndUndefined(value)])
.filter(([, value]) => {
return (
value !== undefined &&
value !== null &&
!(
typeof value === 'object' &&
(Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0)
)
)
})
return cleanedEntries.length > 0 ? Object.fromEntries(cleanedEntries) : undefined
}
return obj
}
async function createPost() {
const upload = await payload.create({
collection: 'upload',
data: {},
filePath: path.resolve(dirname, 'image.jpg'),
})
const relation = await payload.create({
depth: 0,
collection: 'rels',
data: {},
})
return payload.create({
collection: 'posts',
depth: 0,
data: {
number: 1,
text: 'text',
select: 'a',
selectMany: ['a'],
group: {
number: 1,
text: 'text',
},
hasMany: [relation],
hasManyUpload: [upload],
hasOne: relation,
hasManyPoly: [{ relationTo: 'rels', value: relation }],
hasOnePoly: { relationTo: 'rels', value: relation },
blocks: [
{
blockType: 'cta',
ctaText: 'cta-text',
text: 'text',
},
{
blockType: 'intro',
introText: 'intro-text',
text: 'text',
},
],
array: [
{
text: 'text',
number: 1,
},
],
tab: {
text: 'text',
number: 1,
},
unnamedTabNumber: 2,
unnamedTabText: 'text2',
},
})
}
function createPoint() {
return payload.create({ collection: 'points', data: { text: 'some', point: [10, 20] } })
}