fix rls create new record with composite (#17243)

## Context
buildValueFromFilter was not handling composite filters which was needed
for RLS. This PR implements that and fix some issues with RLS

Tested with a few composite + relation fields
<img width="559" height="206" alt="Screenshot 2026-01-19 at 15 38 42"
src="https://github.com/user-attachments/assets/d64afa6e-3e12-4843-a215-a693665112c5"
/>
This commit is contained in:
Weiko
2026-01-20 06:36:38 +01:00
committed by GitHub
parent 39470b74a4
commit b64951634e
17 changed files with 906 additions and 202 deletions
@@ -7,12 +7,22 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
import { buildRecordInputFromFilter } from '@/object-record/record-table/utils/buildRecordInputFromFilter';
import { buildCompositeValueFromSubField } from '@/object-record/record-table/utils/buildValueFromFilter';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isUndefined } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { RelationType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isPlainObject } from 'twenty-shared/utils';
const mergeCompositeValues = (
existingValue: unknown,
incomingValue: unknown,
) =>
isPlainObject(existingValue) && isPlainObject(incomingValue)
? { ...existingValue, ...incomingValue }
: incomingValue;
export const useBuildRecordInputFromRLSPredicates = ({
objectMetadataItem,
@@ -37,9 +47,60 @@ export const useBuildRecordInputFromRLSPredicates = ({
objectMetadataItem.id,
);
const buildRecordInputFromRLSPredicates = (): Partial<ObjectRecord> => {
const recordInput: Partial<ObjectRecord> = {};
const getRecordInputFieldName = (fieldMetadataItem: {
name: string;
type: string;
settings?: { relationType?: RelationType };
}) =>
fieldMetadataItem.type === 'RELATION' &&
fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE
? `${fieldMetadataItem.name}Id`
: fieldMetadataItem.name;
const getWorkspaceMemberFieldValue = ({
workspaceMemberFieldMetadataId,
workspaceMemberSubFieldName,
}: {
workspaceMemberFieldMetadataId?: string | null;
workspaceMemberSubFieldName?: string | null;
}) => {
const workspaceMemberFieldMetadataItem =
workspaceMemberObjectMetadataItem?.fields.find(
(field) => field.id === workspaceMemberFieldMetadataId,
);
if (!isDefined(workspaceMemberFieldMetadataItem)) {
throw new Error(
`Workspace member field metadata item not found for id: ${workspaceMemberFieldMetadataId}`,
);
}
let workspaceMemberFieldValue =
currentWorkspaceMemberRecord?.[workspaceMemberFieldMetadataItem.name];
if (isCompositeFieldType(workspaceMemberFieldMetadataItem.type)) {
if (!workspaceMemberSubFieldName) {
throw new Error(
`Workspace member subfield name not found for field: ${workspaceMemberFieldMetadataItem.name}`,
);
}
const compositeValue = workspaceMemberFieldValue as
| Record<string, unknown>
| undefined;
workspaceMemberFieldValue = compositeValue?.[workspaceMemberSubFieldName];
}
if (isUndefined(workspaceMemberFieldValue)) {
throw new Error(
`Current workspace member field value not found for field: ${workspaceMemberFieldMetadataItem.name}`,
);
}
return workspaceMemberFieldValue;
};
const buildRecordInputFromRLSPredicates = (): Partial<ObjectRecord> => {
const rlsPredicates = objectPermissions.rowLevelPermissionPredicates.filter(
(predicate) => predicate.objectMetadataId === objectMetadataItem.id,
);
@@ -57,6 +118,8 @@ export const useBuildRecordInputFromRLSPredicates = ({
)
.filter(isDefined);
const recordInputFromDynamicFilters: Partial<ObjectRecord> = {};
rlsPredicatesAsRecordFilters.forEach((filter) => {
const fieldMetadataItem = fieldMetadataItemMap.get(
filter.fieldMetadataId,
@@ -67,35 +130,42 @@ export const useBuildRecordInputFromRLSPredicates = ({
}
if (isDefined(filter.rlsDynamicValue)) {
const workspaceMemberFieldMetadataItem =
workspaceMemberObjectMetadataItem?.fields.find(
(field) =>
field.id ===
filter.rlsDynamicValue?.workspaceMemberFieldMetadataId,
);
const recordInputField = getRecordInputFieldName(fieldMetadataItem);
const currentWorkspaceMemberFieldValue = getWorkspaceMemberFieldValue({
workspaceMemberFieldMetadataId:
filter.rlsDynamicValue?.workspaceMemberFieldMetadataId,
workspaceMemberSubFieldName:
filter.rlsDynamicValue?.workspaceMemberSubFieldName,
});
if (!isDefined(workspaceMemberFieldMetadataItem)) {
throw new Error(
`Workspace member field metadata item not found for id: ${filter.rlsDynamicValue?.workspaceMemberFieldMetadataId}`,
);
if (isCompositeFieldType(fieldMetadataItem.type)) {
if (!filter.subFieldName) {
throw new Error(
`Subfield name not found for composite field: ${fieldMetadataItem.name}`,
);
}
const compositeValue = buildCompositeValueFromSubField({
compositeFieldType: fieldMetadataItem.type,
subFieldName: filter.subFieldName,
value: currentWorkspaceMemberFieldValue,
});
if (!compositeValue) {
throw new Error(
`Composite subfield not found for field: ${fieldMetadataItem.name}`,
);
}
recordInputFromDynamicFilters[recordInputField] =
mergeCompositeValues(
recordInputFromDynamicFilters[recordInputField],
compositeValue,
);
} else {
recordInputFromDynamicFilters[recordInputField] =
currentWorkspaceMemberFieldValue;
}
const recordInputField =
fieldMetadataItem.type === 'RELATION' &&
fieldMetadataItem.settings?.relationType === RelationType.MANY_TO_ONE
? `${fieldMetadataItem.name}Id`
: fieldMetadataItem.name;
const currentWorkspaceMemberFieldValue =
currentWorkspaceMemberRecord?.[workspaceMemberFieldMetadataItem.name];
if (isUndefined(currentWorkspaceMemberFieldValue)) {
throw new Error(
`Current workspace member field value not found for field: ${workspaceMemberFieldMetadataItem.name}`,
);
}
recordInput[recordInputField] = currentWorkspaceMemberFieldValue;
}
});
@@ -105,16 +175,24 @@ export const useBuildRecordInputFromRLSPredicates = ({
(filter) => !isDefined(filter.rlsDynamicValue),
);
const recordInputFromFilters = buildRecordInputFromFilter({
const recordInputFromStaticFilters = buildRecordInputFromFilter({
currentRecordFilters: staticFilters,
objectMetadataItem,
currentWorkspaceMember: currentWorkspaceMember ?? undefined,
});
return {
...recordInput,
...recordInputFromFilters,
const mergedRecordInput: Partial<ObjectRecord> = {
...recordInputFromDynamicFilters,
};
Object.entries(recordInputFromStaticFilters).forEach(([key, value]) => {
mergedRecordInput[key] = mergeCompositeValues(
mergedRecordInput[key],
value,
);
});
return mergedRecordInput;
};
return { buildRecordInputFromRLSPredicates };