fix(drizzle): hasMany / poly relationships nested to localized fields / nested blocks to localized fields (#8456)

fixes https://github.com/payloadcms/payload/issues/8455 and
https://github.com/payloadcms/payload/issues/8462

- Builds the `_locale` column for the `_rels` when it's inside of a
localized group / tab
- Properly builds `sanitizedPath` for blocks in the transform-read
function when it's inside of a localized field. This fixes with fields
inside that have its own table (like `_rels`, select `hasMany: true`
etc)

Adds _more_ tests!
This commit is contained in:
Sasha
2024-09-28 17:33:50 +03:00
committed by GitHub
parent fb603448d8
commit 613d3b090e
9 changed files with 677 additions and 6 deletions

View File

@@ -711,7 +711,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
})
if (groupHasLocalizedField) {

View File

@@ -721,7 +721,7 @@ export const traverseFields = ({
rootTableIDColType,
rootTableName,
versions,
withinLocalizedArrayOrBlock,
withinLocalizedArrayOrBlock: withinLocalizedArrayOrBlock || field.localized,
})
if (groupHasLocalizedField) {

View File

@@ -237,12 +237,13 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (field.type === 'blocks') {
const blockFieldPath = `${sanitizedPath}${field.name}`
const blocksByPath = blocks[blockFieldPath]
if (Array.isArray(blocks[blockFieldPath])) {
if (Array.isArray(blocksByPath)) {
if (field.localized) {
result[field.name] = {}
blocks[blockFieldPath].forEach((row) => {
blocksByPath.forEach((row) => {
if (row._uuid) {
row.id = row._uuid
delete row._uuid
@@ -285,7 +286,23 @@ export const traverseFields = <T extends Record<string, unknown>>({
})
})
} else {
result[field.name] = blocks[blockFieldPath].reduce((acc, row, i) => {
// Add locale-specific index to have a proper blockFieldPath for current locale
// because blocks can be in the same array for different locales!
if (withinArrayOrBlockLocale && config.localization) {
for (const locale of config.localization.localeCodes) {
let localeIndex = 0
for (let i = 0; i < blocksByPath.length; i++) {
const row = blocksByPath[i]
if (row._locale === locale) {
row._index = localeIndex
localeIndex++
}
}
}
}
result[field.name] = blocksByPath.reduce((acc, row, i) => {
delete row._order
if (row._uuid) {
row.id = row._uuid
@@ -301,6 +318,10 @@ export const traverseFields = <T extends Record<string, unknown>>({
if (row._locale) {
delete row._locale
}
if (typeof row._index === 'number') {
i = row._index
delete row._index
}
acc.push(
traverseFields<T>({
@@ -350,6 +371,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
} else {
const relationPathMatch = relationships[`${sanitizedPath}${field.name}`]
if (!relationPathMatch) {
if ('hasMany' in field && field.hasMany) {
if (field.localized && config.localization && config.localization.locales) {

View File

@@ -263,6 +263,44 @@ const GroupFields: CollectionConfig = {
},
],
},
{
name: 'localizedGroupManyRel',
type: 'group',
localized: true,
fields: [
{
type: 'relationship',
relationTo: 'email-fields',
name: 'email',
hasMany: true,
},
],
},
{
name: 'localizedGroupPolyRel',
type: 'group',
localized: true,
fields: [
{
type: 'relationship',
relationTo: ['email-fields'],
name: 'email',
},
],
},
{
name: 'localizedGroupPolyHasManyRel',
type: 'group',
localized: true,
fields: [
{
type: 'relationship',
relationTo: ['email-fields'],
name: 'email',
hasMany: true,
},
],
},
],
}

View File

@@ -62,7 +62,6 @@ describe('Fields', () => {
slug: 'users',
credentials: devUser,
})
user = await payload.login({
collection: 'users',
data: {
@@ -1459,6 +1458,201 @@ describe('Fields', () => {
expect(docAll.localizedGroupRel.en.email).toBe(rel_1.id)
expect(docAll.localizedGroupRel.es.email).toBe(rel_2.id)
})
it('should insert/update/read localized group with hasMany relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupManyRel: {
email: [rel_1.id],
},
},
})
expect(doc.localizedGroupManyRel.email).toStrictEqual([rel_1.id])
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupManyRel: {
email: [rel_2.id],
},
},
})
expect(upd.localizedGroupManyRel.email).toStrictEqual([rel_2.id])
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupManyRel.en.email).toStrictEqual([rel_1.id])
expect(docAll.localizedGroupManyRel.es.email).toStrictEqual([rel_2.id])
})
it('should insert/update/read localized group with poly relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupPolyRel: {
email: {
relationTo: 'email-fields',
value: rel_1.id,
},
},
},
})
expect(doc.localizedGroupPolyRel.email).toStrictEqual({
relationTo: 'email-fields',
value: rel_1.id,
})
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupPolyRel: {
email: {
value: rel_2.id,
relationTo: 'email-fields',
},
},
},
})
expect(upd.localizedGroupPolyRel.email).toStrictEqual({
value: rel_2.id,
relationTo: 'email-fields',
})
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupPolyRel.en.email).toStrictEqual({
value: rel_1.id,
relationTo: 'email-fields',
})
expect(docAll.localizedGroupPolyRel.es.email).toStrictEqual({
value: rel_2.id,
relationTo: 'email-fields',
})
})
it('should insert/update/read localized group with poly hasMany relationship inside', async () => {
const rel_1 = await payload.create({
collection: 'email-fields',
data: { email: 'pro123@gmail.com' },
})
const rel_2 = await payload.create({
collection: 'email-fields',
data: { email: 'frank@gmail.com' },
})
const doc = await payload.create({
collection: 'group-fields',
depth: 0,
data: {
group: { text: 'requireddd' },
localizedGroupPolyHasManyRel: {
email: [
{
relationTo: 'email-fields',
value: rel_1.id,
},
],
},
},
})
expect(doc.localizedGroupPolyHasManyRel.email).toStrictEqual([
{
relationTo: 'email-fields',
value: rel_1.id,
},
])
const upd = await payload.update({
collection: 'group-fields',
depth: 0,
id: doc.id,
locale: 'es',
data: {
localizedGroupPolyHasManyRel: {
email: [
{
value: rel_2.id,
relationTo: 'email-fields',
},
],
},
},
})
expect(upd.localizedGroupPolyHasManyRel.email).toStrictEqual([
{
value: rel_2.id,
relationTo: 'email-fields',
},
])
const docAll = await payload.findByID({
collection: 'group-fields',
id: doc.id,
locale: 'all',
depth: 0,
})
expect(docAll.localizedGroupPolyHasManyRel.en.email).toStrictEqual([
{
value: rel_1.id,
relationTo: 'email-fields',
},
])
expect(docAll.localizedGroupPolyHasManyRel.es.email).toStrictEqual([
{
value: rel_2.id,
relationTo: 'email-fields',
},
])
})
})
describe('tabs', () => {

View File

@@ -971,6 +971,23 @@ export interface GroupField {
localizedGroupRel?: {
email?: (string | null) | EmailField;
};
localizedGroupManyRel?: {
email?: (string | EmailField)[] | null;
};
localizedGroupPolyRel?: {
email?: {
relationTo: 'email-fields';
value: string | EmailField;
} | null;
};
localizedGroupPolyHasManyRel?: {
email?:
| {
relationTo: 'email-fields';
value: string | EmailField;
}[]
| null;
};
updatedAt: string;
createdAt: string;
}

View File

@@ -43,5 +43,44 @@ export const NestedFields: CollectionConfig = {
},
],
},
{
name: 'blocks',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'block',
fields: [
{
name: 'nestedBlocks',
type: 'blocks',
blocks: [
{
slug: 'content',
fields: [
{
name: 'relation',
type: 'relationship',
relationTo: ['localized-posts'],
},
],
},
],
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'relation',
type: 'relationship',
relationTo: ['localized-posts'],
},
],
},
],
},
],
},
],
}

