fix: cannot create record from table view — empty morph to-many relation returns null (#21846)
## Problem
Creating a record from the table view (reproduced on **People**) crashes
the client even though the `createOne…` mutation succeeds server-side,
so the record never appears:
```
Cannot read properties of null (reading 'map')
getRecordConnectionFromRecords → getRecordNodeFromRecord → optimistic cache effect → createOneRecord
```
## Root cause
An empty **morph** to-many relation comes back as `null`, while every
other to-many relation comes back as `{ edges: [] }`. The frontend then
runs `null.map` while building the optimistic cache node; the error
escapes the mutation `update`, the rollback evicts the record, and it
never lands in the table.
## Fix
**Server** — plain to-many relations are hydrated to `[]` and formatted
to `{ edges: [] }` by `ObjectRecordsToGraphqlConnectionHelper`; an empty
morph to-many was left undefined and the field was skipped (→ `null`).
Default an unset to-many value to `[]` so it goes through the **same
connection path as plain to-many relations**.
**Frontend** — defensive guard in `getRecordNodeFromRecord`: a to-many
relation whose value isn't an array is skipped instead of crashing,
mirroring the existing guard in `extractTargetRecordsFromRelation`.
Needed regardless, since cached data / SSE / older servers still send
`null`.
## Tests
- Unit: `getRecordNodeFromRecord` skips a null to-many (reproduces the
exact crash without the guard).
- Integration: an empty morph `ONE_TO_MANY` read returns `{ edges: []
}`, not null.
This commit is contained in:
+52
@@ -3,6 +3,7 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche
|
||||
import { mockedPersonRecords } from '~/testing/mock-data/generated/data/people/mock-people-data';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
const peopleMock = [...mockedPersonRecords];
|
||||
|
||||
@@ -96,4 +97,55 @@ describe('getRecordNodeFromRecord', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('skips a to-many relation whose value is null instead of crashing', () => {
|
||||
// Given
|
||||
const objectMetadataItems: EnrichedObjectMetadataItem[] =
|
||||
getTestEnrichedObjectMetadataItemsMock();
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
);
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error('Object metadata item not found');
|
||||
}
|
||||
|
||||
const oneToManyRelationField = objectMetadataItem.fields.find(
|
||||
(field) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.relation?.type === RelationType.ONE_TO_MANY,
|
||||
);
|
||||
|
||||
if (!oneToManyRelationField) {
|
||||
throw new Error('No to-many relation field found on person');
|
||||
}
|
||||
|
||||
const record = {
|
||||
...peopleMock[0],
|
||||
[oneToManyRelationField.name]: null,
|
||||
};
|
||||
const recordGqlFields = {
|
||||
name: true,
|
||||
[oneToManyRelationField.name]: true,
|
||||
};
|
||||
|
||||
// When / Then
|
||||
expect(() =>
|
||||
getRecordNodeFromRecord({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields,
|
||||
record,
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
const result = getRecordNodeFromRecord({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields,
|
||||
record,
|
||||
});
|
||||
|
||||
expect(result).not.toHaveProperty(oneToManyRelationField.name);
|
||||
});
|
||||
});
|
||||
|
||||
+8
@@ -67,6 +67,10 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.relation?.type === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const oneToManyObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.namePlural ===
|
||||
@@ -99,6 +103,10 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
field.settings?.relationType === RelationType.ONE_TO_MANY
|
||||
) {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (field.morphRelations?.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user