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:
Jérémy M
2024-03-15 14:40:58 +01:00
committed by GitHub
parent 52f1b3ac98
commit 94487f6737
760 changed files with 3215 additions and 3155 deletions

View File

@ -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 {}

View File

@ -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,
);
}
}

View File

@ -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 {}

View File

@ -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: '',
};
}
}
}

View File

@ -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 {}

View File

@ -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,
);
}
}

View File

@ -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 {}

View File

@ -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,
);
}
}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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}`,
);
});
});

View File

@ -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}`);
});
});

View File

@ -0,0 +1,5 @@
export const customNamePrefix = '_';
export const computeCustomName = (name: string, isCustom: boolean) => {
return isCustom ? `${customNamePrefix}${name}` : name;
};

View File

@ -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);
};

View File

@ -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,
);
};

View File

@ -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}`,
);
};

View File

@ -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}`);
}
};

View File

@ -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;
};

View File

@ -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>`;
};