feat: refactor folder structure (#4498)
* feat: wip refactor folder structure * Fix * fix position --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PersonModule } from 'src/engine-workspace/repositories/person/person.module';
|
||||
import { WorkspaceMemberModule } from 'src/engine-workspace/repositories/workspace-member/workspace-member.module';
|
||||
import { CreateCompanyAndContactService } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-company-and-contact/create-company-and-contact.service';
|
||||
import { CreateCompanyModule } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-company/create-company.module';
|
||||
import { CreateContactModule } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-contact/create-contact.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
CreateContactModule,
|
||||
CreateCompanyModule,
|
||||
WorkspaceMemberModule,
|
||||
PersonModule,
|
||||
],
|
||||
providers: [CreateCompanyAndContactService],
|
||||
exports: [CreateCompanyAndContactService],
|
||||
})
|
||||
export class CreateCompaniesAndContactsModule {}
|
||||
@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import compact from 'lodash/compact';
|
||||
|
||||
import { Participant } from 'src/business/modules/message/types/gmail-message';
|
||||
import { getDomainNameFromHandle } from 'src/business/modules/message/utils/get-domain-name-from-handle.util';
|
||||
import { CreateCompanyService } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
import { CreateContactService } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { PersonService } from 'src/engine-workspace/repositories/person/person.service';
|
||||
import { WorkspaceMemberService } from 'src/engine-workspace/repositories/workspace-member/workspace-member.service';
|
||||
import { getUniqueParticipantsAndHandles } from 'src/business/modules/message/utils/get-unique-participants-and-handles.util';
|
||||
import { filterOutParticipantsFromCompanyOrWorkspace } from 'src/business/modules/message/utils/filter-out-participants-from-company-or-workspace.util';
|
||||
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||
|
||||
@Injectable()
|
||||
export class CreateCompanyAndContactService {
|
||||
constructor(
|
||||
private readonly personService: PersonService,
|
||||
private readonly createContactService: CreateContactService,
|
||||
private readonly createCompaniesService: CreateCompanyService,
|
||||
private readonly workspaceMemberService: WorkspaceMemberService,
|
||||
) {}
|
||||
|
||||
async createCompaniesAndContacts(
|
||||
selfHandle: string,
|
||||
participants: Participant[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
) {
|
||||
if (!participants || participants.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: This is a feature that may be implemented in the future
|
||||
const isContactAutoCreationForNonWorkEmailsEnabled = false;
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceMemberService.getAllByWorkspaceId(
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const participantsFromOtherCompanies =
|
||||
filterOutParticipantsFromCompanyOrWorkspace(
|
||||
participants,
|
||||
selfHandle,
|
||||
workspaceMembers,
|
||||
);
|
||||
|
||||
const { uniqueParticipants, uniqueHandles } =
|
||||
getUniqueParticipantsAndHandles(participantsFromOtherCompanies);
|
||||
|
||||
if (uniqueHandles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alreadyCreatedContacts = await this.personService.getByEmails(
|
||||
uniqueHandles,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
|
||||
({ email }) => email,
|
||||
);
|
||||
|
||||
const filteredParticipants = uniqueParticipants.filter(
|
||||
(participant) =>
|
||||
!alreadyCreatedContactEmails.includes(participant.handle) &&
|
||||
participant.handle.includes('@') &&
|
||||
(isContactAutoCreationForNonWorkEmailsEnabled ||
|
||||
isWorkEmail(participant.handle)),
|
||||
);
|
||||
|
||||
const filteredParticipantsWithCompanyDomainNames =
|
||||
filteredParticipants?.map((participant) => ({
|
||||
handle: participant.handle,
|
||||
displayName: participant.displayName,
|
||||
companyDomainName: isWorkEmail(participant.handle)
|
||||
? getDomainNameFromHandle(participant.handle)
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
const domainNamesToCreate = compact(
|
||||
filteredParticipantsWithCompanyDomainNames.map(
|
||||
(participant) => participant.companyDomainName,
|
||||
),
|
||||
);
|
||||
|
||||
const companiesObject = await this.createCompaniesService.createCompanies(
|
||||
domainNamesToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const contactsToCreate = filteredParticipantsWithCompanyDomainNames.map(
|
||||
(participant) => ({
|
||||
handle: participant.handle,
|
||||
displayName: participant.displayName,
|
||||
companyId:
|
||||
participant.companyDomainName &&
|
||||
companiesObject[participant.companyDomainName],
|
||||
}),
|
||||
);
|
||||
|
||||
await this.createContactService.createContacts(
|
||||
contactsToCreate,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CreateCompanyService } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-company/create-company.service';
|
||||
import { CompanyModule } from 'src/business/modules/message/repositories/company/company.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, CompanyModule],
|
||||
providers: [CreateCompanyService],
|
||||
exports: [CreateCompanyService],
|
||||
})
|
||||
export class CreateCompanyModule {}
|
||||
@ -0,0 +1,117 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { CompanyService } from 'src/business/modules/message/repositories/company/company.service';
|
||||
import { getCompanyNameFromDomainName } from 'src/business/modules/message/utils/get-company-name-from-domain-name.util';
|
||||
@Injectable()
|
||||
export class CreateCompanyService {
|
||||
private readonly httpService: AxiosInstance;
|
||||
|
||||
constructor(private readonly companyService: CompanyService) {
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://companies.twenty.com',
|
||||
});
|
||||
}
|
||||
|
||||
async createCompanies(
|
||||
domainNames: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<{
|
||||
[domainName: string]: string;
|
||||
}> {
|
||||
if (domainNames.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const uniqueDomainNames = [...new Set(domainNames)];
|
||||
|
||||
const existingCompanies =
|
||||
await this.companyService.getExistingCompaniesByDomainNames(
|
||||
uniqueDomainNames,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const companiesObject = existingCompanies.reduce(
|
||||
(
|
||||
acc: {
|
||||
[domainName: string]: string;
|
||||
},
|
||||
company: {
|
||||
domainName: string;
|
||||
id: string;
|
||||
},
|
||||
) => ({
|
||||
...acc,
|
||||
[company.domainName]: company.id,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const filteredDomainNames = uniqueDomainNames.filter(
|
||||
(domainName) =>
|
||||
!existingCompanies.some(
|
||||
(company: { domainName: string }) =>
|
||||
company.domainName === domainName,
|
||||
),
|
||||
);
|
||||
|
||||
for (const domainName of filteredDomainNames) {
|
||||
companiesObject[domainName] = await this.createCompany(
|
||||
domainName,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
return companiesObject;
|
||||
}
|
||||
|
||||
async createCompany(
|
||||
domainName: string,
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<string> {
|
||||
const companyId = v4();
|
||||
|
||||
const { name, city } = await this.getCompanyInfoFromDomainName(domainName);
|
||||
|
||||
this.companyService.createCompany(
|
||||
workspaceId,
|
||||
{
|
||||
id: companyId,
|
||||
domainName,
|
||||
name,
|
||||
city,
|
||||
},
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return companyId;
|
||||
}
|
||||
|
||||
async getCompanyInfoFromDomainName(domainName: string): Promise<{
|
||||
name: string;
|
||||
city: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await this.httpService.get(`/${domainName}`);
|
||||
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
name: data.name ?? getCompanyNameFromDomainName(domainName),
|
||||
city: data.city,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
name: getCompanyNameFromDomainName(domainName),
|
||||
city: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { CreateContactService } from 'src/engine-workspace/auto-companies-and-contacts-creation/create-contact/create-contact.service';
|
||||
import { PersonModule } from 'src/engine-workspace/repositories/person/person.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, PersonModule],
|
||||
providers: [CreateContactService],
|
||||
exports: [CreateContactService],
|
||||
})
|
||||
export class CreateContactModule {}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { PersonService } from 'src/engine-workspace/repositories/person/person.service';
|
||||
import { getFirstNameAndLastNameFromHandleAndDisplayName } from 'src/business/modules/message/utils/get-first-name-and-last-name-from-handle-and-display-name.util';
|
||||
|
||||
type ContactToCreate = {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
type FormattedContactToCreate = {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CreateContactService {
|
||||
constructor(private readonly personService: PersonService) {}
|
||||
|
||||
public formatContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
): FormattedContactToCreate[] {
|
||||
return contactsToCreate.map((contact) => {
|
||||
const id = v4();
|
||||
|
||||
const { handle, displayName, companyId } = contact;
|
||||
|
||||
const { firstName, lastName } =
|
||||
getFirstNameAndLastNameFromHandleAndDisplayName(handle, displayName);
|
||||
|
||||
return {
|
||||
id,
|
||||
handle,
|
||||
firstName,
|
||||
lastName,
|
||||
companyId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async createContacts(
|
||||
contactsToCreate: ContactToCreate[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
if (contactsToCreate.length === 0) return;
|
||||
|
||||
const formattedContacts = this.formatContacts(contactsToCreate);
|
||||
|
||||
await this.personService.createPeople(
|
||||
formattedContacts,
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PersonService } from 'src/engine-workspace/repositories/person/person.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [PersonService],
|
||||
exports: [PersonService],
|
||||
})
|
||||
export class PersonModule {}
|
||||
@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
import { PersonObjectMetadata } from 'src/business/modules/person/person.object-metadata';
|
||||
|
||||
// TODO: Move outside of the messaging module
|
||||
@Injectable()
|
||||
export class PersonService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
async getByEmails(
|
||||
emails: string[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<PersonObjectMetadata>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}.person WHERE email = ANY($1)`,
|
||||
[emails],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
|
||||
async createPeople(
|
||||
peopleToCreate: {
|
||||
id: string;
|
||||
handle: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
companyId?: string;
|
||||
}[],
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const valuesString = peopleToCreate
|
||||
.map(
|
||||
(_, index) =>
|
||||
`($${index * 5 + 1}, $${index * 5 + 2}, $${index * 5 + 3}, $${
|
||||
index * 5 + 4
|
||||
}, $${index * 5 + 5})`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
return await this.workspaceDataSourceService.executeRawQuery(
|
||||
`INSERT INTO ${dataSourceSchema}.person (id, email, "nameFirstName", "nameLastName", "companyId") VALUES ${valuesString}`,
|
||||
peopleToCreate
|
||||
.map((contact) => [
|
||||
contact.id,
|
||||
contact.handle,
|
||||
contact.firstName,
|
||||
contact.lastName,
|
||||
contact.companyId,
|
||||
])
|
||||
.flat(),
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMemberService } from 'src/engine-workspace/repositories/workspace-member/workspace-member.service';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule],
|
||||
providers: [WorkspaceMemberService],
|
||||
exports: [WorkspaceMemberService],
|
||||
})
|
||||
export class WorkspaceMemberModule {}
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
import { WorkspaceMemberObjectMetadata } from 'src/business/modules/workspace/workspace-member.object-metadata';
|
||||
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
|
||||
|
||||
// TODO: Move outside of the messaging module
|
||||
@Injectable()
|
||||
export class WorkspaceMemberService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
) {}
|
||||
|
||||
public async getByIds(
|
||||
userIds: string[],
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord<WorkspaceMemberObjectMetadata>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const result = await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."workspaceMember" WHERE "userId" = ANY($1)`,
|
||||
[userIds],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getByIdOrFail(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectRecord<WorkspaceMemberObjectMetadata>> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."workspaceMember" WHERE "userId" = $1`,
|
||||
[userId],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!workspaceMembers || workspaceMembers.length === 0) {
|
||||
throw new NotFoundException(
|
||||
`No workspace member found for user ${userId} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return workspaceMembers[0];
|
||||
}
|
||||
|
||||
public async getAllByWorkspaceId(
|
||||
workspaceId: string,
|
||||
transactionManager?: EntityManager,
|
||||
): Promise<ObjectRecord<WorkspaceMemberObjectMetadata>[]> {
|
||||
const dataSourceSchema =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
const workspaceMembers =
|
||||
await this.workspaceDataSourceService.executeRawQuery(
|
||||
`SELECT * FROM ${dataSourceSchema}."workspaceMember"`,
|
||||
[],
|
||||
workspaceId,
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
return workspaceMembers;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLScalarType, GraphQLSchema, isScalarType } from 'graphql';
|
||||
|
||||
import { scalars } from 'src/engine/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@Injectable()
|
||||
export class ScalarsExplorerService {
|
||||
private scalarImplementations: Record<string, GraphQLScalarType>;
|
||||
|
||||
constructor() {
|
||||
this.scalarImplementations = scalars.reduce((acc, scalar) => {
|
||||
acc[scalar.name] = scalar;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
getScalarImplementation(scalarName: string): GraphQLScalarType | undefined {
|
||||
return this.scalarImplementations[scalarName];
|
||||
}
|
||||
|
||||
getUsedScalarNames(schema: GraphQLSchema): string[] {
|
||||
const typeMap = schema.getTypeMap();
|
||||
const usedScalarNames: string[] = [];
|
||||
|
||||
for (const typeName in typeMap) {
|
||||
const type = typeMap[typeName];
|
||||
|
||||
if (isScalarType(type) && !typeName.startsWith('__')) {
|
||||
usedScalarNames.push(type.name);
|
||||
}
|
||||
}
|
||||
|
||||
return usedScalarNames;
|
||||
}
|
||||
|
||||
getScalarResolvers(
|
||||
usedScalarNames: string[],
|
||||
): Record<string, GraphQLScalarType> {
|
||||
const scalarResolvers: Record<string, GraphQLScalarType> = {};
|
||||
|
||||
for (const scalarName of usedScalarNames) {
|
||||
const scalarImplementation = this.getScalarImplementation(scalarName);
|
||||
|
||||
if (scalarImplementation) {
|
||||
scalarResolvers[scalarName] = scalarImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
return scalarResolvers;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { RelationMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/relation-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { RelationMetadataType } from 'src/engine-metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/engine-workspace/utils/deduce-relation-direction.util';
|
||||
|
||||
describe('deduceRelationDirection', () => {
|
||||
it('should return FROM when the current object Metadata ID matches fromObjectMetadataId and id matches fromFieldMetadataId', () => {
|
||||
const fieldMetadata: FieldMetadataInterface = {
|
||||
id: 'field_id',
|
||||
objectMetadataId: 'from_object_id',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'field_name',
|
||||
label: 'Field Name',
|
||||
description: 'Field Description',
|
||||
targetColumnMap: {
|
||||
default: 'default_column',
|
||||
},
|
||||
};
|
||||
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: fieldMetadata.objectMetadataId,
|
||||
toObjectMetadataId: 'to_object_id',
|
||||
fromFieldMetadataId: fieldMetadata.id,
|
||||
toFieldMetadataId: 'to_field_id',
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
const result = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
);
|
||||
|
||||
expect(result).toBe(RelationDirection.FROM);
|
||||
});
|
||||
|
||||
it('should return TO when the current object Metadata ID matches toObjectMetadataId and id matches toFieldMetadataId', () => {
|
||||
// Arrange
|
||||
const fieldMetadata: FieldMetadataInterface = {
|
||||
id: 'field_id',
|
||||
objectMetadataId: 'to_object_id',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'field_name',
|
||||
label: 'Field Name',
|
||||
description: 'Field Description',
|
||||
targetColumnMap: {
|
||||
default: 'default_column',
|
||||
},
|
||||
};
|
||||
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: 'from_object_id',
|
||||
toObjectMetadataId: fieldMetadata.objectMetadataId,
|
||||
fromFieldMetadataId: 'from_field_id',
|
||||
toFieldMetadataId: fieldMetadata.id,
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
const result = deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
);
|
||||
|
||||
expect(result).toBe(RelationDirection.TO);
|
||||
});
|
||||
|
||||
it('should throw an error when the current object Metadata ID does not match any object metadata ID', () => {
|
||||
const fieldMetadata: FieldMetadataInterface = {
|
||||
id: 'field_id',
|
||||
objectMetadataId: 'unrelated_object_id',
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'field_name',
|
||||
label: 'Field Name',
|
||||
description: 'Field Description',
|
||||
targetColumnMap: {
|
||||
default: 'default_column',
|
||||
},
|
||||
};
|
||||
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: 'from_object_id',
|
||||
toObjectMetadataId: 'to_object_id',
|
||||
fromFieldMetadataId: 'from_field_id',
|
||||
toFieldMetadataId: 'to_field_id',
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
deduceRelationDirection(
|
||||
fieldMetadata,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
),
|
||||
).toThrow(
|
||||
`Relation metadata ${relationMetadata.id} is not related to object ${fieldMetadata.objectMetadataId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { getResolverName } from 'src/engine-workspace/utils/get-resolver-name.util';
|
||||
|
||||
describe('getResolverName', () => {
|
||||
const metadata = {
|
||||
nameSingular: 'entity',
|
||||
namePlural: 'entities',
|
||||
};
|
||||
|
||||
it.each([
|
||||
['findMany', 'entities'],
|
||||
['findOne', 'entity'],
|
||||
['createMany', 'createEntities'],
|
||||
['createOne', 'createEntity'],
|
||||
['updateOne', 'updateEntity'],
|
||||
['deleteOne', 'deleteEntity'],
|
||||
['executeQuickActionOnOne', 'executeQuickActionOnEntity'],
|
||||
])('should return correct name for %s resolver', (type, expectedResult) => {
|
||||
expect(
|
||||
getResolverName(metadata, type as WorkspaceResolverBuilderMethodNames),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error for an unknown resolver type', () => {
|
||||
const unknownType = 'unknownType';
|
||||
|
||||
expect(() =>
|
||||
getResolverName(
|
||||
metadata,
|
||||
unknownType as WorkspaceResolverBuilderMethodNames,
|
||||
),
|
||||
).toThrow(`Unknown resolver type: ${unknownType}`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
export const customNamePrefix = '_';
|
||||
|
||||
export const computeCustomName = (name: string, isCustom: boolean) => {
|
||||
return isCustom ? `${customNamePrefix}${name}` : name;
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataEntity } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/engine-metadata/field-metadata/utils/is-composite-field-metadata-type.util';
|
||||
import { BasicFieldMetadataType } from 'src/engine-metadata/workspace-migration/factories/basic-column-action.factory';
|
||||
|
||||
import { computeCustomName } from './compute-custom-name.util';
|
||||
|
||||
export const computeFieldTargetColumn = (
|
||||
fieldMetadata:
|
||||
| FieldMetadataEntity<BasicFieldMetadataType>
|
||||
| FieldMetadataInterface<BasicFieldMetadataType>,
|
||||
) => {
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
throw new Error(
|
||||
"Composite field metadata should not be computed here, as they're split into multiple fields.",
|
||||
);
|
||||
}
|
||||
|
||||
return computeCustomName(fieldMetadata.name, fieldMetadata.isCustom ?? false);
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { computeCustomName } from './compute-custom-name.util';
|
||||
|
||||
export const computeObjectTargetTable = (
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
) => {
|
||||
return computeCustomName(
|
||||
objectMetadata.nameSingular,
|
||||
objectMetadata.isCustom,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { FieldMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/field-metadata.interface';
|
||||
import { RelationMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/relation-metadata.interface';
|
||||
|
||||
export enum RelationDirection {
|
||||
FROM = 'from',
|
||||
TO = 'to',
|
||||
}
|
||||
|
||||
export const deduceRelationDirection = (
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
relationMetadata: RelationMetadataInterface,
|
||||
): RelationDirection => {
|
||||
if (
|
||||
relationMetadata.fromObjectMetadataId === fieldMetadata.objectMetadataId &&
|
||||
relationMetadata.fromFieldMetadataId === fieldMetadata.id
|
||||
) {
|
||||
return RelationDirection.FROM;
|
||||
}
|
||||
|
||||
if (
|
||||
relationMetadata.toObjectMetadataId === fieldMetadata.objectMetadataId &&
|
||||
relationMetadata.toFieldMetadataId === fieldMetadata.id
|
||||
) {
|
||||
return RelationDirection.TO;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Relation metadata ${relationMetadata.id} is not related to object ${fieldMetadata.objectMetadataId}`,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,35 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine-metadata/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
export const getResolverName = (
|
||||
objectMetadata: Pick<ObjectMetadataInterface, 'namePlural' | 'nameSingular'>,
|
||||
type: WorkspaceResolverBuilderMethodNames,
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'findMany':
|
||||
return `${camelCase(objectMetadata.namePlural)}`;
|
||||
case 'findOne':
|
||||
return `${camelCase(objectMetadata.nameSingular)}`;
|
||||
case 'findDuplicates':
|
||||
return `${camelCase(objectMetadata.nameSingular)}Duplicates`;
|
||||
case 'createMany':
|
||||
return `create${pascalCase(objectMetadata.namePlural)}`;
|
||||
case 'createOne':
|
||||
return `create${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'updateOne':
|
||||
return `update${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'deleteOne':
|
||||
return `delete${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'executeQuickActionOnOne':
|
||||
return `executeQuickActionOn${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'updateMany':
|
||||
return `update${pascalCase(objectMetadata.namePlural)}`;
|
||||
case 'deleteMany':
|
||||
return `delete${pascalCase(objectMetadata.namePlural)}`;
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { FieldMetadataType } from 'src/engine-metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const isRelationFieldMetadataType = (
|
||||
type: FieldMetadataType,
|
||||
): type is FieldMetadataType.RELATION => {
|
||||
return type === FieldMetadataType.RELATION;
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
interface ApolloPlaygroundOptions {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export const renderApolloPlayground = ({
|
||||
path = 'graphql',
|
||||
}: ApolloPlaygroundOptions = {}) => {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body style="margin: 0; overflow-x: hidden; overflow-y: hidden">
|
||||
<div id="sandbox" style="height:100vh; width:100vw;"></div>
|
||||
<script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
|
||||
<script>
|
||||
new window.EmbeddedSandbox({
|
||||
target: "#sandbox",
|
||||
// Pass through your server href if you are embedding on an endpoint.
|
||||
// Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
|
||||
initialEndpoint: "http://localhost:3000/${path}",
|
||||
});
|
||||
// advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
Reference in New Issue
Block a user