Remove hasura and hasura-auth (#134)

* Remove hasura and hasura-auth

* Move all models to prisma

* Start implementing graphql

* chore: clean package json

* chore: make the code build

* chore: get initial graphql.tsx file

* feature: use typegql as qgl server

* refactor: small refactoring

* refactor: clean tests

* bugfix: make all filters not case sensitive

* chore: remove unused imports

---------

Co-authored-by: Sammy Teillet <sammy.teillet@gmail.com>
This commit is contained in:
Charles Bochet
2023-05-24 17:20:15 +02:00
committed by GitHub
parent 7192457d0a
commit 5d06398d2e
177 changed files with 12215 additions and 7040 deletions

8125
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,20 +18,26 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"prisma:generate": "npx prisma generate",
"prisma:migrate": "npx prisma migrate deploy"
},
"dependencies": {
"@golevelup/nestjs-hasura": "^3.0.2",
"@nestjs/apollo": "^11.0.5",
"@nestjs/common": "^9.0.0",
"@nestjs/core": "^9.0.0",
"@nestjs/graphql": "^11.0.5",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.13.0",
"apollo-server-express": "^3.12.0",
"graphql": "^16.6.0",
"jest-mock-extended": "^3.0.4",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typegraphql-nestjs": "^0.5.0",
"uuid": "^9.0.0"
},
"devDependencies": {
@ -56,6 +62,7 @@
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0",
"typegraphql-prisma": "^0.25.1",
"typescript": "^4.7.4"
},
"jest": {
@ -76,6 +83,7 @@
"testEnvironment": "node"
},
"prisma": {
"schema": "src/database/schema.prisma"
"schema": "src/database/schema.prisma",
"seed": "ts-node src/database/seeds/index.ts"
}
}

View File

@ -0,0 +1,45 @@
import { Module } from '@nestjs/common';
import { TypeGraphQLModule } from 'typegraphql-nestjs';
import { ApolloDriver } from '@nestjs/apollo';
import { PrismaClient } from '@prisma/client';
import {
CompanyCrudResolver,
CompanyRelationsResolver,
UserCrudResolver,
UserRelationsResolver,
PersonCrudResolver,
PersonRelationsResolver,
WorkspaceCrudResolver,
WorkspaceRelationsResolver,
WorkspaceMemberRelationsResolver,
} from '@generated/type-graphql';
interface Context {
prisma: PrismaClient;
}
const prisma = new PrismaClient();
@Module({
imports: [
TypeGraphQLModule.forRoot({
driver: ApolloDriver,
path: '/',
validate: false,
context: (): Context => ({ prisma }),
}),
],
providers: [
CompanyCrudResolver,
CompanyRelationsResolver,
UserCrudResolver,
UserRelationsResolver,
PersonCrudResolver,
PersonRelationsResolver,
WorkspaceCrudResolver,
WorkspaceRelationsResolver,
WorkspaceMemberRelationsResolver,
],
})
export class ApiModule {}

View File

@ -0,0 +1,18 @@
import { GraphQLScalarType } from 'graphql';
const regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function validate(uuid: unknown): string | never {
if (typeof uuid !== 'string' || !regex.test(uuid)) {
throw new Error('invalid uuid');
}
return uuid;
}
export const CustomUuidScalar = new GraphQLScalarType({
name: 'uuid',
description: 'A simple UUID parser',
serialize: (value) => validate(value),
parseValue: (value) => validate(value),
parseLiteral: (ast) => validate(ast.kind === 'StringValue' ? ast.value : ''),
});

View File

@ -0,0 +1,30 @@
import { Field, GraphQLISODateTime, Int, ObjectType } from '@nestjs/graphql';
import { Company as CompanyDB } from '@prisma/client';
import { CustomUuidScalar } from '../graphql/uuid';
@ObjectType()
export class Company {
@Field(() => CustomUuidScalar)
id: CompanyDB[`id`];
@Field(() => GraphQLISODateTime)
createdAt: CompanyDB[`createdAt`];
@Field(() => GraphQLISODateTime)
updatedAt: CompanyDB[`updatedAt`];
@Field(() => GraphQLISODateTime, { nullable: true })
deletedAt: CompanyDB[`deletedAt`];
@Field(() => String)
name: CompanyDB[`name`];
@Field(() => String)
domainName: CompanyDB[`domainName`];
@Field(() => String)
address: CompanyDB[`address`];
@Field(() => Int)
employees: CompanyDB[`employees`];
}

