feat(db-mongodb,drizzle): add atomic array operations for relationship fields (#13891)
### What?
This PR adds atomic array operations ($append and $remove) for
relationship fields with `hasMany: true` across all database adapters.
These operations allow developers to add or remove specific items from
relationship arrays without replacing the entire array.
New API:
```
// Append relationships (prevents duplicates)
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
categories: { $append: ['featured', 'trending'] }
}
})
// Remove specific relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
tags: { $remove: ['draft', 'private'] }
}
})
// Works with polymorphic relationships
await payload.db.updateOne({
collection: 'posts',
id: 'post123',
data: {
relatedItems: {
$append: [
{ relationTo: 'categories', value: 'category-id' },
{ relationTo: 'tags', value: 'tag-id' }
]
}
}
})
```
### Why?
Currently, updating relationship arrays requires replacing the entire
array which requires fetching existing data before updates. Requiring
more implementation effort and potential for errors when using the API,
in particular for bulk updates.
### How?
#### Cross-Adapter Features:
- Polymorphic relationships: Full support for relationTo:
['collection1', 'collection2']
- Localized relationships: Proper locale handling when fields are
localized
- Duplicate prevention: Ensures `$append` doesn't create duplicates
- Order preservation: Appends to end of array maintaining order
- Bulk operations: Works with `updateMany` for bulk updates
#### MongoDB Implementation:
- Converts `$append` to native `$addToSet` (prevents duplicates in
contrast to `$push`)
- Converts `$remove` to native `$pull` (targeted removal)
#### Drizzle Implementation (Postgres/SQLite):
- Uses optimized batch `INSERT` with duplicate checking for `$append`
- Uses targeted `DELETE` queries for `$remove`
- Implements timestamp-based ordering for performance
- Handles locale columns conditionally based on schema
### Limitations
The current implementation is only on database-adapter level and not
(yet) for the local API. Implementation in the localAPI will be done
separately.
This commit is contained in:
@@ -84,7 +84,7 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
globals: {
|
||||
menu: Menu;
|
||||
@@ -124,13 +124,13 @@ export interface UserAuthOperations {
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
id: number;
|
||||
title?: string | null;
|
||||
content?: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
@@ -149,7 +149,7 @@ export interface Post {
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -193,7 +193,7 @@ export interface Media {
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -217,24 +217,24 @@ export interface User {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
value: number | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -244,10 +244,10 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
id: number;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -267,7 +267,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -393,7 +393,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
* via the `definition` "menu".
|
||||
*/
|
||||
export interface Menu {
|
||||
id: string;
|
||||
id: number;
|
||||
globalText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
|
||||
@@ -148,6 +148,19 @@ export const getConfig: () => Partial<Config> = () => ({
|
||||
relationTo: 'categories-custom-id',
|
||||
name: 'categoryCustomID',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['categories', 'simple'],
|
||||
hasMany: true,
|
||||
name: 'polymorphicRelations',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: ['categories', 'simple'],
|
||||
hasMany: true,
|
||||
localized: true,
|
||||
name: 'localizedPolymorphicRelations',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
@@ -188,6 +201,32 @@ export const getConfig: () => Partial<Config> = () => ({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'testNestedGroup',
|
||||
fields: [
|
||||
{
|
||||
name: 'nestedLocalizedPolymorphicRelation',
|
||||
type: 'relationship',
|
||||
relationTo: ['categories', 'simple'],
|
||||
hasMany: true,
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'nestedLocalizedText',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'nestedText1',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'nestedText2',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
|
||||
@@ -3831,12 +3831,14 @@ describe('database', () => {
|
||||
// Locales used => no optimized row update => need to pass full data, incuding title
|
||||
title: 'post',
|
||||
arrayWithIDsLocalized: {
|
||||
$push: {
|
||||
en: {
|
||||
en: {
|
||||
$push: {
|
||||
text: 'some text 2',
|
||||
id: new mongoose.Types.ObjectId().toHexString(),
|
||||
},
|
||||
es: {
|
||||
},
|
||||
es: {
|
||||
$push: {
|
||||
text: 'some text 2 es',
|
||||
id: new mongoose.Types.ObjectId().toHexString(),
|
||||
},
|
||||
@@ -3845,7 +3847,7 @@ describe('database', () => {
|
||||
},
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
})) as unknown as any
|
||||
})) as unknown as Post
|
||||
|
||||
expect(res.arrayWithIDsLocalized?.en).toHaveLength(2)
|
||||
expect(res.arrayWithIDsLocalized?.en?.[0]?.text).toBe('some text')
|
||||
@@ -3972,12 +3974,14 @@ describe('database', () => {
|
||||
// Locales used => no optimized row update => need to pass full data, incuding title
|
||||
title: 'post',
|
||||
arrayWithIDsLocalized: {
|
||||
$push: {
|
||||
en: {
|
||||
en: {
|
||||
$push: {
|
||||
text: 'some text 2',
|
||||
id: new mongoose.Types.ObjectId().toHexString(),
|
||||
},
|
||||
es: [
|
||||
},
|
||||
es: {
|
||||
$push: [
|
||||
{
|
||||
text: 'some text 2 es',
|
||||
id: new mongoose.Types.ObjectId().toHexString(),
|
||||
@@ -4004,6 +4008,781 @@ describe('database', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationship $push', () => {
|
||||
it('should allow appending relationships using $push with single value', async () => {
|
||||
// First create some category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create a post with initial relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
categories: [cat1.id],
|
||||
},
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(post.categories).toHaveLength(1)
|
||||
expect(post.categories?.[0]).toBe(cat1.id)
|
||||
|
||||
// Append another relationship using $push
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
categories: {
|
||||
$push: cat2.id,
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.categories).toHaveLength(2)
|
||||
// Handle both populated and non-populated relationships
|
||||
const resultIds = result.categories?.map((cat) => cat as string)
|
||||
expect(resultIds).toContain(cat1.id)
|
||||
expect(resultIds).toContain(cat2.id)
|
||||
})
|
||||
|
||||
it('should allow appending relationships using $push with array', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
const cat3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 3' },
|
||||
})
|
||||
|
||||
// Create post with initial relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
categories: [cat1.id],
|
||||
},
|
||||
})
|
||||
|
||||
// Append multiple relationships using $push
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
categories: {
|
||||
$push: [cat2.id, cat3.id],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.categories).toHaveLength(3)
|
||||
// Handle both populated and non-populated relationships
|
||||
const resultIds = result.categories?.map((cat) => cat as string)
|
||||
expect(resultIds).toContain(cat1.id)
|
||||
expect(resultIds).toContain(cat2.id)
|
||||
expect(resultIds).toContain(cat3.id)
|
||||
})
|
||||
|
||||
it('should prevent duplicates when using $push', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create post with initial relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
categories: [cat1.id, cat2.id],
|
||||
},
|
||||
})
|
||||
|
||||
// Try to append existing relationship - should not create duplicates
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
categories: {
|
||||
$push: [cat1.id, cat2.id], // Appending existing items
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.categories).toHaveLength(2) // Should still be 2, no duplicates
|
||||
// Handle both populated and non-populated relationships
|
||||
const resultIds = result.categories?.map((cat) => cat as string)
|
||||
expect(resultIds).toContain(cat1.id)
|
||||
expect(resultIds).toContain(cat2.id)
|
||||
})
|
||||
|
||||
it('should work with updateMany for bulk append operations', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create multiple posts with initial relationships
|
||||
const post1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Post 1',
|
||||
categories: [cat1.id],
|
||||
},
|
||||
})
|
||||
const post2 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Post 2',
|
||||
categories: [cat1.id],
|
||||
},
|
||||
})
|
||||
|
||||
// Append cat2 to all posts using updateMany
|
||||
const result = (await payload.db.updateMany({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
id: { in: [post1.id, post2.id] },
|
||||
},
|
||||
data: {
|
||||
categories: {
|
||||
$push: cat2.id,
|
||||
},
|
||||
},
|
||||
})) as unknown as Post[]
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
result.forEach((post) => {
|
||||
expect(post.categories).toHaveLength(2)
|
||||
const categoryIds = post.categories?.map((cat) => cat as string)
|
||||
expect(categoryIds).toContain(cat1.id)
|
||||
expect(categoryIds).toContain(cat2.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('should append polymorphic relationships using $push', async () => {
|
||||
// Create a category and simple document for the polymorphic relationship
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category' },
|
||||
})
|
||||
const simple = await payload.create({
|
||||
collection: 'simple',
|
||||
data: { text: 'Test Simple' },
|
||||
})
|
||||
|
||||
// Create post with initial polymorphic relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
polymorphicRelations: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
depth: 0, // Don't populate relationships
|
||||
})
|
||||
|
||||
expect(post.polymorphicRelations).toHaveLength(1)
|
||||
expect(post.polymorphicRelations?.[0]).toEqual({
|
||||
relationTo: 'categories',
|
||||
value: category.id,
|
||||
})
|
||||
|
||||
// Append another polymorphic relationship using $push
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
polymorphicRelations: {
|
||||
$push: [
|
||||
{
|
||||
relationTo: 'simple',
|
||||
value: simple.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.polymorphicRelations).toHaveLength(2)
|
||||
expect(result.polymorphicRelations).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category.id,
|
||||
})
|
||||
expect(result.polymorphicRelations).toContainEqual({
|
||||
relationTo: 'simple',
|
||||
value: simple.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should prevent duplicates in polymorphic relationships with $push', async () => {
|
||||
// Create a category
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category' },
|
||||
})
|
||||
|
||||
// Create post with polymorphic relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
polymorphicRelations: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
// Try to append the same relationship - should not create duplicates
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
polymorphicRelations: {
|
||||
$push: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category.id, // Same relationship
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.polymorphicRelations).toHaveLength(1) // Should still be 1, no duplicates
|
||||
expect(result.polymorphicRelations?.[0]).toEqual({
|
||||
relationTo: 'categories',
|
||||
value: category.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle localized polymorphic relationships with $push', async () => {
|
||||
// Create documents for testing
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create post with localized polymorphic relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
localizedPolymorphicRelations: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
// Append relationship using $push with correct localized structure
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
localizedPolymorphicRelations: {
|
||||
en: {
|
||||
$push: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.localizedPolymorphicRelations?.en).toHaveLength(2)
|
||||
expect(result.localizedPolymorphicRelations?.en).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
})
|
||||
expect(result.localizedPolymorphicRelations?.en).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle nested localized polymorphic relationships with $push', async () => {
|
||||
// Create documents for the polymorphic relationship
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create a post with nested localized polymorphic relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Nested $push',
|
||||
testNestedGroup: {
|
||||
nestedLocalizedPolymorphicRelation: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
// Use low-level API to push new items
|
||||
await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
where: { id: { equals: post.id } },
|
||||
data: {
|
||||
'testNestedGroup.nestedLocalizedPolymorphicRelation': {
|
||||
en: {
|
||||
$push: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Verify the operation worked
|
||||
const result = await payload.findByID({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(result.testNestedGroup?.nestedLocalizedPolymorphicRelation).toHaveLength(2)
|
||||
expect(result.testNestedGroup?.nestedLocalizedPolymorphicRelation).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
})
|
||||
expect(result.testNestedGroup?.nestedLocalizedPolymorphicRelation).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationship $remove', () => {
|
||||
it('should allow removing relationships using $remove with single value', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
// Create post with relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
categories: [cat1.id, cat2.id],
|
||||
},
|
||||
})
|
||||
|
||||
expect(post.categories).toHaveLength(2)
|
||||
|
||||
// Remove one relationship using $remove
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
categories: {
|
||||
$remove: cat1.id,
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.categories).toHaveLength(1)
|
||||
expect(result.categories?.[0]).toBe(cat2.id)
|
||||
})
|
||||
|
||||
it('should allow removing relationships using $remove with array', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
const cat3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 3' },
|
||||
})
|
||||
|
||||
// Create post with relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
categories: [cat1.id, cat2.id, cat3.id],
|
||||
},
|
||||
})
|
||||
|
||||
expect(post.categories).toHaveLength(3)
|
||||
|
||||
// Remove multiple relationships using $remove
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
categories: {
|
||||
$remove: [cat1.id, cat3.id],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.categories).toHaveLength(1)
|
||||
expect(result.categories?.[0]).toBe(cat2.id)
|
||||
})
|
||||
|
||||
it('should work with updateMany for bulk remove operations', async () => {
|
||||
// Create category documents
|
||||
const cat1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const cat2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
const cat3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 3' },
|
||||
})
|
||||
|
||||
// Create multiple posts with relationships
|
||||
const post1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Post 1',
|
||||
categories: [cat1.id, cat2.id, cat3.id],
|
||||
},
|
||||
})
|
||||
const post2 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Post 2',
|
||||
categories: [cat1.id, cat2.id, cat3.id],
|
||||
},
|
||||
})
|
||||
|
||||
// Remove cat1 and cat3 from all posts using updateMany
|
||||
const result = (await payload.db.updateMany({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
id: { in: [post1.id, post2.id] },
|
||||
},
|
||||
data: {
|
||||
categories: {
|
||||
$remove: [cat1.id, cat3.id],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post[]
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
result.forEach((post) => {
|
||||
expect(post.categories).toHaveLength(1)
|
||||
const categoryIds = post.categories?.map((cat) => cat as string)
|
||||
expect(categoryIds).toContain(cat2.id)
|
||||
expect(categoryIds).not.toContain(cat1.id)
|
||||
expect(categoryIds).not.toContain(cat3.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove polymorphic relationships using $remove', async () => {
|
||||
// Create documents
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category 1' },
|
||||
})
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category 2' },
|
||||
})
|
||||
|
||||
// Create post with multiple polymorphic relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
polymorphicRelations: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
},
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(post.polymorphicRelations).toHaveLength(2)
|
||||
|
||||
// Remove one polymorphic relationship using $remove
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
polymorphicRelations: {
|
||||
$remove: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.polymorphicRelations).toHaveLength(1)
|
||||
expect(result.polymorphicRelations?.[0]).toEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove multiple polymorphic relationships using $remove', async () => {
|
||||
// Create documents
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category 1' },
|
||||
})
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Test Category 2' },
|
||||
})
|
||||
const simple = await payload.create({
|
||||
collection: 'simple',
|
||||
data: { text: 'Test Simple' },
|
||||
})
|
||||
|
||||
// Create post with multiple polymorphic relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
polymorphicRelations: [
|
||||
{ relationTo: 'categories', value: category1.id },
|
||||
{ relationTo: 'categories', value: category2.id },
|
||||
{ relationTo: 'simple', value: simple.id },
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(post.polymorphicRelations).toHaveLength(3)
|
||||
|
||||
// Remove multiple polymorphic relationships using $remove
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
polymorphicRelations: {
|
||||
$remove: [
|
||||
{ relationTo: 'categories', value: category1.id },
|
||||
{ relationTo: 'simple', value: simple.id },
|
||||
],
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.polymorphicRelations).toHaveLength(1)
|
||||
expect(result.polymorphicRelations?.[0]).toEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle localized polymorphic relationships with $remove', async () => {
|
||||
// Create documents for testing
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
const category3 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 3' },
|
||||
})
|
||||
|
||||
// Create post with multiple localized polymorphic relationships
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Post',
|
||||
localizedPolymorphicRelations: [
|
||||
{ relationTo: 'categories', value: category1.id },
|
||||
{ relationTo: 'categories', value: category2.id },
|
||||
{ relationTo: 'categories', value: category3.id },
|
||||
],
|
||||
},
|
||||
depth: 0,
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
// Remove relationships using $remove with correct localized structure
|
||||
const result = (await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
data: {
|
||||
localizedPolymorphicRelations: {
|
||||
en: {
|
||||
$remove: [
|
||||
{ relationTo: 'categories', value: category1.id },
|
||||
{ relationTo: 'categories', value: category3.id },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as unknown as Post
|
||||
|
||||
expect(result.localizedPolymorphicRelations?.en).toHaveLength(1)
|
||||
expect(result.localizedPolymorphicRelations?.en).toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
expect(result.localizedPolymorphicRelations?.en).not.toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
})
|
||||
expect(result.localizedPolymorphicRelations?.en).not.toContainEqual({
|
||||
relationTo: 'categories',
|
||||
value: category3.id,
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle nested localized polymorphic relationships with $remove', async () => {
|
||||
// Create documents for the polymorphic relationship
|
||||
const category1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 1' },
|
||||
})
|
||||
|
||||
const category2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'Category 2' },
|
||||
})
|
||||
|
||||
const simple1 = await payload.create({
|
||||
collection: 'simple',
|
||||
data: { text: 'Simple 1' },
|
||||
})
|
||||
|
||||
// Create a post with multiple items in nested localized polymorphic relationship
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: 'Test Nested $remove',
|
||||
testNestedGroup: {
|
||||
nestedLocalizedPolymorphicRelation: [
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category1.id,
|
||||
},
|
||||
{
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
},
|
||||
{
|
||||
relationTo: 'simple',
|
||||
value: simple1.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
locale: 'en',
|
||||
})
|
||||
|
||||
// Use low-level API to remove items
|
||||
await payload.db.updateOne({
|
||||
collection: 'posts',
|
||||
where: { id: { equals: post.id } },
|
||||
data: {
|
||||
'testNestedGroup.nestedLocalizedPolymorphicRelation': {
|
||||
en: {
|
||||
$remove: [
|
||||
{ relationTo: 'categories', value: category1.id },
|
||||
{ relationTo: 'simple', value: simple1.id },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Verify the operation worked
|
||||
const result = await payload.findByID({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
locale: 'en',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(result.testNestedGroup?.nestedLocalizedPolymorphicRelation).toHaveLength(1)
|
||||
expect(result.testNestedGroup?.nestedLocalizedPolymorphicRelation?.[0]).toEqual({
|
||||
relationTo: 'categories',
|
||||
value: category2.id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should support x3 nesting blocks', async () => {
|
||||
const res = await payload.create({
|
||||
collection: 'posts',
|
||||
|
||||
@@ -209,6 +209,24 @@ export interface Post {
|
||||
}[]
|
||||
| null;
|
||||
categoryCustomID?: (number | null) | CategoriesCustomId;
|
||||
polymorphicRelations?:
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | {
|
||||
relationTo: 'simple';
|
||||
value: string | Simple;
|
||||
})[]
|
||||
| null;
|
||||
localizedPolymorphicRelations?:
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | {
|
||||
relationTo: 'simple';
|
||||
value: string | Simple;
|
||||
})[]
|
||||
| null;
|
||||
localized?: string | null;
|
||||
text?: string | null;
|
||||
number?: number | null;
|
||||
@@ -234,6 +252,20 @@ export interface Post {
|
||||
};
|
||||
};
|
||||
};
|
||||
testNestedGroup?: {
|
||||
nestedLocalizedPolymorphicRelation?:
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | {
|
||||
relationTo: 'simple';
|
||||
value: string | Simple;
|
||||
})[]
|
||||
| null;
|
||||
nestedLocalizedText?: string | null;
|
||||
nestedText1?: string | null;
|
||||
nestedText2?: string | null;
|
||||
};
|
||||
hasTransaction?: boolean | null;
|
||||
throwAfterChange?: boolean | null;
|
||||
arrayWithIDs?:
|
||||
@@ -840,6 +872,8 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
categoryPoly?: T;
|
||||
categoryPolyMany?: T;
|
||||
categoryCustomID?: T;
|
||||
polymorphicRelations?: T;
|
||||
localizedPolymorphicRelations?: T;
|
||||
localized?: T;
|
||||
text?: T;
|
||||
number?: T;
|
||||
@@ -1421,6 +1455,6 @@ export interface Auth {
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user