### What? - Updated the `countOperation` to respect the `trash` argument. ### Why? - Previously, `count` would incorrectly include trashed documents even when `trash` was not specified. - This change aligns `count` behavior with `find` and other operations, providing accurate counts for normal and trashed documents. ### How? - Applied `appendNonTrashedFilter` in `countOperation` to automatically exclude soft-deleted docs when `trash: false` (default). - Added `trash` argument support in Local API, REST API (`/count` endpoints), and GraphQL (`count<Collection>` queries).
1737 lines
53 KiB
TypeScript
1737 lines
53 KiB
TypeScript
import type { CollectionSlug, Payload } from 'payload'
|
|
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
|
import type { Post, RestrictedCollection } from './payload-types.js'
|
|
|
|
import { regularUser } from '../credentials.js'
|
|
import { idToString } from '../helpers/idToString.js'
|
|
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
|
import { postsSlug } from './collections/Posts/index.js'
|
|
import { restrictedCollectionSlug } from './collections/RestrictedCollection/index.js'
|
|
import { usersSlug } from './collections/Users/index.js'
|
|
|
|
let restClient: NextRESTClient
|
|
let payload: Payload
|
|
let user: any
|
|
|
|
const filename = fileURLToPath(import.meta.url)
|
|
const dirname = path.dirname(filename)
|
|
|
|
describe('trash', () => {
|
|
beforeAll(async () => {
|
|
const initResult = await initPayloadInt(dirname)
|
|
|
|
payload = initResult.payload
|
|
restClient = initResult.restClient
|
|
})
|
|
|
|
afterAll(async () => {
|
|
if (typeof payload.db.destroy === 'function') {
|
|
await payload.db.destroy()
|
|
}
|
|
})
|
|
|
|
let restrictedCollectionDoc: RestrictedCollection
|
|
let postsDocOne: Post
|
|
let postsDocTwo: Post
|
|
|
|
beforeEach(async () => {
|
|
await restClient.login({
|
|
slug: usersSlug,
|
|
credentials: regularUser,
|
|
})
|
|
|
|
user = await payload.login({
|
|
collection: usersSlug,
|
|
data: {
|
|
email: regularUser.email,
|
|
password: regularUser.password,
|
|
},
|
|
})
|
|
|
|
restrictedCollectionDoc = await payload.create({
|
|
collection: restrictedCollectionSlug as CollectionSlug,
|
|
data: {
|
|
title: 'With Access Control one',
|
|
},
|
|
})
|
|
|
|
postsDocOne = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Doc one',
|
|
},
|
|
})
|
|
|
|
postsDocTwo = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Doc two',
|
|
deletedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
// Access control tests use the Pages collection because it has delete access control enabled.
|
|
// The Post collection does not have any access restrictions and is used for general CRUD tests.
|
|
describe('Access control', () => {
|
|
it('should not allow bulk soft-deleting documents when restricted by delete access', async () => {
|
|
await expect(
|
|
payload.update({
|
|
collection: restrictedCollectionSlug as CollectionSlug,
|
|
data: {
|
|
deletedAt: new Date().toISOString(),
|
|
},
|
|
user, // Regular user does not have delete access
|
|
where: {
|
|
// Using where to target multiple documents
|
|
title: {
|
|
equals: restrictedCollectionDoc.title,
|
|
},
|
|
},
|
|
overrideAccess: false, // Override access to false to test access control
|
|
}),
|
|
).rejects.toMatchObject({
|
|
status: 403,
|
|
name: 'Forbidden',
|
|
message: expect.stringContaining('You are not allowed'),
|
|
})
|
|
})
|
|
|
|
it('should not allow soft-deleting a document when restricted by delete access', async () => {
|
|
await expect(
|
|
payload.update({
|
|
collection: restrictedCollectionSlug as CollectionSlug,
|
|
data: {
|
|
deletedAt: new Date().toISOString(),
|
|
},
|
|
id: restrictedCollectionDoc.id, // Using ID to target specific document
|
|
user, // Regular user does not have delete access
|
|
overrideAccess: false, // Override access to false to test access control
|
|
}),
|
|
).rejects.toMatchObject({
|
|
status: 403,
|
|
name: 'Forbidden',
|
|
message: expect.stringContaining('You are not allowed'),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('LOCAL API', () => {
|
|
describe('find', () => {
|
|
it('should return all docs including soft-deleted docs in find with trash: true', async () => {
|
|
const allDocs = await payload.find({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(allDocs.totalDocs).toEqual(2)
|
|
})
|
|
|
|
it('should return only soft-deleted docs in find with trash: true', async () => {
|
|
const trashedDocs = await payload.find({
|
|
collection: postsSlug,
|
|
where: {
|
|
deletedAt: {
|
|
exists: true,
|
|
},
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedDocs.totalDocs).toEqual(1)
|
|
expect(trashedDocs.docs[0]?.id).toEqual(postsDocTwo.id)
|
|
})
|
|
|
|
it('should return only non-soft-deleted docs in find with trash: false', async () => {
|
|
const normalDocs = await payload.find({
|
|
collection: postsSlug,
|
|
trash: false,
|
|
})
|
|
|
|
expect(normalDocs.totalDocs).toEqual(1)
|
|
expect(normalDocs.docs[0]?.id).toEqual(postsDocOne.id)
|
|
})
|
|
|
|
it('should find restored documents after setting deletedAt to null', async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
deletedAt: null,
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
const result = await payload.find({
|
|
collection: postsSlug,
|
|
trash: false, // Normal query should return it now
|
|
})
|
|
|
|
const restored = result.docs.find(
|
|
(doc) => (doc.id as number | string) === (postsDocTwo.id as number | string),
|
|
)
|
|
|
|
expect(restored).toBeDefined()
|
|
expect(restored?.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findDistinct', () => {
|
|
it('should return all unique values for a field (excluding soft-deleted docs by default)', async () => {
|
|
// Add a duplicate title
|
|
await payload.create({
|
|
collection: postsSlug,
|
|
data: { title: 'Doc one' },
|
|
})
|
|
|
|
const result = await payload.findDistinct({
|
|
collection: postsSlug,
|
|
field: 'title',
|
|
})
|
|
|
|
const titles = result.values.map((v) => v.title)
|
|
|
|
// Expect only distinct titles of non-trashed docs
|
|
expect(titles).toContain('Doc one')
|
|
expect(titles).not.toContain('Doc two') // because it's soft-deleted
|
|
expect(titles).toHaveLength(1)
|
|
})
|
|
|
|
it('should include soft-deleted docs when trash: true', async () => {
|
|
const result = await payload.findDistinct({
|
|
collection: postsSlug,
|
|
field: 'title',
|
|
trash: true,
|
|
})
|
|
|
|
const titles = result.values.map((v) => v.title)
|
|
|
|
expect(titles).toContain('Doc one')
|
|
expect(titles).toContain('Doc two') // soft-deleted doc
|
|
})
|
|
|
|
it('should return only distinct values from soft-deleted docs when where[deletedAt][exists]=true', async () => {
|
|
const result = await payload.findDistinct({
|
|
collection: postsSlug,
|
|
field: 'title',
|
|
trash: true,
|
|
where: {
|
|
deletedAt: { exists: true },
|
|
},
|
|
})
|
|
|
|
const titles = result.values.map((v) => v.title)
|
|
expect(titles).toEqual(['Doc two']) // Only the soft-deleted doc
|
|
})
|
|
|
|
it('should respect where filters when retrieving distinct values', async () => {
|
|
const result = await payload.findDistinct({
|
|
collection: postsSlug,
|
|
field: 'title',
|
|
trash: true,
|
|
where: {
|
|
title: { equals: 'Doc two' },
|
|
},
|
|
})
|
|
|
|
const titles = result.values.map((v) => v.title)
|
|
expect(titles).toEqual(['Doc two'])
|
|
})
|
|
})
|
|
|
|
describe('findByID operation', () => {
|
|
it('should return a soft-deleted document when trash: true', async () => {
|
|
const trashedPostDoc: Post = await payload.findByID({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedPostDoc).toBeDefined()
|
|
expect(trashedPostDoc?.id).toEqual(postsDocTwo.id)
|
|
expect(trashedPostDoc?.deletedAt).toBeDefined()
|
|
expect(trashedPostDoc?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to find a soft-deleted document w/o trash: true', async () => {
|
|
await expect(
|
|
payload.findByID({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
|
|
await expect(
|
|
payload.findByID({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
trash: false,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
})
|
|
})
|
|
|
|
describe('findVersions operation', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
it('should return all versions including soft-deleted docs in findVersions with trash: true', async () => {
|
|
const allVersions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(allVersions.totalDocs).toEqual(2)
|
|
expect(allVersions.docs[0]?.parent).toEqual(postsDocTwo.id)
|
|
expect(allVersions.docs[1]?.parent).toEqual(postsDocOne.id)
|
|
})
|
|
|
|
it('should return only soft-deleted docs in findVersions with trash: true', async () => {
|
|
const trashedVersions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
where: {
|
|
'version.deletedAt': {
|
|
exists: true,
|
|
},
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedVersions.totalDocs).toEqual(1)
|
|
expect(trashedVersions.docs[0]?.parent).toEqual(postsDocTwo.id)
|
|
})
|
|
|
|
it('should return only non-soft-deleted docs in findVersions with trash: false', async () => {
|
|
const normalVersions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
trash: false,
|
|
})
|
|
|
|
expect(normalVersions.totalDocs).toEqual(1)
|
|
expect(normalVersions.docs[0]?.parent).toEqual(postsDocOne.id)
|
|
})
|
|
|
|
it('should find versions where version.deletedAt is null after restore', async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
deletedAt: null,
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
const versions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
where: {
|
|
'version.deletedAt': {
|
|
equals: null,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(versions.docs.some((v) => v.parent === postsDocTwo.id)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('findVersionByID operation', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should return a soft-deleted version document when trash: true', async () => {
|
|
const trashedVersions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
where: {
|
|
'version.deletedAt': {
|
|
exists: true,
|
|
},
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedVersions.docs).toHaveLength(1)
|
|
|
|
const version = trashedVersions.docs[0]
|
|
|
|
const trashedVersionDoc = await payload.findVersionByID({
|
|
collection: postsSlug,
|
|
id: version!.id,
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedVersionDoc).toBeDefined()
|
|
expect(trashedVersionDoc?.parent).toEqual(postsDocTwo.id)
|
|
expect(trashedVersionDoc?.version?.deletedAt).toBeDefined()
|
|
expect(trashedVersionDoc?.version?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to find a soft-deleted version document w/o trash: true', async () => {
|
|
const trashedVersions = await payload.findVersions({
|
|
collection: postsSlug,
|
|
where: {
|
|
'version.deletedAt': {
|
|
exists: true,
|
|
},
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(trashedVersions.docs).toHaveLength(1)
|
|
|
|
const version = trashedVersions.docs[0]
|
|
|
|
await expect(
|
|
payload.findVersionByID({
|
|
collection: postsSlug,
|
|
id: version!.id,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
|
|
await expect(
|
|
payload.findVersionByID({
|
|
collection: postsSlug,
|
|
id: version!.id,
|
|
trash: false,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
})
|
|
})
|
|
|
|
describe('updateByID operation', () => {
|
|
it('should update a single soft-deleted document when trash: true', async () => {
|
|
const updatedPostDoc: Post = await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
title: 'Updated Doc Two',
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(updatedPostDoc).toBeDefined()
|
|
expect(updatedPostDoc.id).toEqual(postsDocTwo.id)
|
|
expect(updatedPostDoc.title).toEqual('Updated Doc Two')
|
|
expect(updatedPostDoc.deletedAt).toBeDefined()
|
|
expect(updatedPostDoc.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to update a soft-deleted document w/o trash: true', async () => {
|
|
await expect(
|
|
payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
title: 'Updated Doc Two',
|
|
},
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
|
|
await expect(
|
|
payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
title: 'Updated Doc Two',
|
|
},
|
|
trash: false,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
})
|
|
|
|
it('should update a single normal document when trash: false', async () => {
|
|
const updatedPostDoc: Post = await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocOne.id,
|
|
data: {
|
|
title: 'Updated Doc One',
|
|
},
|
|
})
|
|
|
|
expect(updatedPostDoc).toBeDefined()
|
|
expect(updatedPostDoc.id).toEqual(postsDocOne.id)
|
|
expect(updatedPostDoc.title).toEqual('Updated Doc One')
|
|
expect(updatedPostDoc.deletedAt).toBeFalsy()
|
|
})
|
|
|
|
it('should restore a soft-deleted document by setting deletedAt to null', async () => {
|
|
const restored = await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: {
|
|
deletedAt: null,
|
|
},
|
|
trash: true,
|
|
})
|
|
|
|
expect(restored.deletedAt).toBeNull()
|
|
|
|
// Should now show up in trash: false queries
|
|
const result = await payload.find({
|
|
collection: postsSlug,
|
|
trash: false,
|
|
})
|
|
|
|
const found = result.docs.find((doc) => doc.id === postsDocTwo.id)
|
|
expect(found).toBeDefined()
|
|
expect(found?.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('update operation', () => {
|
|
it('should update only normal document when trash: false', async () => {
|
|
const result = await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Updated Doc',
|
|
},
|
|
trash: false,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(result.docs).toBeDefined()
|
|
expect(result.docs.length).toBeGreaterThan(0)
|
|
|
|
const updatedDoc = result.docs[0]
|
|
|
|
expect(updatedDoc?.id).toEqual(postsDocOne.id)
|
|
expect(updatedDoc?.title).toEqual('Updated Doc')
|
|
expect(updatedDoc?.deletedAt).toBeFalsy()
|
|
})
|
|
|
|
it('should update all documents including soft-deleted documents when trash: true', async () => {
|
|
const result = await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'A New Updated Doc',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(result.docs).toBeDefined()
|
|
expect(result.docs.length).toBeGreaterThan(0)
|
|
|
|
const updatedPostdDocOne = result.docs.find((doc) => doc.id === postsDocOne.id)
|
|
const updatedPostdDocTwo = result.docs.find((doc) => doc.id === postsDocTwo.id)
|
|
|
|
expect(updatedPostdDocOne?.title).toEqual('A New Updated Doc')
|
|
expect(updatedPostdDocOne?.deletedAt).toBeFalsy()
|
|
|
|
expect(updatedPostdDocTwo?.title).toEqual('A New Updated Doc')
|
|
expect(updatedPostdDocTwo?.deletedAt).toBeDefined()
|
|
})
|
|
|
|
it('should only update soft-deleted documents when trash: true and where[deletedAt][exists]=true', async () => {
|
|
const docThree = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Doc three',
|
|
deletedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
|
|
const result = await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Updated Soft Deleted Doc',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
deletedAt: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
expect(result.docs).toBeDefined()
|
|
expect(result.docs[0]?.id).toEqual(docThree.id)
|
|
expect(result.docs[0]?.title).toEqual('Updated Soft Deleted Doc')
|
|
expect(result.docs[0]?.deletedAt).toEqual(docThree.deletedAt)
|
|
expect(result.docs[1]?.id).toEqual(postsDocTwo.id)
|
|
expect(result.docs[1]?.title).toEqual('Updated Soft Deleted Doc')
|
|
expect(result.docs[1]?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
|
|
// Clean up
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
id: docThree.id,
|
|
trash: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('delete operation', () => {
|
|
it('should perma delete all docs including soft-deleted documents when trash: true', async () => {
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
const allDocs = await payload.find({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(allDocs.totalDocs).toEqual(0)
|
|
})
|
|
|
|
it('should only perma delete normal docs when trash: false', async () => {
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
trash: false,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
|
|
const allDocs = await payload.find({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(allDocs.totalDocs).toEqual(1)
|
|
expect(allDocs.docs[0]?.id).toEqual(postsDocTwo.id)
|
|
})
|
|
})
|
|
|
|
describe('deleteByID operation', () => {
|
|
it('should throw NotFound error when trying to delete a soft-deleted document w/o trash: true', async () => {
|
|
await expect(
|
|
payload.delete({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
|
|
await expect(
|
|
payload.delete({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
trash: false,
|
|
}),
|
|
).rejects.toThrow('Not Found')
|
|
})
|
|
|
|
it('should delete a soft-deleted document when trash: true', async () => {
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
trash: true,
|
|
})
|
|
|
|
const allDocs = await payload.find({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(allDocs.totalDocs).toEqual(1)
|
|
expect(allDocs.docs[0]?.id).toEqual(postsDocOne.id)
|
|
})
|
|
})
|
|
|
|
describe('restoreVersion operation', () => {
|
|
it('should throw error when restoring a version of a trashed document', async () => {
|
|
// Create a version of postsDocTwo (which is soft-deleted)
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
id: postsDocTwo.id,
|
|
data: { title: 'Updated Before Restore Attempt' },
|
|
trash: true,
|
|
})
|
|
|
|
const { docs: versions } = await payload.findVersions({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
const version = versions.find((v) => v.parent === postsDocTwo.id)
|
|
|
|
expect(version).toBeDefined()
|
|
|
|
await expect(
|
|
payload.restoreVersion({
|
|
collection: postsSlug,
|
|
id: version!.id,
|
|
}),
|
|
).rejects.toThrow(/Cannot restore a version of a trashed document/i)
|
|
})
|
|
})
|
|
|
|
describe('count operation', () => {
|
|
it('should return total count of non-soft-deleted documents by default (trash: false)', async () => {
|
|
const result = await payload.count({
|
|
collection: postsSlug,
|
|
})
|
|
|
|
expect(result.totalDocs).toEqual(1) // Only postsDocOne
|
|
})
|
|
|
|
it('should return total count of all documents including soft-deleted when trash: true', async () => {
|
|
const result = await payload.count({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
|
|
expect(result.totalDocs).toEqual(2)
|
|
})
|
|
|
|
it('should return count of only soft-deleted documents when where[deletedAt][exists]=true', async () => {
|
|
const result = await payload.count({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
where: { deletedAt: { exists: true } },
|
|
})
|
|
|
|
expect(result.totalDocs).toEqual(1) // Only postsDocTwo
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('REST API', () => {
|
|
describe('find endpoint', () => {
|
|
it('should return all docs including soft-deleted docs in find with trash=true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}?trash=true`)
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(2)
|
|
})
|
|
|
|
it('should return only soft-deleted docs with trash=true and where[deletedAt][exists]=true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}?trash=true&where[deletedAt][exists]=true`)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(1)
|
|
expect(data.docs[0]?.id).toEqual(postsDocTwo.id)
|
|
})
|
|
|
|
it('should return only normal docs when trash=false', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}?trash=false`)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(1)
|
|
expect(data.docs[0]?.id).toEqual(postsDocOne.id)
|
|
})
|
|
|
|
it('should find restored documents after setting deletedAt to null', async () => {
|
|
await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}?trash=true`, {
|
|
body: JSON.stringify({
|
|
deletedAt: null,
|
|
}),
|
|
})
|
|
|
|
const res = await restClient.GET(`/${postsSlug}?trash=false`)
|
|
const data = await res.json()
|
|
|
|
const restored = data.docs.find((doc: Post) => doc.id === postsDocTwo.id)
|
|
|
|
expect(restored).toBeDefined()
|
|
expect(restored.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findByID endpoint', () => {
|
|
it('should return a soft-deleted doc by ID with trash=true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/${postsDocTwo.id}?trash=true`)
|
|
const data = await res.json()
|
|
expect(data?.id).toEqual(postsDocTwo.id)
|
|
expect(data?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should 404 when trying to get a soft-deleted doc without trash=true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/${postsDocTwo.id}`)
|
|
expect(res.status).toBe(404)
|
|
})
|
|
})
|
|
|
|
describe('find versions endpoint', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
it('should return all versions including soft-deleted docs in findVersions with trash: true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/versions?trash=true`)
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(2)
|
|
})
|
|
|
|
it('should return only soft-deleted docs in findVersions with trash: true', async () => {
|
|
const res = await restClient.GET(
|
|
`/${postsSlug}/versions?trash=true&where[version.deletedAt][exists]=true`,
|
|
)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(1)
|
|
expect(data.docs[0]?.parent).toEqual(postsDocTwo.id)
|
|
})
|
|
|
|
it('should return only non-soft-deleted docs in findVersions with trash: false', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/versions?trash=false`)
|
|
const data = await res.json()
|
|
expect(data.docs).toHaveLength(1)
|
|
expect(data.docs[0]?.parent).toEqual(postsDocOne.id)
|
|
})
|
|
|
|
it('should find versions where version.deletedAt is null after restore via REST', async () => {
|
|
await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}?trash=true`, {
|
|
body: JSON.stringify({
|
|
deletedAt: null,
|
|
}),
|
|
})
|
|
|
|
const res = await restClient.GET(
|
|
`/${postsSlug}/versions?trash=true&where[version.deletedAt][equals]=null`,
|
|
)
|
|
const data = await res.json()
|
|
|
|
const version = data.docs.find((v: any) => v.parent === postsDocTwo.id)
|
|
expect(version).toBeDefined()
|
|
expect(version.version.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findVersionByID endpoint', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should return a soft-deleted version document when trash: true', async () => {
|
|
const trashedVersions = await restClient.GET(
|
|
`/${postsSlug}/versions?trash=true&where[version.deletedAt][exists]=true`,
|
|
)
|
|
|
|
const trashedVersionsData = await trashedVersions.json()
|
|
expect(trashedVersionsData.docs).toHaveLength(1)
|
|
|
|
const version = trashedVersionsData.docs[0]
|
|
|
|
const versionDoc = await restClient.GET(`/${postsSlug}/versions/${version!.id}?trash=true`)
|
|
const trashedVersionDoc = await versionDoc.json()
|
|
|
|
expect(trashedVersionDoc).toBeDefined()
|
|
expect(trashedVersionDoc?.parent).toEqual(postsDocTwo.id)
|
|
expect(trashedVersionDoc?.version?.deletedAt).toBeDefined()
|
|
expect(trashedVersionDoc?.version?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to find a soft-deleted version document w/o trash: true', async () => {
|
|
const trashedVersions = await restClient.GET(
|
|
`/${postsSlug}/versions?trash=true&where[version.deletedAt][exists]=true`,
|
|
)
|
|
|
|
const trashedVersionsData = await trashedVersions.json()
|
|
expect(trashedVersionsData.docs).toHaveLength(1)
|
|
|
|
const version = trashedVersionsData.docs[0]
|
|
|
|
const withoutTrash = await restClient.GET(`/${postsSlug}/versions/${version!.id}`)
|
|
expect(withoutTrash.status).toBe(404)
|
|
|
|
const withTrashFalse = await restClient.GET(
|
|
`/${postsSlug}/versions/${version!.id}?trash=false`,
|
|
)
|
|
expect(withTrashFalse.status).toBe(404)
|
|
})
|
|
})
|
|
|
|
describe('updateByID endpoint', () => {
|
|
it('should update a single soft-deleted doc when trash=true', async () => {
|
|
const res = await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}?trash=true`, {
|
|
body: JSON.stringify({
|
|
title: 'Updated via REST',
|
|
}),
|
|
})
|
|
|
|
const result = await res.json()
|
|
expect(result.doc.title).toBe('Updated via REST')
|
|
expect(result.doc.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to update a soft-deleted document w/o trash: true', async () => {
|
|
const res = await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}`, {
|
|
body: JSON.stringify({ title: 'Fail Update' }),
|
|
})
|
|
expect(res.status).toBe(404)
|
|
})
|
|
|
|
it('should update a single normal document when trash: false', async () => {
|
|
const res = await restClient.PATCH(`/${postsSlug}/${postsDocOne.id}?trash=false`, {
|
|
body: JSON.stringify({ title: 'Updated Normal via REST' }),
|
|
})
|
|
const result = await res.json()
|
|
expect(result.doc.title).toBe('Updated Normal via REST')
|
|
expect(result.doc.deletedAt).toBeFalsy()
|
|
})
|
|
|
|
it('should restore a soft-deleted document by setting deletedAt to null', async () => {
|
|
const res = await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}?trash=true`, {
|
|
body: JSON.stringify({
|
|
deletedAt: null,
|
|
}),
|
|
})
|
|
|
|
const result = await res.json()
|
|
expect(result.doc.deletedAt).toBeNull()
|
|
|
|
const check = await restClient.GET(`/${postsSlug}?trash=false`)
|
|
const data = await check.json()
|
|
const restored = data.docs.find((doc: Post) => doc.id === postsDocTwo.id)
|
|
|
|
expect(restored).toBeDefined()
|
|
expect(restored.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('update endpoint', () => {
|
|
it('should update only normal document when trash: false', async () => {
|
|
const query = `?trash=false&where[id][equals]=${postsDocOne.id}`
|
|
|
|
const res = await restClient.PATCH(`/${postsSlug}${query}`, {
|
|
body: JSON.stringify({ title: 'Updated Normal via REST' }),
|
|
})
|
|
|
|
const result = await res.json()
|
|
expect(result.docs).toHaveLength(1)
|
|
expect(result.docs[0].id).toBe(postsDocOne.id)
|
|
expect(result.docs[0].title).toBe('Updated Normal via REST')
|
|
expect(result.docs[0].deletedAt).toBeFalsy()
|
|
})
|
|
|
|
it('should update all documents including soft-deleted documents when trash: true', async () => {
|
|
const query = `?trash=true&where[title][exists]=true`
|
|
|
|
const res = await restClient.PATCH(`/${postsSlug}${query}`, {
|
|
body: JSON.stringify({ title: 'Bulk Updated All' }),
|
|
})
|
|
|
|
const result = await res.json()
|
|
expect(result.docs).toHaveLength(2)
|
|
expect(result.docs.every((doc: Post) => doc.title === 'Bulk Updated All')).toBe(true)
|
|
})
|
|
|
|
it('should only update soft-deleted documents when trash: true and where[deletedAt][exists]=true', async () => {
|
|
const query = `?trash=true&where[deletedAt][exists]=true`
|
|
|
|
const docThree = await payload.create({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Doc three',
|
|
deletedAt: new Date().toISOString(),
|
|
},
|
|
})
|
|
|
|
const res = await restClient.PATCH(`/${postsSlug}${query}`, {
|
|
body: JSON.stringify({ title: 'Updated Soft Deleted Doc' }),
|
|
})
|
|
|
|
const result = await res.json()
|
|
expect(result.docs).toHaveLength(2)
|
|
|
|
expect(result.docs).toBeDefined()
|
|
expect(result.docs[0]?.id).toEqual(docThree.id)
|
|
expect(result.docs[0]?.title).toEqual('Updated Soft Deleted Doc')
|
|
expect(result.docs[0]?.deletedAt).toEqual(docThree.deletedAt)
|
|
expect(result.docs[1]?.id).toEqual(postsDocTwo.id)
|
|
expect(result.docs[1]?.title).toEqual('Updated Soft Deleted Doc')
|
|
expect(result.docs[1]?.deletedAt).toEqual(postsDocTwo.deletedAt)
|
|
|
|
// Clean up
|
|
await payload.delete({
|
|
collection: postsSlug,
|
|
id: docThree.id,
|
|
trash: true,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('delete endpoint', () => {
|
|
it('should perma delete all docs including soft-deleted documents when trash: true', async () => {
|
|
const query = `?trash=true&where[title][exists]=true`
|
|
|
|
const res = await restClient.DELETE(`/${postsSlug}${query}`)
|
|
expect(res.status).toBe(200)
|
|
|
|
const result = await res.json()
|
|
expect(result.docs).toHaveLength(2)
|
|
|
|
const check = await restClient.GET(`/${postsSlug}?trash=true`)
|
|
const checkData = await check.json()
|
|
expect(checkData.docs).toHaveLength(0)
|
|
})
|
|
|
|
it('should only perma delete normal docs when trash: false', async () => {
|
|
const query = `?trash=false&where[title][exists]=true`
|
|
|
|
const res = await restClient.DELETE(`/${postsSlug}${query}`)
|
|
expect(res.status).toBe(200)
|
|
|
|
const result = await res.json()
|
|
expect(result.docs).toHaveLength(1)
|
|
expect(result.docs[0]?.id).toBe(postsDocOne.id)
|
|
|
|
const check = await restClient.GET(`/${postsSlug}?trash=true`)
|
|
const checkData = await check.json()
|
|
|
|
// Make sure postsDocTwo (soft-deleted) is still there
|
|
expect(checkData.docs.some((doc: Post) => doc.id === postsDocTwo.id)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('deleteByID endpoint', () => {
|
|
it('should throw NotFound error when trying to delete a soft-deleted document w/o trash: true', async () => {
|
|
const res = await restClient.DELETE(`/${postsSlug}/${postsDocTwo.id}`)
|
|
expect(res.status).toBe(404)
|
|
})
|
|
|
|
it('should delete a soft-deleted document when trash: true', async () => {
|
|
const res = await restClient.DELETE(`/${postsSlug}/${postsDocTwo.id}?trash=true`)
|
|
expect(res.status).toBe(200)
|
|
const result = await res.json()
|
|
expect(result.doc.id).toBe(postsDocTwo.id)
|
|
})
|
|
})
|
|
|
|
describe('restoreVersion operation', () => {
|
|
it('should throw error when restoring a version of a trashed document', async () => {
|
|
const updateRes = await restClient.PATCH(`/${postsSlug}/${postsDocTwo.id}?trash=true`, {
|
|
body: JSON.stringify({ title: 'Updated Soft Deleted for Restore Test' }),
|
|
})
|
|
expect(updateRes.status).toBe(200)
|
|
|
|
const { docs: versions } = await payload.findVersions({
|
|
collection: postsSlug,
|
|
trash: true,
|
|
})
|
|
const version = versions.find((v) => v.parent === postsDocTwo.id)
|
|
|
|
const res = await restClient.POST(`/${postsSlug}/versions/${version!.id}`)
|
|
const body = await res.json()
|
|
|
|
expect(res.status).toBe(403)
|
|
expect(body.message ?? body.errors?.[0]?.message).toMatch(
|
|
'Cannot restore a version of a trashed document',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('count endpoint', () => {
|
|
it('should return count of non-soft-deleted docs by default (trash=false)', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/count`)
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.totalDocs).toEqual(1)
|
|
})
|
|
|
|
it('should return count of all docs including soft-deleted when trash=true', async () => {
|
|
const res = await restClient.GET(`/${postsSlug}/count?trash=true`)
|
|
expect(res.status).toBe(200)
|
|
const data = await res.json()
|
|
expect(data.totalDocs).toEqual(2)
|
|
})
|
|
|
|
it('should return count of only soft-deleted docs with trash=true & where[deletedAt][exists]=true', async () => {
|
|
const res = await restClient.GET(
|
|
`/${postsSlug}/count?trash=true&where[deletedAt][exists]=true`,
|
|
)
|
|
const data = await res.json()
|
|
expect(data.totalDocs).toEqual(1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('GRAPHQL API', () => {
|
|
describe('find query', () => {
|
|
it('should return all docs including soft-deleted docs in find with trash=true', async () => {
|
|
const query = `
|
|
query {
|
|
Posts(trash: true) {
|
|
docs {
|
|
id
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.Posts.docs).toHaveLength(2)
|
|
})
|
|
|
|
it('should return only soft-deleted docs with trash=true and where[deletedAt][exists]=true', async () => {
|
|
const query = `
|
|
query {
|
|
Posts(
|
|
trash: true
|
|
where: { deletedAt: { exists: true } }
|
|
) {
|
|
docs {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.Posts.docs).toHaveLength(1)
|
|
expect(res.data.Posts.docs[0].id).toEqual(postsDocTwo.id)
|
|
})
|
|
|
|
it('should return only normal docs when trash=false', async () => {
|
|
const query = `
|
|
query {
|
|
Posts(trash: false) {
|
|
docs {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.Posts.docs).toHaveLength(1)
|
|
expect(res.data.Posts.docs[0].id).toEqual(postsDocOne.id)
|
|
expect(res.data.Posts.docs[0].deletedAt).toBeNull()
|
|
})
|
|
|
|
it('should find restored documents after setting deletedAt to null', async () => {
|
|
const mutation = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true, data: {
|
|
deletedAt: null
|
|
}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
await restClient.GRAPHQL_POST({ body: JSON.stringify({ query: mutation }) })
|
|
|
|
const query = `
|
|
query {
|
|
Posts(trash: false) {
|
|
docs {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
const restored = res.data.Posts.docs.find((doc: Post) => doc.id === postsDocTwo.id)
|
|
expect(restored).toBeDefined()
|
|
expect(restored.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findByID query', () => {
|
|
it('should return a soft-deleted doc by ID with trash=true', async () => {
|
|
const query = `
|
|
query {
|
|
Post(id: ${idToString(postsDocTwo.id, payload)}, trash: true) {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.Post.id).toBe(postsDocTwo.id)
|
|
expect(res.data.Post.deletedAt).toBe(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should 404 when trying to get a soft-deleted doc without trash=true', async () => {
|
|
const query = `
|
|
query {
|
|
Post(id: ${idToString(postsDocTwo.id, payload)}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
expect(res.errors?.[0]?.message).toMatch(/not found/i)
|
|
})
|
|
})
|
|
|
|
describe('find versions query', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
it('should return all versions including soft-deleted docs in findVersions with trash: true', async () => {
|
|
const query = `
|
|
query {
|
|
versionsPosts(trash: true) {
|
|
docs {
|
|
id
|
|
version {
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.versionsPosts.docs).toHaveLength(2)
|
|
})
|
|
|
|
it('should return only soft-deleted docs in findVersions with trash: true', async () => {
|
|
const query = `
|
|
query {
|
|
versionsPosts(
|
|
trash: true,
|
|
where: {
|
|
version__deletedAt: {
|
|
exists: true
|
|
}
|
|
}
|
|
) {
|
|
docs {
|
|
id
|
|
version {
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
const { docs } = res.data.versionsPosts
|
|
|
|
// Should only include soft-deleted versions
|
|
expect(docs).toHaveLength(1)
|
|
|
|
for (const doc of docs) {
|
|
expect(doc.version.deletedAt).toBeDefined()
|
|
}
|
|
})
|
|
|
|
it('should return only non-soft-deleted docs in findVersions with trash: false', async () => {
|
|
const query = `
|
|
query {
|
|
versionsPosts(trash: false) {
|
|
docs {
|
|
id
|
|
version {
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
const { docs } = res.data.versionsPosts
|
|
|
|
// All versions returned should NOT have deletedAt set
|
|
for (const doc of docs) {
|
|
expect(doc.version.deletedAt).toBeNull()
|
|
}
|
|
})
|
|
|
|
it('should find versions where version.deletedAt is null after restore', async () => {
|
|
const mutation = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true, data: { deletedAt: null }) {
|
|
id
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
`
|
|
await restClient.GRAPHQL_POST({ body: JSON.stringify({ query: mutation }) })
|
|
|
|
const query = `
|
|
query {
|
|
versionsPosts(
|
|
trash: true,
|
|
where: {
|
|
version__deletedAt: {
|
|
equals: null
|
|
}
|
|
}
|
|
) {
|
|
docs {
|
|
id
|
|
parent {
|
|
id
|
|
}
|
|
version {
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
const version = res.data.versionsPosts.docs.find(
|
|
(v: any) => String(v.parent.id) === String(postsDocTwo.id),
|
|
)
|
|
expect(version).toBeDefined()
|
|
expect(version.version.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findVersionByID endpoint', () => {
|
|
beforeAll(async () => {
|
|
await payload.update({
|
|
collection: postsSlug,
|
|
data: {
|
|
title: 'Some updated title',
|
|
},
|
|
trash: true,
|
|
where: {
|
|
title: {
|
|
exists: true,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should return a soft-deleted document when trash: true', async () => {
|
|
// First, get the version ID of the soft-deleted trash enabled doc
|
|
const listQuery = `
|
|
query {
|
|
versionsPosts(
|
|
trash: true,
|
|
where: {
|
|
version__deletedAt: {
|
|
exists: true
|
|
}
|
|
}
|
|
) {
|
|
docs {
|
|
id
|
|
version {
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const listRes = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: listQuery }) })
|
|
.then((r) => r.json())
|
|
|
|
const trashedVersion = listRes.data.versionsPosts.docs[0]
|
|
|
|
const detailQuery = `
|
|
query {
|
|
versionPost(id: ${idToString(trashedVersion.id, payload)}, trash: true) {
|
|
id
|
|
version {
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: detailQuery }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.versionPost.id).toBe(trashedVersion.id)
|
|
expect(res.data.versionPost.version.deletedAt).toBe(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to find a soft-deleted version document w/o trash: true', async () => {
|
|
// First, get the version ID of the soft-deleted trash enabled doc
|
|
const listQuery = `
|
|
query {
|
|
versionsPosts(
|
|
trash: true,
|
|
where: {
|
|
version__deletedAt: {
|
|
exists: true
|
|
}
|
|
}
|
|
) {
|
|
docs {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const listRes = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: listQuery }) })
|
|
.then((r) => r.json())
|
|
|
|
const trashedVersion = listRes.data.versionsPosts.docs[0]
|
|
|
|
const detailQuery = `
|
|
query {
|
|
versionPost(id: ${idToString(trashedVersion.id, payload)}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: detailQuery }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.errors?.[0]?.message).toMatch(/not found/i)
|
|
})
|
|
})
|
|
|
|
describe('updateByID query', () => {
|
|
it('should update a single soft-deleted doc when trash=true', async () => {
|
|
const query = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true, data: { title: "Updated Soft Deleted via GQL" }) {
|
|
id
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.updatePost.id).toBe(postsDocTwo.id)
|
|
expect(res.data.updatePost.title).toBe('Updated Soft Deleted via GQL')
|
|
expect(res.data.updatePost.deletedAt).toBe(postsDocTwo.deletedAt)
|
|
})
|
|
|
|
it('should throw NotFound error when trying to update a soft-deleted document w/o trash: true', async () => {
|
|
const query = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, data: { title: "Should Fail" }) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
expect(res.errors?.[0]?.message).toMatch(/not found/i)
|
|
})
|
|
|
|
it('should update a single normal document when trash: false', async () => {
|
|
const query = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocOne.id, payload)}, trash: false, data: { title: "Updated Normal via GQL" }) {
|
|
id
|
|
title
|
|
deletedAt
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.updatePost.id).toBe(postsDocOne.id)
|
|
expect(res.data.updatePost.title).toBe('Updated Normal via GQL')
|
|
expect(res.data.updatePost.deletedAt).toBeNull()
|
|
})
|
|
|
|
it('should restore a soft-deleted document by setting deletedAt to null', async () => {
|
|
const mutation = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true, data: {
|
|
deletedAt: null
|
|
}) {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: mutation }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.updatePost.deletedAt).toBeNull()
|
|
|
|
const query = `
|
|
query {
|
|
Posts(trash: false) {
|
|
docs {
|
|
id
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const restored = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
const match = restored.data.Posts.docs.find((doc: Post) => doc.id === postsDocTwo.id)
|
|
expect(match).toBeDefined()
|
|
expect(match.deletedAt).toBeNull()
|
|
})
|
|
})
|
|
|
|
// describe('update endpoint', () => {
|
|
// it.todo('should update only normal document when trash: false')
|
|
|
|
// it.todo('should update all documents including soft-deleted documents when trash: true')
|
|
|
|
// it.todo(
|
|
// 'should only update soft-deleted documents when trash: true and where[deletedAt][exists]=true',
|
|
// )
|
|
// })
|
|
|
|
// describe('delete endpoint', () => {
|
|
// it.todo('should perma delete all docs including soft-deleted documents when trash: true')
|
|
|
|
// it.todo('should only perma delete normal docs when trash: false')
|
|
// })
|
|
|
|
describe('deleteByID query', () => {
|
|
it('should throw NotFound error when trying to delete a soft-deleted document w/o trash: true', async () => {
|
|
const query = `
|
|
mutation {
|
|
deletePost(id: ${idToString(postsDocTwo.id, payload)}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.errors?.[0]?.message).toMatch(/not found/i)
|
|
})
|
|
|
|
it('should delete a soft-deleted document when trash: true', async () => {
|
|
const query = `
|
|
mutation {
|
|
deletePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
expect(res.data.deletePost.id).toBe(postsDocTwo.id)
|
|
})
|
|
})
|
|
|
|
describe('restoreVersion operation', () => {
|
|
it('should throw error when restoring a version of a trashed document', async () => {
|
|
const updateMutation = `
|
|
mutation {
|
|
updatePost(id: ${idToString(postsDocTwo.id, payload)}, trash: true, data: {
|
|
title: "Soft Deleted Version"
|
|
}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
await restClient.GRAPHQL_POST({ body: JSON.stringify({ query: updateMutation }) })
|
|
|
|
const versionQuery = `
|
|
query {
|
|
versionsPosts(
|
|
trash: true,
|
|
where: {
|
|
version__deletedAt: { exists: true }
|
|
}
|
|
) {
|
|
docs {
|
|
id
|
|
parent {
|
|
id
|
|
}
|
|
version {
|
|
deletedAt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const versionRes = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: versionQuery }) })
|
|
.then((r) => r.json())
|
|
|
|
const version = versionRes.data.versionsPosts.docs.find((v: any) => v?.version?.deletedAt)
|
|
|
|
expect(version?.id).toBeDefined()
|
|
|
|
expect(version).toBeDefined()
|
|
|
|
const restoreMutation = `
|
|
mutation {
|
|
restoreVersionPost(id: ${idToString(version.id, payload)}) {
|
|
id
|
|
}
|
|
}
|
|
`
|
|
const restoreRes = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query: restoreMutation }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(restoreRes.errors?.[0]?.message).toMatch(
|
|
/Cannot restore a version of a trashed document/i,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('count query', () => {
|
|
it('should return count of non-soft-deleted documents by default (trash=false)', async () => {
|
|
const query = `
|
|
query {
|
|
countPosts {
|
|
totalDocs
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.countPosts.totalDocs).toBe(1)
|
|
})
|
|
|
|
it('should return count of all documents including soft-deleted when trash=true', async () => {
|
|
const query = `
|
|
query {
|
|
countPosts(trash: true) {
|
|
totalDocs
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.countPosts.totalDocs).toBe(2)
|
|
})
|
|
|
|
it('should return count of only soft-deleted docs with where[deletedAt][exists]=true', async () => {
|
|
const query = `
|
|
query {
|
|
countPosts(trash: true, where: { deletedAt: { exists: true } }) {
|
|
totalDocs
|
|
}
|
|
}
|
|
`
|
|
const res = await restClient
|
|
.GRAPHQL_POST({ body: JSON.stringify({ query }) })
|
|
.then((r) => r.json())
|
|
|
|
expect(res.data.countPosts.totalDocs).toBe(1)
|
|
})
|
|
})
|
|
})
|
|
})
|