View File

@ -0,0 +1,39 @@
import { Field, GraphQLISODateTime, ObjectType } from '@nestjs/graphql';
import { Person as PersonDB } from '@prisma/client';
import { Company } from './company.model';
@ObjectType()
export class Person {
@Field(() => String)
id: PersonDB[`id`];
@Field(() => GraphQLISODateTime)
createdAt: PersonDB[`createdAt`];
@Field(() => GraphQLISODateTime)
updatedAt: PersonDB[`updatedAt`];
@Field(() => GraphQLISODateTime, { nullable: true })
deletedAt: PersonDB[`deletedAt`];
@Field(() => String)
firstname: PersonDB[`firstname`];
@Field(() => String)
lastname: PersonDB[`lastname`];
@Field(() => String)
email: PersonDB[`email`];
@Field(() => String)
phone: PersonDB[`phone`];
@Field(() => String)
city: PersonDB[`city`];
@Field(() => String, { nullable: true })
companyId: PersonDB[`companyId`];
@Field(() => Company, { nullable: true })
company: Company;
}

View File

@ -0,0 +1,17 @@
import { Field, GraphQLISODateTime, ObjectType } from '@nestjs/graphql';
import { User as UserDB } from '@prisma/client';
@ObjectType()
export class User {
@Field(() => String)
id: UserDB[`id`];
@Field(() => GraphQLISODateTime)
createdAt: UserDB[`createdAt`];
@Field(() => GraphQLISODateTime)
updatedAt: UserDB[`updatedAt`];
@Field(() => GraphQLISODateTime, { nullable: true })
deletedAt: UserDB[`deletedAt`];
}

View File

@ -0,0 +1,17 @@
import { Field, GraphQLISODateTime, ObjectType } from '@nestjs/graphql';
import { Workspace as WorkspaceDB } from '@prisma/client';
@ObjectType()
export class Workspace {
@Field(() => String)
id: WorkspaceDB[`id`];
@Field(() => GraphQLISODateTime)
createdAt: WorkspaceDB[`createdAt`];
@Field(() => GraphQLISODateTime)
updatedAt: WorkspaceDB[`updatedAt`];
@Field(() => GraphQLISODateTime, { nullable: true })
deletedAt: WorkspaceDB[`deletedAt`];
}

View File

@ -0,0 +1,15 @@
import { Company } from '@generated/type-graphql';
import { PrismaService } from '../../database/prisma.service';
import { Resolver, Query } from 'type-graphql';
@Resolver(() => Company)
export class CompanyResolver {
constructor(private prisma: PrismaService) {}
@Query(() => [Company])
async getCompaniesOfSammy(): Promise<Company[] | null> {
return await this.prisma.company.findMany({
where: {},
});
}
}

View File

@ -0,0 +1,22 @@
import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { Person } from '../models/person.model';
import { PersonRepository } from 'src/entities/person/person.repository';
import { CompanyRepository } from 'src/entities/company/company.repository';
@Resolver(() => Person)
export class PersonResolver {
constructor(
private readonly personRepository: PersonRepository,
private readonly companyRepository: CompanyRepository,
) {}
@Query(() => [Person])
async getPeople() {
return this.personRepository.findMany({});
}
@ResolveField()
company(@Parent() person: Person) {
return this.companyRepository.findOne(person.companyId);
}
}

View File

@ -0,0 +1,14 @@
import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { User } from '../models/user.model';
import { WorkspaceRepository } from 'src/entities/workspace/workspace.repository';
import { Workspace } from '../models/workspace.model';
@Resolver(() => Workspace)
export class WorkspaceResolver {
constructor(private readonly workspaceRepository: WorkspaceRepository) {}
@Query(() => [Workspace])
async getWorkspaces() {
return this.workspaceRepository.findMany({});
}
}

