Adapt rest api to field permissions (#13314)
Closes https://github.com/twentyhq/core-team-issues/issues/1217 We should only query and return the fields that are readable when using the rest api. This is behind a feature flag.
This commit is contained in:
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3574,6 +3574,11 @@ msgstr "john.doe"
|
||||
msgid "john.doe@example.com"
|
||||
msgstr "john.doe@example.com"
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr "Join {workspaceName} team"
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3571,6 +3571,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
Binary file not shown.
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -3579,6 +3579,11 @@ msgstr ""
|
||||
msgid "john.doe@example.com"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: fSPjtl
|
||||
#: src/pages/auth/SignInUp.tsx
|
||||
msgid "Join {workspaceName} team"
|
||||
msgstr ""
|
||||
|
||||
#. js-lingui-id: VIK/N0
|
||||
#: src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx
|
||||
#~ msgid "JSON keys cannot contain spaces"
|
||||
|
||||
@ -83,7 +83,8 @@ export const SignInUp = () => {
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (isDefined(workspaceInviteHash)) {
|
||||
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
|
||||
const workspaceName = workspaceFromInviteHash?.displayName ?? '';
|
||||
return t`Join ${workspaceName} team`;
|
||||
}
|
||||
|
||||
if (signInUpStep === SignInUpStep.WorkspaceSelection) {
|
||||
|
||||
@ -12,7 +12,7 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api
|
||||
@Injectable()
|
||||
export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { objectMetadata, repository } =
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const body = request.body;
|
||||
@ -63,6 +63,7 @@ export class RestApiCreateManyHandler extends RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
if (records.length !== body.length) {
|
||||
|
||||
@ -12,7 +12,7 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api
|
||||
@Injectable()
|
||||
export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { objectMetadata, repository } =
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const overriddenBody = await this.recordInputTransformerService.process({
|
||||
@ -46,6 +46,7 @@ export class RestApiCreateOneHandler extends RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
@ -15,10 +15,17 @@ export class RestApiDeleteOneHandler extends RestApiBaseHandler {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadata, repository } =
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const selectOptions = this.getSelectOptionsFromRestrictedFields({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
|
||||
const recordToDelete = await repository.findOneOrFail({
|
||||
where: { id: recordId },
|
||||
select: selectOptions,
|
||||
});
|
||||
|
||||
await repository.delete(recordId);
|
||||
|
||||
@ -17,8 +17,12 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
this.validate(request);
|
||||
|
||||
const { repository, objectMetadata, objectMetadataItemWithFieldsMaps } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const existingRecordsQueryBuilder = repository.createQueryBuilder(
|
||||
objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
@ -60,6 +64,7 @@ export class RestApiFindDuplicatesHandler extends RestApiBaseHandler {
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
extraFilters: duplicateCondition,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const paginatedResult = this.formatPaginatedDuplicatesResult({
|
||||
|
||||
@ -7,8 +7,12 @@ import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api
|
||||
@Injectable()
|
||||
export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
async handle(request: Request) {
|
||||
const { repository, objectMetadata, objectMetadataItemWithFieldsMaps } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const {
|
||||
records,
|
||||
@ -22,6 +26,7 @@ export class RestApiFindManyHandler extends RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
return this.formatPaginatedResult({
|
||||
|
||||
@ -18,8 +18,12 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
);
|
||||
}
|
||||
|
||||
const { repository, objectMetadata, objectMetadataItemWithFieldsMaps } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
const {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
} = await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const { records } = await this.findRecords({
|
||||
request,
|
||||
@ -27,6 +31,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records?.[0];
|
||||
|
||||
@ -20,7 +20,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
throw new BadRequestException('Record ID not found');
|
||||
}
|
||||
|
||||
const { objectMetadata, repository } =
|
||||
const { objectMetadata, repository, restrictedFields } =
|
||||
await this.getRepositoryAndMetadataOrFail(request);
|
||||
|
||||
const recordToUpdate = await repository.findOneOrFail({
|
||||
@ -42,6 +42,7 @@ export class RestApiUpdateOneHandler extends RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const record = records[0];
|
||||
|
||||
@ -2,7 +2,8 @@ import { BadRequestException, Inject } from '@nestjs/common';
|
||||
|
||||
import { Request } from 'express';
|
||||
import chunk from 'lodash.chunk';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { FieldMetadataType, RestrictedFields } from 'twenty-shared/types';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { In, ObjectLiteral } from 'typeorm';
|
||||
|
||||
@ -25,6 +26,9 @@ import {
|
||||
import { computeCursorArgFilter } from 'src/engine/api/utils/compute-cursor-arg-filter.utils';
|
||||
import { CreatedByFromAuthContextService } from 'src/engine/core-modules/actor/services/created-by-from-auth-context.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
@ -34,6 +38,7 @@ import { WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/wo
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatResult as formatGetManyData } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util';
|
||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||
|
||||
export interface PageInfo {
|
||||
@ -81,6 +86,8 @@ export abstract class RestApiBaseHandler {
|
||||
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
|
||||
@Inject()
|
||||
protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService;
|
||||
@Inject()
|
||||
protected readonly featureFlagService: FeatureFlagService;
|
||||
|
||||
protected abstract handle(
|
||||
request: Request,
|
||||
@ -134,11 +141,48 @@ export abstract class RestApiBaseHandler {
|
||||
roleId,
|
||||
);
|
||||
|
||||
let restrictedFields: RestrictedFields = {};
|
||||
|
||||
if (
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IS_FIELDS_PERMISSIONS_ENABLED,
|
||||
workspace.id,
|
||||
)
|
||||
) {
|
||||
if (roleId) {
|
||||
const objectMetadataPermissions =
|
||||
await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
roleIds: roleId ? [roleId] : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(
|
||||
objectMetadataPermissions?.[roleId]?.[
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
]?.restrictedFields,
|
||||
)
|
||||
) {
|
||||
throw new InternalServerError(
|
||||
'Fields permissions not found for role',
|
||||
);
|
||||
}
|
||||
|
||||
restrictedFields =
|
||||
objectMetadataPermissions[roleId][
|
||||
objectMetadata.objectMetadataMapItem.id
|
||||
].restrictedFields;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
objectMetadata,
|
||||
repository,
|
||||
workspaceDataSource,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
restrictedFields,
|
||||
};
|
||||
}
|
||||
|
||||
@ -201,6 +245,7 @@ export abstract class RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth,
|
||||
restrictedFields,
|
||||
}: {
|
||||
recordIds: string[];
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
@ -209,6 +254,7 @@ export abstract class RestApiBaseHandler {
|
||||
objectMetadataMapItem: ObjectMetadataItemWithFieldMaps;
|
||||
};
|
||||
depth: Depth | undefined;
|
||||
restrictedFields: RestrictedFields;
|
||||
}) {
|
||||
const relations = this.getRelations({
|
||||
objectMetadata,
|
||||
@ -217,7 +263,17 @@ export abstract class RestApiBaseHandler {
|
||||
|
||||
const relationsChunk = chunk(relations, 50);
|
||||
|
||||
let selectOptions = undefined;
|
||||
|
||||
if (!isEmpty(restrictedFields)) {
|
||||
selectOptions = this.getSelectOptionsFromRestrictedFields({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
const recordsWithoutRelations = await repository.find({
|
||||
...(selectOptions && { select: selectOptions }),
|
||||
where: { id: In(recordIds) },
|
||||
});
|
||||
|
||||
@ -227,6 +283,7 @@ export abstract class RestApiBaseHandler {
|
||||
|
||||
for (const relationChunk of relationsChunk) {
|
||||
const records = await repository.find({
|
||||
...(selectOptions && { select: selectOptions }),
|
||||
where: { id: In(recordIds) },
|
||||
relations: relationChunk,
|
||||
});
|
||||
@ -254,6 +311,36 @@ export abstract class RestApiBaseHandler {
|
||||
};
|
||||
}
|
||||
|
||||
public getSelectOptionsFromRestrictedFields({
|
||||
restrictedFields,
|
||||
objectMetadata,
|
||||
}: {
|
||||
restrictedFields: RestrictedFields;
|
||||
objectMetadata: { objectMetadataMapItem: ObjectMetadataItemWithFieldMaps };
|
||||
}) {
|
||||
const restrictedFieldsIds = Object.entries(restrictedFields)
|
||||
.filter(([_, value]) => value.canRead === false)
|
||||
.map(([key]) => key);
|
||||
|
||||
const fieldMetadataIdToColumnNamesMap = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadata.objectMetadataMapItem,
|
||||
);
|
||||
|
||||
const restrictedFieldsColumnNames: string[] = restrictedFieldsIds
|
||||
.map((fieldId) => fieldMetadataIdToColumnNamesMap.get(fieldId))
|
||||
.filter(isDefined)
|
||||
.flat();
|
||||
|
||||
const allColumnNames = [...fieldMetadataIdToColumnNamesMap.values()].flat();
|
||||
|
||||
return Object.fromEntries(
|
||||
allColumnNames.map((columnName) => [
|
||||
columnName,
|
||||
!restrictedFieldsColumnNames.includes(columnName),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
public formatResult<T>({
|
||||
operation,
|
||||
objectNameSingular,
|
||||
@ -303,6 +390,7 @@ export abstract class RestApiBaseHandler {
|
||||
objectMetadata,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
extraFilters,
|
||||
restrictedFields,
|
||||
}: {
|
||||
request: Request;
|
||||
recordId?: string;
|
||||
@ -313,6 +401,7 @@ export abstract class RestApiBaseHandler {
|
||||
};
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
extraFilters?: Partial<ObjectRecordFilter>;
|
||||
restrictedFields: RestrictedFields;
|
||||
}) {
|
||||
const objectMetadataNameSingular =
|
||||
objectMetadata.objectMetadataMapItem.nameSingular;
|
||||
@ -373,6 +462,7 @@ export abstract class RestApiBaseHandler {
|
||||
repository,
|
||||
objectMetadata,
|
||||
depth: this.depthInputFactory.create(request),
|
||||
restrictedFields,
|
||||
});
|
||||
|
||||
const hasMoreRecords = records.length < totalCount;
|
||||
|
||||
@ -15,6 +15,7 @@ import { RestApiCoreService } from 'src/engine/api/rest/core/services/rest-api-c
|
||||
import { RestApiService } from 'src/engine/api/rest/rest-api.service';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@ -40,6 +41,7 @@ const restApiCoreResolvers = [
|
||||
RecordTransformerModule,
|
||||
WorkspacePermissionsCacheModule,
|
||||
ActorModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
controllers: [RestApiCoreController],
|
||||
providers: [
|
||||
|
||||
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
ObjectRecordsPermissions,
|
||||
ObjectRecordsPermissionsByRoleId,
|
||||
RestrictedFields,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
@ -203,10 +204,7 @@ export class WorkspacePermissionsCacheService {
|
||||
let canUpdate = role.canUpdateAllObjectRecords;
|
||||
let canSoftDelete = role.canSoftDeleteAllObjectRecords;
|
||||
let canDestroy = role.canDestroyAllObjectRecords;
|
||||
const restrictedFields: Record<
|
||||
string,
|
||||
{ canRead?: boolean | null; canUpdate?: boolean | null }
|
||||
> = {};
|
||||
const restrictedFields: RestrictedFields = {};
|
||||
|
||||
if (
|
||||
standardId &&
|
||||
|
||||
@ -0,0 +1,211 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { getFieldMetadataIdToColumnNamesMap } from 'src/engine/twenty-orm/utils/get-field-metadata-id-to-column-names-map.util';
|
||||
|
||||
describe('getFieldMetadataIdToColumnNamesMap', () => {
|
||||
const createMockObjectMetadataItemWithFieldMaps = (
|
||||
fieldsById: Record<string, any>,
|
||||
): ObjectMetadataItemWithFieldMaps =>
|
||||
({
|
||||
id: 'test-object-id',
|
||||
nameSingular: 'test',
|
||||
namePlural: 'tests',
|
||||
labelSingular: 'Test',
|
||||
labelPlural: 'Tests',
|
||||
description: 'Test object',
|
||||
icon: 'IconTest',
|
||||
targetTableName: 'test',
|
||||
isCustom: false,
|
||||
isRemote: false,
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
isAuditLogged: false,
|
||||
isSearchable: false,
|
||||
labelIdentifierFieldMetadataId: '',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
workspaceId: 'test-workspace-id',
|
||||
indexMetadatas: [],
|
||||
fieldsById,
|
||||
fieldIdByName: {},
|
||||
fieldIdByJoinColumnName: {},
|
||||
}) as unknown as ObjectMetadataItemWithFieldMaps;
|
||||
|
||||
const createMockFieldMetadata = (
|
||||
id: string,
|
||||
name: string,
|
||||
type: FieldMetadataType,
|
||||
) => ({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
label: name,
|
||||
objectMetadataId: 'test-object-id',
|
||||
isLabelSyncedWithName: true,
|
||||
isNullable: true,
|
||||
isUnique: false,
|
||||
workspaceId: 'test-workspace-id',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
describe('with simple field types', () => {
|
||||
it('should return a map with single column name for simple field types', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'age',
|
||||
FieldMetadataType.NUMBER,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result.get('field-1')).toEqual(['name']);
|
||||
expect(result.get('field-2')).toEqual(['age']);
|
||||
expect(result.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with composite field types', () => {
|
||||
it('should return multiple column names for FULL_NAME composite type', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result.get('field-1')).toEqual([
|
||||
'fullNameFirstName',
|
||||
'fullNameLastName',
|
||||
]);
|
||||
expect(result.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should return multiple column names for CURRENCY composite type', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result.get('field-1')).toEqual([
|
||||
'priceAmountMicros',
|
||||
'priceCurrencyCode',
|
||||
]);
|
||||
expect(result.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle multiple composite fields', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
'field-3': createMockFieldMetadata(
|
||||
'field-3',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result.get('field-1')).toEqual([
|
||||
'fullNameFirstName',
|
||||
'fullNameLastName',
|
||||
]);
|
||||
expect(result.get('field-2')).toEqual([
|
||||
'priceAmountMicros',
|
||||
'priceCurrencyCode',
|
||||
]);
|
||||
expect(result.get('field-3')).toEqual(['name']);
|
||||
expect(result.size).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mixed field types', () => {
|
||||
it('should handle both simple and composite field types', () => {
|
||||
const fieldsById = {
|
||||
'field-1': createMockFieldMetadata(
|
||||
'field-1',
|
||||
'name',
|
||||
FieldMetadataType.TEXT,
|
||||
),
|
||||
'field-2': createMockFieldMetadata(
|
||||
'field-2',
|
||||
'fullName',
|
||||
FieldMetadataType.FULL_NAME,
|
||||
),
|
||||
'field-3': createMockFieldMetadata(
|
||||
'field-3',
|
||||
'age',
|
||||
FieldMetadataType.NUMBER,
|
||||
),
|
||||
'field-4': createMockFieldMetadata(
|
||||
'field-4',
|
||||
'price',
|
||||
FieldMetadataType.CURRENCY,
|
||||
),
|
||||
};
|
||||
|
||||
const objectMetadataItemWithFieldMaps =
|
||||
createMockObjectMetadataItemWithFieldMaps(fieldsById);
|
||||
|
||||
const result = getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps,
|
||||
);
|
||||
|
||||
expect(result.get('field-1')).toEqual(['name']);
|
||||
expect(result.get('field-2')).toEqual([
|
||||
'fullNameFirstName',
|
||||
'fullNameLastName',
|
||||
]);
|
||||
expect(result.get('field-3')).toEqual(['age']);
|
||||
expect(result.get('field-4')).toEqual([
|
||||
'priceAmountMicros',
|
||||
'priceCurrencyCode',
|
||||
]);
|
||||
expect(result.size).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
import { InternalServerError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import {
|
||||
computeColumnName,
|
||||
computeCompositeColumnName,
|
||||
} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
|
||||
export function getFieldMetadataIdToColumnNamesMap(
|
||||
objectMetadataItemWithFieldMaps: ObjectMetadataItemWithFieldMaps,
|
||||
) {
|
||||
const fieldMetadataToColumnNamesMap = new Map<string, string[]>();
|
||||
|
||||
for (const [fieldMetadataId, fieldMetadata] of Object.entries(
|
||||
objectMetadataItemWithFieldMaps.fieldsById,
|
||||
)) {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const compositeType = compositeTypeDefinitions.get(fieldMetadata.type);
|
||||
|
||||
if (!compositeType) {
|
||||
throw new InternalServerError(
|
||||
`Composite type not found for field metadata type ${fieldMetadata.type}`,
|
||||
);
|
||||
}
|
||||
|
||||
compositeType.properties.forEach((compositeProperty) => {
|
||||
const columnName = computeCompositeColumnName(
|
||||
fieldMetadata.name,
|
||||
compositeProperty,
|
||||
);
|
||||
|
||||
const existingColumns =
|
||||
fieldMetadataToColumnNamesMap.get(fieldMetadataId) ?? [];
|
||||
|
||||
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [
|
||||
...existingColumns,
|
||||
columnName,
|
||||
]);
|
||||
});
|
||||
} else {
|
||||
const columnName = computeColumnName(fieldMetadata);
|
||||
|
||||
fieldMetadataToColumnNamesMap.set(fieldMetadataId, [columnName]);
|
||||
}
|
||||
}
|
||||
|
||||
return fieldMetadataToColumnNamesMap;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const updateFeatureFlagFactory = (
|
||||
workspaceId: string,
|
||||
featureFlag: string,
|
||||
value: boolean,
|
||||
) => ({
|
||||
query: gql`
|
||||
mutation UpdateWorkspaceFeatureFlag(
|
||||
$workspaceId: String!
|
||||
$featureFlag: String!
|
||||
$value: Boolean!
|
||||
) {
|
||||
updateWorkspaceFeatureFlag(
|
||||
workspaceId: $workspaceId
|
||||
featureFlag: $featureFlag
|
||||
value: $value
|
||||
)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
workspaceId,
|
||||
featureFlag,
|
||||
value,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,39 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const createUpsertFieldPermissionsOperation = (
|
||||
roleId: string,
|
||||
fieldPermissions: Array<{
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
canReadFieldValue?: boolean | null;
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
}>,
|
||||
selectedFields: string[] = [
|
||||
'id',
|
||||
'roleId',
|
||||
'objectMetadataId',
|
||||
'fieldMetadataId',
|
||||
'canReadFieldValue',
|
||||
'canUpdateFieldValue',
|
||||
],
|
||||
) => ({
|
||||
query: gql`
|
||||
mutation UpsertFieldPermissions(
|
||||
$roleId: String!
|
||||
$fieldPermissions: [FieldPermissionInput!]!
|
||||
) {
|
||||
upsertFieldPermissions(
|
||||
upsertFieldPermissionsInput: {
|
||||
roleId: $roleId
|
||||
fieldPermissions: $fieldPermissions
|
||||
}
|
||||
) {
|
||||
${selectedFields.join('\n')}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
roleId,
|
||||
fieldPermissions,
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import { createUpsertFieldPermissionsOperation } from 'test/integration/graphql/utils/upsert-field-permissions-operation-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
|
||||
export const upsertFieldPermissions = async ({
|
||||
roleId,
|
||||
fieldPermissions,
|
||||
selectedFields,
|
||||
}: {
|
||||
roleId: string;
|
||||
fieldPermissions: Array<{
|
||||
objectMetadataId: string;
|
||||
fieldMetadataId: string;
|
||||
canReadFieldValue?: boolean | null;
|
||||
canUpdateFieldValue?: boolean | null;
|
||||
}>;
|
||||
selectedFields?: string[];
|
||||
}) => {
|
||||
const operation = createUpsertFieldPermissionsOperation(
|
||||
roleId,
|
||||
fieldPermissions,
|
||||
selectedFields,
|
||||
);
|
||||
|
||||
const response = await makeMetadataAPIRequest(operation);
|
||||
|
||||
return {
|
||||
data: response.body.data,
|
||||
errors: response.body.errors,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,174 @@
|
||||
import gql from 'graphql-tag';
|
||||
import { TEST_PERSON_1_ID } from 'test/integration/constants/test-person-ids.constants';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
|
||||
import { upsertFieldPermissions } from 'test/integration/graphql/utils/upsert-field-permissions.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
|
||||
describe('Restricted fields', () => {
|
||||
let personCity: string;
|
||||
let adminRoleId: string;
|
||||
let personObjectId: string;
|
||||
let emailsFieldId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
personCity = generateRecordName(TEST_PERSON_1_ID);
|
||||
|
||||
await makeRestAPIRequest({
|
||||
method: 'post',
|
||||
path: '/people',
|
||||
body: {
|
||||
id: TEST_PERSON_1_ID,
|
||||
city: personCity,
|
||||
emails: {
|
||||
primaryEmail: 'test@test.com',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Get object metadata IDs for Person and Company
|
||||
const getObjectMetadataOperation = {
|
||||
query: gql`
|
||||
query {
|
||||
objects(paging: { first: 1000 }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
nameSingular
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const objectMetadataResponse = await makeMetadataAPIRequest(
|
||||
getObjectMetadataOperation,
|
||||
);
|
||||
const objects = objectMetadataResponse.body.data.objects.edges;
|
||||
|
||||
personObjectId = objects.find(
|
||||
(obj: any) => obj.node.nameSingular === 'person',
|
||||
)?.node.id;
|
||||
|
||||
// Get field metadata ID for email field
|
||||
const getFieldMetadataOperation = {
|
||||
query: gql`
|
||||
query {
|
||||
fields(paging: { first: 1000 }) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
object {
|
||||
nameSingular
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const fieldMetadataResponse = await makeMetadataAPIRequest(
|
||||
getFieldMetadataOperation,
|
||||
);
|
||||
const fields = fieldMetadataResponse.body.data.fields.edges;
|
||||
|
||||
emailsFieldId = fields.find(
|
||||
(field: any) =>
|
||||
field.node.name === 'emails' &&
|
||||
field.node.object.nameSingular === 'person',
|
||||
).node.id;
|
||||
|
||||
// Get admin role ID
|
||||
const getRolesOperation = {
|
||||
query: gql`
|
||||
query {
|
||||
getRoles {
|
||||
id
|
||||
label
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const rolesResponse = await makeMetadataAPIRequest(getRolesOperation);
|
||||
|
||||
adminRoleId = rolesResponse.body.data.getRoles.find(
|
||||
(role: any) => role.label === 'Member',
|
||||
)?.id;
|
||||
|
||||
// Create field permission restricting read access to email field
|
||||
await upsertFieldPermissions({
|
||||
roleId: adminRoleId,
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectMetadataId: personObjectId,
|
||||
fieldMetadataId: emailsFieldId,
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('With Feature flag enabled', () => {
|
||||
beforeAll(async () => {
|
||||
const enablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
true,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(enablePermissionsQuery);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const disablePermissionsQuery = updateFeatureFlagFactory(
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||
false,
|
||||
);
|
||||
|
||||
await makeGraphqlAPIRequest(disablePermissionsQuery);
|
||||
});
|
||||
it('should hide fields when user has restricted read permissions', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person.id).toBeDefined();
|
||||
expect(person.emails).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With feature flag disabled', () => {
|
||||
it('should query all fields despite field permission restriction', async () => {
|
||||
await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/people/${TEST_PERSON_1_ID}`,
|
||||
bearer: APPLE_JONY_MEMBER_ACCESS_TOKEN,
|
||||
})
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
const person = res.body.data.person;
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person.id).toBeDefined();
|
||||
expect(person.emails).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
4
packages/twenty-shared/src/types/RestrictedFields.ts
Normal file
4
packages/twenty-shared/src/types/RestrictedFields.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type RestrictedFields = Record<
|
||||
string,
|
||||
{ canRead?: boolean | null; canUpdate?: boolean | null }
|
||||
>;
|
||||
@ -13,6 +13,7 @@ export { FieldMetadataType } from './FieldMetadataType';
|
||||
export type { IsExactly } from './IsExactly';
|
||||
export type { ObjectRecordsPermissions } from './ObjectRecordsPermissions';
|
||||
export type { ObjectRecordsPermissionsByRoleId } from './ObjectRecordsPermissionsByRoleId';
|
||||
export type { RestrictedFields } from './RestrictedFields';
|
||||
export type { StepFilterGroup, StepFilter } from './StepFilters';
|
||||
export { StepLogicalOperator } from './StepFilters';
|
||||
export { ViewFilterOperand } from './ViewFilterOperand';
|
||||
|
||||
Reference in New Issue
Block a user