feat: support relationship writes using objects instead of IDs (#9253)

### What?
Previously, this code led to a validation error because `movie` is an
object and you needed to use `movie.id` instead.
```ts
const movie = await payload.create({ collection: 'movies', data: {} })
const result = await payload.create({
  collection: 'object-writes',
  data: {
    many: [movie],
    manyPoly: [{ relationTo: 'movies', value: movie }],
    one: movie,
    onePoly: {
      relationTo: 'movies',
      value: movie,
    },
  },
})
```
While it's simple to modify this example, it's more painful when you
have a data with `depth` > 0 and then you want to update that document.

### Why?
Better DX as less checks needed, and TypeScript says that we can pass an
object.

### How?
Sanitizes the field value in the root `beforeValidate` hook
This commit is contained in:
Sasha
2024-11-17 11:25:32 +02:00
committed by GitHub
parent 35917c67d7
commit d21fca9156
4 changed files with 136 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { RequestContext } from '../../../index.js'
import type { JsonObject, JsonValue, PayloadRequest } from '../../../types/index.js'
@@ -91,7 +91,6 @@ export const promise = async <T>({
// Sanitize incoming data
switch (field.type) {
case 'array':
case 'blocks': {
// Handle cases of arrays being intentionally set to 0
if (siblingData[field.name] === '0' || siblingData[field.name] === 0) {
@@ -140,7 +139,6 @@ export const promise = async <T>({
break
}
case 'relationship':
case 'upload': {
if (
siblingData[field.name] === '' ||
@@ -163,6 +161,15 @@ export const promise = async <T>({
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === relatedDoc.relationTo,
)
if (
typeof relatedDoc.value === 'object' &&
relatedDoc.value &&
'id' in relatedDoc.value
) {
relatedDoc.value = relatedDoc.value.id
}
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
@@ -181,6 +188,11 @@ export const promise = async <T>({
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === value.relationTo,
)
if (typeof value.value === 'object' && value.value && 'id' in value.value) {
value.value = (value.value as TypeWithID).id
}
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
@@ -198,6 +210,10 @@ export const promise = async <T>({
(collection) => collection.slug === field.relationTo,
)
if (typeof relatedDoc === 'object' && relatedDoc && 'id' in relatedDoc) {
value[i] = relatedDoc.id
}
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
@@ -214,6 +230,10 @@ export const promise = async <T>({
(collection) => collection.slug === field.relationTo,
)
if (typeof value === 'object' && value && 'id' in value) {
siblingData[field.name] = value.id
}
if (relatedCollection?.fields) {
const relationshipIDField = relatedCollection.fields.find(
(collectionField) =>
@@ -375,7 +395,6 @@ export const promise = async <T>({
}
case 'collapsible':
case 'row': {
await traverseFields({
id,

View File

@@ -341,6 +341,33 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'object-writes',
fields: [
{
type: 'relationship',
relationTo: 'movies',
name: 'one',
},
{
type: 'relationship',
relationTo: 'movies',
name: 'many',
hasMany: true,
},
{
type: 'relationship',
relationTo: ['movies'],
name: 'onePoly',
},
{
type: 'relationship',
relationTo: ['movies'],
name: 'manyPoly',
hasMany: true,
},
],
},
],
onInit: async (payload) => {
await payload.create({

View File

@@ -1181,7 +1181,7 @@ describe('Relationships', () => {
})
})
describe('Creating', () => {
describe('Writing', () => {
describe('With transactions', () => {
it('should be able to create filtered relations within a transaction', async () => {
const req = {} as PayloadRequest
@@ -1208,6 +1208,52 @@ describe('Relationships', () => {
expect(withRelation.filteredRelation.id).toEqual(related.id)
})
})
describe('With passing an object', () => {
it('should create with passing an object', async () => {
const movie = await payload.create({ collection: 'movies', data: {} })
const result = await payload.create({
collection: 'object-writes',
data: {
many: [movie],
manyPoly: [{ relationTo: 'movies', value: movie }],
one: movie,
onePoly: {
relationTo: 'movies',
value: movie,
},
},
})
expect(result.many[0]).toStrictEqual(movie)
expect(result.one).toStrictEqual(movie)
expect(result.manyPoly[0]).toStrictEqual({ relationTo: 'movies', value: movie })
expect(result.onePoly).toStrictEqual({ relationTo: 'movies', value: movie })
})
it('should update with passing an object', async () => {
const movie = await payload.create({ collection: 'movies', data: {} })
const { id } = await payload.create({ collection: 'object-writes', data: {} })
const result = await payload.update({
collection: 'object-writes',
id,
data: {
many: [movie],
manyPoly: [{ relationTo: 'movies', value: movie }],
one: movie,
onePoly: {
relationTo: 'movies',
value: movie,
},
},
})
expect(result.many[0]).toStrictEqual(movie)
expect(result.one).toStrictEqual(movie)
expect(result.manyPoly[0]).toStrictEqual({ relationTo: 'movies', value: movie })
expect(result.onePoly).toStrictEqual({ relationTo: 'movies', value: movie })
})
})
})
describe('Polymorphic Relationships', () => {

View File

@@ -27,6 +27,7 @@ export interface Config {
pages: Page;
'rels-to-pages': RelsToPage;
'rels-to-pages-and-custom-text-ids': RelsToPagesAndCustomTextId;
'object-writes': ObjectWrite;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -50,6 +51,7 @@ export interface Config {
pages: PagesSelect<false> | PagesSelect<true>;
'rels-to-pages': RelsToPagesSelect<false> | RelsToPagesSelect<true>;
'rels-to-pages-and-custom-text-ids': RelsToPagesAndCustomTextIdsSelect<false> | RelsToPagesAndCustomTextIdsSelect<true>;
'object-writes': ObjectWritesSelect<false> | ObjectWritesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -311,6 +313,27 @@ export interface RelsToPagesAndCustomTextId {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "object-writes".
*/
export interface ObjectWrite {
id: string;
one?: (string | null) | Movie;
many?: (string | Movie)[] | null;
onePoly?: {
relationTo: 'movies';
value: string | Movie;
} | null;
manyPoly?:
| {
relationTo: 'movies';
value: string | Movie;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
@@ -382,6 +405,10 @@ export interface PayloadLockedDocument {
relationTo: 'rels-to-pages-and-custom-text-ids';
value: string | RelsToPagesAndCustomTextId;
} | null)
| ({
relationTo: 'object-writes';
value: string | ObjectWrite;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -609,6 +636,18 @@ export interface RelsToPagesAndCustomTextIdsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "object-writes_select".
*/
export interface ObjectWritesSelect<T extends boolean = true> {
one?: T;
many?: T;
onePoly?: T;
manyPoly?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".