View File

@@ -1975,6 +1975,260 @@ describe('Localization', () => {
expect(retrieved.array.en[0].text).toEqual(['hello', 'goodbye'])
expect(retrieved.array.es[0].text).toEqual(['hola', 'adios'])
})
it('should allow for relationship in new tables within blocks inside of localized blocks to be stored', async () => {
const randomDoc = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[0]
const randomDoc2 = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[1]
const docEn = await payload.create({
collection: 'nested-field-tables',
depth: 0,
data: {
blocks: [
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
],
},
})
expect(docEn.blocks[0].nestedBlocks[0].relation.value).toBe(randomDoc.id)
expect(docEn.blocks[1].nestedBlocks[0].relation.value).toBe(randomDoc.id)
expect(docEn.blocks[2].nestedBlocks[0].relation.value).toBe(randomDoc.id)
const docEs = await payload.update({
id: docEn.id,
depth: 0,
locale: 'es',
collection: 'nested-field-tables',
data: {
blocks: [
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
{
blockType: 'block',
nestedBlocks: [
{
blockType: 'content',
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
],
},
})
expect(docEs.blocks[0].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
expect(docEs.blocks[1].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
expect(docEs.blocks[2].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
const docAll = await payload.findByID({
collection: 'nested-field-tables',
id: docEn.id,
locale: 'all',
depth: 0,
})
expect(docAll.blocks.en[0].nestedBlocks[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.en[1].nestedBlocks[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.en[2].nestedBlocks[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.es[0].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
expect(docAll.blocks.es[1].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
expect(docAll.blocks.es[2].nestedBlocks[0].relation.value).toBe(randomDoc2.id)
})
it('should allow for relationship in new tables within arrays inside of localized blocks to be stored', async () => {
const randomDoc = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[0]
const randomDoc2 = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[1]
const docEn = await payload.create({
collection: 'nested-field-tables',
depth: 0,
data: {
blocks: [
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc.id,
},
},
],
},
],
},
})
expect(docEn.blocks[0].array[0].relation.value).toBe(randomDoc.id)
expect(docEn.blocks[1].array[0].relation.value).toBe(randomDoc.id)
expect(docEn.blocks[2].array[0].relation.value).toBe(randomDoc.id)
const docEs = await payload.update({
id: docEn.id,
depth: 0,
locale: 'es',
collection: 'nested-field-tables',
data: {
blocks: [
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
{
blockType: 'block',
array: [
{
relation: {
relationTo: 'localized-posts',
value: randomDoc2.id,
},
},
],
},
],
},
})
expect(docEs.blocks[0].array[0].relation.value).toBe(randomDoc2.id)
expect(docEs.blocks[1].array[0].relation.value).toBe(randomDoc2.id)
expect(docEs.blocks[2].array[0].relation.value).toBe(randomDoc2.id)
const docAll = await payload.findByID({
collection: 'nested-field-tables',
id: docEn.id,
locale: 'all',
depth: 0,
})
expect(docAll.blocks.en[0].array[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.en[1].array[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.en[2].array[0].relation.value).toBe(randomDoc.id)
expect(docAll.blocks.es[0].array[0].relation.value).toBe(randomDoc2.id)
expect(docAll.blocks.es[1].array[0].relation.value).toBe(randomDoc2.id)
expect(docAll.blocks.es[2].array[0].relation.value).toBe(randomDoc2.id)
})
})
describe('localized with unique', () => {

View File

@@ -27,6 +27,7 @@ export interface Config {
'localized-sort': LocalizedSort;
'blocks-same-name': BlocksSameName;
'localized-within-localized': LocalizedWithinLocalized;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
@@ -166,6 +167,33 @@ export interface NestedFieldTable {
id?: string | null;
}[]
| null;
blocks?:
| {
nestedBlocks?:
| {
relation?: {
relationTo: 'localized-posts';
value: string | LocalizedPost;
} | null;
id?: string | null;
blockName?: string | null;
blockType: 'content';
}[]
| null;
array?:
| {
relation?: {
relationTo: 'localized-posts';
value: string | LocalizedPost;
} | null;
id?: string | null;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'block';
}[]
| null;
updatedAt: string;
createdAt: string;
}
@@ -492,6 +520,85 @@ export interface LocalizedWithinLocalized {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'blocks-fields';
value: string | BlocksField;
} | null)
| ({
relationTo: 'nested-arrays';
value: string | NestedArray;
} | null)
| ({
relationTo: 'nested-field-tables';
value: string | NestedFieldTable;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null)
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
} | null)
| ({
relationTo: 'array-fields';
value: string | ArrayField;
} | null)
| ({
relationTo: 'localized-required';
value: string | LocalizedRequired;
} | null)
| ({
relationTo: 'with-localized-relationship';
value: string | WithLocalizedRelationship;
} | null)
| ({
relationTo: 'relationship-localized';
value: string | RelationshipLocalized;
} | null)
| ({
relationTo: 'dummy';
value: string | Dummy;
} | null)
| ({
relationTo: 'nested';
value: string | Nested;
} | null)
| ({
relationTo: 'groups';
value: string | Group;
} | null)
| ({
relationTo: 'tabs';
value: string | Tab;
} | null)
| ({
relationTo: 'localized-sort';
value: string | LocalizedSort;
} | null)
| ({
relationTo: 'blocks-same-name';
value: string | BlocksSameName;
} | null)
| ({
relationTo: 'localized-within-localized';
value: string | LocalizedWithinLocalized;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".