diff --git a/src/fields/hooks/afterRead/relationshipPopulationPromise.ts b/src/fields/hooks/afterRead/relationshipPopulationPromise.ts index e0f3ef341..9549bd743 100644 --- a/src/fields/hooks/afterRead/relationshipPopulationPromise.ts +++ b/src/fields/hooks/afterRead/relationshipPopulationPromise.ts @@ -10,6 +10,7 @@ type PopulateArgs = { data: Record field: RelationshipField | UploadField index?: number + key?: string showHiddenFields: boolean } @@ -22,6 +23,7 @@ const populate = async ({ data, field, index, + key, showHiddenFields, }: PopulateArgs) => { const dataToUpdate = dataReference; @@ -33,7 +35,7 @@ const populate = async ({ let relationshipValue; const shouldPopulate = depth && currentDepth <= depth; - if (typeof id !== 'string' && typeof id !== 'number' && typeof id?.toString === 'function') { + if (typeof id !== 'string' && typeof id !== 'number' && typeof id?.toString === 'function' && typeof id !== 'object') { id = id.toString(); } @@ -55,11 +57,17 @@ const populate = async ({ relationshipValue = id; } - if (typeof index === 'number') { + if (typeof index === 'number' && typeof key === 'string') { if (Array.isArray(field.relationTo)) { - dataToUpdate[field.name][index].value = relationshipValue; + dataToUpdate[field.name][key][index].value = relationshipValue; } else { - dataToUpdate[field.name][index] = relationshipValue; + dataToUpdate[field.name][key][index] = relationshipValue; + } + } else if (typeof index === 'number' || typeof key === 'string') { + if (Array.isArray(field.relationTo)) { + dataToUpdate[field.name][index ?? key].value = relationshipValue; + } else { + dataToUpdate[field.name][index ?? key] = relationshipValue; } } else if (Array.isArray(field.relationTo)) { dataToUpdate[field.name].value = relationshipValue; @@ -90,27 +98,67 @@ const relationshipPopulationPromise = async ({ }: PromiseArgs): Promise => { const resultingDoc = siblingDoc; const populateDepth = fieldHasMaxDepth(field) && field.maxDepth < depth ? field.maxDepth : depth; + const rowPromises = []; - if (fieldSupportsMany(field) && field.hasMany && Array.isArray(siblingDoc[field.name])) { - const rowPromises = []; - - siblingDoc[field.name].forEach((relatedDoc, index) => { - const rowPromise = async () => { - if (relatedDoc) { - await populate({ - depth: populateDepth, - currentDepth, - req, - overrideAccess, - data: relatedDoc, - dataReference: resultingDoc, - field, - index, - showHiddenFields, + if (fieldSupportsMany(field) && field.hasMany) { + if (req.locale === 'all' && typeof siblingDoc[field.name] === 'object') { + Object.keys(siblingDoc[field.name]).forEach((key) => { + if (Array.isArray(siblingDoc[field.name][key])) { + siblingDoc[field.name][key].forEach((relatedDoc, index) => { + const rowPromise = async () => { + await populate({ + depth: populateDepth, + currentDepth, + req, + overrideAccess, + data: siblingDoc[field.name][key][index], + dataReference: resultingDoc, + field, + index, + key, + showHiddenFields, + }); + }; + rowPromises.push(rowPromise()); }); } - }; + }); + } else if (Array.isArray(siblingDoc[field.name])) { + siblingDoc[field.name].forEach((relatedDoc, index) => { + const rowPromise = async () => { + if (relatedDoc) { + await populate({ + depth: populateDepth, + currentDepth, + req, + overrideAccess, + data: relatedDoc, + dataReference: resultingDoc, + field, + index, + showHiddenFields, + }); + } + }; + rowPromises.push(rowPromise()); + }); + } + } else if (typeof siblingDoc[field.name] === 'object' && req.locale === 'all') { + Object.keys(siblingDoc[field.name]).forEach((key) => { + const rowPromise = async () => { + await populate({ + depth: populateDepth, + currentDepth, + req, + overrideAccess, + data: siblingDoc[field.name][key], + dataReference: resultingDoc, + field, + key, + showHiddenFields, + }); + }; rowPromises.push(rowPromise()); }); @@ -127,6 +175,7 @@ const relationshipPopulationPromise = async ({ showHiddenFields, }); } + await Promise.all(rowPromises); }; export default relationshipPopulationPromise; diff --git a/test/localization/config.ts b/test/localization/config.ts index 770002967..ad067cce9 100644 --- a/test/localization/config.ts +++ b/test/localization/config.ts @@ -1,6 +1,6 @@ import { buildConfig } from '../buildConfig'; import { devUser } from '../credentials'; -import { LocalizedPost } from './payload-types'; +import { LocalizedPost, RelationshipLocalized } from './payload-types'; import { defaultLocale, englishTitle, @@ -21,7 +21,7 @@ export type LocalizedPostAllLocale = LocalizedPost & { export const slug = 'localized-posts'; export const withLocalizedRelSlug = 'with-localized-relationship'; - +export const relationshipLocalizedSlug = 'relationship-localized'; export const withRequiredLocalizedFields = 'localized-required'; const openAccess = { @@ -133,6 +133,37 @@ export default buildConfig({ }, ], }, + { + slug: relationshipLocalizedSlug, + fields: [ + { + name: 'relationship', + type: 'relationship', + relationTo: slug, + localized: true, + }, + { + name: 'relationshipHasMany', + type: 'relationship', + relationTo: slug, + hasMany: true, + localized: true, + }, + { + name: 'relationMultiRelationTo', + type: 'relationship', + relationTo: [slug, 'dummy'], + localized: true, + }, + { + name: 'relationMultiRelationToHasMany', + type: 'relationship', + relationTo: [slug, 'dummy'], + hasMany: true, + localized: true, + }, + ], + }, { slug: 'dummy', access: openAccess, @@ -210,10 +241,10 @@ export default buildConfig({ }, }); - await payload.create({ + await payload.create({ collection: withLocalizedRelSlug, data: { - localizedRelationship: localizedRelation.id, + relationship: localizedRelation.id, localizedRelationHasManyField: [localizedRelation.id, localizedRelation2.id], localizedRelationMultiRelationTo: { relationTo: collection, value: localizedRelation.id }, localizedRelationMultiRelationToHasMany: [ @@ -222,5 +253,18 @@ export default buildConfig({ ], }, }); + await payload.create({ + collection: relationshipLocalizedSlug, + locale: 'en', + data: { + relationship: localizedRelation.id, + relationshipHasMany: [localizedRelation.id, localizedRelation2.id], + relationMultiRelationTo: { relationTo: collection, value: localizedRelation.id }, + relationMultiRelationToHasMany: [ + { relationTo: slug, value: localizedRelation.id }, + { relationTo: slug, value: localizedRelation2.id }, + ], + }, + }); }, }); diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index b3c02f653..c44808e52 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -2,9 +2,14 @@ import mongoose from 'mongoose'; import { GraphQLClient } from 'graphql-request'; import { initPayloadTest } from '../helpers/configHelpers'; import payload from '../../src'; -import type { LocalizedPost, WithLocalizedRelationship, LocalizedRequired } from './payload-types'; +import type { + LocalizedPost, + WithLocalizedRelationship, + LocalizedRequired, + RelationshipLocalized, +} from './payload-types'; import type { LocalizedPostAllLocale } from './config'; -import config, { slug, withLocalizedRelSlug, withRequiredLocalizedFields } from './config'; +import config, { relationshipLocalizedSlug, slug, withLocalizedRelSlug, withRequiredLocalizedFields } from './config'; import { defaultLocale, englishTitle, @@ -191,271 +196,283 @@ describe('Localization', () => { expect(result.docs[0].id).toEqual(localizedPost.id); }); + }); + }); + describe('Localized Relationship', () => { + let localizedRelation: LocalizedPost; + let localizedRelation2: LocalizedPost; + let withRelationship: WithLocalizedRelationship; - describe('Localized Relationship', () => { - let localizedRelation: LocalizedPost; - let localizedRelation2: LocalizedPost; - let withRelationship: WithLocalizedRelationship; + beforeAll(async () => { + localizedRelation = await createLocalizedPost({ + title: { + [defaultLocale]: relationEnglishTitle, + [spanishLocale]: relationSpanishTitle, + }, + }); + localizedRelation2 = await createLocalizedPost({ + title: { + [defaultLocale]: relationEnglishTitle2, + [spanishLocale]: relationSpanishTitle2, + }, + }); - beforeAll(async () => { - localizedRelation = await createLocalizedPost({ - title: { - [defaultLocale]: relationEnglishTitle, - [spanishLocale]: relationSpanishTitle, + withRelationship = await payload.create({ + collection: withLocalizedRelSlug, + data: { + localizedRelationship: localizedRelation.id, + localizedRelationHasManyField: [localizedRelation.id, localizedRelation2.id], + localizedRelationMultiRelationTo: { relationTo: slug, value: localizedRelation.id }, + localizedRelationMultiRelationToHasMany: [ + { relationTo: slug, value: localizedRelation.id }, + { relationTo: slug, value: localizedRelation2.id }, + ], + }, + }); + }); + + describe('regular relationship', () => { + it('can query localized relationship', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationship.title': { + equals: localizedRelation.title, }, - }); - localizedRelation2 = await createLocalizedPost({ - title: { - [defaultLocale]: relationEnglishTitle2, - [spanishLocale]: relationSpanishTitle2, - }, - }); + }, + }); - withRelationship = await payload.create({ + expect(result.docs[0].id).toEqual(withRelationship.id); + }); + + it('specific locale', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + locale: spanishLocale, + where: { + 'localizedRelationship.title': { + equals: relationSpanishTitle, + }, + }, + }); + + expect(result.docs[0].id).toEqual(withRelationship.id); + }); + + it('all locales', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + locale: 'all', + where: { + 'localizedRelationship.title.es': { + equals: relationSpanishTitle, + }, + }, + }); + + expect(result.docs[0].id).toEqual(withRelationship.id); + }); + + it('populates relationships with all locales', async () => { + // the relationship fields themselves are localized on this collection + const result = await payload.find({ + collection: relationshipLocalizedSlug, + locale: 'all', + depth: 1, + }); + expect((result.docs[0].relationship as any).en.id).toBeDefined(); + expect((result.docs[0].relationshipHasMany as any).en[0].id).toBeDefined(); + expect((result.docs[0].relationMultiRelationTo as any).en.value.id).toBeDefined(); + expect((result.docs[0].relationMultiRelationToHasMany as any).en[0].value.id).toBeDefined(); + }); + }); + + describe('relationship - hasMany', () => { + it('default locale', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationHasManyField.title': { + equals: localizedRelation.title, + }, + }, + }); + + expect(result.docs[0].id).toEqual(withRelationship.id); + + // Second relationship + const result2 = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationHasManyField.title': { + equals: localizedRelation2.title, + }, + }, + }); + + expect(result2.docs[0].id).toEqual(withRelationship.id); + }); + + it('specific locale', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + locale: spanishLocale, + where: { + 'localizedRelationHasManyField.title': { + equals: relationSpanishTitle, + }, + }, + }); + + expect(result.docs[0].id).toEqual(withRelationship.id); + + // Second relationship + const result2 = await payload.find({ + collection: withLocalizedRelSlug, + locale: spanishLocale, + where: { + 'localizedRelationHasManyField.title': { + equals: relationSpanishTitle2, + }, + }, + }); + + expect(result2.docs[0].id).toEqual(withRelationship.id); + }); + + it('relationship population uses locale', async () => { + const result = await payload.findByID({ + collection: withLocalizedRelSlug, + depth: 1, + id: withRelationship.id, + locale: spanishLocale, + }); + expect((result.localizedRelationship as LocalizedPost).title).toEqual(relationSpanishTitle); + }); + + it('all locales', async () => { + const queryRelation = (where: Where) => { + return payload.find({ collection: withLocalizedRelSlug, - data: { - localizedRelationship: localizedRelation.id, - localizedRelationHasManyField: [localizedRelation.id, localizedRelation2.id], - localizedRelationMultiRelationTo: { relationTo: slug, value: localizedRelation.id }, - localizedRelationMultiRelationToHasMany: [ - { relationTo: slug, value: localizedRelation.id }, - { relationTo: slug, value: localizedRelation2.id }, - ], + locale: 'all', + where, + }); + }; + + const result = await queryRelation({ + 'localizedRelationHasManyField.title.en': { + equals: relationEnglishTitle, + }, + }); + + expect(result.docs[0].id).toEqual(withRelationship.id); + + // First relationship - spanish + const result2 = await queryRelation({ + 'localizedRelationHasManyField.title.es': { + equals: relationSpanishTitle, + }, + }); + + expect(result2.docs[0].id).toEqual(withRelationship.id); + + // Second relationship - english + const result3 = await queryRelation({ + 'localizedRelationHasManyField.title.en': { + equals: relationEnglishTitle2, + }, + }); + + expect(result3.docs[0].id).toEqual(withRelationship.id); + + // Second relationship - spanish + const result4 = await queryRelation({ + 'localizedRelationHasManyField.title.es': { + equals: relationSpanishTitle2, + }, + }); + + expect(result4.docs[0].id).toEqual(withRelationship.id); + }); + }); + + describe('relationTo multi', () => { + it('by id', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationMultiRelationTo.value': { + equals: localizedRelation.id, }, - }); + }, }); - describe('regular relationship', () => { - it('can query localized relationship', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationship.title': { - equals: localizedRelation.title, - }, - }, - }); + expect(result.docs[0].id).toEqual(withRelationship.id); - expect(result.docs[0].id).toEqual(withRelationship.id); - }); - - it('specific locale', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - locale: spanishLocale, - where: { - 'localizedRelationship.title': { - equals: relationSpanishTitle, - }, - }, - }); - - expect(result.docs[0].id).toEqual(withRelationship.id); - }); - - it('all locales', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - locale: 'all', - where: { - 'localizedRelationship.title.es': { - equals: relationSpanishTitle, - }, - }, - }); - - expect(result.docs[0].id).toEqual(withRelationship.id); - }); + // Second relationship + const result2 = await payload.find({ + collection: withLocalizedRelSlug, + locale: spanishLocale, + where: { + 'localizedRelationMultiRelationTo.value': { + equals: localizedRelation.id, + }, + }, }); - describe('relationship - hasMany', () => { - it('default locale', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationHasManyField.title': { - equals: localizedRelation.title, - }, - }, - }); + expect(result2.docs[0].id).toEqual(withRelationship.id); + }); + }); - expect(result.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - const result2 = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationHasManyField.title': { - equals: localizedRelation2.title, - }, - }, - }); - - expect(result2.docs[0].id).toEqual(withRelationship.id); - }); - - it('specific locale', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - locale: spanishLocale, - where: { - 'localizedRelationHasManyField.title': { - equals: relationSpanishTitle, - }, - }, - }); - - expect(result.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - const result2 = await payload.find({ - collection: withLocalizedRelSlug, - locale: spanishLocale, - where: { - 'localizedRelationHasManyField.title': { - equals: relationSpanishTitle2, - }, - }, - }); - - expect(result2.docs[0].id).toEqual(withRelationship.id); - }); - - it('relationship population uses locale', async () => { - const result = await payload.findByID({ - collection: withLocalizedRelSlug, - depth: 1, - id: withRelationship.id, - locale: spanishLocale, - }); - expect((result.localizedRelationship as LocalizedPost).title).toEqual(relationSpanishTitle); - }); - - it('all locales', async () => { - const queryRelation = (where: Where) => { - return payload.find({ - collection: withLocalizedRelSlug, - locale: 'all', - where, - }); - }; - - const result = await queryRelation({ - 'localizedRelationHasManyField.title.en': { - equals: relationEnglishTitle, - }, - }); - - expect(result.docs[0].id).toEqual(withRelationship.id); - - // First relationship - spanish - const result2 = await queryRelation({ - 'localizedRelationHasManyField.title.es': { - equals: relationSpanishTitle, - }, - }); - - expect(result2.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - english - const result3 = await queryRelation({ - 'localizedRelationHasManyField.title.en': { - equals: relationEnglishTitle2, - }, - }); - - expect(result3.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - spanish - const result4 = await queryRelation({ - 'localizedRelationHasManyField.title.es': { - equals: relationSpanishTitle2, - }, - }); - - expect(result4.docs[0].id).toEqual(withRelationship.id); - }); + describe('relationTo multi hasMany', () => { + it('by id', async () => { + const result = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationMultiRelationToHasMany.value': { + equals: localizedRelation.id, + }, + }, }); - describe('relationTo multi', () => { - it('by id', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationMultiRelationTo.value': { - equals: localizedRelation.id, - }, - }, - }); + expect(result.docs[0].id).toEqual(withRelationship.id); - expect(result.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - const result2 = await payload.find({ - collection: withLocalizedRelSlug, - locale: spanishLocale, - where: { - 'localizedRelationMultiRelationTo.value': { - equals: localizedRelation.id, - }, - }, - }); - - expect(result2.docs[0].id).toEqual(withRelationship.id); - }); + // First relationship - spanish locale + const result2 = await payload.find({ + collection: withLocalizedRelSlug, + locale: spanishLocale, + where: { + 'localizedRelationMultiRelationToHasMany.value': { + equals: localizedRelation.id, + }, + }, }); - describe('relationTo multi hasMany', () => { - it('by id', async () => { - const result = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationMultiRelationToHasMany.value': { - equals: localizedRelation.id, - }, - }, - }); + expect(result2.docs[0].id).toEqual(withRelationship.id); - expect(result.docs[0].id).toEqual(withRelationship.id); - - // First relationship - spanish locale - const result2 = await payload.find({ - collection: withLocalizedRelSlug, - locale: spanishLocale, - where: { - 'localizedRelationMultiRelationToHasMany.value': { - equals: localizedRelation.id, - }, - }, - }); - - expect(result2.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - const result3 = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationMultiRelationToHasMany.value': { - equals: localizedRelation2.id, - }, - }, - }); - - expect(result3.docs[0].id).toEqual(withRelationship.id); - - // Second relationship - spanish locale - const result4 = await payload.find({ - collection: withLocalizedRelSlug, - where: { - 'localizedRelationMultiRelationToHasMany.value': { - equals: localizedRelation2.id, - }, - }, - }); - - expect(result4.docs[0].id).toEqual(withRelationship.id); - }); + // Second relationship + const result3 = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationMultiRelationToHasMany.value': { + equals: localizedRelation2.id, + }, + }, }); + + expect(result3.docs[0].id).toEqual(withRelationship.id); + + // Second relationship - spanish locale + const result4 = await payload.find({ + collection: withLocalizedRelSlug, + where: { + 'localizedRelationMultiRelationToHasMany.value': { + equals: localizedRelation2.id, + }, + }, + }); + + expect(result4.docs[0].id).toEqual(withRelationship.id); }); }); }); diff --git a/test/localization/payload-types.ts b/test/localization/payload-types.ts index 6bfc1edbe..d0ddb398d 100644 --- a/test/localization/payload-types.ts +++ b/test/localization/payload-types.ts @@ -63,7 +63,7 @@ export interface LocalizedRequired { export interface WithLocalizedRelationship { id: string; localizedRelationship?: string | LocalizedPost; - localizedRelationHasManyField?: (string | LocalizedPost)[]; + localizedRelationHasManyField?: string[] | LocalizedPost[]; localizedRelationMultiRelationTo?: | { value: string | LocalizedPost; @@ -73,16 +73,27 @@ export interface WithLocalizedRelationship { value: string | Dummy; relationTo: 'dummy'; }; - localizedRelationMultiRelationToHasMany?: ( - | { - value: string | LocalizedPost; - relationTo: 'localized-posts'; - } - | { - value: string | Dummy; - relationTo: 'dummy'; - } - )[]; + localizedRelationMultiRelationToHasMany?: + | ( + | { + value: string; + relationTo: 'localized-posts'; + } + | { + value: string; + relationTo: 'dummy'; + } + )[] + | ( + | { + value: LocalizedPost; + relationTo: 'localized-posts'; + } + | { + value: Dummy; + relationTo: 'dummy'; + } + )[]; createdAt: string; updatedAt: string; } @@ -96,3 +107,44 @@ export interface Dummy { createdAt: string; updatedAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "relationship-localized". + */ +export interface RelationshipLocalized { + id: string; + relationship?: string | LocalizedPost; + relationshipHasMany?: string[] | LocalizedPost[]; + relationMultiRelationTo?: + | { + value: string | LocalizedPost; + relationTo: 'localized-posts'; + } + | { + value: string | Dummy; + relationTo: 'dummy'; + }; + relationMultiRelationToHasMany?: + | ( + | { + value: string; + relationTo: 'localized-posts'; + } + | { + value: string; + relationTo: 'dummy'; + } + )[] + | ( + | { + value: LocalizedPost; + relationTo: 'localized-posts'; + } + | { + value: Dummy; + relationTo: 'dummy'; + } + )[]; + createdAt: string; + updatedAt: string; +}