fix(db-postgres, db-sqlite): hasMany text, number, poly relationship, blocks, arrays within localized fields (#7900)

## Description

In Postgres, localized blocks or arrays that contain other array / block
/ relationship fields were not properly storing locales in the database.

Now they are! Need to check a few things yet:

- Ensure test coverage is sufficient
- Test localized array, with non-localized array inside of it
- Test localized block with relationship field within it
- Ensure `_rels` table gets the `locale` column added if a single
non-localized relationship exists within a localized array / block

Fixes step 6 as identified in #7805
This commit is contained in:
James Mikrut
2024-08-28 13:43:12 -04:00
committed by GitHub
parent 3d9051ad34
commit 18b0806b5b
17 changed files with 990 additions and 96 deletions

View File

@@ -0,0 +1,37 @@
import type { CollectionConfig } from 'payload'
export const blocksCollectionSlug = 'blocks-fields'
export const BlocksCollection: CollectionConfig = {
slug: blocksCollectionSlug,
fields: [
{
name: 'content',
label: 'Content',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'blockInsideBlock',
fields: [
{
name: 'content',
type: 'blocks',
blocks: [
{
slug: 'textBlock',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
},
],
},
],
},
],
}

View File

@@ -0,0 +1,42 @@
import type { CollectionConfig } from 'payload'
export const NestedArray: CollectionConfig = {
slug: 'nested-arrays',
fields: [
{
name: 'arrayWithBlocks',
type: 'array',
localized: true,
fields: [
{
name: 'blocksWithinArray',
type: 'blocks',
blocks: [
{
slug: 'someBlock',
fields: [
{
name: 'relationWithinBlock',
type: 'relationship',
relationTo: 'localized-posts',
},
],
},
],
},
],
},
{
name: 'arrayWithLocalizedRelation',
type: 'array',
fields: [
{
name: 'localizedRelation',
type: 'relationship',
localized: true,
relationTo: 'localized-posts',
},
],
},
],
}

View File

@@ -0,0 +1,47 @@
import type { CollectionConfig } from 'payload'
export const NestedFields: CollectionConfig = {
slug: 'nested-field-tables',
fields: [
{
name: 'array',
type: 'array',
localized: true,
fields: [
{
name: 'relation',
type: 'relationship',
relationTo: ['localized-posts'],
},
{
name: 'hasManyRelation',
type: 'relationship',
hasMany: true,
relationTo: 'localized-posts',
},
{
name: 'hasManyPolyRelation',
type: 'relationship',
hasMany: true,
relationTo: ['localized-posts'],
},
{
name: 'select',
type: 'select',
hasMany: true,
options: ['one', 'two', 'three'],
},
{
name: 'number',
type: 'number',
hasMany: true,
},
{
name: 'text',
type: 'text',
hasMany: true,
},
],
},
],
}

View File

@@ -7,7 +7,10 @@ import type { LocalizedPost } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { ArrayCollection } from './collections/Array/index.js'
import { BlocksCollection } from './collections/Blocks/index.js'
import { Group } from './collections/Group/index.js'
import { NestedArray } from './collections/NestedArray/index.js'
import { NestedFields } from './collections/NestedFields/index.js'
import { NestedToArrayAndBlock } from './collections/NestedToArrayAndBlock/index.js'
import { Tab } from './collections/Tab/index.js'
import {
@@ -50,6 +53,9 @@ export default buildConfigWithDefaults({
},
},
collections: [
BlocksCollection,
NestedArray,
NestedFields,
{
auth: true,
fields: [

View File

@@ -1395,6 +1395,328 @@ describe('Localization', () => {
expect(docEs.deep.blocks[0].title).toBe('hello es')
})
})
describe('nested blocks', () => {
let id
it('should allow creating nested blocks per locale', async () => {
const doc = await payload.create({
collection: 'blocks-fields',
data: {
content: [
{
blockType: 'blockInsideBlock',
content: [
{
blockType: 'textBlock',
text: 'hello',
},
],
},
],
},
})
id = doc.id
await payload.update({
collection: 'blocks-fields',
id,
locale: 'es',
data: {
content: [
{
blockType: 'blockInsideBlock',
content: [
{
blockType: 'textBlock',
text: 'hola',
},
],
},
],
},
})
const retrieved = await payload.findByID({
collection: 'blocks-fields',
id,
locale: 'all',
})
expect(retrieved.content.en[0].content).toHaveLength(1)
expect(retrieved.content.es[0].content).toHaveLength(1)
})
})
describe('nested arrays', () => {
it('should not duplicate block rows for blocks within localized array fields', 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 blocksWithinArrayEN = [
{
blockName: '1',
blockType: 'someBlock',
relationWithinBlock: randomDoc.id,
},
{
blockName: '2',
blockType: 'someBlock',
relationWithinBlock: randomDoc.id,
},
{
blockName: '3',
blockType: 'someBlock',
relationWithinBlock: randomDoc.id,
},
]
const blocksWithinArrayES = [
{
blockName: '1',
blockType: 'someBlock',
relationWithinBlock: randomDoc2.id,
},
{
blockName: '2',
blockType: 'someBlock',
relationWithinBlock: randomDoc2.id,
},
{
blockName: '3',
blockType: 'someBlock',
relationWithinBlock: randomDoc2.id,
},
]
const createdEnDoc = await payload.create({
collection: 'nested-arrays',
locale: 'en',
depth: 0,
data: {
arrayWithBlocks: [
{
blocksWithinArray: blocksWithinArrayEN as any,
},
],
},
})
const updatedEsDoc = await payload.update({
collection: 'nested-arrays',
id: createdEnDoc.id,
depth: 0,
locale: 'es',
data: {
arrayWithBlocks: [
{
blocksWithinArray: blocksWithinArrayES as any,
},
],
},
})
const esArrayBlocks = updatedEsDoc.arrayWithBlocks[0].blocksWithinArray
// recursively remove any id field within esArrayRow
const removeId = (obj) => {
if (obj instanceof Object) {
delete obj.id
Object.values(obj).forEach(removeId)
}
}
removeId(esArrayBlocks)
removeId(createdEnDoc.arrayWithBlocks[0].blocksWithinArray)
expect(esArrayBlocks).toEqual(blocksWithinArrayES)
expect(createdEnDoc.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN)
// pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc
const enDoc2 = await payload.findByID({
id: createdEnDoc.id,
collection: 'nested-arrays',
locale: 'en',
depth: 0,
})
removeId(enDoc2.arrayWithBlocks[0].blocksWithinArray)
expect(enDoc2.arrayWithBlocks[0].blocksWithinArray).toEqual(blocksWithinArrayEN)
})
it('should update localized relation within unLocalized array', async () => {
const randomTextDoc = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[0]
const randomTextDoc2 = (
await payload.find({
collection: 'localized-posts',
depth: 0,
})
).docs[1]
const createdEnDoc = await payload.create({
collection: 'nested-arrays',
locale: 'en',
depth: 0,
data: {
arrayWithLocalizedRelation: [
{
localizedRelation: randomTextDoc.id,
},
],
},
})
const updatedEsDoc = await payload.update({
collection: 'nested-arrays',
id: createdEnDoc.id,
depth: 0,
locale: 'es',
data: {
arrayWithLocalizedRelation: [
{
id: createdEnDoc.arrayWithLocalizedRelation[0].id,
localizedRelation: randomTextDoc2.id,
},
],
},
})
expect(updatedEsDoc.arrayWithLocalizedRelation).toHaveLength(1)
expect(updatedEsDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc2.id)
expect(createdEnDoc.arrayWithLocalizedRelation).toHaveLength(1)
expect(createdEnDoc.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id)
// pull enDoc again and make sure the update of esDoc did not mess with the data of enDoc
const enDoc2 = await payload.findByID({
id: createdEnDoc.id,
collection: 'nested-arrays',
locale: 'en',
depth: 0,
})
expect(enDoc2.arrayWithLocalizedRelation).toHaveLength(1)
expect(enDoc2.arrayWithLocalizedRelation[0].localizedRelation).toBe(randomTextDoc.id)
})
})
describe('nested fields', () => {
it('should allow for fields which could contain new tables within localized arrays 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 newDoc = await payload.create({
collection: 'nested-field-tables',
data: {
array: [
{
relation: {
value: randomDoc.id,
relationTo: 'localized-posts',
},
hasManyRelation: [randomDoc.id, randomDoc2.id],
hasManyPolyRelation: [
{
relationTo: 'localized-posts',
value: randomDoc.id,
},
{
relationTo: 'localized-posts',
value: randomDoc2.id,
},
],
number: [1, 2],
text: ['hello', 'goodbye'],
select: ['one'],
},
],
},
})
await payload.update({
collection: 'nested-field-tables',
id: newDoc.id,
locale: 'es',
data: {
array: [
{
relation: {
value: randomDoc2.id,
relationTo: 'localized-posts',
},
hasManyRelation: [randomDoc2.id, randomDoc.id],
hasManyPolyRelation: [
{
relationTo: 'localized-posts',
value: randomDoc2.id,
},
{
relationTo: 'localized-posts',
value: randomDoc.id,
},
],
select: ['two', 'three'],
text: ['hola', 'adios'],
number: [3, 4],
},
],
},
})
const retrieved = await payload.findByID({
collection: 'nested-field-tables',
id: newDoc.id,
depth: 0,
locale: 'all',
})
expect(retrieved.array.en[0].relation.value).toStrictEqual(randomDoc.id)
expect(retrieved.array.es[0].relation.value).toStrictEqual(randomDoc2.id)
expect(retrieved.array.en[0].hasManyRelation).toEqual([randomDoc.id, randomDoc2.id])
expect(retrieved.array.es[0].hasManyRelation).toEqual([randomDoc2.id, randomDoc.id])
expect(retrieved.array.en[0].hasManyPolyRelation).toEqual([
{ value: randomDoc.id, relationTo: 'localized-posts' },
{ value: randomDoc2.id, relationTo: 'localized-posts' },
])
expect(retrieved.array.es[0].hasManyPolyRelation).toEqual([
{ value: randomDoc2.id, relationTo: 'localized-posts' },
{ value: randomDoc.id, relationTo: 'localized-posts' },
])
expect(retrieved.array.en[0].number).toEqual([1, 2])
expect(retrieved.array.es[0].number).toEqual([3, 4])
expect(retrieved.array.en[0].select).toEqual(['one'])
expect(retrieved.array.es[0].select).toEqual(['two', 'three'])
expect(retrieved.array.en[0].text).toEqual(['hello', 'goodbye'])
expect(retrieved.array.es[0].text).toEqual(['hola', 'adios'])
})
})
})
async function createLocalizedPost(data: {

View File

@@ -11,6 +11,9 @@ export interface Config {
users: UserAuthOperations;
};
collections: {
'blocks-fields': BlocksField;
'nested-arrays': NestedArray;
'nested-fields': NestedField;
users: User;
'localized-posts': LocalizedPost;
'array-fields': ArrayField;
@@ -19,15 +22,20 @@ export interface Config {
'relationship-localized': RelationshipLocalized;
dummy: Dummy;
nested: Nested;
groups: Group;
tabs: Tab;
'localized-sort': LocalizedSort;
'blocks-same-name': BlocksSameName;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
db: {
defaultIDType: number;
};
globals: {
'global-array': GlobalArray;
};
locale: 'en' | 'es' | 'hu' | 'pt' | 'ar';
locale: 'en' | 'es' | 'pt' | 'ar' | 'hu';
user: User & {
collection: 'users';
};
@@ -35,23 +43,125 @@ export interface Config {
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
password: string;
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "blocks-fields".
*/
export interface BlocksField {
id: number;
content?:
| {
content?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'textBlock';
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'blockInsideBlock';
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "nested-arrays".
*/
export interface NestedArray {
id: number;
arrayWithBlocks?:
| {
blocksWithinArray?:
| {
relationWithinBlock?: (number | null) | LocalizedPost;
id?: string | null;
blockName?: string | null;
blockType: 'someBlock';
}[]
| null;
id?: string | null;
}[]
| null;
arrayWithLocalizedRelation?:
| {
localizedRelation?: (number | null) | LocalizedPost;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts".
*/
export interface LocalizedPost {
id: number;
title?: string | null;
description?: string | null;
localizedDescription?: string | null;
localizedCheckbox?: boolean | null;
children?: (number | LocalizedPost)[] | null;
group?: {
children?: string | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "nested-fields".
*/
export interface NestedField {
id: number;
array?:
| {
relation?: {
relationTo: 'localized-posts';
value: number | LocalizedPost;
} | null;
hasManyRelation?: (number | LocalizedPost)[] | null;
hasManyPolyRelation?:
| {
relationTo: 'localized-posts';
value: number | LocalizedPost;
}[]
| null;
select?: ('one' | 'two' | 'three')[] | null;
number?: number[] | null;
text?: string[] | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
relation?: (string | null) | LocalizedPost;
id: number;
relation?: (number | null) | LocalizedPost;
updatedAt: string;
createdAt: string;
email: string;
@@ -63,29 +173,12 @@ export interface User {
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts".
*/
export interface LocalizedPost {
id: string;
title?: string | null;
localizedDescription?: string | null;
description?: string | null;
localizedCheckbox?: boolean | null;
children?: (string | LocalizedPost)[] | null;
group?: {
children?: string | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "array-fields".
*/
export interface ArrayField {
id: string;
id: number;
items?:
| {
text: string;
@@ -100,7 +193,7 @@ export interface ArrayField {
* via the `definition` "localized-required".
*/
export interface LocalizedRequired {
id: string;
id: number;
title: string;
layout: (
| {
@@ -124,27 +217,27 @@ export interface LocalizedRequired {
* via the `definition` "with-localized-relationship".
*/
export interface WithLocalizedRelationship {
id: string;
localizedRelationship?: (string | null) | LocalizedPost;
localizedRelationHasManyField?: (string | LocalizedPost)[] | null;
id: number;
localizedRelationship?: (number | null) | LocalizedPost;
localizedRelationHasManyField?: (number | LocalizedPost)[] | null;
localizedRelationMultiRelationTo?:
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
value: number | LocalizedPost;
} | null)
| ({
relationTo: 'dummy';
value: string | Dummy;
value: number | Dummy;
} | null);
localizedRelationMultiRelationToHasMany?:
| (
| {
relationTo: 'localized-posts';
value: string | LocalizedPost;
value: number | LocalizedPost;
}
| {
relationTo: 'dummy';
value: string | Dummy;
value: number | Dummy;
}
)[]
| null;
@@ -156,7 +249,7 @@ export interface WithLocalizedRelationship {
* via the `definition` "dummy".
*/
export interface Dummy {
id: string;
id: number;
name?: string | null;
updatedAt: string;
createdAt: string;
@@ -166,33 +259,33 @@ export interface Dummy {
* via the `definition` "relationship-localized".
*/
export interface RelationshipLocalized {
id: string;
relationship?: (string | null) | LocalizedPost;
relationshipHasMany?: (string | LocalizedPost)[] | null;
id: number;
relationship?: (number | null) | LocalizedPost;
relationshipHasMany?: (number | LocalizedPost)[] | null;
relationMultiRelationTo?:
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
value: number | LocalizedPost;
} | null)
| ({
relationTo: 'dummy';
value: string | Dummy;
value: number | Dummy;
} | null);
relationMultiRelationToHasMany?:
| (
| {
relationTo: 'localized-posts';
value: string | LocalizedPost;
value: number | LocalizedPost;
}
| {
relationTo: 'dummy';
value: string | Dummy;
value: number | Dummy;
}
)[]
| null;
arrayField?:
| {
nestedRelation?: (string | null) | LocalizedPost;
nestedRelation?: (number | null) | LocalizedPost;
id?: string | null;
}[]
| null;
@@ -204,7 +297,7 @@ export interface RelationshipLocalized {
* via the `definition` "nested".
*/
export interface Nested {
id: string;
id: number;
blocks?:
| {
array?:
@@ -222,12 +315,74 @@ export interface Nested {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "groups".
*/
export interface Group {
id: number;
groupLocalized?: {
title?: string | null;
};
group?: {
title?: string | null;
};
deep?: {
array?:
| {
title?: string | null;
id?: string | null;
}[]
| null;
blocks?:
| {
title?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'first';
}[]
| null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "tabs".
*/
export interface Tab {
id: number;
tabLocalized?: {
title?: string | null;
};
tab?: {
title?: string | null;
};
deep?: {
array?:
| {
title?: string | null;
id?: string | null;
}[]
| null;
blocks?:
| {
title?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'first';
}[]
| null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-sort".
*/
export interface LocalizedSort {
id: string;
id: number;
title?: string | null;
date?: string | null;
updatedAt: string;
@@ -238,7 +393,7 @@ export interface LocalizedSort {
* via the `definition` "blocks-same-name".
*/
export interface BlocksSameName {
id: string;
id: number;
blocks?:
| (
| {
@@ -263,10 +418,10 @@ export interface BlocksSameName {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@@ -286,7 +441,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -297,7 +452,7 @@ export interface PayloadMigration {
* via the `definition` "global-array".
*/
export interface GlobalArray {
id: string;
id: number;
array?:
| {
text?: string | null;
@@ -317,6 +472,6 @@ export interface Auth {
declare module 'payload' {
// @ts-ignore
// @ts-ignore
export interface GeneratedTypes extends Config {}
}
}