Files
twenty/packages/twenty-server/src/engine/twenty-orm/utils/apply-row-level-permission-predicates.util.ts
T
Raphaël Bosi b11f77df2a [FRONT COMPONENTS] Introduce conditionalAvailabilityExpression to command menu items (#18319)
## PR Description

- Uses `expr-eval` to enable front components (SDK plugins) to define
conditional availability as declarative expressions.
- Moves shared types and constants to `twenty-shared`
- Introduces a `conditionalAvailabilityExpression` field on
`CommandMenuItemEntity`, allowing command menu items to store an
`expr-eval` compatible expression string that is evaluated against a
CommandMenuContext to determine if the item should be shown.
- Creates an esbuild transform plugin
`conditional-availability-transform-plugin` in `twenty-sdk` that
converts TypeScript conditional availability expressions into
`expr-eval` compatible syntax at build time, so SDK developers can write
natural TS expressions that get transformed to evaluable strings.
- Removes deprecated `forceRegisteredActionsByKey` state and its usage.
- Creates `useCommandMenuContext` hook that builds the full
`CommandMenuContext` object from React state, which is then passed to
`useCommandMenuItemFrontComponentActions` for evaluating conditional
availability expressions.
2026-03-04 16:33:58 +01:00

242 lines
6.9 KiB
TypeScript

/* @license Enterprise */
import {
Brackets,
NotBrackets,
type ObjectLiteral,
type WhereExpressionBuilder,
} from 'typeorm';
import { FeatureFlagKey } from 'twenty-shared/types';
import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface';
import { type WorkspaceInternalContext } from 'src/engine/twenty-orm/interfaces/workspace-internal-context.interface';
import { GraphqlQueryFilterFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder';
import { buildRowLevelPermissionRecordFilter } from 'src/engine/twenty-orm/utils/build-row-level-permission-record-filter.util';
type ApplyRowLevelPermissionPredicatesArgs<T extends ObjectLiteral> = {
queryBuilder: WorkspaceSelectQueryBuilder<T>;
objectMetadata: FlatObjectMetadata;
internalContext: WorkspaceInternalContext;
authContext: AuthContext;
featureFlagMap: FeatureFlagMap;
};
export const applyRowLevelPermissionPredicates = <T extends ObjectLiteral>({
queryBuilder,
objectMetadata,
internalContext,
authContext,
featureFlagMap,
}: ApplyRowLevelPermissionPredicatesArgs<T>): void => {
if (
featureFlagMap[
FeatureFlagKey.IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED
] !== true
) {
return;
}
const roleId = authContext.userWorkspaceId
? internalContext.userWorkspaceRoleMap[authContext.userWorkspaceId]
: undefined;
const recordFilter = buildRowLevelPermissionRecordFilter({
flatRowLevelPermissionPredicateMaps:
internalContext.flatRowLevelPermissionPredicateMaps,
flatRowLevelPermissionPredicateGroupMaps:
internalContext.flatRowLevelPermissionPredicateGroupMaps,
flatFieldMetadataMaps: internalContext.flatFieldMetadataMaps,
objectMetadata,
roleId,
authContext,
});
if (!recordFilter || Object.keys(recordFilter).length === 0) {
return;
}
const isUpdateOrDeleteQuery =
queryBuilder.expressionMap.queryType === 'update' ||
queryBuilder.expressionMap.queryType === 'soft-delete' ||
queryBuilder.expressionMap.queryType === 'delete';
applyObjectRecordFilterToQueryBuilder({
queryBuilder,
objectNameSingular: objectMetadata.nameSingular,
recordFilter,
fieldParser: new GraphqlQueryFilterFieldParser(
objectMetadata,
internalContext.flatFieldMetadataMaps,
),
useDirectTableReference: isUpdateOrDeleteQuery,
});
};
const applyObjectRecordFilterToQueryBuilder = <T extends ObjectLiteral>({
queryBuilder,
objectNameSingular,
recordFilter,
fieldParser,
useDirectTableReference = false,
}: {
queryBuilder: WorkspaceSelectQueryBuilder<T>;
objectNameSingular: string;
recordFilter: Record<string, unknown>;
fieldParser: GraphqlQueryFilterFieldParser;
useDirectTableReference?: boolean;
}): void => {
if (!recordFilter || Object.keys(recordFilter).length === 0) {
return;
}
const whereCondition = new Brackets((qb) => {
Object.entries(recordFilter).forEach(([key, value], index) => {
parseKeyFilter({
queryBuilder: qb,
objectNameSingular,
key,
value,
isFirst: index === 0,
fieldParser,
useDirectTableReference,
});
});
});
if (queryBuilder.expressionMap.wheres.length === 0) {
queryBuilder.where(whereCondition);
} else {
queryBuilder.andWhere(whereCondition);
}
};
const parseKeyFilter = ({
queryBuilder,
objectNameSingular,
key,
value,
isFirst,
fieldParser,
useDirectTableReference = false,
}: {
queryBuilder: WhereExpressionBuilder;
objectNameSingular: string;
key: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
isFirst: boolean;
fieldParser: GraphqlQueryFilterFieldParser;
useDirectTableReference?: boolean;
}): void => {
switch (key) {
case 'and': {
const andWhereCondition = new Brackets((qb) => {
value.forEach((filter: Record<string, unknown>, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterKey, subFilterValue], subIndex) => {
parseKeyFilter({
queryBuilder: qb2,
objectNameSingular,
key: subFilterKey,
value: subFilterValue,
isFirst: subIndex === 0,
fieldParser,
useDirectTableReference,
});
},
);
});
if (index === 0) {
qb.where(whereCondition);
} else {
qb.andWhere(whereCondition);
}
});
});
if (isFirst) {
queryBuilder.where(andWhereCondition);
} else {
queryBuilder.andWhere(andWhereCondition);
}
break;
}
case 'or': {
const orWhereCondition = new Brackets((qb) => {
value.forEach((filter: Record<string, unknown>, index: number) => {
const whereCondition = new Brackets((qb2) => {
Object.entries(filter).forEach(
([subFilterKey, subFilterValue], subIndex) => {
parseKeyFilter({
queryBuilder: qb2,
objectNameSingular,
key: subFilterKey,
value: subFilterValue,
isFirst: subIndex === 0,
fieldParser,
useDirectTableReference,
});
},
);
});
if (index === 0) {
qb.where(whereCondition);
} else {
qb.orWhere(whereCondition);
}
});
});
if (isFirst) {
queryBuilder.where(orWhereCondition);
} else {
queryBuilder.andWhere(orWhereCondition);
}
break;
}
case 'not': {
const notWhereCondition = new NotBrackets((qb) => {
Object.entries(value).forEach(
([subFilterKey, subFilterValue], subIndex) => {
parseKeyFilter({
queryBuilder: qb,
objectNameSingular,
key: subFilterKey,
value: subFilterValue,
isFirst: subIndex === 0,
fieldParser,
useDirectTableReference,
});
},
);
});
if (isFirst) {
queryBuilder.where(notWhereCondition);
} else {
queryBuilder.andWhere(notWhereCondition);
}
break;
}
default:
fieldParser.parse(
queryBuilder,
objectNameSingular,
key,
value,
isFirst,
useDirectTableReference,
);
break;
}
};