feat: add new ACTOR field type and createdBy standard fields (#6324)
This pull request introduces a new `FieldMetadataType` called `ACTOR`.
The primary objective of this new type is to add an extra column to the
following objects: `person`, `company`, `opportunity`, `note`, `task`,
and all custom objects.
This composite type contains three properties:
- `source`
```typescript
export enum FieldActorSource {
EMAIL = 'EMAIL',
CALENDAR = 'CALENDAR',
API = 'API',
IMPORT = 'IMPORT',
MANUAL = 'MANUAL',
}
```
- `workspaceMemberId`
- This property can be `undefined` in some cases and refers to the
member who created the record.
- `name`
- Serves as a fallback if the `workspaceMember` is deleted and is used
for other source types like `API`.
### Functionality
The pre-hook system has been updated to allow real-time argument
updates. When a record is created, a pre-hook can now compute and update
the arguments accordingly. This enhancement enables the `createdBy`
field to be populated with the correct values based on the
`authContext`.
The `authContext` now includes:
- An optional User entity
- An optional ApiKey entity
- The workspace entity
This provides access to the necessary data for the `createdBy` field.
In the GraphQL API, only the `source` can be specified in the
`createdBy` input. This allows the front-end to specify the source when
creating records from a CSV file.
### Front-End Handling
On the front-end, `orderBy` and `filter` are only applied to the name
property of the `ACTOR` composite type. Currently, we are unable to
apply these operations to the workspace member relation. This means that
if a workspace member changes their first name or last name, there may
be a mismatch because the name will differ from the new one. The name
displayed on the screen is based on the workspace member entity when
available.
### Missing Components
Currently, this PR does not include a `createdBy` value for the `MAIL`
and `CALENDAR` sources. These records are created in a job, and at
present, we only have access to the workspaceId within the job. To
address this, we should use a function similar to
`loadServiceWithContext`, which was recently removed from `TwentyORM`.
This function would allow us to pass the `authContext` to the jobs
without disrupting existing jobs.
Another PR will be created to handle these cases.
### Related Issues
Fixes issue #5155.
### Additional Notes
This PR doesn't include the migrations of the current records and views.
Everything works properly when the database is reset but this part is
still missing for now. We'll add that in another PR.
- There is a minor issue: front-end tests are broken since this commit:
[80c0fc7ff1).
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
|
||||
@ -196,6 +197,16 @@ const fieldRichTextMock = {
|
||||
defaultValue: null,
|
||||
};
|
||||
|
||||
const fieldActorMock = {
|
||||
name: 'fieldActor',
|
||||
type: FieldMetadataType.ACTOR,
|
||||
isNullable: true,
|
||||
defaultValue: {
|
||||
source: FieldActorSource.MANUAL,
|
||||
name: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const fields = [
|
||||
fieldUuidMock,
|
||||
fieldTextMock,
|
||||
@ -218,6 +229,7 @@ export const fields = [
|
||||
fieldAddressMock,
|
||||
fieldRawJsonMock,
|
||||
fieldRichTextMock,
|
||||
fieldActorMock,
|
||||
];
|
||||
|
||||
export const objectMetadataItemMock = {
|
||||
|
||||
@ -15,7 +15,7 @@ import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
||||
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
|
||||
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@ -65,7 +65,7 @@ export class GraphQLConfigService
|
||||
autoSchemaFile: true,
|
||||
include: [CoreEngineModule],
|
||||
conditionalSchema: async (context) => {
|
||||
let user: User | undefined;
|
||||
let user: User | null | undefined;
|
||||
let workspace: Workspace | undefined;
|
||||
|
||||
try {
|
||||
@ -136,7 +136,7 @@ export class GraphQLConfigService
|
||||
|
||||
async createSchema(
|
||||
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
||||
data: JwtData,
|
||||
data: AuthContext,
|
||||
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
||||
// Create a new contextId for each request
|
||||
const contextId = ContextIdFactory.create();
|
||||
@ -155,9 +155,6 @@ export class GraphQLConfigService
|
||||
},
|
||||
);
|
||||
|
||||
return await workspaceFactory.createGraphQLSchema(
|
||||
data.workspace.id,
|
||||
data.user?.id,
|
||||
);
|
||||
return await workspaceFactory.createGraphQLSchema(data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ describe('QueryRunnerArgsFactory', () => {
|
||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||
] as FieldMetadataInterface[],
|
||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||
workspaceId,
|
||||
authContext: { workspace: { id: workspaceId } },
|
||||
} as WorkspaceQueryRunnerOptions;
|
||||
|
||||
let factory: QueryRunnerArgsFactory;
|
||||
@ -149,6 +149,7 @@ describe('QueryRunnerArgsFactory', () => {
|
||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||
] as FieldMetadataInterface[],
|
||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||
authContext: { workspace: { id: workspaceId } },
|
||||
} as WorkspaceQueryRunnerOptions;
|
||||
|
||||
const args = {
|
||||
|
||||
@ -129,7 +129,7 @@ export class QueryRunnerArgsFactory {
|
||||
isCustom: options.objectMetadataItem.isCustom,
|
||||
nameSingular: options.objectMetadataItem.nameSingular,
|
||||
},
|
||||
options.workspaceId,
|
||||
options.authContext.workspace.id,
|
||||
argPositionBackfillInput.argIndex,
|
||||
),
|
||||
];
|
||||
@ -157,7 +157,7 @@ export class QueryRunnerArgsFactory {
|
||||
isCustom: options.objectMetadataItem.isCustom,
|
||||
nameSingular: options.objectMetadataItem.nameSingular,
|
||||
},
|
||||
options.workspaceId,
|
||||
options.authContext.workspace.id,
|
||||
argPositionBackfillInput.argIndex,
|
||||
),
|
||||
],
|
||||
|
||||
@ -3,9 +3,10 @@ import { GraphQLResolveInfo } from 'graphql';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
export interface WorkspaceQueryRunnerOptions {
|
||||
workspaceId: string;
|
||||
userId: string | undefined;
|
||||
authContext: AuthContext;
|
||||
info: GraphQLResolveInfo;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
export interface WorkspaceQueryHookInstance {
|
||||
execute(
|
||||
userId: string | undefined,
|
||||
workspaceId: string,
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: ResolverArgs,
|
||||
): Promise<void>;
|
||||
): Promise<ResolverArgs>;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
|
||||
@ -36,8 +37,23 @@ export class WorkspaceQueryHookStorage {
|
||||
|
||||
getWorkspaceQueryPreHookInstances(
|
||||
key: WorkspaceQueryHookKey,
|
||||
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] | undefined {
|
||||
return this.preHookInstances.get(key);
|
||||
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] {
|
||||
const methodName = key.split('.')?.[1] as
|
||||
| WorkspaceResolverBuilderMethodNames
|
||||
| undefined;
|
||||
let wildcardInstances: WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] =
|
||||
[];
|
||||
|
||||
if (!methodName) {
|
||||
throw new Error(`Can't split workspace query hook key: ${key}`);
|
||||
}
|
||||
|
||||
// Retrive wildcard pre-hook instances
|
||||
if (this.preHookInstances.has(`*.${methodName}`)) {
|
||||
wildcardInstances = this.preHookInstances.get(`*.${methodName}`)!;
|
||||
}
|
||||
|
||||
return [...wildcardInstances, ...(this.preHookInstances.get(key) ?? [])];
|
||||
}
|
||||
|
||||
registerWorkspaceQueryPostHookInstance(
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
import { Injector } from '@nestjs/core/injector/injector';
|
||||
import { Module } from '@nestjs/core/injector/module';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
|
||||
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
|
||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
@ -73,7 +73,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
instance: object,
|
||||
host: Module,
|
||||
isRequestScoped: boolean,
|
||||
) {
|
||||
): Promise<ReturnType<WorkspaceQueryHookInstance['execute']>> {
|
||||
const methodName = 'execute';
|
||||
|
||||
if (isRequestScoped) {
|
||||
@ -83,7 +83,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
this.moduleRef.registerRequestByContextId(
|
||||
{
|
||||
req: {
|
||||
workspaceId: payload?.[1],
|
||||
workspaceId: payload?.[0].workspace.id,
|
||||
},
|
||||
},
|
||||
contextId,
|
||||
@ -97,9 +97,9 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||
contextId,
|
||||
);
|
||||
|
||||
await contextInstance[methodName].call(contextInstance, ...payload);
|
||||
return contextInstance[methodName].call(contextInstance, ...payload);
|
||||
} else {
|
||||
await instance[methodName].call(instance, ...payload);
|
||||
return instance[methodName].call(instance, ...payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
|
||||
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryHookService {
|
||||
@ -17,27 +20,33 @@ export class WorkspaceQueryHookService {
|
||||
public async executePreQueryHooks<
|
||||
T extends WorkspaceResolverBuilderMethodNames,
|
||||
>(
|
||||
userId: string | undefined,
|
||||
workspaceId: string,
|
||||
authContext: AuthContext,
|
||||
// TODO: We should allow wildcard for object name
|
||||
objectName: string,
|
||||
methodName: T,
|
||||
payload: WorkspacePreQueryHookPayload<T>,
|
||||
): Promise<void> {
|
||||
): Promise<WorkspacePreQueryHookPayload<T>> {
|
||||
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
|
||||
const preHookInstances =
|
||||
this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key);
|
||||
|
||||
if (!preHookInstances) {
|
||||
return;
|
||||
return payload;
|
||||
}
|
||||
|
||||
for (const preHookInstance of preHookInstances) {
|
||||
await this.workspaceQueryHookExplorer.handleHook(
|
||||
[userId, workspaceId, payload],
|
||||
// Deep merge all return of handleHook into payload before returning it
|
||||
const hookPayload = await this.workspaceQueryHookExplorer.handleHook(
|
||||
[authContext, objectName, payload],
|
||||
preHookInstance.instance,
|
||||
preHookInstance.host,
|
||||
preHookInstance.isRequestScoped,
|
||||
);
|
||||
|
||||
// TODO: Is it really a good idea ?
|
||||
payload = merge(payload, hookPayload);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||
|
||||
import {
|
||||
PGGraphQLMutation,
|
||||
@ -90,11 +89,19 @@ export class WorkspaceQueryRunnerService {
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<IConnection<Record> | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const start = performance.now();
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.FindMany,
|
||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||
@ -104,15 +111,7 @@ export class WorkspaceQueryRunnerService {
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
const end = performance.now();
|
||||
|
||||
this.logger.log(
|
||||
@ -125,7 +124,7 @@ export class WorkspaceQueryRunnerService {
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,10 +141,18 @@ export class WorkspaceQueryRunnerService {
|
||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.FindOne,
|
||||
)) as FindOneResolverArgs<Filter>;
|
||||
@ -155,20 +162,12 @@ export class WorkspaceQueryRunnerService {
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
const parsedResult = await this.parseResult<IConnection<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
return parsedResult?.edges?.[0]?.node;
|
||||
@ -192,10 +191,18 @@ export class WorkspaceQueryRunnerService {
|
||||
);
|
||||
}
|
||||
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findDuplicates',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.FindDuplicates,
|
||||
)) as FindDuplicatesResolverArgs<TRecord>;
|
||||
@ -206,7 +213,7 @@ export class WorkspaceQueryRunnerService {
|
||||
existingRecords = await this.duplicateService.findExistingRecords(
|
||||
computedArgs.ids,
|
||||
objectMetadataItem,
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
if (!existingRecords || existingRecords.length === 0) {
|
||||
@ -223,21 +230,13 @@ export class WorkspaceQueryRunnerService {
|
||||
existingRecords,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'findDuplicates',
|
||||
computedArgs,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
return this.parseResult<IConnection<TRecord>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@ -246,7 +245,7 @@ export class WorkspaceQueryRunnerService {
|
||||
args: CreateManyResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
@ -260,33 +259,33 @@ export class WorkspaceQueryRunnerService {
|
||||
}
|
||||
});
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
ResolverArgsType.CreateMany,
|
||||
)) as CreateManyResolverArgs<Record>;
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'createMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||
computedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'insertInto',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
@ -299,8 +298,8 @@ export class WorkspaceQueryRunnerService {
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
||||
name: `${objectMetadataItem.nameSingular}.created`,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
@ -325,7 +324,7 @@ export class WorkspaceQueryRunnerService {
|
||||
? await this.duplicateService.findExistingRecords(
|
||||
ids as string[],
|
||||
options.objectMetadataItem,
|
||||
options.workspaceId,
|
||||
options.authContext.workspace.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
@ -381,10 +380,10 @@ export class WorkspaceQueryRunnerService {
|
||||
args: UpdateOneResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
@ -402,27 +401,27 @@ export class WorkspaceQueryRunnerService {
|
||||
);
|
||||
}
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
@ -434,8 +433,8 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||
name: `${objectMetadataItem.nameSingular}.updated`,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
userId: authContext.user?.id,
|
||||
recordId: existingRecord.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
@ -452,10 +451,10 @@ export class WorkspaceQueryRunnerService {
|
||||
args: UpdateManyResolverArgs<Partial<Record>>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { userId, workspaceId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
@ -471,27 +470,31 @@ export class WorkspaceQueryRunnerService {
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
});
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateMany',
|
||||
args,
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'updateMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||
hookedArgs,
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'update',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
@ -514,8 +517,8 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||
name: `${objectMetadataItem.nameSingular}.updated`,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
userId: authContext.user?.id,
|
||||
recordId: existingRecord.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
@ -536,34 +539,38 @@ export class WorkspaceQueryRunnerService {
|
||||
args: DeleteManyResolverArgs<Filter>,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record[] | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
|
||||
const maximumRecordAffected = this.environmentService.get(
|
||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||
);
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
});
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteMany',
|
||||
args,
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteMany',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
||||
hookedArgs,
|
||||
{
|
||||
...options,
|
||||
atMost: maximumRecordAffected,
|
||||
},
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'deleteFrom',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
@ -576,8 +583,8 @@ export class WorkspaceQueryRunnerService {
|
||||
parsedResults.forEach((record) => {
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
name: `${objectMetadataItem.nameSingular}.deleted`,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
userId: authContext.user?.id,
|
||||
recordId: record.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
@ -593,18 +600,26 @@ export class WorkspaceQueryRunnerService {
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryRunnerOptions,
|
||||
): Promise<Record | undefined> {
|
||||
const { workspaceId, userId, objectMetadataItem } = options;
|
||||
const { authContext, objectMetadataItem } = options;
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
objectMetadataItem.nameSingular,
|
||||
);
|
||||
|
||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||
assertIsValidUuid(args.id);
|
||||
|
||||
const hookedArgs =
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
authContext,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||
args,
|
||||
hookedArgs,
|
||||
options,
|
||||
);
|
||||
|
||||
@ -612,22 +627,14 @@ export class WorkspaceQueryRunnerService {
|
||||
where: { id: args.id },
|
||||
});
|
||||
|
||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||
userId,
|
||||
workspaceId,
|
||||
objectMetadataItem.nameSingular,
|
||||
'deleteOne',
|
||||
args,
|
||||
);
|
||||
|
||||
const result = await this.execute(query, workspaceId);
|
||||
const result = await this.execute(query, authContext.workspace.id);
|
||||
|
||||
const parsedResults = (
|
||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||
result,
|
||||
objectMetadataItem,
|
||||
'deleteFrom',
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
)
|
||||
)?.records;
|
||||
|
||||
@ -639,8 +646,8 @@ export class WorkspaceQueryRunnerService {
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||
name: `${objectMetadataItem.nameSingular}.deleted`,
|
||||
workspaceId,
|
||||
userId,
|
||||
workspaceId: authContext.workspace.id,
|
||||
userId: authContext.user?.id,
|
||||
recordId: args.id,
|
||||
objectMetadata: objectMetadataItem,
|
||||
properties: {
|
||||
@ -808,7 +815,7 @@ export class WorkspaceQueryRunnerService {
|
||||
CallWebhookJobsJob.name,
|
||||
{
|
||||
record: jobData,
|
||||
workspaceId: options.workspaceId,
|
||||
workspaceId: options.authContext.workspace.id,
|
||||
operation,
|
||||
objectMetadataItem: options.objectMetadataItem,
|
||||
},
|
||||
@ -816,65 +823,4 @@ export class WorkspaceQueryRunnerService {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async handleDeleteWorkspaceMember(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) {
|
||||
if (objectMetadataItem.nameSingular !== 'workspaceMember') {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceMemberResult = await this.executeAndParse<IRecord>(
|
||||
`
|
||||
query {
|
||||
workspaceMemberCollection(filter: {id: {eq: "${id}"}}) {
|
||||
edges {
|
||||
node {
|
||||
userId: userId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return workspaceMemberResult.edges?.[0]?.node;
|
||||
}
|
||||
|
||||
async handleDeleteBlocklistItem(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
objectMetadataItem: ObjectMetadataInterface,
|
||||
) {
|
||||
if (objectMetadataItem.standardId !== STANDARD_OBJECT_IDS.blocklist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blocklistItemResult = await this.executeAndParse<IRecord>(
|
||||
`
|
||||
query {
|
||||
blocklistCollection(filter: {id: {eq: "${id}"}}) {
|
||||
edges {
|
||||
node {
|
||||
handle
|
||||
workspaceMember {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
objectMetadataItem,
|
||||
'',
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return blocklistItemResult.edges?.[0]?.node;
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,12 +25,11 @@ export class CreateManyResolverFactory
|
||||
): Resolver<CreateManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.createMany(args, {
|
||||
return this.workspaceQueryRunnerService.createMany(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class CreateOneResolverFactory
|
||||
): Resolver<CreateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.createOne(args, {
|
||||
return this.workspaceQueryRunnerService.createOne(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class DeleteManyResolverFactory
|
||||
): Resolver<DeleteManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.deleteMany(args, {
|
||||
return this.workspaceQueryRunnerService.deleteMany(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class DeleteOneResolverFactory
|
||||
): Resolver<DeleteOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.deleteOne(args, {
|
||||
return this.workspaceQueryRunnerService.deleteOne(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -31,12 +31,11 @@ export class ExecuteQuickActionOnOneResolverFactory
|
||||
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.executeQuickActionOnOne(args, {
|
||||
return this.executeQuickActionOnOne(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
userId: internalContext.userId,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
@ -55,7 +54,7 @@ export class ExecuteQuickActionOnOneResolverFactory
|
||||
case 'company': {
|
||||
await this.quickActionsService.executeQuickActionOnCompany(
|
||||
args.id,
|
||||
options.workspaceId,
|
||||
options.authContext.workspace.id,
|
||||
options.objectMetadataItem,
|
||||
);
|
||||
break;
|
||||
@ -63,7 +62,7 @@ export class ExecuteQuickActionOnOneResolverFactory
|
||||
case 'person': {
|
||||
await this.quickActionsService.createCompanyFromPerson(
|
||||
args.id,
|
||||
options.workspaceId,
|
||||
options.authContext.workspace.id,
|
||||
options.objectMetadataCollection,
|
||||
);
|
||||
break;
|
||||
|
||||
@ -25,12 +25,11 @@ export class FindDuplicatesResolverFactory
|
||||
): Resolver<FindDuplicatesResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.findDuplicates(args, {
|
||||
return this.workspaceQueryRunnerService.findDuplicates(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class FindManyResolverFactory
|
||||
): Resolver<FindManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.findMany(args, {
|
||||
return this.workspaceQueryRunnerService.findMany(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class FindOneResolverFactory
|
||||
): Resolver<FindOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.findOne(args, {
|
||||
return this.workspaceQueryRunnerService.findOne(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class UpdateManyResolverFactory
|
||||
): Resolver<UpdateManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.updateMany(args, {
|
||||
return this.workspaceQueryRunnerService.updateMany(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -25,12 +25,11 @@ export class UpdateOneResolverFactory
|
||||
): Resolver<UpdateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return async (_source, args, context, info) => {
|
||||
return (_source, args, context, info) => {
|
||||
try {
|
||||
return await this.workspaceQueryRunnerService.updateOne(args, {
|
||||
return this.workspaceQueryRunnerService.updateOne(args, {
|
||||
authContext: internalContext.authContext,
|
||||
objectMetadataItem: internalContext.objectMetadataItem,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
userId: internalContext.userId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||
|
||||
@ -8,6 +8,7 @@ import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||
import { ExecuteQuickActionOnOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
import { FindDuplicatesResolverFactory } from './factories/find-duplicates-resolver.factory';
|
||||
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
||||
@ -40,8 +41,7 @@ export class WorkspaceResolverFactory {
|
||||
) {}
|
||||
|
||||
async create(
|
||||
workspaceId: string,
|
||||
userId: string | undefined,
|
||||
authContext: AuthContext,
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||
): Promise<IResolvers> {
|
||||
@ -82,8 +82,7 @@ export class WorkspaceResolverFactory {
|
||||
}
|
||||
|
||||
resolvers.Query[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
userId,
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadata,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
objectMetadataCollection: objectMetadataCollection,
|
||||
@ -106,8 +105,7 @@ export class WorkspaceResolverFactory {
|
||||
}
|
||||
|
||||
resolvers.Mutation[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
userId,
|
||||
authContext,
|
||||
objectMetadataItem: objectMetadata,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
objectMetadataCollection: objectMetadataCollection,
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLEnumType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import {
|
||||
CompositeProperty,
|
||||
CompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import {
|
||||
FieldMetadataComplexOption,
|
||||
FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||
|
||||
export interface EnumTypeDefinition {
|
||||
target: string;
|
||||
type: GraphQLEnumType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CompositeEnumTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(EnumTypeDefinitionFactory.name);
|
||||
|
||||
public create(
|
||||
compositeType: CompositeType,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): EnumTypeDefinition[] {
|
||||
const enumTypeDefinitions: EnumTypeDefinition[] = [];
|
||||
|
||||
for (const compositeProperty of compositeType.properties) {
|
||||
if (!isEnumFieldMetadataType(compositeProperty.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = computeCompositePropertyTarget(
|
||||
compositeType.type,
|
||||
compositeProperty,
|
||||
);
|
||||
|
||||
enumTypeDefinitions.push({
|
||||
target,
|
||||
type: this.generateEnum(compositeType.type, compositeProperty, options),
|
||||
});
|
||||
}
|
||||
|
||||
return enumTypeDefinitions;
|
||||
}
|
||||
|
||||
private generateEnum(
|
||||
compositeType: FieldMetadataType,
|
||||
compositeProperty: CompositeProperty,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLEnumType {
|
||||
// FixMe: It's a hack until Typescript get fixed on union types for reduce function
|
||||
// https://github.com/microsoft/TypeScript/issues/36390
|
||||
const enumOptions = compositeProperty.options as Array<
|
||||
FieldMetadataDefaultOption | FieldMetadataComplexOption
|
||||
>;
|
||||
|
||||
if (!enumOptions) {
|
||||
this.logger.error(
|
||||
`Enum options are not defined for ${compositeProperty.name}`,
|
||||
{
|
||||
compositeProperty,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Enum options are not defined for ${compositeProperty.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
return new GraphQLEnumType({
|
||||
name: `${pascalCase(compositeType.toString())}${pascalCase(
|
||||
compositeProperty.name,
|
||||
)}Enum`,
|
||||
description: compositeProperty.description,
|
||||
values: enumOptions.reduce(
|
||||
(acc, enumOption) => {
|
||||
// Key must match this regex: /^[_A-Za-z][_0-9A-Za-z]+$/
|
||||
acc[enumOption.value] = {
|
||||
value: enumOption.value,
|
||||
description: enumOption.label,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as { [key: string]: { value: string; description: string } },
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -12,9 +12,15 @@ import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
|
||||
const hiddenAllowListKind = [
|
||||
InputTypeDefinitionKind.Create,
|
||||
InputTypeDefinitionKind.Update,
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class CompositeInputTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(
|
||||
@ -58,12 +64,19 @@ export class CompositeInputTypeDefinitionFactory {
|
||||
}
|
||||
|
||||
// Skip hidden fields
|
||||
if (property.hidden === true || property.hidden === 'input') {
|
||||
if (
|
||||
property.hidden === true ||
|
||||
(property.hidden === 'input' && hiddenAllowListKind.includes(kind))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = computeCompositePropertyTarget(
|
||||
compositeType.type,
|
||||
property,
|
||||
);
|
||||
const type = this.inputTypeFactory.create(
|
||||
property.name,
|
||||
target,
|
||||
property.type,
|
||||
kind,
|
||||
options,
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
ObjectTypeDefinitionKind,
|
||||
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||
|
||||
@Injectable()
|
||||
export class CompositeObjectTypeDefinitionFactory {
|
||||
@ -62,8 +63,12 @@ export class CompositeObjectTypeDefinitionFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = computeCompositePropertyTarget(
|
||||
compositeType.type,
|
||||
property,
|
||||
);
|
||||
const type = this.outputTypeFactory.create(
|
||||
property.name,
|
||||
target,
|
||||
property.type,
|
||||
kind,
|
||||
options,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
|
||||
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
@ -27,6 +28,7 @@ export const workspaceSchemaBuilderFactories = [
|
||||
ObjectTypeDefinitionFactory,
|
||||
CompositeObjectTypeDefinitionFactory,
|
||||
EnumTypeDefinitionFactory,
|
||||
CompositeEnumTypeDefinitionFactory,
|
||||
RelationTypeFactory,
|
||||
ExtendObjectTypeDefinitionFactory,
|
||||
ConnectionTypeFactory,
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
export interface WorkspaceSchemaBuilderContext {
|
||||
workspaceId: string;
|
||||
userId: string | undefined;
|
||||
authContext: AuthContext;
|
||||
objectMetadataItem: ObjectMetadataInterface;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
objectMetadataCollection: ObjectMetadataInterface[];
|
||||
|
||||
@ -7,6 +7,7 @@ import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-sche
|
||||
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
|
||||
|
||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||
import {
|
||||
@ -32,6 +33,7 @@ export class TypeDefinitionsGenerator {
|
||||
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
|
||||
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
|
||||
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
|
||||
private readonly compositeEnumTypeDefinitionFactory: CompositeEnumTypeDefinitionFactory,
|
||||
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
|
||||
private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
|
||||
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
||||
@ -62,10 +64,24 @@ export class TypeDefinitionsGenerator {
|
||||
);
|
||||
|
||||
// Generate composite types first because they can be used in metadata objects
|
||||
this.generateCompositeEnumTypeDefs(compositeTypeCollection, options);
|
||||
this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
|
||||
this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
|
||||
}
|
||||
|
||||
private generateCompositeEnumTypeDefs(
|
||||
compositeTypes: CompositeType[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
) {
|
||||
const enumTypeDefs = compositeTypes
|
||||
.map((compositeType) =>
|
||||
this.compositeEnumTypeDefinitionFactory.create(compositeType, options),
|
||||
)
|
||||
.flat();
|
||||
|
||||
this.typeDefinitionsStorage.addEnumTypes(enumTypeDefs);
|
||||
}
|
||||
|
||||
private generateCompositeObjectTypeDefs(
|
||||
compositeTypes: CompositeType[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export const computeCompositePropertyTarget = (
|
||||
type: FieldMetadataType,
|
||||
compositeProperty: CompositeProperty,
|
||||
): string => {
|
||||
return `${type.toString()}->${compositeProperty.name}`;
|
||||
};
|
||||
@ -11,6 +11,7 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-
|
||||
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
|
||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSchemaFactory {
|
||||
@ -23,17 +24,14 @@ export class WorkspaceSchemaFactory {
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async createGraphQLSchema(
|
||||
workspaceId: string | undefined,
|
||||
userId: string | undefined,
|
||||
): Promise<GraphQLSchema> {
|
||||
if (!workspaceId) {
|
||||
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
||||
if (!authContext.workspace?.id) {
|
||||
return new GraphQLSchema({});
|
||||
}
|
||||
|
||||
const dataSourcesMetadata =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
// Can'f find any data sources for this workspace
|
||||
@ -42,30 +40,37 @@ export class WorkspaceSchemaFactory {
|
||||
}
|
||||
|
||||
// Validate cache version
|
||||
await this.workspaceCacheStorageService.validateCacheVersion(workspaceId);
|
||||
await this.workspaceCacheStorageService.validateCacheVersion(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
// Get object metadata from cache
|
||||
let objectMetadataCollection =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataCollection(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
// If object metadata is not cached, get it from the database
|
||||
if (!objectMetadataCollection) {
|
||||
objectMetadataCollection =
|
||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
||||
await this.objectMetadataService.findManyWithinWorkspace(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
await this.workspaceCacheStorageService.setObjectMetadataCollection(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
objectMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
// Get typeDefs from cache
|
||||
let typeDefs =
|
||||
await this.workspaceCacheStorageService.getTypeDefs(workspaceId);
|
||||
let typeDefs = await this.workspaceCacheStorageService.getTypeDefs(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
let usedScalarNames =
|
||||
await this.workspaceCacheStorageService.getUsedScalarNames(workspaceId);
|
||||
await this.workspaceCacheStorageService.getUsedScalarNames(
|
||||
authContext.workspace.id,
|
||||
);
|
||||
|
||||
// If typeDefs are not cached, generate them
|
||||
if (!typeDefs || !usedScalarNames) {
|
||||
@ -80,18 +85,17 @@ export class WorkspaceSchemaFactory {
|
||||
typeDefs = printSchema(autoGeneratedSchema);
|
||||
|
||||
await this.workspaceCacheStorageService.setTypeDefs(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
typeDefs,
|
||||
);
|
||||
await this.workspaceCacheStorageService.setUsedScalarNames(
|
||||
workspaceId,
|
||||
authContext.workspace.id,
|
||||
usedScalarNames,
|
||||
);
|
||||
}
|
||||
|
||||
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
|
||||
workspaceId,
|
||||
userId,
|
||||
authContext,
|
||||
objectMetadataCollection,
|
||||
workspaceResolverBuilderMethodNames,
|
||||
);
|
||||
|
||||
@ -135,5 +135,14 @@ export const mapFieldMetadataToGraphqlQuery = (
|
||||
addressLng
|
||||
}
|
||||
`;
|
||||
} else if (fieldType === FieldMetadataType.ACTOR) {
|
||||
return `
|
||||
${field.name}
|
||||
{
|
||||
source
|
||||
workspaceMemberId
|
||||
name
|
||||
}
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -39,6 +39,7 @@ import {
|
||||
AppToken,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
@ -131,7 +132,7 @@ export class AuthService {
|
||||
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
||||
|
||||
if (workspaceMember) {
|
||||
user.workspaceMember = workspaceMember;
|
||||
user.workspaceMember = workspaceMember as WorkspaceMember;
|
||||
}
|
||||
|
||||
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||
|
||||
@ -42,7 +42,7 @@ import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/val
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||
@ -208,7 +208,7 @@ export class TokenService {
|
||||
return !!token;
|
||||
}
|
||||
|
||||
async validateToken(request: Request): Promise<JwtData> {
|
||||
async validateToken(request: Request): Promise<AuthContext> {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
if (!token) {
|
||||
@ -219,11 +219,11 @@ export class TokenService {
|
||||
this.environmentService.get('ACCESS_TOKEN_SECRET'),
|
||||
);
|
||||
|
||||
const { user, workspace } = await this.jwtStrategy.validate(
|
||||
const { user, apiKey, workspace } = await this.jwtStrategy.validate(
|
||||
decoded as JwtPayload,
|
||||
);
|
||||
|
||||
return { user, workspace };
|
||||
return { user, apiKey, workspace };
|
||||
}
|
||||
|
||||
async verifyLoginToken(loginToken: string): Promise<string> {
|
||||
|
||||
@ -15,9 +15,10 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
|
||||
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
|
||||
export type PassportUser = { user?: User; workspace: Workspace };
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@ -37,15 +38,24 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<PassportUser> {
|
||||
async validate(payload: JwtPayload): Promise<AuthContext> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
id: payload.workspaceId ?? payload.sub,
|
||||
});
|
||||
let user: User | null = null;
|
||||
let apiKey: ApiKeyWorkspaceEntity | null = null;
|
||||
|
||||
if (!workspace) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
if (payload.jti) {
|
||||
// TODO: Check why it's not working
|
||||
// const apiKeyRepository =
|
||||
// await this.twentyORMGlobalManager.getRepositoryForWorkspace<ApiKeyWorkspaceEntity>(
|
||||
// workspace.id,
|
||||
// 'apiKey',
|
||||
// );
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspace.id,
|
||||
@ -54,20 +64,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const apiKey = await workspaceDataSource?.query(
|
||||
const res = await workspaceDataSource?.query(
|
||||
`SELECT * FROM ${dataSourceMetadata.schema}."apiKey" WHERE id = $1`,
|
||||
[payload.jti],
|
||||
);
|
||||
|
||||
apiKey = res?.[0];
|
||||
|
||||
assert(
|
||||
apiKey.length === 1 && !apiKey?.[0].revokedAt,
|
||||
apiKey && !apiKey.revokedAt,
|
||||
'This API Key is revoked',
|
||||
ForbiddenException,
|
||||
);
|
||||
}
|
||||
|
||||
let user;
|
||||
|
||||
if (payload.workspaceId) {
|
||||
user = await this.userRepository.findOne({
|
||||
where: { id: payload.sub },
|
||||
@ -78,6 +88,6 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
}
|
||||
}
|
||||
|
||||
return { user, workspace };
|
||||
return { user, apiKey, workspace };
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
|
||||
export type AuthContext = {
|
||||
user?: User | null | undefined;
|
||||
apiKey?: ApiKeyWorkspaceEntity | null | undefined;
|
||||
workspace: Workspace;
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
export type JwtData = {
|
||||
user?: User | undefined;
|
||||
workspace: Workspace;
|
||||
};
|
||||
@ -137,6 +137,21 @@ describe('computeSchemaComponents', () => {
|
||||
fieldRichText: {
|
||||
type: 'string',
|
||||
},
|
||||
fieldActor: {
|
||||
properties: {
|
||||
source: {
|
||||
type: 'string',
|
||||
},
|
||||
workspaceMemberId: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
'ObjectName with Relations': {
|
||||
|
||||
@ -71,6 +71,7 @@ const getSchemaComponentsProperties = (
|
||||
case FieldMetadataType.CURRENCY:
|
||||
case FieldMetadataType.FULL_NAME:
|
||||
case FieldMetadataType.ADDRESS:
|
||||
case FieldMetadataType.ACTOR:
|
||||
itemProperty = {
|
||||
type: 'object',
|
||||
properties: compositeTypeDefintions
|
||||
|
||||
@ -8,6 +8,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
@ -40,6 +41,10 @@ describe('UserService', () => {
|
||||
provide: WorkspaceService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: TwentyORMGlobalManager,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@ -1,18 +1,22 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export class UserService extends TypeOrmQueryService<User> {
|
||||
constructor(
|
||||
@ -22,73 +26,46 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
private readonly typeORMService: TypeORMService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly workspaceService: WorkspaceService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {
|
||||
super(userRepository);
|
||||
}
|
||||
|
||||
async loadWorkspaceMember(user: User) {
|
||||
const dataSourcesMetadata =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
user.defaultWorkspace.id,
|
||||
if (
|
||||
user.defaultWorkspace.activationStatus !==
|
||||
WorkspaceActivationStatus.ACTIVE
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
user.defaultWorkspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
if (!dataSourcesMetadata.length) {
|
||||
return;
|
||||
}
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (dataSourcesMetadata.length > 1) {
|
||||
throw new Error(
|
||||
`user '${user.id}' default workspace '${user.defaultWorkspace.id}' has multiple data source metadata`,
|
||||
);
|
||||
}
|
||||
|
||||
const dataSourceMetadata = dataSourcesMetadata[0];
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||
|
||||
const workspaceMembers = await workspaceDataSource?.query(
|
||||
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
|
||||
);
|
||||
|
||||
if (!workspaceMembers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(
|
||||
workspaceMembers.length === 1,
|
||||
'WorkspaceMember not found or too many found',
|
||||
);
|
||||
|
||||
const userWorkspaceMember = new WorkspaceMember();
|
||||
|
||||
userWorkspaceMember.id = workspaceMembers[0].id;
|
||||
userWorkspaceMember.colorScheme = workspaceMembers[0].colorScheme;
|
||||
userWorkspaceMember.locale = workspaceMembers[0].locale;
|
||||
userWorkspaceMember.avatarUrl = workspaceMembers[0].avatarUrl;
|
||||
userWorkspaceMember.name = {
|
||||
firstName: workspaceMembers[0].nameFirstName,
|
||||
lastName: workspaceMembers[0].nameLastName,
|
||||
};
|
||||
userWorkspaceMember.timeZone = workspaceMembers[0].timeZone;
|
||||
userWorkspaceMember.dateFormat = workspaceMembers[0].dateFormat;
|
||||
userWorkspaceMember.timeFormat = workspaceMembers[0].timeFormat;
|
||||
|
||||
return userWorkspaceMember;
|
||||
return workspaceMember;
|
||||
}
|
||||
|
||||
async loadWorkspaceMembers(dataSource: DataSourceEntity) {
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSource);
|
||||
async loadWorkspaceMembers(workspace: Workspace) {
|
||||
if (workspace.activationStatus !== WorkspaceActivationStatus.ACTIVE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await workspaceDataSource?.query(
|
||||
`
|
||||
SELECT *
|
||||
FROM ${dataSource.schema}."workspaceMember" AS s
|
||||
INNER JOIN core.user AS u
|
||||
ON s."userId" = u.id
|
||||
`,
|
||||
);
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspace.id,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
return workspaceMemberRepository.find();
|
||||
}
|
||||
|
||||
async deleteUser(userId: string): Promise<User> {
|
||||
@ -136,6 +113,7 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
payload.properties = {
|
||||
before: workspaceMember,
|
||||
};
|
||||
payload.name = 'workspaceMember.deleted';
|
||||
payload.recordId = workspaceMember.id;
|
||||
|
||||
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
||||
|
||||
@ -112,9 +112,7 @@ export class UserResolver {
|
||||
@ResolveField(() => WorkspaceMember, {
|
||||
nullable: true,
|
||||
})
|
||||
async workspaceMember(
|
||||
@Parent() user: User,
|
||||
): Promise<WorkspaceMember | undefined> {
|
||||
async workspaceMember(@Parent() user: User): Promise<WorkspaceMember | null> {
|
||||
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
||||
|
||||
if (workspaceMember && workspaceMember.avatarUrl) {
|
||||
@ -126,7 +124,31 @@ export class UserResolver {
|
||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||
}
|
||||
|
||||
return workspaceMember;
|
||||
// TODO: Fix typing disrepency between Entity and DTO
|
||||
return workspaceMember as WorkspaceMember | null;
|
||||
}
|
||||
|
||||
@ResolveField(() => [WorkspaceMember], {
|
||||
nullable: true,
|
||||
})
|
||||
async workspaceMembers(@Parent() user: User): Promise<WorkspaceMember[]> {
|
||||
const workspaceMembers = await this.userService.loadWorkspaceMembers(
|
||||
user.defaultWorkspace,
|
||||
);
|
||||
|
||||
for (const workspaceMember of workspaceMembers) {
|
||||
if (workspaceMember.avatarUrl) {
|
||||
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||
workspace_member_id: workspaceMember.id,
|
||||
workspace_id: user.defaultWorkspaceId,
|
||||
});
|
||||
|
||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix typing disrepency between Entity and DTO
|
||||
return workspaceMembers as WorkspaceMember[];
|
||||
}
|
||||
|
||||
@ResolveField(() => String, {
|
||||
|
||||
@ -8,5 +8,5 @@ export interface ExceptionHandlerOptions {
|
||||
name: string;
|
||||
};
|
||||
document?: string;
|
||||
user?: ExceptionHandlerUser;
|
||||
user?: ExceptionHandlerUser | null;
|
||||
}
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import {
|
||||
CompositeProperty,
|
||||
CompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export enum FieldActorSource {
|
||||
EMAIL = 'EMAIL',
|
||||
CALENDAR = 'CALENDAR',
|
||||
API = 'API',
|
||||
IMPORT = 'IMPORT',
|
||||
MANUAL = 'MANUAL',
|
||||
}
|
||||
|
||||
export const actorCompositeType: CompositeType = {
|
||||
type: FieldMetadataType.ACTOR,
|
||||
properties: [
|
||||
{
|
||||
name: 'source',
|
||||
type: FieldMetadataType.SELECT,
|
||||
hidden: false,
|
||||
isRequired: true,
|
||||
options: Object.keys(FieldActorSource).map((key, index) => ({
|
||||
label: `${FieldActorSource[key].toLowerCase()}`,
|
||||
value: key,
|
||||
position: index,
|
||||
})),
|
||||
} as CompositeProperty<FieldMetadataType.SELECT>,
|
||||
{
|
||||
name: 'workspaceMemberId',
|
||||
type: FieldMetadataType.UUID,
|
||||
hidden: 'input',
|
||||
isRequired: false,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: FieldMetadataType.TEXT,
|
||||
hidden: 'input',
|
||||
isRequired: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type ActorMetadata = {
|
||||
source: FieldActorSource;
|
||||
workspaceMemberId?: string;
|
||||
name: string;
|
||||
};
|
||||
@ -1,24 +1,13 @@
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
CurrencyMetadata,
|
||||
currencyCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import {
|
||||
FullNameMetadata,
|
||||
fullNameCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import {
|
||||
LinkMetadata,
|
||||
linkCompositeType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
addressCompositeType,
|
||||
AddressMetadata,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
import { actorCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/address.composite-type';
|
||||
import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
|
||||
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
|
||||
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export type CompositeFieldsDefinitionFunction = (
|
||||
fieldMetadata?: FieldMetadataInterface,
|
||||
@ -33,10 +22,5 @@ export const compositeTypeDefintions = new Map<
|
||||
[FieldMetadataType.CURRENCY, currencyCompositeType],
|
||||
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
|
||||
[FieldMetadataType.ADDRESS, addressCompositeType],
|
||||
[FieldMetadataType.ACTOR, actorCompositeType],
|
||||
]);
|
||||
|
||||
export type CompositeMetadataTypes =
|
||||
| AddressMetadata
|
||||
| CurrencyMetadata
|
||||
| FullNameMetadata
|
||||
| LinkMetadata;
|
||||
|
||||
@ -6,7 +6,9 @@ import {
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
Matches,
|
||||
ValidateIf,
|
||||
} from 'class-validator';
|
||||
@ -158,3 +160,18 @@ export class FieldMetadataDefaultValueLinks {
|
||||
@IsObject()
|
||||
secondaryLinks: object | null;
|
||||
}
|
||||
|
||||
export class FieldMetadataDefaultActor {
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
source: string;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
workspaceMemberId?: string | null;
|
||||
|
||||
@ValidateIf((_object, value) => value !== null)
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ export enum FieldMetadataType {
|
||||
ADDRESS = 'ADDRESS',
|
||||
RAW_JSON = 'RAW_JSON',
|
||||
RICH_TEXT = 'RICH_TEXT',
|
||||
ACTOR = 'ACTOR',
|
||||
}
|
||||
|
||||
@Entity('fieldMetadata')
|
||||
|
||||
@ -21,6 +21,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceStatusModule } from 'src/engine/workspace-manager/workspace-status/workspace-manager.module';
|
||||
import { CreatedByPreQueryHook } from 'src/engine/metadata-modules/field-metadata/query-hooks/created-by.pre-query-hook';
|
||||
|
||||
import { FieldMetadataEntity } from './field-metadata.entity';
|
||||
import { FieldMetadataService } from './field-metadata.service';
|
||||
@ -42,7 +43,11 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
||||
DataSourceModule,
|
||||
TypeORMModule,
|
||||
],
|
||||
services: [IsFieldMetadataDefaultValue, FieldMetadataService],
|
||||
services: [
|
||||
IsFieldMetadataDefaultValue,
|
||||
FieldMetadataService,
|
||||
CreatedByPreQueryHook,
|
||||
],
|
||||
resolvers: [
|
||||
{
|
||||
EntityClass: FieldMetadataEntity,
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export interface CompositeProperty {
|
||||
export interface CompositeProperty<
|
||||
Type extends FieldMetadataType = FieldMetadataType,
|
||||
> {
|
||||
name: string;
|
||||
description?: string;
|
||||
type: FieldMetadataType;
|
||||
type: Type;
|
||||
hidden: 'input' | 'output' | true | false;
|
||||
isRequired: boolean;
|
||||
isArray?: boolean;
|
||||
options?: FieldMetadataOptions<Type>;
|
||||
}
|
||||
|
||||
export interface CompositeType {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
FieldMetadataDefaultActor,
|
||||
FieldMetadataDefaultValueAddress,
|
||||
FieldMetadataDefaultValueBoolean,
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
@ -46,6 +47,7 @@ type FieldMetadataDefaultValueMapping = {
|
||||
[FieldMetadataType.MULTI_SELECT]: FieldMetadataDefaultValueString;
|
||||
[FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson;
|
||||
[FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText;
|
||||
[FieldMetadataType.ACTOR]: FieldMetadataDefaultActor;
|
||||
};
|
||||
|
||||
export type FieldMetadataClassValidation =
|
||||
|
||||
@ -0,0 +1,103 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
type CustomWorkspaceItem = Omit<
|
||||
CustomWorkspaceEntity,
|
||||
'createdAt' | 'updatedAt'
|
||||
> & {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
@WorkspaceQueryHook(`*.createMany`)
|
||||
export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
objectName: string,
|
||||
payload: CreateManyResolverArgs<CustomWorkspaceItem>,
|
||||
): Promise<CreateManyResolverArgs<CustomWorkspaceItem>> {
|
||||
let createdBy: ActorMetadata | null = null;
|
||||
|
||||
// TODO: Once all objects have it, we can remove this check
|
||||
const createdByFieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||
where: {
|
||||
object: {
|
||||
nameSingular: objectName,
|
||||
},
|
||||
name: 'createdBy',
|
||||
workspaceId: authContext.workspace.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!createdByFieldMetadata) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
// If user is logged in, we use the workspace member
|
||||
if (authContext.user) {
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
authContext.workspace.id,
|
||||
'workspaceMember',
|
||||
);
|
||||
|
||||
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||
where: {
|
||||
userId: authContext.user?.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!workspaceMember) {
|
||||
throw new Error(
|
||||
`Workspace member can't be found for user ${authContext.user.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
createdBy = {
|
||||
source: FieldActorSource.MANUAL,
|
||||
workspaceMemberId: workspaceMember.id,
|
||||
name: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (authContext.apiKey) {
|
||||
createdBy = {
|
||||
source: FieldActorSource.API,
|
||||
name: authContext.apiKey.name,
|
||||
};
|
||||
}
|
||||
|
||||
for (const datum of payload.data) {
|
||||
// Front-end can fill the source field
|
||||
if (createdBy && (!datum.createdBy || !datum.createdBy.name)) {
|
||||
datum.createdBy = {
|
||||
...createdBy,
|
||||
source: datum.createdBy?.source ?? createdBy.source,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,14 @@ export const isCompositeFieldMetadataType = (
|
||||
| FieldMetadataType.CURRENCY
|
||||
| FieldMetadataType.FULL_NAME
|
||||
| FieldMetadataType.ADDRESS
|
||||
| FieldMetadataType.LINKS => {
|
||||
| FieldMetadataType.LINKS
|
||||
| FieldMetadataType.ACTOR => {
|
||||
return [
|
||||
FieldMetadataType.LINK,
|
||||
FieldMetadataType.CURRENCY,
|
||||
FieldMetadataType.FULL_NAME,
|
||||
FieldMetadataType.ADDRESS,
|
||||
FieldMetadataType.LINKS,
|
||||
FieldMetadataType.ACTOR,
|
||||
].includes(type);
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||
|
||||
import {
|
||||
FieldMetadataDefaultActor,
|
||||
FieldMetadataDefaultValueAddress,
|
||||
FieldMetadataDefaultValueBoolean,
|
||||
FieldMetadataDefaultValueCurrency,
|
||||
@ -51,6 +52,7 @@ export const defaultValueValidatorsMap = {
|
||||
[FieldMetadataType.RICH_TEXT]: [FieldMetadataDefaultValueString],
|
||||
[FieldMetadataType.RAW_JSON]: [FieldMetadataDefaultValueRawJson],
|
||||
[FieldMetadataType.LINKS]: [FieldMetadataDefaultValueLinks],
|
||||
[FieldMetadataType.ACTOR]: [FieldMetadataDefaultActor],
|
||||
};
|
||||
|
||||
type ValidationResult = {
|
||||
|
||||
@ -47,17 +47,29 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
const columnActions: WorkspaceMigrationColumnCreate[] = [];
|
||||
|
||||
for (const property of compositeType.properties) {
|
||||
if (property.type === FieldMetadataType.RELATION) {
|
||||
throw new WorkspaceMigrationException(
|
||||
`Relation type not supported for composite columns`,
|
||||
WorkspaceMigrationExceptionCode.INVALID_COMPOSITE_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const columnName = computeCompositeColumnName(fieldMetadata, property);
|
||||
const defaultValue = fieldMetadata.defaultValue?.[property.name];
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
const enumOptions = property.options
|
||||
? [...property.options.map((option) => option.value)]
|
||||
: undefined;
|
||||
|
||||
columnActions.push({
|
||||
action: WorkspaceMigrationColumnActionType.CREATE,
|
||||
columnName,
|
||||
columnType: fieldMetadataTypeToColumnType(property.type),
|
||||
enum: enumOptions,
|
||||
isNullable: fieldMetadata.isNullable || !property.isRequired,
|
||||
defaultValue: serializedDefaultValue,
|
||||
isArray: property.isArray,
|
||||
isArray:
|
||||
property.type === FieldMetadataType.MULTI_SELECT || property.isArray,
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,6 +115,16 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
alteredProperty.type === FieldMetadataType.RELATION ||
|
||||
currentProperty.type === FieldMetadataType.RELATION
|
||||
) {
|
||||
throw new WorkspaceMigrationException(
|
||||
`Relation type not supported for composite columns`,
|
||||
WorkspaceMigrationExceptionCode.INVALID_COMPOSITE_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
const currentColumnName = computeCompositeColumnName(
|
||||
currentFieldMetadata,
|
||||
currentProperty,
|
||||
@ -114,26 +136,53 @@ export class CompositeColumnActionFactory extends ColumnActionAbstractFactory<Co
|
||||
const defaultValue =
|
||||
alteredFieldMetadata.defaultValue?.[alteredProperty.name];
|
||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||
const enumOptions = alteredProperty.options
|
||||
? [
|
||||
...alteredProperty.options.map((option) => {
|
||||
const currentOption = currentProperty.options?.find(
|
||||
(currentOption) => currentOption.id === option.id,
|
||||
);
|
||||
|
||||
// The id is the same, but the value is different, so we need to alter the enum
|
||||
if (currentOption && currentOption.value !== option.value) {
|
||||
return {
|
||||
from: currentOption.value,
|
||||
to: option.value,
|
||||
};
|
||||
}
|
||||
|
||||
return option.value;
|
||||
}),
|
||||
]
|
||||
: undefined;
|
||||
|
||||
columnActions.push({
|
||||
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||
currentColumnDefinition: {
|
||||
columnName: currentColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(currentProperty.type),
|
||||
enum: currentProperty.options
|
||||
? [...currentProperty.options.map((option) => option.value)]
|
||||
: undefined,
|
||||
isNullable:
|
||||
currentFieldMetadata.isNullable || !currentProperty.isRequired,
|
||||
defaultValue: serializeDefaultValue(
|
||||
currentFieldMetadata.defaultValue?.[currentProperty.name],
|
||||
),
|
||||
isArray: currentProperty.isArray,
|
||||
isArray:
|
||||
currentProperty.type === FieldMetadataType.MULTI_SELECT ||
|
||||
currentProperty.isArray,
|
||||
},
|
||||
alteredColumnDefinition: {
|
||||
columnName: alteredColumnName,
|
||||
columnType: fieldMetadataTypeToColumnType(alteredProperty.type),
|
||||
enum: enumOptions,
|
||||
isNullable:
|
||||
alteredFieldMetadata.isNullable || !alteredProperty.isRequired,
|
||||
defaultValue: serializedDefaultValue,
|
||||
isArray: alteredProperty.isArray,
|
||||
isArray:
|
||||
alteredProperty.type === FieldMetadataType.MULTI_SELECT ||
|
||||
alteredProperty.isArray,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -11,4 +11,5 @@ export enum WorkspaceMigrationExceptionCode {
|
||||
NO_FACTORY_FOUND = 'NO_FACTORY_FOUND',
|
||||
INVALID_ACTION = 'INVALID_ACTION',
|
||||
INVALID_FIELD_METADATA = 'INVALID_FIELD_METADATA',
|
||||
INVALID_COMPOSITE_TYPE = 'INVALID_COMPOSITE_TYPE',
|
||||
}
|
||||
|
||||
@ -96,6 +96,7 @@ export class WorkspaceMigrationFactory {
|
||||
{ factory: this.compositeColumnActionFactory },
|
||||
],
|
||||
[FieldMetadataType.LINKS, { factory: this.compositeColumnActionFactory }],
|
||||
[FieldMetadataType.ACTOR, { factory: this.compositeColumnActionFactory }],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ExceptionHandlerService } from 'src/engine/integrations/exception-handler/exception-handler.service';
|
||||
import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service';
|
||||
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
|
||||
@ -45,7 +45,7 @@ export class GraphQLHydrateRequestFromTokenMiddleware
|
||||
return next();
|
||||
}
|
||||
|
||||
let data: JwtData;
|
||||
let data: AuthContext;
|
||||
|
||||
try {
|
||||
data = await this.tokenService.validateToken(req);
|
||||
@ -54,6 +54,7 @@ export class GraphQLHydrateRequestFromTokenMiddleware
|
||||
);
|
||||
|
||||
req.user = data.user;
|
||||
req.apiKey = data.apiKey;
|
||||
req.workspace = data.workspace;
|
||||
req.workspaceId = data.workspace.id;
|
||||
req.cacheVersion = cacheVersion;
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import {
|
||||
ActorMetadata,
|
||||
FieldActorSource,
|
||||
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import {
|
||||
RelationMetadataType,
|
||||
@ -40,6 +44,19 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceIsSystem()
|
||||
position: number | null;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
|
||||
type: FieldMetadataType.ACTOR,
|
||||
label: 'Created by',
|
||||
icon: 'IconCreativeCommonsSa',
|
||||
description: 'The creator of the record',
|
||||
defaultValue: {
|
||||
source: `'${FieldActorSource.MANUAL}'`,
|
||||
name: "''",
|
||||
},
|
||||
})
|
||||
createdBy: ActorMetadata;
|
||||
|
||||
@WorkspaceRelation({
|
||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.activityTargets,
|
||||
label: 'Activities',
|
||||
|
||||
@ -125,7 +125,11 @@ export class EntitySchemaColumnFactory {
|
||||
};
|
||||
|
||||
if (isEnumFieldMetadataType(compositeProperty.type)) {
|
||||
throw new Error('Enum composite properties are not yet supported');
|
||||
const values = compositeProperty.options?.map((option) => option.value);
|
||||
|
||||
if (values && values.length > 0) {
|
||||
entitySchemaColumnMap[columnName].enum = values;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class TwentyORMGlobalManager {
|
||||
@ -11,10 +12,30 @@ export class TwentyORMGlobalManager {
|
||||
private readonly workspaceDataSourceFactory: WorkspaceDatasourceFactory,
|
||||
) {}
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntity: Type<T>,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
objectMetadataName: string,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepositoryForWorkspace<T extends ObjectLiteral>(
|
||||
workspaceId: string,
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
let objectMetadataName: string;
|
||||
|
||||
if (typeof workspaceEntityOrobjectMetadataName === 'string') {
|
||||
objectMetadataName = workspaceEntityOrobjectMetadataName;
|
||||
} else {
|
||||
objectMetadataName = convertClassNameToObjectMetadataName(
|
||||
workspaceEntityOrobjectMetadataName.name,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceDataSource = await this.workspaceDataSourceFactory.create(
|
||||
workspaceId,
|
||||
null,
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Type } from '@nestjs/common';
|
||||
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkspaceDatasourceFactory } from 'src/engine/twenty-orm/factories/workspace-datasource.factory';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util';
|
||||
|
||||
@Injectable()
|
||||
export class TwentyORMManager {
|
||||
@ -13,12 +14,30 @@ export class TwentyORMManager {
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
) {}
|
||||
|
||||
async getRepository<T extends ObjectLiteral>(
|
||||
workspaceEntity: Type<T>,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepository<T extends ObjectLiteral>(
|
||||
objectMetadataName: string,
|
||||
): Promise<WorkspaceRepository<T>>;
|
||||
|
||||
async getRepository<T extends ObjectLiteral>(
|
||||
workspaceEntityOrobjectMetadataName: Type<T> | string,
|
||||
): Promise<WorkspaceRepository<T>> {
|
||||
const { workspaceId, cacheVersion } =
|
||||
this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
let objectMetadataName: string;
|
||||
|
||||
if (typeof workspaceEntityOrobjectMetadataName === 'string') {
|
||||
objectMetadataName = workspaceEntityOrobjectMetadataName;
|
||||
} else {
|
||||
objectMetadataName = convertClassNameToObjectMetadataName(
|
||||
workspaceEntityOrobjectMetadataName.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('Workspace not found');
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,9 @@ export const companyPrefillDemoData = async (
|
||||
'employees',
|
||||
'linkedinLinkPrimaryLinkUrl',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName'
|
||||
])
|
||||
.orIgnore()
|
||||
.values(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,9 @@ export const personPrefillDemoData = async (
|
||||
avatarUrl: person.avatarUrl,
|
||||
position: index,
|
||||
companyId: companies[Math.floor(index / 2)].id,
|
||||
createdBySource: person.createdBySource,
|
||||
createdByWorkspaceMemberId: person.createdByWorkspaceMemberId,
|
||||
createdByName: person.createdByName
|
||||
}));
|
||||
|
||||
await entityManager
|
||||
@ -36,6 +39,9 @@ export const personPrefillDemoData = async (
|
||||
'avatarUrl',
|
||||
'position',
|
||||
'companyId',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(people)
|
||||
|
||||
@ -18,6 +18,9 @@ export const companyPrefillData = async (
|
||||
'addressAddressCountry',
|
||||
'employees',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
@ -32,6 +35,9 @@ export const companyPrefillData = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 5000,
|
||||
position: 1,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
name: 'Qonto',
|
||||
@ -44,6 +50,9 @@ export const companyPrefillData = async (
|
||||
addressAddressCountry: 'France',
|
||||
employees: 800,
|
||||
position: 2,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
name: 'Stripe',
|
||||
@ -56,6 +65,9 @@ export const companyPrefillData = async (
|
||||
addressAddressCountry: 'Ireland',
|
||||
employees: 8000,
|
||||
position: 3,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
name: 'Figma',
|
||||
@ -68,6 +80,9 @@ export const companyPrefillData = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 800,
|
||||
position: 4,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
name: 'Notion',
|
||||
@ -80,6 +95,9 @@ export const companyPrefillData = async (
|
||||
addressAddressCountry: 'United States',
|
||||
employees: 400,
|
||||
position: 5,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
// FixMe: Is this file a duplicate of src/database/typeorm-seeds/workspace/people.ts
|
||||
export const personPrefillData = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
@ -14,6 +15,9 @@ export const personPrefillData = async (
|
||||
'email',
|
||||
'avatarUrl',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
@ -25,6 +29,9 @@ export const personPrefillData = async (
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-3.png',
|
||||
position: 1,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
nameFirstName: 'Alexandre',
|
||||
@ -34,6 +41,9 @@ export const personPrefillData = async (
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-89.png',
|
||||
position: 2,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
nameFirstName: 'Patrick',
|
||||
@ -43,6 +53,9 @@ export const personPrefillData = async (
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-47.png',
|
||||
position: 3,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
nameFirstName: 'Dylan',
|
||||
@ -52,6 +65,9 @@ export const personPrefillData = async (
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-40.png',
|
||||
position: 4,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
{
|
||||
nameFirstName: 'Ivan',
|
||||
@ -61,6 +77,9 @@ export const personPrefillData = async (
|
||||
avatarUrl:
|
||||
'https://twentyhq.github.io/placeholder-images/people/image-68.png',
|
||||
position: 5,
|
||||
createdBySource: 'MANUAL',
|
||||
createdByWorkspaceMemberId: null,
|
||||
createdByName: 'System',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
|
||||
@ -39,7 +39,7 @@ export const companiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
COMPANY_STANDARD_FIELD_IDS.accountOwner
|
||||
COMPANY_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
@ -48,7 +48,7 @@ export const companiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
COMPANY_STANDARD_FIELD_IDS.accountOwner
|
||||
],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
@ -57,7 +57,7 @@ export const companiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
COMPANY_STANDARD_FIELD_IDS.employees
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
@ -66,10 +66,19 @@ export const companiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
COMPANY_STANDARD_FIELD_IDS.linkedinLink
|
||||
COMPANY_STANDARD_FIELD_IDS.employees
|
||||
],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
COMPANY_STANDARD_FIELD_IDS.linkedinLink
|
||||
],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
{
|
||||
@ -77,7 +86,7 @@ export const companiesAllView = async (
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.company].fields[
|
||||
COMPANY_STANDARD_FIELD_IDS.address
|
||||
],
|
||||
position: 6,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
|
||||
@ -32,7 +32,16 @@ export const notesAllView = async (
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
|
||||
NOTE_STANDARD_FIELD_IDS.body
|
||||
],
|
||||
position: 0,
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
|
||||
NOTE_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
@ -41,7 +50,7 @@ export const notesAllView = async (
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.note].fields[
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
],
|
||||
position: 0,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
|
||||
@ -36,7 +36,7 @@ export const opportunitiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.closeDate
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
@ -45,12 +45,21 @@ export const opportunitiesAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.company
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.closeDate
|
||||
],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.company
|
||||
],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
|
||||
@ -39,7 +39,7 @@ export const opportunitiesByStageView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.closeDate
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
@ -48,12 +48,21 @@ export const opportunitiesByStageView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.company
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.closeDate
|
||||
],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
OPPORTUNITY_STANDARD_FIELD_IDS.company
|
||||
],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.opportunity].fields[
|
||||
|
||||
@ -39,7 +39,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.company
|
||||
PERSON_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
@ -48,7 +48,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.phone
|
||||
PERSON_STANDARD_FIELD_IDS.company
|
||||
],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
@ -57,7 +57,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
PERSON_STANDARD_FIELD_IDS.phone
|
||||
],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
@ -66,7 +66,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.city
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
@ -75,7 +75,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.jobTitle
|
||||
PERSON_STANDARD_FIELD_IDS.city
|
||||
],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
@ -84,7 +84,7 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.linkedinLink
|
||||
PERSON_STANDARD_FIELD_IDS.jobTitle
|
||||
],
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
@ -93,12 +93,21 @@ export const peopleAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.xLink
|
||||
PERSON_STANDARD_FIELD_IDS.linkedinLink
|
||||
],
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.person].fields[
|
||||
PERSON_STANDARD_FIELD_IDS.xLink
|
||||
],
|
||||
position: 9,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@ -49,7 +49,7 @@ export const tasksAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||
TASK_STANDARD_FIELD_IDS.dueAt
|
||||
TASK_STANDARD_FIELD_IDS.createdBy
|
||||
],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
@ -58,7 +58,7 @@ export const tasksAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||
TASK_STANDARD_FIELD_IDS.assignee
|
||||
TASK_STANDARD_FIELD_IDS.dueAt
|
||||
],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
@ -67,7 +67,7 @@ export const tasksAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||
TASK_STANDARD_FIELD_IDS.body
|
||||
TASK_STANDARD_FIELD_IDS.assignee
|
||||
],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
@ -76,12 +76,21 @@ export const tasksAllView = async (
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
TASK_STANDARD_FIELD_IDS.body
|
||||
],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId:
|
||||
objectMetadataMap[STANDARD_OBJECT_IDS.task].fields[
|
||||
BASE_OBJECT_STANDARD_FIELD_IDS.createdAt
|
||||
],
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
/*
|
||||
TODO: Add later, since we don't have real-time it probably doesn't work well?
|
||||
{
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { render } from '@react-email/render';
|
||||
import { In } from 'typeorm';
|
||||
import {
|
||||
CleanInactiveWorkspaceEmail,
|
||||
DeleteInactiveWorkspaceEmail,
|
||||
} from 'twenty-emails';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { CleanInactiveWorkspacesCommandOptions } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command';
|
||||
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
|
||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||
|
||||
const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24;
|
||||
|
||||
@ -36,6 +39,8 @@ export class CleanInactiveWorkspaceJob {
|
||||
private readonly inactiveDaysBeforeEmail;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly typeORMService: TypeORMService,
|
||||
@ -94,8 +99,20 @@ export class CleanInactiveWorkspaceJob {
|
||||
daysSinceInactive: number,
|
||||
isDryRun: boolean,
|
||||
) {
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: dataSource.workspaceId },
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
this.logger.error(
|
||||
`Workspace with id ${dataSource.workspaceId} not found in database`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceMembers =
|
||||
await this.userService.loadWorkspaceMembers(dataSource);
|
||||
await this.userService.loadWorkspaceMembers(workspace);
|
||||
|
||||
const workspaceDataSource =
|
||||
await this.typeORMService.connectToDataSource(dataSource);
|
||||
@ -110,7 +127,7 @@ export class CleanInactiveWorkspaceJob {
|
||||
`${getDryRunLogHeader(isDryRun)}Sending workspace ${
|
||||
dataSource.workspaceId
|
||||
} inactive since ${daysSinceInactive} days emails to users ['${workspaceMembers
|
||||
.map((workspaceUser) => workspaceUser.email)
|
||||
.map((workspaceUser) => workspaceUser.userEmail)
|
||||
.join(', ')}']`,
|
||||
);
|
||||
|
||||
@ -121,7 +138,7 @@ export class CleanInactiveWorkspaceJob {
|
||||
workspaceMembers.forEach((workspaceMember) => {
|
||||
const emailData = {
|
||||
daysLeft: this.inactiveDaysBeforeDelete - daysSinceInactive,
|
||||
userName: `${workspaceMember.nameFirstName} ${workspaceMember.nameLastName}`,
|
||||
userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`,
|
||||
workspaceDisplayName: `${displayName}`,
|
||||
};
|
||||
const emailTemplate = CleanInactiveWorkspaceEmail(emailData);
|
||||
@ -133,7 +150,7 @@ export class CleanInactiveWorkspaceJob {
|
||||
});
|
||||
|
||||
this.emailService.send({
|
||||
to: workspaceMember.email,
|
||||
to: workspaceMember.userEmail,
|
||||
bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'),
|
||||
from: `${this.environmentService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
|
||||
@ -126,6 +126,7 @@ export const COMPANY_STANDARD_FIELD_IDS = {
|
||||
annualRecurringRevenue: '20202020-602a-495c-9776-f5d5b11d227b',
|
||||
idealCustomerProfile: '20202020-ba6b-438a-8213-2c5ba28d76a2',
|
||||
position: '20202020-9b4e-462b-991d-a0ee33326454',
|
||||
createdBy: '20202020-fabc-451d-ab7d-412170916baa',
|
||||
people: '20202020-3213-4ddf-9494-6422bcff8d7c',
|
||||
accountOwner: '20202020-95b8-4e10-9881-edb5d4765f9d',
|
||||
activityTargets: '20202020-c2a5-4c9b-9d9a-582bcd57fbc8',
|
||||
@ -267,6 +268,7 @@ export const NOTE_STANDARD_FIELD_IDS = {
|
||||
position: '20202020-368d-4dc2-943f-ed8a49c7fdfb',
|
||||
title: '20202020-faeb-4c76-8ba6-ccbb0b4a965f',
|
||||
body: '20202020-e63d-4e70-95be-a78cd9abe7ef',
|
||||
createdBy: '20202020-0d79-4e21-ab77-5a394eff97be',
|
||||
noteTargets: '20202020-1f25-43fe-8b00-af212fdde823',
|
||||
attachments: '20202020-4986-4c92-bf19-39934b149b16',
|
||||
timelineActivities: '20202020-7030-42f8-929c-1a57b25d6bce',
|
||||
@ -287,6 +289,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = {
|
||||
probabilityDeprecated: '20202020-69d4-45f3-9703-690b09fafcf0',
|
||||
stage: '20202020-6f76-477d-8551-28cd65b2b4b9',
|
||||
position: '20202020-806d-493a-bbc6-6313e62958e2',
|
||||
createdBy: '20202020-a63e-4a62-8e63-42a51828f831',
|
||||
pointOfContact: '20202020-8dfb-42fc-92b6-01afb759ed16',
|
||||
company: '20202020-cbac-457e-b565-adece5fc815f',
|
||||
favorites: '20202020-a1c2-4500-aaae-83ba8a0e827a',
|
||||
@ -307,6 +310,7 @@ export const PERSON_STANDARD_FIELD_IDS = {
|
||||
city: '20202020-5243-4ffb-afc5-2c675da41346',
|
||||
avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21',
|
||||
position: '20202020-fcd5-4231-aff5-fff583eaa0b1',
|
||||
createdBy: '20202020-f6ab-4d98-af24-a3d5b664148a',
|
||||
company: '20202020-e2f3-448e-b34c-2d625f0025fd',
|
||||
pointOfContactForOpportunities: '20202020-911b-4a7d-b67b-918aa9a5b33a',
|
||||
activityTargets: '20202020-dee7-4b7f-b50a-1f50bd3be452',
|
||||
@ -325,6 +329,7 @@ export const TASK_STANDARD_FIELD_IDS = {
|
||||
body: '20202020-ce13-43f4-8821-69388fe1fd26',
|
||||
dueAt: '20202020-fd99-40da-951b-4cb9a352fce3',
|
||||
status: '20202020-70bc-48f9-89c5-6aa730b151e0',
|
||||
createdBy: '20202020-1a04-48ab-a567-576965ae5387',
|
||||
taskTargets: '20202020-de9c-4d0e-a452-713d4a3e5fc7',
|
||||
attachments: '20202020-794d-4783-a8ff-cecdb15be139',
|
||||
assignee: '20202020-065a-4f42-a906-e20422c1753f',
|
||||
@ -433,6 +438,7 @@ export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = {
|
||||
export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = {
|
||||
name: '20202020-ba07-4ffd-ba63-009491f5749c',
|
||||
position: '20202020-c2bd-4e16-bb9a-c8b0411bf49d',
|
||||
createdBy: '20202020-be0e-4971-865b-32ca87cbb315',
|
||||
activityTargets: '20202020-7f42-40ae-b96c-c8a61acc83bf',
|
||||
noteTargets: '20202020-01fd-4f37-99dc-9427a444018a',
|
||||
taskTargets: '20202020-0860-4566-b865-bff3c626c303',
|
||||
|
||||
Reference in New Issue
Block a user