Skip to content

Commit a88e941

Browse files
committed
Let a create state who may read the record with shareWith
createOne and createMany accept a shareWith argument next to data: a list of workspace members, roles or everyone, each with an access level. On a PRIVATE object the share rows are written in the same transaction as the record. A person creating a record gets one OWNER FULL row by default plus one MANUAL row per entry; an API key, an application or a system caller must pass shareWith or the create is refused with a message naming the argument. OPEN objects ignore the argument. Rows are written whether or not the record sharing flag is on, so the data is ready when the flag flips.
1 parent 41ad5bf commit a88e941

27 files changed

Lines changed: 1232 additions & 39 deletions

packages/twenty-docs/developers/extend/apps/logic/logic-functions.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,8 +803,28 @@ const { createCompany } = await client.mutation({
803803
name: true,
804804
},
805805
});
806+
807+
// Create a record of a private object and choose who can read it
808+
const { createContract } = await client.mutation({
809+
createContract: {
810+
__args: {
811+
data: {
812+
name: 'Acme Corp renewal',
813+
},
814+
shareWith: [
815+
{
816+
roleId: '7f0c1b6e-2d4a-4c8e-9b3f-5a6d7e8f9a0b',
817+
accessLevel: 'READ_WRITE',
818+
},
819+
],
820+
},
821+
id: true,
822+
},
823+
});
806824
```
807825

826+
On an object whose readability is `PRIVATE`, `shareWith` lists who gets access to the created record: each entry targets exactly one of `workspaceMemberId`, `roleId` or `everyone: true` with an `accessLevel` of `READ`, `READ_WRITE` or `FULL`. A workspace member creating a record always receives a `FULL` owner grant, so the argument is optional for them; a logic function acting as its application, an API key or a system caller must pass it or the create is rejected. On other objects the argument is ignored.
827+
808828
The client uses a selection-set syntax: pass `true` to include a field, use `__args` for arguments, and nest objects for relations. You get full autocompletion and type checking based on your workspace schema.
809829

810830
<Note>

packages/twenty-server/src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service.ts

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
22

33
import { msg } from '@lingui/core/macro';
44
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
5-
import { ObjectRecord } from 'twenty-shared/types';
5+
import { MetadataReadability, ObjectRecord } from 'twenty-shared/types';
66
import { isDefined } from 'twenty-shared/utils';
77
import {
88
Brackets,
@@ -15,9 +15,11 @@ import {
1515
import { CommonBaseQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-base-query-runner.service';
1616
import { type ConflictingFieldGroup } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/types/conflicting-field-group.type';
1717
import { PartialObjectRecordWithId } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/types/partial-object-record-with-id.type';
18+
import { buildRecordShareInputsForCreatedRecords } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/build-record-share-inputs-for-created-records.util';
1819
import { buildWhereConditions } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/build-where-conditions.util';
1920
import { categorizeRecords } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/categorize-records.util';
2021
import { getConflictingFields } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/get-conflicting-fields.util';
22+
import { validateShareWithArg } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/utils/validate-share-with-arg.util';
2123
import {
2224
CommonQueryRunnerException,
2325
CommonQueryRunnerExceptionCode,
@@ -33,6 +35,7 @@ import {
3335
} from 'src/engine/api/common/types/common-query-args.type';
3436
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
3537
import { type NestedRelationsReadPathOptions } from 'src/engine/api/common/types/nested-relations-read-path-options.type';
38+
import { type ShareWithInput } from 'src/engine/api/common/types/share-with-input.type';
3639
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
3740
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
3841
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
@@ -46,6 +49,7 @@ import { buildFieldMapsFromFlatObjectMetadata } from 'src/engine/metadata-module
4649
import { type FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
4750
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
4851
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
52+
import { RecordShareService } from 'src/engine/record-share/services/record-share.service';
4953
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace-repository';
5054
import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
5155
import { containsNestedRelationCreate } from 'src/engine/twenty-orm/utils/contains-nested-relation-create.util';
@@ -60,23 +64,38 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
6064
> {
6165
protected readonly operationName = CommonQueryNames.CREATE_MANY;
6266

63-
constructor(private readonly recordPositionService: RecordPositionService) {
67+
constructor(
68+
private readonly recordPositionService: RecordPositionService,
69+
private readonly recordShareService: RecordShareService,
70+
) {
6471
super();
6572
}
6673

6774
async run(
6875
args: CommonExtendedInput<CreateManyQueryArgs>,
6976
queryRunnerContext: CommonExtendedQueryRunnerContext,
7077
): Promise<ObjectRecord[]> {
78+
const isPrivateObject =
79+
queryRunnerContext.flatObjectMetadata.readability ===
80+
MetadataReadability.PRIVATE;
81+
82+
if (isPrivateObject) {
83+
validateShareWithArg({
84+
authContext: queryRunnerContext.authContext,
85+
shareWith: args.shareWith,
86+
});
87+
}
88+
7189
if (
7290
!isDefined(queryRunnerContext.transactionScope) &&
73-
containsNestedRelationCreate(
74-
args.data,
75-
getNestedRelationFieldNames({
76-
flatObjectMetadata: queryRunnerContext.flatObjectMetadata,
77-
flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps,
78-
}),
79-
)
91+
(isPrivateObject ||
92+
containsNestedRelationCreate(
93+
args.data,
94+
getNestedRelationFieldNames({
95+
flatObjectMetadata: queryRunnerContext.flatObjectMetadata,
96+
flatFieldMetadataMaps: queryRunnerContext.flatFieldMetadataMaps,
97+
}),
98+
))
8099
) {
81100
return this.workspaceOrmManager.runInWorkspaceTransaction(
82101
(transactionScope) =>
@@ -135,7 +154,13 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
135154
flatObjectMetadata,
136155
flatObjectMetadataMaps,
137156
flatFieldMetadataMaps,
138-
repository,
157+
repository:
158+
isPrivateObject && isDefined(queryRunnerContext.transactionScope)
159+
? queryRunnerContext.transactionScope.getRepository(
160+
flatObjectMetadata.nameSingular,
161+
{ shouldBypassPermissionChecks: true },
162+
)
163+
: repository,
139164
selectedFieldsResult: args.selectedFieldsResult,
140165
});
141166

@@ -268,14 +293,23 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
268293

269294
const writeRepository = this.getWriteRepository(queryRunnerContext);
270295

271-
return writeRepository.runInsert({
296+
const insertResult = await writeRepository.runInsert({
272297
records: await this.resolveNestedRelationsForCreate({
273298
records: args.data,
299+
shareWith: args.shareWith,
274300
queryRunnerContext,
275301
writeRepository,
276302
}),
277303
columnsToReturn: selectedColumns,
278304
});
305+
306+
await this.insertRecordShares({
307+
insertResult,
308+
shareWith: args.shareWith,
309+
queryRunnerContext,
310+
});
311+
312+
return insertResult;
279313
}
280314

281315
return this.performUpsertOperation({
@@ -359,6 +393,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
359393
flatFieldMetadataMaps,
360394
result,
361395
columnsToReturn,
396+
shareWith: args.shareWith,
362397
queryRunnerContext,
363398
});
364399
}
@@ -367,6 +402,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
367402
recordsToInsert: recordsToInsertWithPosition,
368403
result,
369404
columnsToReturn,
405+
shareWith: args.shareWith,
370406
queryRunnerContext,
371407
});
372408

@@ -469,13 +505,15 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
469505
flatFieldMetadataMaps,
470506
result,
471507
columnsToReturn,
508+
shareWith,
472509
queryRunnerContext,
473510
}: {
474511
partialRecordsToUpdate: PartialObjectRecordWithId[];
475512
flatObjectMetadata: FlatObjectMetadata;
476513
flatFieldMetadataMaps: FlatEntityMaps<OrmFlatFieldMetadata>;
477514
result: InsertResult;
478515
columnsToReturn: string[];
516+
shareWith?: ShareWithInput[];
479517
queryRunnerContext: CommonExtendedQueryRunnerContext;
480518
}): Promise<void> {
481519
const updateInputs = partialRecordsToUpdate
@@ -494,6 +532,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
494532
const writeRepository = this.getWriteRepository(queryRunnerContext);
495533
const resolvedData = await this.resolveNestedRelationsForCreate({
496534
records: updateInputs.map((input) => input.data),
535+
shareWith,
497536
queryRunnerContext,
498537
writeRepository,
499538
});
@@ -518,11 +557,13 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
518557
recordsToInsert,
519558
result,
520559
columnsToReturn,
560+
shareWith,
521561
queryRunnerContext,
522562
}: {
523563
recordsToInsert: Partial<ObjectRecord>[];
524564
result: InsertResult;
525565
columnsToReturn: string[];
566+
shareWith?: ShareWithInput[];
526567
queryRunnerContext: CommonExtendedQueryRunnerContext;
527568
}): Promise<void> {
528569
if (recordsToInsert.length === 0) {
@@ -534,23 +575,60 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
534575
const insertResult = await writeRepository.runInsert({
535576
records: await this.resolveNestedRelationsForCreate({
536577
records: recordsToInsert,
578+
shareWith,
537579
queryRunnerContext,
538580
writeRepository,
539581
}),
540582
columnsToReturn,
541583
});
542584

585+
await this.insertRecordShares({
586+
insertResult,
587+
shareWith,
588+
queryRunnerContext,
589+
});
590+
543591
result.identifiers.push(...insertResult.identifiers);
544592
result.generatedMaps.push(...insertResult.generatedMaps);
545593
result.raw.push(...insertResult.raw);
546594
}
547595

596+
private async insertRecordShares({
597+
insertResult,
598+
shareWith,
599+
queryRunnerContext,
600+
}: {
601+
insertResult: InsertResult;
602+
shareWith?: ShareWithInput[];
603+
queryRunnerContext: CommonExtendedQueryRunnerContext;
604+
}): Promise<void> {
605+
const { authContext, flatObjectMetadata, transactionScope } =
606+
queryRunnerContext;
607+
608+
if (flatObjectMetadata.readability !== MetadataReadability.PRIVATE) {
609+
return;
610+
}
611+
612+
await this.recordShareService.insertMany({
613+
workspaceId: authContext.workspace.id,
614+
recordShares: buildRecordShareInputsForCreatedRecords({
615+
recordIds: insertResult.generatedMaps.map((record) => record.id),
616+
objectMetadataId: flatObjectMetadata.id,
617+
authContext,
618+
shareWith,
619+
}),
620+
transactionScope,
621+
});
622+
}
623+
548624
private resolveNestedRelationsForCreate({
549625
records,
626+
shareWith,
550627
queryRunnerContext,
551628
writeRepository,
552629
}: {
553630
records: Partial<ObjectRecord>[];
631+
shareWith?: ShareWithInput[];
554632
queryRunnerContext: CommonExtendedQueryRunnerContext;
555633
writeRepository: WorkspaceRepository;
556634
}): Promise<Partial<ObjectRecord>[]> {
@@ -591,6 +669,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
591669
const { results } = await this.execute(
592670
{
593671
data: targetRecords,
672+
shareWith,
594673
selectedFields: { id: true },
595674
},
596675
{

0 commit comments

Comments
 (0)