fix: error on graphql multiple queries (#3985)

This commit is contained in:
Dan Ribbens
2023-11-08 12:38:25 -05:00
committed by GitHub
parent 611438177b
commit 57da3c99a7
31 changed files with 292 additions and 143 deletions

View File

@@ -4,6 +4,7 @@ export const commitTransaction: CommitTransaction = async function commitTransac
if (!this.sessions[id]?.inTransaction()) { if (!this.sessions[id]?.inTransaction()) {
return return
} }
await this.sessions[id].commitTransaction() await this.sessions[id].commitTransaction()
await this.sessions[id].endSession() await this.sessions[id].endSession()
delete this.sessions[id] delete this.sessions[id]

View File

@@ -3,10 +3,20 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction( export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '', id = '',
) { ) {
if (!this.sessions[id]?.inTransaction()) { // if multiple operations are using the same transaction, the first will flow through and delete the session.
this.payload.logger.warn('rollbackTransaction called when no transaction exists') // subsequent calls should be ignored.
if (!this.sessions[id]) {
return return
} }
// when session exists but is not inTransaction something unexpected is happening to the session
if (!this.sessions[id].inTransaction()) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
delete this.sessions[id]
return
}
// the first call for rollback should be aborted and deleted causing any other operations with the same transaction to fail
await this.sessions[id].abortTransaction() await this.sessions[id].abortTransaction()
await this.sessions[id].endSession() await this.sessions[id].endSession()
delete this.sessions[id] delete this.sessions[id]

View File

@@ -1,6 +1,7 @@
import type { CommitTransaction } from 'payload/database' import type { CommitTransaction } from 'payload/database'
export const commitTransaction: CommitTransaction = async function commitTransaction(id) { export const commitTransaction: CommitTransaction = async function commitTransaction(id) {
// if the session was deleted it has already been aborted
if (!this.sessions[id]) { if (!this.sessions[id]) {
return return
} }

View File

@@ -3,12 +3,15 @@ import type { RollbackTransaction } from 'payload/database'
export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction( export const rollbackTransaction: RollbackTransaction = async function rollbackTransaction(
id = '', id = '',
) { ) {
// if multiple operations are using the same transaction, the first will flow through and delete the session.
// subsequent calls should be ignored.
if (!this.sessions[id]) { if (!this.sessions[id]) {
this.payload.logger.warn('rollbackTransaction called when no transaction exists')
return return
} }
// end the session promise in failure by calling reject
await this.sessions[id].reject() await this.sessions[id].reject()
// delete the session causing any other operations with the same transaction to fail
delete this.sessions[id] delete this.sessions[id]
} }

View File

@@ -1,3 +1,4 @@
import type { PayloadRequest } from '../../../express/types'
import type { Payload } from '../../../payload' import type { Payload } from '../../../payload'
import formatName from '../../../graphql/utilities/formatName' import formatName from '../../../graphql/utilities/formatName'
@@ -18,7 +19,7 @@ const formatConfigNames = (results, configs) => {
function accessResolver(payload: Payload) { function accessResolver(payload: Payload) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const accessResults = await access(options) const accessResults = await access(options)

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import forgotPassword from '../../operations/forgotPassword' import forgotPassword from '../../operations/forgotPassword'
@@ -11,7 +12,7 @@ function forgotPasswordResolver(collection: Collection): any {
}, },
disableEmail: args.disableEmail, disableEmail: args.disableEmail,
expiration: args.expiration, expiration: args.expiration,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
await forgotPassword(options) await forgotPassword(options)

View File

@@ -1,10 +1,12 @@
import type { PayloadRequest } from '../../../express/types'
import init from '../../operations/init' import init from '../../operations/init'
function initResolver(collection: string) { function initResolver(collection: string) {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
collection, collection,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
return init(options) return init(options)

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import login from '../../operations/login' import login from '../../operations/login'
@@ -11,7 +12,7 @@ function loginResolver(collection: Collection) {
password: args.password, password: args.password,
}, },
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
res: context.res, res: context.res,
} }

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import logout from '../../operations/logout' import logout from '../../operations/logout'
@@ -6,7 +7,7 @@ function logoutResolver(collection: Collection): any {
async function resolver(_, args, context) { async function resolver(_, args, context) {
const options = { const options = {
collection, collection,
req: context.req, req: { ...context.req } as PayloadRequest,
res: context.res, res: context.res,
} }

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import me from '../../operations/me' import me from '../../operations/me'
@@ -7,10 +8,11 @@ function meResolver(collection: Collection): any {
const options = { const options = {
collection, collection,
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
return me(options) return me(options)
} }
return resolver return resolver
} }

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import getExtractJWT from '../../getExtractJWT' import getExtractJWT from '../../getExtractJWT'
import refresh from '../../operations/refresh' import refresh from '../../operations/refresh'
@@ -17,7 +18,7 @@ function refreshResolver(collection: Collection) {
const options = { const options = {
collection, collection,
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
res: context.res, res: context.res,
token, token,
} }

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import resetPassword from '../../operations/resetPassword' import resetPassword from '../../operations/resetPassword'
@@ -13,7 +14,7 @@ function resetPasswordResolver(collection: Collection) {
collection, collection,
data: args, data: args,
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
res: context.res, res: context.res,
} }

View File

@@ -1,4 +1,5 @@
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import unlock from '../../operations/unlock' import unlock from '../../operations/unlock'
@@ -7,12 +8,13 @@ function unlockResolver(collection: Collection) {
const options = { const options = {
collection, collection,
data: { email: args.email }, data: { email: args.email },
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await unlock(options) const result = await unlock(options)
return result return result
} }
return resolver return resolver
} }

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type { Collection } from '../../../collections/config/types' import type { Collection } from '../../../collections/config/types'
import type { PayloadRequest } from '../../../express/types'
import verifyEmail from '../../operations/verifyEmail' import verifyEmail from '../../operations/verifyEmail'
@@ -11,7 +12,7 @@ function verifyEmailResolver(collection: Collection) {
const options = { const options = {
api: 'GraphQL', api: 'GraphQL',
collection, collection,
req: context.req, req: { ...context.req } as PayloadRequest,
res: context.res, res: context.res,
token: args.token, token: args.token,
} }

View File

@@ -37,7 +37,7 @@ export default function createResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data, data: args.data,
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await create(options) const result = await create(options)

View File

@@ -30,7 +30,7 @@ export default function getDeleteResolver<TSlug extends keyof GeneratedTypes['co
id: args.id, id: args.id,
collection, collection,
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await deleteByID(options) const result = await deleteByID(options)

View File

@@ -18,7 +18,7 @@ export function docAccessResolver(): Resolver {
async function resolver(_, args, context) { async function resolver(_, args, context) {
return docAccess({ return docAccess({
id: args.id, id: args.id,
req: context.req, req: { ...context.req } as PayloadRequest,
}) })
} }

View File

@@ -36,7 +36,7 @@ export default function findResolver(collection: Collection): Resolver {
draft: args.draft, draft: args.draft,
limit: args.limit, limit: args.limit,
page: args.page, page: args.page,
req: context.req, req: { ...context.req } as PayloadRequest,
sort: args.sort, sort: args.sort,
where: args.where, where: args.where,
} }

View File

@@ -31,7 +31,7 @@ export default function findVersionByIDResolver(collection: Collection): Resolve
collection, collection,
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await findVersionByID(options) const result = await findVersionByID(options)

View File

@@ -35,7 +35,7 @@ export default function findVersionsResolver(collection: Collection): Resolver {
depth: 0, depth: 0,
limit: args.limit, limit: args.limit,
page: args.page, page: args.page,
req: context.req, req: { ...context.req } as PayloadRequest,
sort: args.sort, sort: args.sort,
where: args.where, where: args.where,
} }

View File

@@ -23,7 +23,7 @@ export default function restoreVersionResolver(collection: Collection): Resolver
id: args.id, id: args.id,
collection, collection,
depth: 0, depth: 0,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await restoreVersion(options) const result = await restoreVersion(options)

View File

@@ -36,7 +36,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['colle
data: args.data, data: args.data,
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await updateByID<TSlug>(options) const result = await updateByID<TSlug>(options)

View File

@@ -16,7 +16,7 @@ export function docAccessResolver(global: SanitizedGlobalConfig): Resolver {
async function resolver(_, context) { async function resolver(_, context) {
return docAccess({ return docAccess({
globalConfig: global, globalConfig: global,
req: context.req, req: { ...context.req } as PayloadRequest,
}) })
} }

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import type { Document } from '../../../types' import type { Document, PayloadRequest } from '../../../types'
import type { SanitizedGlobalConfig } from '../../config/types' import type { SanitizedGlobalConfig } from '../../config/types'
import findOne from '../../operations/findOne' import findOne from '../../operations/findOne'
@@ -16,7 +16,7 @@ export default function findOneResolver(globalConfig: SanitizedGlobalConfig): Do
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
globalConfig, globalConfig,
req: context.req, req: { ...context.req } as PayloadRequest,
slug, slug,
} }

View File

@@ -31,7 +31,7 @@ export default function findVersionByIDResolver(globalConfig: SanitizedGlobalCon
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
globalConfig, globalConfig,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await findVersionByID(options) const result = await findVersionByID(options)

View File

@@ -29,7 +29,7 @@ export default function findVersionsResolver(globalConfig: SanitizedGlobalConfig
globalConfig, globalConfig,
limit: args.limit, limit: args.limit,
page: args.page, page: args.page,
req: context.req, req: { ...context.req } as PayloadRequest,
sort: args.sort, sort: args.sort,
where: args.where, where: args.where,
} }

View File

@@ -22,7 +22,7 @@ export default function restoreVersionResolver(globalConfig: SanitizedGlobalConf
id: args.id, id: args.id,
depth: 0, depth: 0,
globalConfig, globalConfig,
req: context.req, req: { ...context.req } as PayloadRequest,
} }
const result = await restoreVersion(options) const result = await restoreVersion(options)

View File

@@ -35,7 +35,7 @@ export default function updateResolver<TSlug extends keyof GeneratedTypes['globa
depth: 0, depth: 0,
draft: args.draft, draft: args.draft,
globalConfig, globalConfig,
req: context.req, req: { ...context.req } as PayloadRequest,
slug, slug,
} }

View File

@@ -12,14 +12,13 @@ export interface Relation {
const openAccess = { const openAccess = {
create: () => true, create: () => true,
delete: () => true,
read: () => true, read: () => true,
update: () => true, update: () => true,
delete: () => true,
} }
const collectionWithName = (collectionSlug: string): CollectionConfig => { const collectionWithName = (collectionSlug: string): CollectionConfig => {
return { return {
slug: collectionSlug,
access: openAccess, access: openAccess,
fields: [ fields: [
{ {
@@ -27,6 +26,7 @@ const collectionWithName = (collectionSlug: string): CollectionConfig => {
type: 'text', type: 'text',
}, },
], ],
slug: collectionSlug,
} }
} }
@@ -35,47 +35,27 @@ export const relationSlug = 'relation'
export const pointSlug = 'point' export const pointSlug = 'point'
export const errorOnHookSlug = 'error-on-hooks'
export default buildConfigWithDefaults({ export default buildConfigWithDefaults({
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
queries: (GraphQL) => {
return {
QueryWithInternalError: {
type: new GraphQL.GraphQLObjectType({
name: 'QueryWithInternalError',
fields: {
text: {
type: GraphQL.GraphQLString,
},
},
}),
resolve: () => {
// Throwing an internal error with potentially sensitive data
throw new Error('Lost connection to the Pentagon. Secret data: ******')
},
},
}
},
},
collections: [ collections: [
{ {
slug: 'users',
auth: true,
access: openAccess, access: openAccess,
auth: true,
fields: [], fields: [],
slug: 'users',
}, },
{ {
slug: pointSlug,
access: openAccess, access: openAccess,
fields: [ fields: [
{ {
type: 'point',
name: 'point', name: 'point',
type: 'point',
}, },
], ],
slug: pointSlug,
}, },
{ {
slug,
access: openAccess, access: openAccess,
fields: [ fields: [
{ {
@@ -92,173 +72,173 @@ export default buildConfigWithDefaults({
}, },
{ {
name: 'min', name: 'min',
type: 'number',
min: 10, min: 10,
type: 'number',
}, },
// Relationship // Relationship
{ {
name: 'relationField', name: 'relationField',
type: 'relationship',
relationTo: relationSlug, relationTo: relationSlug,
type: 'relationship',
}, },
{ {
name: 'relationToCustomID', name: 'relationToCustomID',
type: 'relationship',
relationTo: 'custom-ids', relationTo: 'custom-ids',
type: 'relationship',
}, },
// Relation hasMany // Relation hasMany
{ {
name: 'relationHasManyField', name: 'relationHasManyField',
type: 'relationship',
relationTo: relationSlug,
hasMany: true, hasMany: true,
relationTo: relationSlug,
type: 'relationship',
}, },
// Relation multiple relationTo // Relation multiple relationTo
{ {
name: 'relationMultiRelationTo', name: 'relationMultiRelationTo',
type: 'relationship',
relationTo: [relationSlug, 'dummy'], relationTo: [relationSlug, 'dummy'],
type: 'relationship',
}, },
// Relation multiple relationTo hasMany // Relation multiple relationTo hasMany
{ {
name: 'relationMultiRelationToHasMany', name: 'relationMultiRelationToHasMany',
type: 'relationship',
relationTo: [relationSlug, 'dummy'],
hasMany: true, hasMany: true,
relationTo: [relationSlug, 'dummy'],
type: 'relationship',
}, },
{ {
name: 'A1', name: 'A1',
type: 'group',
fields: [ fields: [
{ {
type: 'text',
name: 'A2', name: 'A2',
defaultValue: 'textInRowInGroup', defaultValue: 'textInRowInGroup',
type: 'text',
}, },
], ],
type: 'group',
}, },
{ {
name: 'B1', name: 'B1',
type: 'group',
fields: [ fields: [
{ {
type: 'collapsible',
label: 'Collapsible',
fields: [ fields: [
{ {
type: 'text',
name: 'B2', name: 'B2',
defaultValue: 'textInRowInGroup', defaultValue: 'textInRowInGroup',
type: 'text',
}, },
], ],
label: 'Collapsible',
type: 'collapsible',
}, },
], ],
type: 'group',
}, },
{ {
name: 'C1', name: 'C1',
type: 'group',
fields: [ fields: [
{ {
type: 'text',
name: 'C2Text', name: 'C2Text',
type: 'text',
}, },
{ {
type: 'row',
fields: [ fields: [
{ {
type: 'collapsible',
label: 'Collapsible2',
fields: [ fields: [
{ {
name: 'C2', name: 'C2',
type: 'group',
fields: [ fields: [
{ {
type: 'row',
fields: [ fields: [
{ {
type: 'collapsible',
label: 'Collapsible2',
fields: [ fields: [
{ {
type: 'text',
name: 'C3', name: 'C3',
type: 'text',
}, },
], ],
label: 'Collapsible2',
type: 'collapsible',
}, },
], ],
type: 'row',
}, },
], ],
type: 'group',
}, },
], ],
label: 'Collapsible2',
type: 'collapsible',
}, },
], ],
type: 'row',
}, },
], ],
type: 'group',
}, },
{ {
type: 'tabs',
tabs: [ tabs: [
{ {
label: 'Tab1',
name: 'D1', name: 'D1',
fields: [ fields: [
{ {
name: 'D2', name: 'D2',
type: 'group',
fields: [ fields: [
{ {
type: 'row',
fields: [ fields: [
{ {
type: 'collapsible',
label: 'Collapsible2',
fields: [ fields: [
{ {
type: 'tabs',
tabs: [ tabs: [
{ {
label: 'Tab1',
fields: [ fields: [
{ {
name: 'D3', name: 'D3',
type: 'group',
fields: [ fields: [
{ {
type: 'row',
fields: [ fields: [
{ {
type: 'collapsible',
label: 'Collapsible2',
fields: [ fields: [
{ {
type: 'text',
name: 'D4', name: 'D4',
type: 'text',
}, },
], ],
label: 'Collapsible2',
type: 'collapsible',
}, },
], ],
type: 'row',
}, },
], ],
type: 'group',
}, },
], ],
label: 'Tab1',
}, },
], ],
type: 'tabs',
}, },
], ],
label: 'Collapsible2',
type: 'collapsible',
}, },
], ],
type: 'row',
}, },
], ],
type: 'group',
}, },
], ],
label: 'Tab1',
}, },
], ],
type: 'tabs',
}, },
], ],
slug,
}, },
{ {
slug: 'custom-ids',
access: { access: {
read: () => true, read: () => true,
}, },
@@ -272,47 +252,98 @@ export default buildConfigWithDefaults({
type: 'text', type: 'text',
}, },
], ],
slug: 'custom-ids',
}, },
collectionWithName(relationSlug), collectionWithName(relationSlug),
collectionWithName('dummy'), collectionWithName('dummy'),
{ {
slug: 'payload-api-test-ones', ...collectionWithName(errorOnHookSlug),
access: {
read: () => true,
},
fields: [ fields: [
{ {
name: 'payloadAPI', name: 'title',
type: 'text', type: 'text',
hooks: {
afterRead: [({ req }) => req.payloadAPI],
}, },
{
name: 'errorBeforeChange',
type: 'checkbox',
},
],
hooks: {
afterDelete: [
({ doc }) => {
if (doc?.errorAfterDelete) {
throw new Error('Error After Delete Thrown')
}
},
],
beforeChange: [
({ originalDoc }) => {
if (originalDoc?.errorBeforeChange) {
throw new Error('Error Before Change Thrown')
}
}, },
], ],
}, },
},
{ {
slug: 'payload-api-test-twos',
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: 'payloadAPI', name: 'payloadAPI',
type: 'text',
hooks: { hooks: {
afterRead: [({ req }) => req.payloadAPI], afterRead: [({ req }) => req.payloadAPI],
}, },
type: 'text',
},
],
slug: 'payload-api-test-ones',
},
{
access: {
read: () => true,
},
fields: [
{
name: 'payloadAPI',
hooks: {
afterRead: [({ req }) => req.payloadAPI],
},
type: 'text',
}, },
{ {
name: 'relation', name: 'relation',
type: 'relationship',
relationTo: 'payload-api-test-ones', relationTo: 'payload-api-test-ones',
type: 'relationship',
}, },
], ],
slug: 'payload-api-test-twos',
}, },
], ],
graphQL: {
queries: (GraphQL) => {
return {
QueryWithInternalError: {
resolve: () => {
// Throwing an internal error with potentially sensitive data
throw new Error('Lost connection to the Pentagon. Secret data: ******')
},
type: new GraphQL.GraphQLObjectType({
name: 'QueryWithInternalError',
fields: {
text: {
type: GraphQL.GraphQLString,
},
},
}),
},
}
},
schemaOutputFile: path.resolve(__dirname, 'schema.graphql'),
},
onInit: async (payload) => { onInit: async (payload) => {
const user = await payload.create({ await payload.create({
collection: 'users', collection: 'users',
data: { data: {
email: devUser.email, email: devUser.email,
@@ -331,8 +362,8 @@ export default buildConfigWithDefaults({
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'has custom ID relation',
relationToCustomID: 1, relationToCustomID: 1,
title: 'has custom ID relation',
}, },
}) })
@@ -353,23 +384,23 @@ export default buildConfigWithDefaults({
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'with-description',
description: 'description', description: 'description',
title: 'with-description',
}, },
}) })
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'numPost1',
number: 1, number: 1,
title: 'numPost1',
}, },
}) })
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'numPost2',
number: 2, number: 2,
title: 'numPost2',
}, },
}) })
@@ -390,15 +421,15 @@ export default buildConfigWithDefaults({
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'rel to hasMany',
relationHasManyField: rel1.id, relationHasManyField: rel1.id,
title: 'rel to hasMany',
}, },
}) })
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'rel to hasMany 2',
relationHasManyField: rel2.id, relationHasManyField: rel2.id,
title: 'rel to hasMany 2',
}, },
}) })
@@ -406,11 +437,11 @@ export default buildConfigWithDefaults({
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'rel to multi',
relationMultiRelationTo: { relationMultiRelationTo: {
relationTo: relationSlug, relationTo: relationSlug,
value: rel2.id, value: rel2.id,
}, },
title: 'rel to multi',
}, },
}) })
@@ -418,7 +449,6 @@ export default buildConfigWithDefaults({
await payload.create({ await payload.create({
collection: slug, collection: slug,
data: { data: {
title: 'rel to multi hasMany',
relationMultiRelationToHasMany: [ relationMultiRelationToHasMany: [
{ {
relationTo: relationSlug, relationTo: relationSlug,
@@ -429,6 +459,7 @@ export default buildConfigWithDefaults({
value: rel2.id, value: rel2.id,
}, },
], ],
title: 'rel to multi hasMany',
}, },
}) })

View File

@@ -5,7 +5,8 @@ import type { Post } from './payload-types'
import payload from '../../packages/payload/src' import payload from '../../packages/payload/src'
import { mapAsync } from '../../packages/payload/src/utilities/mapAsync' import { mapAsync } from '../../packages/payload/src/utilities/mapAsync'
import { initPayloadTest } from '../helpers/configHelpers' import { initPayloadTest } from '../helpers/configHelpers'
import configPromise, { pointSlug, slug } from './config' import { idToString } from '../helpers/idToString'
import configPromise, { errorOnHookSlug, pointSlug, slug } from './config'
const title = 'title' const title = 'title'
@@ -42,8 +43,7 @@ describe('collections-graphql', () => {
beforeEach(async () => { beforeEach(async () => {
existingDoc = await createPost() existingDoc = await createPost()
existingDocGraphQLID = existingDocGraphQLID = idToString(existingDoc.id, payload)
payload.db.defaultIDType === 'number' ? existingDoc.id : `"${existingDoc.id}"`
}) })
it('should create', async () => { it('should create', async () => {
@@ -67,7 +67,7 @@ describe('collections-graphql', () => {
title title
} }
}` }`
const response = await client.request(query, { title }) const response = (await client.request(query, { title })) as any
const doc: Post = response.createPost const doc: Post = response.createPost
expect(doc).toMatchObject({ title }) expect(doc).toMatchObject({ title })
@@ -102,6 +102,92 @@ describe('collections-graphql', () => {
expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id })) expect(docs).toContainEqual(expect.objectContaining({ id: existingDoc.id }))
}) })
it('should read using multiple queries', async () => {
const query = `query {
postIDs: Posts {
docs {
id
}
}
posts: Posts {
docs {
id
title
}
}
}`
const response = await client.request(query)
const { postIDs, posts } = response
expect(postIDs.docs).toBeDefined()
expect(posts.docs).toBeDefined()
})
it('should commit or rollback multiple mutations independently', async () => {
const firstTitle = 'first title'
const secondTitle = 'second title'
const first = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: true,
title: firstTitle,
},
})
const second = await payload.create({
collection: errorOnHookSlug,
data: {
errorBeforeChange: true,
title: secondTitle,
},
})
const updated = 'updated title'
const query = `mutation {
createPost(data: {title: "${title}"}) {
id
title
}
updateFirst: updateErrorOnHook(id: ${idToString(
first.id,
payload,
)}, data: {title: "${updated}"}) {
title
}
updateSecond: updateErrorOnHook(id: ${idToString(
second.id,
payload,
)}, data: {title: "${updated}"}) {
id
title
}
}`
client.requestConfig.errorPolicy = 'all'
const response = await client.request(query)
client.requestConfig.errorPolicy = 'none'
const createdResult = await payload.findByID({
id: response.createPost.id,
collection: slug,
})
const updateFirstResult = await payload.findByID({
id: first.id,
collection: errorOnHookSlug,
})
const updateSecondResult = await payload.findByID({
id: second.id,
collection: errorOnHookSlug,
})
expect(response?.createPost.id).toBeDefined()
expect(response?.updateFirst).toBeNull()
expect(response?.updateSecond).toBeNull()
expect(createdResult).toMatchObject(response.createPost)
expect(updateFirstResult).toMatchObject(first)
expect(updateSecondResult).toStrictEqual(second)
})
it('should retain payload api', async () => { it('should retain payload api', async () => {
const query = ` const query = `
query { query {

View File

@@ -0,0 +1,4 @@
import type { Payload } from '../../packages/payload/src'
export const idToString = (id: number | string, payload: Payload): string =>
`${payload.db.defaultIDType === 'number' ? id : `"${id}"`}`