View File

@ -3,21 +3,10 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HealthController } from './health.controller';
import { TerminusModule } from '@nestjs/terminus';
import { HasuraModule } from '@golevelup/nestjs-hasura';
import { UserService } from './user/user.service';
import { UserModule } from './user/user.module';
import { ApiModule } from './api/api.module';
@Module({
imports: [
UserModule,
TerminusModule,
HasuraModule.forRoot(HasuraModule, {
webhookConfig: {
secretFactory: process.env.HASURA_EVENT_HANDLER_SECRET_HEADER || '',
secretHeader: 'secret-header',
},
}),
],
imports: [TerminusModule, ApiModule],
controllers: [AppController, HealthController],
providers: [AppService, UserService],
providers: [AppService],
})
export class AppModule {}

View File

@ -0,0 +1,103 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"lastSeen" TIMESTAMP(3),
"disabled" BOOLEAN NOT NULL DEFAULT false,
"displayName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"avatarUrl" TEXT,
"locale" TEXT NOT NULL,
"phoneNumber" TEXT,
"passwordHash" TEXT,
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
"metadata" JSONB,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workspaces" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"domainName" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "workspace_members" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"userId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "workspace_members_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "companies" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"name" TEXT NOT NULL,
"domainName" TEXT NOT NULL,
"address" TEXT NOT NULL,
"employees" INTEGER NOT NULL,
"accountOwnerId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "companies_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "people" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"firstname" TEXT NOT NULL,
"lastname" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"city" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "people_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "workspaces_domainName_key" ON "workspaces"("domainName");
-- CreateIndex
CREATE UNIQUE INDEX "workspace_members_userId_key" ON "workspace_members"("userId");
-- AddForeignKey
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "companies" ADD CONSTRAINT "companies_accountOwnerId_fkey" FOREIGN KEY ("accountOwnerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "companies" ADD CONSTRAINT "companies_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "people" ADD CONSTRAINT "people_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "people" ADD CONSTRAINT "people_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,17 @@
-- DropForeignKey
ALTER TABLE "companies" DROP CONSTRAINT "companies_accountOwnerId_fkey";
-- DropForeignKey
ALTER TABLE "people" DROP CONSTRAINT "people_companyId_fkey";
-- AlterTable
ALTER TABLE "companies" ALTER COLUMN "accountOwnerId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "people" ALTER COLUMN "companyId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "companies" ADD CONSTRAINT "companies_accountOwnerId_fkey" FOREIGN KEY ("accountOwnerId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "people" ADD CONSTRAINT "people_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "companies" ALTER COLUMN "employees" DROP NOT NULL;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -2,29 +2,96 @@ generator client {
provider = "prisma-client-js"
}
generator typegraphql {
provider = "typegraphql-prisma"
output = "../../node_modules/@generated/type-graphql"
}
datasource db {
provider = "postgresql"
url = env("SERVER_DATABASE_URL")
}
model User {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
lastSeen DateTime?
disabled Boolean @default(false)
displayName String
email String @unique
avatarUrl String?
locale String
phoneNumber String?
passwordHash String?
emailVerified Boolean @default(false)
metadata Json?
WorkspaceMember WorkspaceMember?
companies Company[]
@@map("users")
}
model Workspace {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
domainName String @unique
displayName String
WorkspaceMember WorkspaceMember[]
companies Company[]
people Person[]
@@map("workspaces")
}
model WorkspaceMember {
id String @id
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted_at DateTime?
user_id String @unique
workspace_id String
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
userId String @unique
user User @relation(fields: [userId], references: [id])
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
@@map("workspace_members")
}
model Workspace {
id String @id
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted_at DateTime?
domain_name String @unique
display_name String
model Company {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
name String
domainName String
address String
employees Int?
accountOwnerId String?
accountOwner User? @relation(fields: [accountOwnerId], references: [id])
people Person[]
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
@@map("workspaces")
@@map("companies")
}
model Person {
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
firstname String
lastname String
email String
phone String
city String
companyId String?
company Company? @relation(fields: [companyId], references: [id])
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
@@map("people")
}

View File

@ -0,0 +1,150 @@
import { PrismaClient } from '@prisma/client'
export const seedCompanies = async (prisma: PrismaClient) => {
await prisma.company.upsert({
where: { id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408' },
update: {},
create: {
id: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
name: 'Linkedin',
domainName: 'linkedin.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
})
await prisma.company.upsert({
where: { id: '118995f3-5d81-46d6-bf83-f7fd33ea6102' },
update: {},
create: {
id: '118995f3-5d81-46d6-bf83-f7fd33ea6102',
name: 'Facebook',
domainName: 'facebook.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
})
await prisma.company.upsert({
where: { id: '04b2e9f5-0713-40a5-8216-82802401d33e' },
update: {},
create: {
id: '04b2e9f5-0713-40a5-8216-82802401d33e',
name: 'Qonto',
domainName: 'qonto.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '460b6fb1-ed89-413a-b31a-962986e67bb4' },
update: {},
create: {
id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
name: 'Microsoft',
domainName: 'microsoft.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '89bb825c-171e-4bcc-9cf7-43448d6fb278' },
update: {},
create: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
name: 'Airbnb',
domainName: 'airbnb.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '0d940997-c21e-4ec2-873b-de4264d89025' },
update: {},
create: {
id: '0d940997-c21e-4ec2-873b-de4264d89025',
name: 'Google',
domainName: 'google.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944' },
update: {},
create: {
id: '1d3a1c6e-707e-44dc-a1d2-30030bf1a944',
name: 'Netflix',
domainName: 'netflix.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '7a93d1e5-3f74-492d-a101-2a70f50a1645' },
update: {},
create: {
id: '7a93d1e5-3f74-492d-a101-2a70f50a1645',
name: 'Libeo',
domainName: 'libeo.io',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: '9d162de6-cfbf-4156-a790-e39854dcd4eb' },
update: {},
create: {
id: '9d162de6-cfbf-4156-a790-e39854dcd4eb',
name: 'Claap',
domainName: 'claap.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: 'aaffcfbd-f86b-419f-b794-02319abe8637' },
update: {},
create: {
id: 'aaffcfbd-f86b-419f-b794-02319abe8637',
name: 'Hasura',
domainName: 'hasura.io',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: 'f33dc242-5518-4553-9433-42d8eb82834b' },
update: {},
create: {
id: 'f33dc242-5518-4553-9433-42d8eb82834b',
name: 'Wework',
domainName: 'wework.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678' },
update: {},
create: {
id: 'a7bc68d5-f79e-40dd-bd06-c36e6abb4678',
name: 'Samsung',
domainName: 'samsung.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
await prisma.company.upsert({
where: { id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d' },
update: {},
create: {
id: 'a674fa6c-1455-4c57-afaf-dd5dc086361d',
name: 'Algolia',
domainName: 'algolia.com',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
address: '',
},
});
}

View File

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
import { seedCompanies } from "./companies";
import { seedWorkspaces } from "./workspaces";
import { seedPeople } from "./people";
const seed = async () => {
const prisma = new PrismaClient()
await seedWorkspaces(prisma)
await seedCompanies(prisma)
await seedPeople(prisma)
await prisma.$disconnect()
}
seed()

View File

@ -0,0 +1,182 @@
import { PrismaClient } from '@prisma/client'
export const seedPeople = async (prisma: PrismaClient) => {
await prisma.person.upsert({
where: { id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5' },
update: {},
create: {
id: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
firstname: 'Christoph',
lastname: 'Callisto',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33789012345',
city: 'Seattle',
companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
email: 'christoph.calisto@linkedin.com'
},
})
await prisma.person.upsert({
where: { id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0' },
update: {},
create: {
id: '0aa00beb-ac73-4797-824e-87a1f5aea9e0',
firstname: 'Sylvie',
lastname: 'Palmer',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33780123456',
city: 'Los Angeles',
companyId: 'fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
email: 'sylvie.palmer@linkedin.com'
},
})
await prisma.person.upsert({
where: { id: '93c72d2e-f517-42fd-80ae-14173b3b70ae' },
update: {},
create: {
id: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
firstname: 'Christopher',
lastname: 'Gonzalez',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33789012345',
city: 'Seattle',
companyId: '04b2e9f5-0713-40a5-8216-82802401d33e',
email: 'christopher.gonzalez@qonto.com'
},
});
await prisma.person.upsert({
where: { id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e' },
update: {},
create: {
id: 'eeeacacf-eee1-4690-ad2c-8619e5b56a2e',
firstname: 'Ashley',
lastname: 'Parker',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33780123456',
city: 'Los Angeles',
companyId: '04b2e9f5-0713-40a5-8216-82802401d33e',
email: 'ashley.parker@qonto.com'
},
});
await prisma.person.upsert({
where: { id: '9b324a88-6784-4449-afdf-dc62cb8702f2' },
update: {},
create: {
id: '9b324a88-6784-4449-afdf-dc62cb8702f2',
firstname: 'Nicholas',
lastname: 'Wright',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33781234567',
city: 'Seattle',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
email: 'nicholas.wright@microsoft.com'
},
});
await prisma.person.upsert({
where: { id: '1d151852-490f-4466-8391-733cfd66a0c8' },
update: {},
create: {
id: '1d151852-490f-4466-8391-733cfd66a0c8',
firstname: 'Isabella',
lastname: 'Scott',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33782345678',
city: 'New York',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
email: 'isabella.scott@microsoft.com'
},
});
await prisma.person.upsert({
where: { id: '98406e26-80f1-4dff-b570-a74942528de3' },
update: {},
create: {
id: '98406e26-80f1-4dff-b570-a74942528de3',
firstname: 'Matthew',
lastname: 'Green',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33783456789',
city: 'Seattle',
companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
email: 'matthew.green@microsoft.com'
},
});
await prisma.person.upsert({
where: { id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35' },
update: {},
create: {
id: 'a2e78a5f-338b-46df-8811-fa08c7d19d35',
firstname: 'Elizabeth',
lastname: 'Baker',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33784567890',
city: 'New York',
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
email: 'elizabeth.baker@airbnb.com'
},
});
await prisma.person.upsert({
where: { id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016' },
update: {},
create: {
id: 'ca1f5bf3-64ad-4b0e-bbfd-e9fd795b7016',
firstname: 'Christopher',
lastname: 'Nelson',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33785678901',
city: 'San Francisco',
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
email: 'christopher.nelson@airbnb.com'
},
});
await prisma.person.upsert({
where: { id: '56955422-5d54-41b7-ba36-f0d20e1417ae' },
update: {},
create: {
id: '56955422-5d54-41b7-ba36-f0d20e1417ae',
firstname: 'Avery',
lastname: 'Carter',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33786789012',
city: 'New York',
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
email: 'avery.carter@airbnb.com'
},
});
await prisma.person.upsert({
where: { id: '755035db-623d-41fe-92e7-dd45b7c568e1' },
update: {},
create: {
id: '755035db-623d-41fe-92e7-dd45b7c568e1',
firstname: 'Ethan',
lastname: 'Mitchell',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33787890123',
city: 'Los Angeles',
companyId: '0d940997-c21e-4ec2-873b-de4264d89025',
email: 'ethan.mitchell@google.com'
},
});
await prisma.person.upsert({
where: { id: '240da2ec-2d40-4e49-8df4-9c6a049190df' },
update: {},
create: {
id: '240da2ec-2d40-4e49-8df4-9c6a049190df',
firstname: 'Madison',
lastname: 'Perez',
workspaceId: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
phone: '+33788901234',
city: 'Seattle',
companyId: '0d940997-c21e-4ec2-873b-de4264d89025',
email: 'madison.perez@google.com'
},
});
}

View File

@ -0,0 +1,21 @@
import { PrismaClient } from '@prisma/client'
export const seedWorkspaces = async (prisma: PrismaClient) => {
await prisma.workspace.upsert({
where: { domainName: 'twenty.com' },
update: {},
create: {
id: '7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
displayName: 'Twenty',
domainName: 'twenty.com',
},
})
await prisma.workspace.upsert({
where: { domainName: 'claap.com' },
update: {},
create: {
id: '7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
displayName: 'Claap',
domainName: 'claap.com',
},
})
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CompanyRepository } from './company.repository';
import { PrismaModule } from 'src/database/prisma.module';
@Module({
imports: [PrismaModule],
providers: [CompanyRepository],
exports: [CompanyRepository]
})
export class CompanyModule {}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { Company, Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CompanyRepository {
constructor(private prisma: PrismaService) {}
async findMany(params: {
skip?: number;
take?: number;
cursor?: Prisma.CompanyWhereUniqueInput;
where?: Prisma.CompanyWhereInput;
orderBy?: Prisma.CompanyOrderByWithRelationInput;
}): Promise<Company[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.company.findMany({ skip, take, cursor, where, orderBy });
}
async findOne(id: string | null) {
if (id === null) return null;
const company = await this.prisma.company.findUnique({ where: { id } });
return company;
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PersonRepository } from './person.repository';
import { PrismaModule } from 'src/database/prisma.module';
@Module({
imports: [PrismaModule],
providers: [PersonRepository],
exports: [PersonRepository]
})
export class PersonModule {}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { Person, Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class PersonRepository {
constructor(private prisma: PrismaService) {}
async findMany(params: {
skip?: number;
take?: number;
cursor?: Prisma.PersonWhereUniqueInput;
where?: Prisma.PersonWhereInput;
orderBy?: Prisma.PersonOrderByWithRelationInput;
}): Promise<Person[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.person.findMany({ skip, take, cursor, where, orderBy });
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { PrismaModule } from 'src/database/prisma.module';
@Module({
imports: [PrismaModule],
providers: [UserRepository],
exports: [UserRepository]
})
export class UserModule {}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class UserRepository {
constructor(private prisma: PrismaService) {}
async findMany(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({ skip, take, cursor, where, orderBy });
}
}

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { WorkspaceRepository } from './workspace.repository';
import { PrismaModule } from 'src/database/prisma.module';
@Module({
imports: [PrismaModule],
providers: [WorkspaceRepository],
exports: [WorkspaceRepository]
})
export class WorkspaceModule {}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { Workspace, Prisma } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class WorkspaceRepository {
constructor(private prisma: PrismaService) {}
async findMany(params: {
skip?: number;
take?: number;
cursor?: Prisma.WorkspaceWhereUniqueInput;
where?: Prisma.WorkspaceWhereInput;
orderBy?: Prisma.WorkspaceOrderByWithRelationInput;
}): Promise<Workspace[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.workspace.findMany({ skip, take, cursor, where, orderBy });
}
}

View File

@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create(AppModule, { cors: true });
await app.listen(3000);
}
bootstrap();

View File

@ -1,12 +0,0 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/database/prisma.module';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';
import { WorkspaceRepository } from './workspace.repository';
@Module({
imports: [PrismaModule],
providers: [UserRepository, UserService, WorkspaceRepository],
exports: [UserService, UserRepository, WorkspaceRepository],
})
export class UserModule {}

View File

@ -1,26 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Prisma, WorkspaceMember } from '@prisma/client';
import { PrismaService } from '../database/prisma.service';
@Injectable()
export class UserRepository {
constructor(private prisma: PrismaService) {}
async upsertWorkspaceMember(params: { data: Prisma.WorkspaceMemberCreateInput }): Promise<WorkspaceMember> {
const { data } = params;
return await this.prisma.workspaceMember.upsert({
where: {
user_id: data.user_id,
},
create: {
id: data.id,
user_id: data.user_id,
workspace_id: data.workspace_id,
},
update: {
}
});
}
}

View File

@ -1,104 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';
import { WorkspaceRepository } from './workspace.repository';
import { PrismaService } from '../database/prisma.service';
import {
MockContext,
createMockContext,
} from '../database/client-mock/context';
import { DeepMockProxy } from 'jest-mock-extended';
describe('UserService', () => {
let mockCtx: MockContext;
let service: UserService;
let mockedPrismaService: DeepMockProxy<PrismaService>;
beforeEach(async () => {
mockCtx = createMockContext();
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
UserRepository,
WorkspaceRepository,
PrismaService,
],
})
.overrideProvider(PrismaService)
.useValue(mockCtx.prisma)
.compile();
service = module.get<UserService>(UserService);
mockedPrismaService =
module.get<DeepMockProxy<PrismaService>>(PrismaService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('upsertWorkspaceMember should not upsert if email is absent', () => {
service.handleUserCreated({
event: {
data: { new: { id: 1, email: ''}, old: null },
session_variables: {},
op: 'INSERT'
},
id: '1',
table: { schema: 'auth', name: 'users' },
trigger: { name: 'user-created' },
delivery_info: { current_retry: 0, max_retries: 0},
created_at: '2021-03-01T00:00:00.000Z',
});
expect(mockedPrismaService.workspace.findUnique).toHaveBeenCalledTimes(0);
});
it('upsertWorkspaceMember should upsert if domain name is found from email', async () => {
mockedPrismaService.workspace.findUnique.mockResolvedValue({
id: 2,
display_name: 'test',
domain_name: 'domain.namexxx',
created_at: new Date(),
updated_at: new Date(),
deleted_at: null,
});
mockedPrismaService.workspaceMember.upsert.mockResolvedValue({
id: 1,
user_id: '1',
workspace_id: 1,
created_at: new Date(),
updated_at: new Date(),
deleted_at: null,
});
await service.handleUserCreated({
event: {
data: { new: { id: 1, email: 'test@domain.name' }, old: null },
session_variables: {},
op: 'INSERT',
},
id: '1',
table: { schema: 'auth', name: 'users' },
trigger: { name: 'user-created' },
delivery_info: { current_retry: 0, max_retries: 0 },
created_at: '2021-03-01T00:00:00.000Z',
});
expect(mockedPrismaService.workspace.findUnique).toHaveBeenCalledWith({
where: { domain_name: 'domain.name' },
});
expect(mockedPrismaService.workspaceMember.upsert).toHaveBeenCalledWith(
{
where: {
user_id: '1',
},
create: {
user_id: '1',
workspace_id: 2,
},
update: {},
},
);
});
});

View File

@ -1,51 +0,0 @@
import {
HasuraInsertEvent,
TrackedHasuraEventHandler,
} from '@golevelup/nestjs-hasura';
import { UserRepository } from './user.repository';
import { Injectable, Response } from '@nestjs/common';
import { WorkspaceRepository } from './workspace.repository';
import { v4 } from 'uuid';
interface User {
id: number;
email: string;
}
@Injectable()
export class UserService {
constructor(
private repository: UserRepository,
private workspaceRepository: WorkspaceRepository,
) {}
@TrackedHasuraEventHandler({
triggerName: 'user-created',
tableName: 'users',
schema: 'auth',
definition: { type: 'insert' },
})
async handleUserCreated(evt: HasuraInsertEvent<User>) {
const emailDomain = evt.event.data.new.email.split('@')[1];
if (!emailDomain) {
return;
}
const workspace = await this.workspaceRepository.findWorkspaceByDomainName({
where: { domain_name: emailDomain },
});
if (!workspace) {
return;
}
const workspaceMember = await this.repository.upsertWorkspaceMember({
data: {
id: v4(),
user_id: String(evt.event.data.new.id),
workspace_id: workspace.id,
},
});
}
}

View File

@ -1,14 +0,0 @@
import { Injectable } from '@nestjs/common';
import { Prisma, Workspace } from '@prisma/client';
import { PrismaService } from '../database/prisma.service';
@Injectable()
export class WorkspaceRepository {
constructor(private prisma: PrismaService) {}
async findWorkspaceByDomainName(
data: Prisma.WorkspaceFindUniqueArgs,
): Promise<Workspace | null> {
return await this.prisma.workspace.findUnique(data);
}
}