├── packages └── server │ ├── hermod │ ├── src │ │ └── index.ts │ └── package.json │ ├── umbriel │ ├── src │ │ ├── modules │ │ │ └── contacts │ │ │ │ ├── index.ts │ │ │ │ ├── infra │ │ │ │ ├── http │ │ │ │ │ ├── index.ts │ │ │ │ │ └── contacts.ts │ │ │ │ ├── kafka │ │ │ │ │ └── consumers │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── AddUserToTeamConsumer.ts │ │ │ │ └── prisma │ │ │ │ │ ├── PrismaTagRepo.ts │ │ │ │ │ ├── PrismaContactRepo.ts │ │ │ │ │ └── PrismaContactSubscriptionsRepo.ts │ │ │ │ ├── useCases │ │ │ │ └── contacts │ │ │ │ │ ├── getContacts │ │ │ │ │ ├── GetContactsDTO.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── GetContactsUseCase.ts │ │ │ │ │ └── GetContactsController.ts │ │ │ │ │ └── subscribeContact │ │ │ │ │ ├── SubscribeContactDTO.ts │ │ │ │ │ ├── SubscribeContactErrors.ts │ │ │ │ │ ├── SubscribeContactUseCase.spec.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SubscribeContactController.ts │ │ │ │ │ └── SubscribeContactUseCase.ts │ │ │ │ ├── repositories │ │ │ │ ├── IContactRepo.ts │ │ │ │ ├── ITagRepo.ts │ │ │ │ └── IContactSubscriptionsRepo.ts │ │ │ │ ├── mappers │ │ │ │ ├── ContactSubscriptionMap.ts │ │ │ │ ├── TagMap.ts │ │ │ │ └── ContactMap.ts │ │ │ │ └── domain │ │ │ │ ├── ContactSubscriptions.ts │ │ │ │ ├── TagSlug.ts │ │ │ │ ├── ContactEmail.ts │ │ │ │ ├── Tag.ts │ │ │ │ ├── ContactSubscription.ts │ │ │ │ └── Contact.ts │ │ ├── infra │ │ │ ├── kafka │ │ │ │ ├── consumers.ts │ │ │ │ └── client.ts │ │ │ ├── prisma │ │ │ │ └── client.ts │ │ │ └── http │ │ │ │ ├── routes.ts │ │ │ │ └── server.ts │ │ └── index.ts │ ├── prisma │ │ ├── migrations │ │ │ ├── migrate.lock │ │ │ ├── 20200611222402-add_slug_to_tags │ │ │ │ ├── steps.json │ │ │ │ ├── README.md │ │ │ │ └── schema.prisma │ │ │ └── 20200609124237-create_base_templates │ │ │ │ ├── schema.prisma │ │ │ │ ├── README.md │ │ │ │ └── steps.json │ │ └── schema.prisma │ ├── jest.config.js │ ├── tsconfig.json │ └── package.json │ ├── atlas │ ├── .env.example │ ├── src │ │ ├── modules │ │ │ └── users │ │ │ │ ├── index.ts │ │ │ │ ├── useCases │ │ │ │ ├── login │ │ │ │ │ ├── LoginUseCase.spec.ts │ │ │ │ │ ├── ILoginDTO.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── LoginErrors.ts │ │ │ │ │ ├── LoginController.ts │ │ │ │ │ └── LoginUseCase.ts │ │ │ │ ├── subscribeUserToMailing │ │ │ │ │ ├── ISubscribeUserToMailingDTO.ts │ │ │ │ │ ├── SubscribeUserToMailingErrors.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── SubscribeUserToMailingUseCase.ts │ │ │ │ └── createUser │ │ │ │ │ ├── ICreateUserDTO.ts │ │ │ │ │ ├── CreateUserErrors.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── CreateUserController.ts │ │ │ │ │ └── CreateUserUseCase.ts │ │ │ │ ├── services │ │ │ │ ├── index.ts │ │ │ │ └── umbriel │ │ │ │ │ ├── IUmbrielService.ts │ │ │ │ │ └── implementations │ │ │ │ │ └── UmbrielService.ts │ │ │ │ ├── repositories │ │ │ │ ├── IUserRepo.ts │ │ │ │ └── fakes │ │ │ │ │ └── FakeUserRepo.ts │ │ │ │ ├── subscribers │ │ │ │ ├── index.ts │ │ │ │ ├── AfterUserLoggedIn.ts │ │ │ │ ├── AfterUserEmailChanged.ts │ │ │ │ └── AfterUserCreated.ts │ │ │ │ ├── infra │ │ │ │ ├── http │ │ │ │ │ └── routes │ │ │ │ │ │ └── index.ts │ │ │ │ └── prisma │ │ │ │ │ └── PrismaUserRepo.ts │ │ │ │ ├── domain │ │ │ │ ├── events │ │ │ │ │ ├── UserCreatedEvent.ts │ │ │ │ │ ├── UserLoggedInEvent.ts │ │ │ │ │ └── UserEmailChangedEvent.ts │ │ │ │ ├── Team.ts │ │ │ │ ├── UserEmail.ts │ │ │ │ ├── JWT.ts │ │ │ │ ├── UserPassword.ts │ │ │ │ └── User.ts │ │ │ │ └── mappers │ │ │ │ └── UserMap.ts │ │ ├── index.ts │ │ ├── config │ │ │ ├── auth.ts │ │ │ └── app.ts │ │ └── infra │ │ │ ├── prisma │ │ │ └── client.ts │ │ │ ├── kafka │ │ │ └── client.ts │ │ │ └── http │ │ │ ├── routes.ts │ │ │ └── server.ts │ ├── prisma │ │ ├── .env │ │ ├── migrations │ │ │ ├── migrate.lock │ │ │ ├── 20200515170312-create-users │ │ │ │ ├── schema.prisma │ │ │ │ ├── README.md │ │ │ │ └── steps.json │ │ │ ├── 20200604122553-add_cascade_on_deletes │ │ │ │ ├── steps.json │ │ │ │ ├── schema.prisma │ │ │ │ └── README.md │ │ │ ├── 20200603231715-create_teams_and_addresses │ │ │ │ ├── schema.prisma │ │ │ │ ├── README.md │ │ │ │ └── steps.json │ │ │ └── 20200603234442-create_user_email_history │ │ │ │ ├── schema.prisma │ │ │ │ ├── README.md │ │ │ │ └── steps.json │ │ └── schema.prisma │ ├── tsconfig.json │ ├── jest.config.js │ ├── Dockerfile │ └── package.json │ ├── _shared │ ├── src │ │ └── core │ │ │ ├── domain │ │ │ ├── events │ │ │ │ ├── IHandle.ts │ │ │ │ ├── IDomainEvent.ts │ │ │ │ └── DomainEvents.ts │ │ │ ├── UseCase.ts │ │ │ ├── UniqueEntityID.ts │ │ │ ├── Entity.ts │ │ │ ├── ValueObject.ts │ │ │ ├── AggregateRoot.ts │ │ │ └── WatchedList.ts │ │ │ ├── infra │ │ │ ├── Mapper.ts │ │ │ └── BaseController.ts │ │ │ └── logic │ │ │ ├── UseCaseError.ts │ │ │ ├── AppError.ts │ │ │ ├── Result.ts │ │ │ └── Guard.ts │ ├── tsconfig.json │ └── package.json │ ├── mercury │ ├── jest.config.js │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── consumer.ts │ ├── babel.config.js │ └── .eslintrc.json ├── .eslintignore ├── .gitignore ├── prettier.config.js ├── .github └── diagram.png ├── lerna.json ├── jest.config.js ├── README.md ├── tsconfig.json ├── package.json └── docker-compose.yml /packages/server/hermod/src/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.js -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/server/atlas/.env.example: -------------------------------------------------------------------------------- 1 | APP_SECRET=myappsecret -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/index.ts: -------------------------------------------------------------------------------- 1 | import './subscribers'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Yarn/NPM 2 | node_modules 3 | 4 | # Build 5 | dist 6 | 7 | # Environment 8 | .env -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5' 4 | } -------------------------------------------------------------------------------- /.github/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diego3g/node-microservices-ddd/HEAD/.github/diagram.png -------------------------------------------------------------------------------- /packages/server/umbriel/src/infra/kafka/consumers.ts: -------------------------------------------------------------------------------- 1 | import '@modules/contacts/infra/kafka/consumers'; 2 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:docker@localhost:5432/skylab?schema=public" -------------------------------------------------------------------------------- /packages/server/atlas/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import '@infra/http/server'; 4 | import '@modules/users'; 5 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/LoginUseCase.spec.ts: -------------------------------------------------------------------------------- 1 | test('hey', () => { 2 | expect(1 + 1).toBe(2); 3 | }); 4 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/events/IHandle.ts: -------------------------------------------------------------------------------- 1 | export interface IHandle { 2 | setupSubscriptions(): void; 3 | } 4 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/http/index.ts: -------------------------------------------------------------------------------- 1 | import contactsRouter from './contacts'; 2 | 3 | export { contactsRouter }; 4 | -------------------------------------------------------------------------------- /packages/server/atlas/src/config/auth.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | secret: process.env.APP_SECRET, 3 | tokenExpiryTimeInSeconds: 8 * 60 * 60, 4 | }; 5 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/server/*" 4 | ], 5 | "useWorkspaces": true, 6 | "npmClient": "yarn", 7 | "version": "0.0.0" 8 | } -------------------------------------------------------------------------------- /packages/server/_shared/src/core/infra/Mapper.ts: -------------------------------------------------------------------------------- 1 | export default interface IMapper { 2 | toPersistence(t: T): any; 3 | toDomain(raw: any): T; 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/atlas/src/config/app.ts: -------------------------------------------------------------------------------- 1 | const isProduction = process.env.NODE_ENV === 'production'; 2 | 3 | export default { 4 | isProduction, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # Prisma Migrate lockfile v1 2 | 3 | 20200609124237-create_base_templates 4 | 20200611222402-add_slug_to_tags -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/services/index.ts: -------------------------------------------------------------------------------- 1 | import { UmbrielService } from './umbriel/implementations/UmbrielService'; 2 | 3 | export { UmbrielService }; 4 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/UseCase.ts: -------------------------------------------------------------------------------- 1 | export interface IUseCase { 2 | execute(request?: IRequest): Promise | IResponse; 3 | } 4 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | import '@infra/http/server'; 4 | import '@infra/kafka/consumers'; 5 | 6 | import '@modules/contacts'; 7 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/kafka/consumers/index.ts: -------------------------------------------------------------------------------- 1 | import { AddUserToTeamConsumer } from './AddUserToTeamConsumer'; 2 | 3 | new AddUserToTeamConsumer(); 4 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/subscribeUserToMailing/ISubscribeUserToMailingDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ISubscribeUserToMailingRequestDTO { 2 | userId: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/getContacts/GetContactsDTO.ts: -------------------------------------------------------------------------------- 1 | export interface IGetContactsResponseDTO { 2 | id: string; 3 | email: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/createUser/ICreateUserDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ICreateUserRequestDTO { 2 | name: string; 3 | email: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/server/atlas/src/infra/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '.prisma/client'; 2 | 3 | const prisma = new PrismaClient({ 4 | log: ['query'], 5 | }); 6 | 7 | export { prisma }; 8 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/infra/prisma/client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | const prisma = new PrismaClient({ 4 | log: ['warn'], 5 | }); 6 | 7 | export { prisma }; 8 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/ILoginDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ILoginRequestDTO { 2 | email: string; 3 | password: string; 4 | } 5 | 6 | export interface ILoginResponseDTO { 7 | token: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/events/IDomainEvent.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from '../UniqueEntityID'; 2 | 3 | export interface IDomainEvent { 4 | dateTimeOccurred: Date; 5 | getAggregateId(): UniqueEntityID; 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/migrate.lock: -------------------------------------------------------------------------------- 1 | # Prisma Migrate lockfile v1 2 | 3 | 20200515170312-create-users 4 | 20200603231715-create_teams_and_addresses 5 | 20200603234442-create_user_email_history 6 | 20200604122553-add_cascade_on_deletes 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | preset: 'ts-jest', 4 | projects: [ 5 | "/packages/server/**/jest.config.js" 6 | ], 7 | testEnvironment: "node", 8 | testMatch: [ 9 | "*.spec.ts" 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /packages/server/atlas/src/infra/kafka/client.ts: -------------------------------------------------------------------------------- 1 | import { Kafka, logLevel } from 'kafkajs'; 2 | 3 | const kafka = new Kafka({ 4 | brokers: ['localhost:9092'], 5 | logLevel: logLevel.WARN, 6 | clientId: 'atlas', 7 | }); 8 | 9 | export { kafka }; 10 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/SubscribeContactDTO.ts: -------------------------------------------------------------------------------- 1 | export interface ISubscribeContactRequestDTO { 2 | email: string; 3 | tags: Array<{ 4 | title: string; 5 | integrationId?: string; 6 | }>; 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/_shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "strictPropertyInitialization": false 7 | }, 8 | "include": [ 9 | "src" 10 | ] 11 | } -------------------------------------------------------------------------------- /packages/server/atlas/src/infra/http/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import userRouter from '@modules/users/infra/http/routes'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use('/users', userRouter); 8 | 9 | export { router }; 10 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/infra/kafka/client.ts: -------------------------------------------------------------------------------- 1 | import { Kafka, logLevel } from 'kafkajs'; 2 | 3 | const kafka = new Kafka({ 4 | brokers: ['localhost:9092'], 5 | logLevel: logLevel.WARN, 6 | clientId: 'umbriel', 7 | }); 8 | 9 | export { kafka }; 10 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/infra/http/routes.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { contactsRouter } from '@modules/contacts/infra/http'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use('/contacts', contactsRouter); 8 | 9 | export { router }; 10 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/services/umbriel/IUmbrielService.ts: -------------------------------------------------------------------------------- 1 | import { Team } from '@modules/users/domain/Team'; 2 | import { User } from '@modules/users/domain/User'; 3 | 4 | export interface IUmbrielService { 5 | addUserToTeam(user: User, team: Team): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/logic/UseCaseError.ts: -------------------------------------------------------------------------------- 1 | interface IUseCaseErrorError { 2 | message: string; 3 | } 4 | 5 | export abstract class UseCaseError implements IUseCaseErrorError { 6 | public readonly message: string; 7 | 8 | constructor(message: string) { 9 | this.message = message; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/repositories/IUserRepo.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../domain/User'; 2 | import { UserEmail } from '../domain/UserEmail'; 3 | 4 | export interface IUserRepo { 5 | findByEmail(email: string | UserEmail): Promise; 6 | findById(id: string): Promise; 7 | save(user: User): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/atlas/src/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import 'express-async-errors'; 4 | import { router } from './routes'; 5 | 6 | const server = express(); 7 | 8 | server.use(express.json()); 9 | server.use(router); 10 | 11 | server.listen(3333, () => { 12 | console.log('Atlas running!'); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/infra/http/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import 'express-async-errors'; 4 | import { router } from './routes'; 5 | 6 | const server = express(); 7 | 8 | server.use(express.json()); 9 | server.use(router); 10 | 11 | server.listen(3334, () => { 12 | console.log('Umbriel running!'); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/subscribers/index.ts: -------------------------------------------------------------------------------- 1 | import { subscribeUserToMailingUseCase } from '../useCases/subscribeUserToMailing'; 2 | import { AfterUserCreated } from './AfterUserCreated'; 3 | import { AfterUserLoggedIn } from './AfterUserLoggedIn'; 4 | 5 | // Subscribers 6 | new AfterUserCreated(subscribeUserToMailingUseCase); 7 | new AfterUserLoggedIn(); 8 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/repositories/IContactRepo.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from '../domain/Contact'; 2 | import { ContactEmail } from '../domain/ContactEmail'; 3 | 4 | export interface IContactRepo { 5 | findByEmail(email: string | ContactEmail): Promise; 6 | getContacts(): Promise; 7 | save(contact: Contact): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/repositories/ITagRepo.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from '../domain/Tag'; 2 | import { TagSlug } from '../domain/TagSlug'; 3 | 4 | export interface ITagRepo { 5 | findTagBySlug(slug: TagSlug): Promise; 6 | findTagBySlugBulk(slugs: TagSlug[]): Promise; 7 | save(tag: Tag): Promise; 8 | saveBulk(tags: Tag[]): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200515170312-create-users/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | id String @default(uuid()) @id 12 | email String @unique 13 | name String 14 | password String 15 | 16 | @@map(name: "users") 17 | } -------------------------------------------------------------------------------- /packages/server/atlas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "strictPropertyInitialization": false, 7 | "paths": { 8 | "@modules/*": ["./src/modules/*"], 9 | "@infra/*": ["./src/infra/*"], 10 | "@config/*": ["./src/config/*"] 11 | } 12 | }, 13 | "include": [ 14 | "src" 15 | ] 16 | } -------------------------------------------------------------------------------- /packages/server/atlas/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { name } = require('./package.json'); 3 | const { compilerOptions } = require('./tsconfig.json'); 4 | 5 | module.exports = { 6 | displayName: name, 7 | name, 8 | preset: 'ts-jest', 9 | moduleNameMapper: pathsToModuleNameMapper( 10 | compilerOptions.paths, 11 | { prefix: '' } 12 | ) 13 | }; 14 | -------------------------------------------------------------------------------- /packages/server/mercury/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { name } = require('./package.json'); 3 | const { compilerOptions } = require('./tsconfig.json'); 4 | 5 | module.exports = { 6 | displayName: name, 7 | name, 8 | preset: 'ts-jest', 9 | moduleNameMapper: pathsToModuleNameMapper( 10 | compilerOptions.paths, 11 | { prefix: '' } 12 | ) 13 | }; 14 | -------------------------------------------------------------------------------- /packages/server/umbriel/jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { name } = require('./package.json'); 3 | const { compilerOptions } = require('./tsconfig.json'); 4 | 5 | module.exports = { 6 | displayName: name, 7 | name, 8 | preset: 'ts-jest', 9 | moduleNameMapper: pathsToModuleNameMapper( 10 | compilerOptions.paths, 11 | { prefix: '' } 12 | ) 13 | }; 14 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/subscribeUserToMailing/SubscribeUserToMailingErrors.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@server/shared/src/core/logic/Result'; 2 | import { UseCaseError } from '@server/shared/src/core/logic/UseCaseError'; 3 | 4 | export class UserNotFoundError extends Result { 5 | constructor() { 6 | super(false, { 7 | message: `User not found.`, 8 | } as UseCaseError); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microservice studies 2 | 3 | This project aims to implement a monorepo Node.js microservice structure with DDD. 4 | 5 | ## Tools 6 | 7 | - TypeScript 💙 8 | - Babel (for builds); 9 | - Lerna & Yarn Workspaces (Monorepo structure); 10 | - Docker & Docker Compose (Setup environment); 11 | - Prisma (ORM replacement); 12 | - Apache Kafka (Async communication); 13 | 14 | ## Application Diagram 15 | 16 | ![Application Diagram](/.github/diagram.png) 17 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/createUser/CreateUserErrors.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@server/shared/src/core/logic/Result'; 2 | import { UseCaseError } from '@server/shared/src/core/logic/UseCaseError'; 3 | 4 | export class AccountAlreadyExists extends Result { 5 | constructor(email: string) { 6 | super(false, { 7 | message: `The email ${email} associated for this account already exists`, 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/mercury/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "strictPropertyInitialization": false, 7 | "paths": { 8 | "@modules/*": ["./src/modules/*"], 9 | "@shared/*": ["@server/shared/lib/*"], 10 | "@infra/*": ["./src/infra/*"], 11 | "@config/*": ["./src/config/*"] 12 | } 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/repositories/IContactSubscriptionsRepo.ts: -------------------------------------------------------------------------------- 1 | import { ContactSubscription } from '../domain/ContactSubscription'; 2 | import { ContactSubscriptions } from '../domain/ContactSubscriptions'; 3 | 4 | export interface IContactSubscriptionsRepo { 5 | save(subscription: ContactSubscription): Promise; 6 | saveBulk(subscriptions: ContactSubscriptions): Promise; 7 | delete(subscription: ContactSubscription): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/umbriel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | "strictPropertyInitialization": false, 7 | "paths": { 8 | "@modules/*": ["./src/modules/*"], 9 | "@shared/*": ["@server/shared/src/*"], 10 | "@infra/*": ["./src/infra/*"], 11 | "@config/*": ["./src/config/*"] 12 | } 13 | }, 14 | "include": [ 15 | "src" 16 | ] 17 | } -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaUserRepo } from '@modules/users/infra/prisma/PrismaUserRepo'; 2 | 3 | import { LoginController } from './LoginController'; 4 | import { LoginUseCase } from './LoginUseCase'; 5 | 6 | const prismaUserRepo = new PrismaUserRepo(); 7 | 8 | const loginUseCase = new LoginUseCase(prismaUserRepo); 9 | const loginController = new LoginController(loginUseCase); 10 | 11 | export { loginUseCase, loginController }; 12 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/SubscribeContactErrors.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@server/shared/src/core/logic/Result'; 2 | import { UseCaseError } from '@server/shared/src/core/logic/UseCaseError'; 3 | 4 | export class ContactAlreadyExists extends Result { 5 | constructor(email: string) { 6 | super(false, { 7 | message: `The email ${email} associated for this contact already exists`, 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/createUser/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaUserRepo } from '@modules/users/infra/prisma/PrismaUserRepo'; 2 | 3 | import { CreateUserController } from './CreateUserController'; 4 | import { CreateUserUseCase } from './CreateUserUseCase'; 5 | 6 | const prismaUserRepo = new PrismaUserRepo(); 7 | 8 | const createUserUseCase = new CreateUserUseCase(prismaUserRepo); 9 | const createUserController = new CreateUserController(createUserUseCase); 10 | 11 | export { createUserUseCase, createUserController }; 12 | -------------------------------------------------------------------------------- /packages/server/atlas/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | ENV TINI_VERSION v0.18.0 4 | 5 | RUN mkdir -p /home/node/api/node_modules && chown -R node:node /home/node/api 6 | 7 | WORKDIR /home/node/api 8 | 9 | COPY --chown=node:node package.json yarn.* ./ 10 | 11 | USER node 12 | 13 | RUN yarn 14 | 15 | COPY --chown=node:node . . 16 | 17 | EXPOSE 3333 18 | 19 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 20 | 21 | RUN chmod +x /tini 22 | 23 | ENTRYPOINT [ "/tini", "--" ] 24 | 25 | CMD [ "yarn", "start" ] -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/infra/http/routes/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { createUserController } from '@modules/users/useCases/createUser'; 4 | import { loginController } from '@modules/users/useCases/login'; 5 | 6 | const userRouter = express.Router(); 7 | 8 | userRouter.post('/', (request, response) => 9 | createUserController.execute(request, response) 10 | ); 11 | 12 | userRouter.post('/login', (request, response) => 13 | loginController.execute(request, response) 14 | ); 15 | 16 | export default userRouter; 17 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200611222402-add_slug_to_tags/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateField", 6 | "model": "Tag", 7 | "field": "slug", 8 | "type": "String", 9 | "arity": "Required" 10 | }, 11 | { 12 | "tag": "CreateDirective", 13 | "location": { 14 | "path": { 15 | "tag": "Field", 16 | "model": "Tag", 17 | "field": "slug" 18 | }, 19 | "directive": "unique" 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/getContacts/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaContactRepo } from '@modules/contacts/infra/prisma/PrismaContactRepo'; 2 | 3 | import { GetContactsController } from './GetContactsController'; 4 | import { GetContactsUseCase } from './GetContactsUseCase'; 5 | 6 | const prismaContactRepo = new PrismaContactRepo(); 7 | 8 | const getContactsUseCase = new GetContactsUseCase(prismaContactRepo); 9 | const getContactsController = new GetContactsController(getContactsUseCase); 10 | 11 | export { getContactsUseCase, getContactsController }; 12 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/logic/AppError.ts: -------------------------------------------------------------------------------- 1 | import { Result } from './Result'; 2 | import { UseCaseError } from './UseCaseError'; 3 | 4 | export class UnexpectedError extends Result { 5 | public constructor(err: any) { 6 | super(false, { 7 | message: `An unexpected error occurred.`, 8 | error: err, 9 | } as UseCaseError); 10 | 11 | console.log(`[AppError]: An unexpected error occurred`); 12 | console.error(err); 13 | } 14 | 15 | public static create(err: any): UnexpectedError { 16 | return new UnexpectedError(err); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/subscribeUserToMailing/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaUserRepo } from '@modules/users/infra/prisma/PrismaUserRepo'; 2 | import { UmbrielService } from '@modules/users/services'; 3 | 4 | import { SubscribeUserToMailingUseCase } from './SubscribeUserToMailingUseCase'; 5 | 6 | const prismaUserRepo = new PrismaUserRepo(); 7 | const umbrielService = new UmbrielService(); 8 | 9 | const subscribeUserToMailingUseCase = new SubscribeUserToMailingUseCase( 10 | prismaUserRepo, 11 | umbrielService 12 | ); 13 | 14 | export { subscribeUserToMailingUseCase }; 15 | -------------------------------------------------------------------------------- /packages/server/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript' 12 | ], 13 | plugins: [ 14 | ["module-resolver", { 15 | alias: { 16 | "@modules": "./src/modules", 17 | "@core": "./src/core", 18 | "@shared": "./src/shared", 19 | "@infra": "./src/infra", 20 | "@config": "./src/config" 21 | } 22 | }] 23 | ], 24 | ignore: [ 25 | "**/*.spec.ts" 26 | ] 27 | }; -------------------------------------------------------------------------------- /packages/server/_shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/shared", 3 | "description": "Shared between all server packages.", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "babel src --root-mode upward --extensions \".js,.ts\" --ignore *.spec.ts --out-dir dist --copy-files", 9 | "test": "jest" 10 | }, 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "shallow-equal-object": "^1.1.1", 14 | "uuidv4": "^6.1.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^14.0.1", 18 | "typescript": "^3.9.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/events/UserCreatedEvent.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@server/shared/src/core/domain/events/IDomainEvent'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | 4 | import { User } from '../User'; 5 | 6 | export class UserCreatedEvent implements IDomainEvent { 7 | public dateTimeOccurred: Date; 8 | 9 | public user: User; 10 | 11 | constructor(user: User) { 12 | this.dateTimeOccurred = new Date(); 13 | this.user = user; 14 | } 15 | 16 | getAggregateId(): UniqueEntityID { 17 | return this.user.id; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/events/UserLoggedInEvent.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@server/shared/src/core/domain/events/IDomainEvent'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | 4 | import { User } from '../User'; 5 | 6 | export class UserLoggedInEvent implements IDomainEvent { 7 | public dateTimeOccurred: Date; 8 | 9 | public user: User; 10 | 11 | constructor(user: User) { 12 | this.dateTimeOccurred = new Date(); 13 | this.user = user; 14 | } 15 | 16 | getAggregateId(): UniqueEntityID { 17 | return this.user.id; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/http/contacts.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { getContactsController } from '@modules/contacts/useCases/contacts/getContacts'; 4 | import { subscribeContactController } from '@modules/contacts/useCases/contacts/subscribeContact'; 5 | 6 | const contactsRouter = express.Router(); 7 | 8 | contactsRouter.get('/', (request, response) => 9 | getContactsController.execute(request, response) 10 | ); 11 | 12 | contactsRouter.post('/', (request, response) => 13 | subscribeContactController.execute(request, response) 14 | ); 15 | 16 | export default contactsRouter; 17 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/mappers/ContactSubscriptionMap.ts: -------------------------------------------------------------------------------- 1 | import { ContactTags as PersistenceSubscription } from '.prisma/client'; 2 | 3 | import { ContactSubscription } from '../domain/ContactSubscription'; 4 | 5 | export class ContactSubscriptionMap { 6 | static toPersistence( 7 | contactSubscription: ContactSubscription 8 | ): Partial { 9 | return { 10 | id: contactSubscription.id.toValue(), 11 | contactId: contactSubscription.contactId, 12 | tagId: contactSubscription.tagId, 13 | subscribed: contactSubscription.subscribed, 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/LoginErrors.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '@server/shared/src/core/logic/Result'; 2 | import { UseCaseError } from '@server/shared/src/core/logic/UseCaseError'; 3 | 4 | export class UserNameDoesntExistError extends Result { 5 | constructor() { 6 | super(false, { 7 | message: `Username or password incorrect.`, 8 | } as UseCaseError); 9 | } 10 | } 11 | 12 | export class PasswordDoesntMatchError extends Result { 13 | constructor() { 14 | super(false, { 15 | message: `Password doesn't match.`, 16 | } as UseCaseError); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/server/mercury/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/mercury", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "babel src --root-mode upward --extensions \".js,.ts\" --out-dir dist --copy-files --no-copy-ignored", 8 | "start": "node dist/index.js", 9 | "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpileOnly --no-notify src/index.ts", 10 | "test": "jest" 11 | }, 12 | "dependencies": { 13 | "kafkajs": "^1.12.0" 14 | }, 15 | "devDependencies": { 16 | "ts-node-dev": "^1.0.0-pre.44", 17 | "tsconfig-paths": "^3.9.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/UniqueEntityID.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from 'uuidv4'; 2 | 3 | export class UniqueEntityID { 4 | private value: string; 5 | 6 | constructor(id?: string) { 7 | this.value = id || uuid(); 8 | } 9 | 10 | equals(id?: UniqueEntityID): boolean { 11 | if (id === null || id === undefined) { 12 | return false; 13 | } 14 | 15 | if (!(id instanceof UniqueEntityID)) { 16 | return false; 17 | } 18 | 19 | return id.toValue() === this.value; 20 | } 21 | 22 | toString(): string { 23 | return String(this.value); 24 | } 25 | 26 | toValue(): string { 27 | return this.value; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "target": "es2017", 6 | "sourceMap": true, 7 | "allowJs": true, 8 | "removeComments": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "pretty": true, 13 | "resolveJsonModule": true, 14 | "types": ["node", "jest"], 15 | "typeRoots" : ["./node_modules/@types", "./src/@types"], 16 | "experimentalDecorators": true, 17 | "emitDecoratorMetadata": true 18 | }, 19 | "include": [ 20 | "packages/**/**/*" 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/hermod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/hermod", 3 | "description": "Push Notifications", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "babel src --root-mode upward --extensions \".js,.ts\" --out-dir dist --copy-files --no-copy-ignored", 9 | "start": "node dist/index.js", 10 | "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpileOnly --no-notify src/index.ts", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@server/shared": "*", 15 | "dotenv": "^8.2.0" 16 | }, 17 | "devDependencies": { 18 | "ts-node-dev": "^1.0.0-pre.44", 19 | "tsconfig-paths": "^3.9.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/Entity.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from './UniqueEntityID'; 2 | 3 | export abstract class Entity { 4 | protected readonly _id: UniqueEntityID; 5 | 6 | public readonly props: T; 7 | 8 | constructor(props: T, id?: UniqueEntityID) { 9 | this._id = id || new UniqueEntityID(); 10 | this.props = props; 11 | } 12 | 13 | public equals(object?: Entity): boolean { 14 | if (object === null || object === undefined) { 15 | return false; 16 | } 17 | 18 | if (this === object) { 19 | return true; 20 | } 21 | 22 | if (!(object instanceof Entity)) { 23 | return false; 24 | } 25 | 26 | return this._id.equals(object._id); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/ContactSubscriptions.ts: -------------------------------------------------------------------------------- 1 | import { WatchedList } from '@server/shared/src/core/domain/WatchedList'; 2 | 3 | import { ContactSubscription } from './ContactSubscription'; 4 | 5 | export class ContactSubscriptions extends WatchedList { 6 | private constructor(initialSubscriptions: ContactSubscription[]) { 7 | super(initialSubscriptions); 8 | } 9 | 10 | public compareItems(a: ContactSubscription, b: ContactSubscription): boolean { 11 | return a.equals(b); 12 | } 13 | 14 | public static create( 15 | subscriptions?: ContactSubscription[] 16 | ): ContactSubscriptions { 17 | return new ContactSubscriptions(subscriptions || []); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200604122553-add_cascade_on_deletes/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "UpdateField", 6 | "model": "UserAddress", 7 | "field": "User", 8 | "arity": "Required" 9 | }, 10 | { 11 | "tag": "UpdateField", 12 | "model": "UserAddress", 13 | "field": "userId", 14 | "arity": "Required" 15 | }, 16 | { 17 | "tag": "UpdateField", 18 | "model": "UserEmailHistory", 19 | "field": "User", 20 | "arity": "Required" 21 | }, 22 | { 23 | "tag": "UpdateField", 24 | "model": "UserEmailHistory", 25 | "field": "userId", 26 | "arity": "Required" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/SubscribeContactUseCase.spec.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from '@infra/prisma/client'; 2 | 3 | import { subscribeContactUseCase } from '.'; 4 | 5 | describe('Subscribe Contact', () => { 6 | afterAll(async () => { 7 | await prisma.disconnect(); 8 | }); 9 | 10 | it('should be able to subscribe new contact', async () => { 11 | await subscribeContactUseCase.execute({ 12 | email: 'diego@rocketseat.com.br', 13 | tags: [ 14 | { title: 'GoStack 10.0', integrationId: '123456' }, 15 | { title: 'GoStack 11.0', integrationId: '441234' }, 16 | { title: 'GoStack 12.0', integrationId: '124125' }, 17 | ], 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/events/UserEmailChangedEvent.ts: -------------------------------------------------------------------------------- 1 | import { IDomainEvent } from '@server/shared/src/core/domain/events/IDomainEvent'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | 4 | import { User } from '../User'; 5 | import { UserEmail } from '../UserEmail'; 6 | 7 | export class UserEmailChangedEvent implements IDomainEvent { 8 | public dateTimeOccurred: Date; 9 | 10 | public lastEmail: UserEmail; 11 | 12 | public user: User; 13 | 14 | constructor(user: User, lastEmail?: UserEmail) { 15 | this.dateTimeOccurred = new Date(); 16 | this.user = user; 17 | this.lastEmail = lastEmail; 18 | } 19 | 20 | getAggregateId(): UniqueEntityID { 21 | return this.user.id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/ValueObject.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from 'shallow-equal-object'; 2 | 3 | interface IValueObjectProps { 4 | [index: string]: any; 5 | } 6 | 7 | /** 8 | * @desc ValueObjects are objects that we determine their 9 | * equality through their structrual property. 10 | */ 11 | 12 | export abstract class ValueObject { 13 | public readonly props: T; 14 | 15 | constructor(props: T) { 16 | this.props = Object.freeze(props); 17 | } 18 | 19 | public equals(vo?: ValueObject): boolean { 20 | if (vo === null || vo === undefined) { 21 | return false; 22 | } 23 | 24 | if (vo.props === undefined) { 25 | return false; 26 | } 27 | 28 | return shallowEqual(this.props, vo.props); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/subscribers/AfterUserLoggedIn.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | import { IHandle } from '@server/shared/src/core/domain/events/IHandle'; 3 | 4 | import { UserLoggedInEvent } from '../domain/events/UserLoggedInEvent'; 5 | 6 | export class AfterUserLoggedIn implements IHandle { 7 | constructor() { 8 | this.setupSubscriptions(); 9 | } 10 | 11 | setupSubscriptions(): void { 12 | DomainEvents.register( 13 | this.onUserLoggedInEvent.bind(this), 14 | UserLoggedInEvent.name 15 | ); 16 | } 17 | 18 | private async onUserLoggedInEvent(event: UserLoggedInEvent): Promise { 19 | const { user } = event; 20 | 21 | console.log('logged in', user); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/server/mercury/src/consumer.ts: -------------------------------------------------------------------------------- 1 | import { Kafka } from 'kafkajs'; 2 | 3 | const kafka = new Kafka({ 4 | clientId: '@server/mercury', 5 | brokers: ['localhost:9092'], 6 | }); 7 | 8 | const consumer = kafka.consumer({ groupId: '@server/mercury' }); 9 | 10 | async function main(): Promise { 11 | await consumer.connect(); 12 | 13 | await consumer.subscribe({ 14 | topic: 'mercury.send-mail', 15 | fromBeginning: true, 16 | }); 17 | 18 | await consumer.run({ 19 | eachMessage: async ({ partition, message }) => { 20 | console.log({ 21 | partition, 22 | offset: message.offset, 23 | value: message.value.toString(), 24 | }); 25 | }, 26 | }); 27 | } 28 | 29 | main() 30 | .then(() => { 31 | console.log('Mercury running!'); 32 | }) 33 | .catch(console.error); 34 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/subscribers/AfterUserEmailChanged.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | import { IHandle } from '@server/shared/src/core/domain/events/IHandle'; 3 | 4 | import { UserEmailChangedEvent } from '../domain/events/UserEmailChangedEvent'; 5 | 6 | export class AfterUserEmailChanged implements IHandle { 7 | constructor() { 8 | this.setupSubscriptions(); 9 | } 10 | 11 | setupSubscriptions(): void { 12 | DomainEvents.register( 13 | this.onUserEmailChangedEvent.bind(this), 14 | UserEmailChangedEvent.name 15 | ); 16 | } 17 | 18 | private async onUserEmailChangedEvent( 19 | event: UserEmailChangedEvent 20 | ): Promise { 21 | const { user } = event; 22 | 23 | console.log(user); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/services/umbriel/implementations/UmbrielService.ts: -------------------------------------------------------------------------------- 1 | import client from '@infra/kafka/client'; 2 | import { Team } from '@modules/users/domain/Team'; 3 | import { User } from '@modules/users/domain/User'; 4 | 5 | import { IUmbrielService } from '../IUmbrielService'; 6 | 7 | export class UmbrielService implements IUmbrielService { 8 | async addUserToTeam(user: User, team: Team): Promise { 9 | const userWithTeamMessage = { 10 | user: { 11 | id: user.id.toValue(), 12 | email: user.email.value, 13 | }, 14 | team: { 15 | id: team.id.toValue(), 16 | title: team.title, 17 | }, 18 | }; 19 | 20 | await client.producer().send({ 21 | topic: 'umbriel.add-user-to-team', 22 | messages: [{ value: JSON.stringify(userWithTeamMessage) }], 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/repositories/fakes/FakeUserRepo.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@modules/users/domain/User'; 2 | import { UserEmail } from '@modules/users/domain/UserEmail'; 3 | 4 | import { IUserRepo } from '../IUserRepo'; 5 | 6 | export default class FakeUserRepo implements IUserRepo { 7 | private users: User[] = []; 8 | 9 | public async findByEmail(email: string | UserEmail): Promise { 10 | return this.users.find((user) => { 11 | return email instanceof UserEmail 12 | ? user.email.value === email.value 13 | : user.email.value === email; 14 | }); 15 | } 16 | 17 | public async findById(id: string): Promise { 18 | return this.users.find((user) => { 19 | return user.id.toValue() === id; 20 | }); 21 | } 22 | 23 | public async save(user: User): Promise { 24 | this.users.push(user); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/umbriel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/umbriel", 3 | "description": "Batch email platform", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "babel src --root-mode upward --extensions \".js,.ts\" --out-dir dist --copy-files --no-copy-ignored", 9 | "start": "node dist/index.js", 10 | "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpileOnly --no-notify src/index.ts", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@prisma/client": "^2.0.0", 15 | "@server/shared": "*", 16 | "dotenv": "^8.2.0", 17 | "express": "^4.17.1", 18 | "express-async-errors": "^3.1.1", 19 | "kafkajs": "^1.12.0" 20 | }, 21 | "devDependencies": { 22 | "@prisma/cli": "^2.0.0", 23 | "ts-node-dev": "^1.0.0-pre.44", 24 | "tsconfig-paths": "^3.9.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/Team.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@server/shared/src/core/domain/AggregateRoot'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | interface ITeamProps { 7 | title: string; 8 | } 9 | 10 | export class Team extends AggregateRoot { 11 | get title(): string { 12 | return this.props.title; 13 | } 14 | 15 | private constructor(props: ITeamProps, id?: UniqueEntityID) { 16 | super(props, id); 17 | } 18 | 19 | public static create(props: ITeamProps, id?: UniqueEntityID): Result { 20 | const guardResult = Guard.againstNullOrUndefined(props.title, 'title'); 21 | 22 | if (!guardResult.succeeded) { 23 | return Result.fail(guardResult.message); 24 | } 25 | 26 | const team = new Team(props, id); 27 | 28 | return Result.ok(team); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaContactRepo } from '@modules/contacts/infra/prisma/PrismaContactRepo'; 2 | import { PrismaContactSubscriptionsRepo } from '@modules/contacts/infra/prisma/PrismaContactSubscriptionsRepo'; 3 | import { PrismaTagRepo } from '@modules/contacts/infra/prisma/PrismaTagRepo'; 4 | 5 | import { SubscribeContactController } from './SubscribeContactController'; 6 | import { SubscribeContactUseCase } from './SubscribeContactUseCase'; 7 | 8 | const prismaContactSubscriptionRepo = new PrismaContactSubscriptionsRepo(); 9 | const prismaTagRepo = new PrismaTagRepo(); 10 | const prismaContactRepo = new PrismaContactRepo(prismaContactSubscriptionRepo); 11 | 12 | const subscribeContactUseCase = new SubscribeContactUseCase( 13 | prismaContactRepo, 14 | prismaTagRepo 15 | ); 16 | 17 | const subscribeContactController = new SubscribeContactController( 18 | subscribeContactUseCase 19 | ); 20 | 21 | export { subscribeContactUseCase, subscribeContactController }; 22 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/mappers/TagMap.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 2 | 3 | import { Tag as PersistenceTag } from '.prisma/client'; 4 | 5 | import { Tag } from '../domain/Tag'; 6 | import { TagSlug } from '../domain/TagSlug'; 7 | 8 | export class TagMap { 9 | static toDomain(raw: PersistenceTag): Tag { 10 | const tagSlugOrError = TagSlug.create(raw.slug); 11 | 12 | const tagOrError = Tag.create( 13 | { 14 | title: raw.title, 15 | slug: tagSlugOrError.getValue(), 16 | }, 17 | new UniqueEntityID(raw.id) 18 | ); 19 | 20 | if (tagOrError.isFailure) { 21 | console.log(tagOrError.error); 22 | } 23 | 24 | if (tagOrError.isSuccess) { 25 | return tagOrError.getValue(); 26 | } 27 | 28 | return null; 29 | } 30 | 31 | static toPersistence(tag: Tag): PersistenceTag { 32 | return { 33 | id: tag.id.toValue(), 34 | title: tag.title, 35 | slug: tag.slug.value, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/getContacts/GetContactsUseCase.ts: -------------------------------------------------------------------------------- 1 | import { IUseCase } from '@server/shared/src/core/domain/UseCase'; 2 | import * as GenericAppError from '@server/shared/src/core/logic/AppError'; 3 | import { 4 | Result, 5 | failure, 6 | Either, 7 | success, 8 | } from '@server/shared/src/core/logic/Result'; 9 | 10 | import { Contact } from '@modules/contacts/domain/Contact'; 11 | import { IContactRepo } from '@modules/contacts/repositories/IContactRepo'; 12 | 13 | type Response = Either>; 14 | 15 | export class GetContactsUseCase implements IUseCase> { 16 | private contactRepo: IContactRepo; 17 | 18 | constructor(contactRepo: IContactRepo) { 19 | this.contactRepo = contactRepo; 20 | } 21 | 22 | async execute(): Promise { 23 | try { 24 | const contacts = await this.contactRepo.getContacts(); 25 | 26 | return success(Result.ok(contacts)); 27 | } catch (err) { 28 | return failure(new GenericAppError.UnexpectedError(err)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/server/atlas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@server/atlas", 3 | "description": "Information about users permissions and settings.", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "babel src --root-mode upward --extensions \".js,.ts\" --out-dir dist --copy-files --no-copy-ignored", 9 | "start": "node dist/index.js", 10 | "dev": "ts-node-dev -r tsconfig-paths/register --respawn --transpileOnly --no-notify src/index.ts", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@server/shared": "*", 15 | "@prisma/client": "^2.0.0", 16 | "bcryptjs": "^2.4.3", 17 | "dotenv": "^8.2.0", 18 | "express": "^4.17.1", 19 | "express-async-errors": "^3.1.1", 20 | "jsonwebtoken": "^8.5.1", 21 | "kafkajs": "^1.12.0" 22 | }, 23 | "devDependencies": { 24 | "@prisma/cli": "^2.0.0", 25 | "@types/bcryptjs": "^2.4.2", 26 | "@types/express": "^4.17.6", 27 | "@types/jsonwebtoken": "^8.5.0", 28 | "@types/node": "^14.0.1", 29 | "ts-node-dev": "^1.0.0-pre.44", 30 | "typescript": "^3.9.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/TagSlug.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@server/shared/src/core/domain/ValueObject'; 2 | import { Result } from '@server/shared/src/core/logic/Result'; 3 | 4 | export interface ITagSlugProps { 5 | value: string; 6 | } 7 | 8 | export class TagSlug extends ValueObject { 9 | get value(): string { 10 | return this.props.value; 11 | } 12 | 13 | private constructor(props: ITagSlugProps) { 14 | super(props); 15 | } 16 | 17 | private static format(slug: string) { 18 | return slug 19 | .toLocaleLowerCase() 20 | .replace(/[^\w ^-]+/g, '') 21 | .replace(/ +/g, '-'); 22 | } 23 | 24 | public static createFromIntegration( 25 | title: string, 26 | integrationId?: string 27 | ): Result { 28 | const slug = this.format(title).concat( 29 | integrationId ? `-${integrationId}` : '' 30 | ); 31 | 32 | return Result.ok(new TagSlug({ value: slug })); 33 | } 34 | 35 | public static create(slug: string): Result { 36 | return Result.ok(new TagSlug({ value: this.format(slug) })); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/UserEmail.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@server/shared/src/core/domain/ValueObject'; 2 | import { Result } from '@server/shared/src/core/logic/Result'; 3 | 4 | export interface IUserEmailProps { 5 | value: string; 6 | } 7 | 8 | export class UserEmail extends ValueObject { 9 | get value(): string { 10 | return this.props.value; 11 | } 12 | 13 | private constructor(props: IUserEmailProps) { 14 | super(props); 15 | } 16 | 17 | private static isValidEmail(email: string) { 18 | const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 19 | return regex.test(email); 20 | } 21 | 22 | private static format(email: string) { 23 | return email.trim().toLowerCase(); 24 | } 25 | 26 | public static create(email: string): Result { 27 | if (!this.isValidEmail(email)) { 28 | return Result.fail('Email address not valid'); 29 | } 30 | 31 | return Result.ok(new UserEmail({ value: this.format(email) })); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200515170312-create-users/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200515170312-create-users` 2 | 3 | This migration has been generated by Diego Fernandes at 5/15/2020, 5:03:12 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "public"."users" ( 10 | "email" text NOT NULL ,"id" text NOT NULL ,"name" text NOT NULL ,"password" text NOT NULL , 11 | PRIMARY KEY ("id")) 12 | 13 | CREATE UNIQUE INDEX "users.email" ON "public"."users"("email") 14 | ``` 15 | 16 | ## Changes 17 | 18 | ```diff 19 | diff --git schema.prisma schema.prisma 20 | migration ..20200515170312-create-users 21 | --- datamodel.dml 22 | +++ datamodel.dml 23 | @@ -1,0 +1,17 @@ 24 | +datasource db { 25 | + provider = "postgresql" 26 | + url = env("DATABASE_URL") 27 | +} 28 | + 29 | +generator client { 30 | + provider = "prisma-client-js" 31 | +} 32 | + 33 | +model User { 34 | + id String @default(uuid()) @id 35 | + email String @unique 36 | + name String 37 | + password String 38 | + 39 | + @@map(name: "users") 40 | +} 41 | ``` 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/subscribers/AfterUserCreated.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | import { IHandle } from '@server/shared/src/core/domain/events/IHandle'; 3 | 4 | import { UserCreatedEvent } from '../domain/events/UserCreatedEvent'; 5 | import { SubscribeUserToMailingUseCase } from '../useCases/subscribeUserToMailing/SubscribeUserToMailingUseCase'; 6 | 7 | export class AfterUserCreated implements IHandle { 8 | private subscribeUserToMailing: SubscribeUserToMailingUseCase; 9 | 10 | constructor(subscribeUserToMailing: SubscribeUserToMailingUseCase) { 11 | this.setupSubscriptions(); 12 | 13 | this.subscribeUserToMailing = subscribeUserToMailing; 14 | } 15 | 16 | setupSubscriptions(): void { 17 | DomainEvents.register( 18 | this.onUserCreatedEvent.bind(this), 19 | UserCreatedEvent.name 20 | ); 21 | } 22 | 23 | private async onUserCreatedEvent(event: UserCreatedEvent): Promise { 24 | const { user } = event; 25 | 26 | this.subscribeUserToMailing.execute({ 27 | userId: user.id.toValue(), 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/ContactEmail.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@server/shared/src/core/domain/ValueObject'; 2 | import { Result } from '@server/shared/src/core/logic/Result'; 3 | 4 | export interface IContactEmailProps { 5 | value: string; 6 | } 7 | 8 | export class ContactEmail extends ValueObject { 9 | get value(): string { 10 | return this.props.value; 11 | } 12 | 13 | private constructor(props: IContactEmailProps) { 14 | super(props); 15 | } 16 | 17 | private static isValidEmail(email: string) { 18 | const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 19 | return regex.test(email); 20 | } 21 | 22 | private static format(email: string) { 23 | return email.trim().toLowerCase(); 24 | } 25 | 26 | public static create(email: string): Result { 27 | if (!this.isValidEmail(email)) { 28 | return Result.fail('Email address not valid'); 29 | } 30 | 31 | return Result.ok(new ContactEmail({ value: this.format(email) })); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/getContacts/GetContactsController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from '@server/shared/src/core/infra/BaseController'; 2 | 3 | import { ContactMap } from '@modules/contacts/mappers/ContactMap'; 4 | 5 | import { GetContactsUseCase } from './GetContactsUseCase'; 6 | 7 | export class GetContactsController extends BaseController { 8 | private useCase: GetContactsUseCase; 9 | 10 | constructor(useCase: GetContactsUseCase) { 11 | super(); 12 | 13 | this.useCase = useCase; 14 | } 15 | 16 | async executeImpl(): Promise { 17 | try { 18 | const result = await this.useCase.execute(); 19 | 20 | if (result.isFailure()) { 21 | const error = result.value; 22 | 23 | switch (error.constructor) { 24 | default: 25 | return this.fail(error.errorValue().message); 26 | } 27 | } else { 28 | const contacts = result.value.getValue(); 29 | 30 | return this.ok({ 31 | contacts: contacts.map((c) => ContactMap.toDTO(c)), 32 | }); 33 | } 34 | } catch (err) { 35 | return this.fail(err); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200611222402-add_slug_to_tags/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200611222402-add_slug_to_tags` 2 | 3 | This migration has been generated by Diego Fernandes at 6/11/2020, 10:24:02 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | ALTER TABLE "public"."tags" ADD COLUMN "slug" text NOT NULL ; 10 | 11 | CREATE UNIQUE INDEX "tags.slug" ON "public"."tags"("slug") 12 | ``` 13 | 14 | ## Changes 15 | 16 | ```diff 17 | diff --git schema.prisma schema.prisma 18 | migration 20200609124237-create_base_templates..20200611222402-add_slug_to_tags 19 | --- datamodel.dml 20 | +++ datamodel.dml 21 | @@ -2,9 +2,9 @@ 22 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 23 | datasource db { 24 | provider = "postgresql" 25 | - url = "***" 26 | + url = env("DATABASE_URL") 27 | } 28 | generator client { 29 | provider = "prisma-client-js" 30 | @@ -38,8 +38,9 @@ 31 | model Tag { 32 | @@map(name: "tags") 33 | id String @default(uuid()) @id 34 | + slug String @unique 35 | title String 36 | contacts ContactTags[] 37 | } 38 | ``` 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/createUser/CreateUserController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from '@server/shared/src/core/infra/BaseController'; 2 | 3 | import * as CreateUserErrors from './CreateUserErrors'; 4 | import { CreateUserUseCase } from './CreateUserUseCase'; 5 | import { ICreateUserRequestDTO } from './ICreateUserDTO'; 6 | 7 | export class CreateUserController extends BaseController { 8 | private useCase: CreateUserUseCase; 9 | 10 | constructor(useCase: CreateUserUseCase) { 11 | super(); 12 | 13 | this.useCase = useCase; 14 | } 15 | 16 | protected async executeImpl(): Promise { 17 | const dto = this.request.body as ICreateUserRequestDTO; 18 | 19 | try { 20 | const result = await this.useCase.execute(dto); 21 | 22 | if (result.isFailure()) { 23 | const error = result.value; 24 | 25 | switch (error.constructor) { 26 | case CreateUserErrors.AccountAlreadyExists: 27 | return this.conflict(error.errorValue().message); 28 | default: 29 | return this.fail(error.errorValue().message); 30 | } 31 | } else { 32 | return this.created(); 33 | } 34 | } catch (err) { 35 | return this.fail(err); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/JWT.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | import authConfig from '@config/auth'; 7 | 8 | interface IJWTProps { 9 | sub: string; 10 | } 11 | 12 | interface IJWTPayload { 13 | name: string; 14 | email: string; 15 | } 16 | 17 | export class JWT { 18 | public sub: string; 19 | 20 | public token: string; 21 | 22 | private constructor(props: IJWTProps) { 23 | this.sub = props.sub; 24 | } 25 | 26 | private static signJwt(props: IJWTProps, payload: IJWTPayload) { 27 | return jwt.sign(payload, authConfig.secret, { 28 | subject: props.sub, 29 | expiresIn: authConfig.tokenExpiryTimeInSeconds, 30 | }); 31 | } 32 | 33 | public static create(props: IJWTProps, payload: IJWTPayload): Result { 34 | const guardResult = Guard.againstNullOrUndefined(props.sub, 'sub'); 35 | 36 | if (!guardResult.succeeded) { 37 | return Result.fail(guardResult.message); 38 | } 39 | 40 | const signedToken = this.signJwt(props, payload); 41 | const jwtToken = new JWT(props); 42 | 43 | jwtToken.token = signedToken; 44 | 45 | return Result.ok(jwtToken); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/Tag.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@server/shared/src/core/domain/AggregateRoot'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | import { TagSlug } from './TagSlug'; 7 | 8 | interface ITagProps { 9 | title: string; 10 | slug: TagSlug; 11 | } 12 | 13 | export class Tag extends AggregateRoot { 14 | get title(): string { 15 | return this.props.title; 16 | } 17 | 18 | get slug(): TagSlug { 19 | return this.props.slug; 20 | } 21 | 22 | private constructor(props: ITagProps, id?: UniqueEntityID) { 23 | super(props, id); 24 | } 25 | 26 | public static create(props: ITagProps, id?: UniqueEntityID): Result { 27 | const guardedProps = [ 28 | { argument: props.title, argumentName: 'title' }, 29 | { argument: props.slug, argumentName: 'slug' }, 30 | ]; 31 | 32 | const guardResult = Guard.againstNullOrUndefinedBulk(guardedProps); 33 | 34 | if (!guardResult.succeeded) { 35 | return Result.fail(guardResult.message); 36 | } 37 | 38 | const tag = new Tag(props, id); 39 | 40 | return Result.ok(tag); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/mappers/UserMap.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 2 | 3 | import { User as PersistenceUser } from '.prisma/client'; 4 | 5 | import { User } from '../domain/User'; 6 | import { UserEmail } from '../domain/UserEmail'; 7 | import { UserPassword } from '../domain/UserPassword'; 8 | 9 | export class UserMap { 10 | public static toDomain(raw: PersistenceUser): User { 11 | const userEmailOrError = UserEmail.create(raw.email); 12 | const userPasswordOrError = UserPassword.create({ 13 | value: raw.password, 14 | hashed: true, 15 | }); 16 | 17 | const userOrError = User.create( 18 | { 19 | name: raw.name, 20 | email: userEmailOrError.getValue(), 21 | password: userPasswordOrError.getValue(), 22 | }, 23 | new UniqueEntityID(raw.id) 24 | ); 25 | 26 | if (userOrError.isFailure) { 27 | console.log(userOrError.error); 28 | } 29 | 30 | if (userOrError.isSuccess) { 31 | return userOrError.getValue(); 32 | } 33 | 34 | return null; 35 | } 36 | 37 | public static async toPersistence(user: User): Promise { 38 | return { 39 | id: user.id.toString(), 40 | name: user.name, 41 | email: user.email.value, 42 | password: await user.password.getHashedValue(), 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/mappers/ContactMap.ts: -------------------------------------------------------------------------------- 1 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 2 | 3 | import { Contact as PersistenceContact } from '.prisma/client'; 4 | 5 | import { Contact } from '../domain/Contact'; 6 | import { ContactEmail } from '../domain/ContactEmail'; 7 | import { IGetContactsResponseDTO } from '../useCases/contacts/getContacts/GetContactsDTO'; 8 | 9 | export class ContactMap { 10 | static toDomain(raw: PersistenceContact): Contact { 11 | const contactEmailOrError = ContactEmail.create(raw.email); 12 | 13 | const userOrError = Contact.create( 14 | { 15 | email: contactEmailOrError.getValue(), 16 | }, 17 | new UniqueEntityID(raw.id) 18 | ); 19 | 20 | if (userOrError.isFailure) { 21 | console.log(userOrError.error); 22 | } 23 | 24 | if (userOrError.isSuccess) { 25 | return userOrError.getValue(); 26 | } 27 | 28 | return null; 29 | } 30 | 31 | static toPersistence( 32 | contact: Contact 33 | ): Pick { 34 | return { 35 | id: contact.id.toValue(), 36 | email: contact.email.value, 37 | }; 38 | } 39 | 40 | static toDTO(contact: Contact): IGetContactsResponseDTO { 41 | return { 42 | id: contact.id.toValue(), 43 | email: contact.email.value, 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/LoginController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from '@server/shared/src/core/infra/BaseController'; 2 | 3 | import { ILoginRequestDTO } from './ILoginDTO'; 4 | import * as LoginErrors from './LoginErrors'; 5 | import { LoginUseCase } from './LoginUseCase'; 6 | 7 | export class LoginController extends BaseController { 8 | private useCase: LoginUseCase; 9 | 10 | constructor(useCase: LoginUseCase) { 11 | super(); 12 | 13 | this.useCase = useCase; 14 | } 15 | 16 | async executeImpl(): Promise { 17 | const dto = this.request.body as ILoginRequestDTO; 18 | 19 | try { 20 | const result = await this.useCase.execute(dto); 21 | 22 | if (result.isFailure()) { 23 | const error = result.value; 24 | 25 | switch (error.constructor) { 26 | case LoginErrors.UserNameDoesntExistError: 27 | return this.notFound(error.errorValue().message); 28 | case LoginErrors.PasswordDoesntMatchError: 29 | return this.clientError(error.errorValue().message); 30 | default: 31 | return this.fail(error.errorValue().message); 32 | } 33 | } else { 34 | const loginResponse = result.value.getValue(); 35 | 36 | return this.ok(loginResponse); 37 | } 38 | } catch (err) { 39 | return this.fail(err); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603231715-create_teams_and_addresses/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | @@map(name: "users") 12 | 13 | id String @default(uuid()) @id 14 | email String @unique 15 | name String 16 | password String 17 | teams Team[] @relation(references: [id]) 18 | addresses UserAddress[] 19 | } 20 | 21 | model UserAddress { 22 | @@map(name: "user_addresses") 23 | 24 | id String @default(uuid()) @id 25 | country String 26 | foreignCountry Boolean @default(false) 27 | postalCode String 28 | streetName String 29 | number String 30 | complement String 31 | neighborhood String 32 | city String 33 | state String 34 | type String @default("shipping") 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @default(now()) 37 | User User? @relation(fields: [userId], references: [id]) 38 | userId String? 39 | } 40 | 41 | model Team { 42 | @@map(name: "teams") 43 | 44 | id String @default(uuid()) @id 45 | title String 46 | createdAt DateTime @default(now()) 47 | updatedAt DateTime @default(now()) 48 | users User[] @relation(references: [id]) 49 | } 50 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/kafka/consumers/AddUserToTeamConsumer.ts: -------------------------------------------------------------------------------- 1 | import { convertTextToSlug } from '@server/shared/src/utils/convertTextToSlug'; 2 | 3 | import { kafka } from '@infra/kafka/client'; 4 | import { subscribeContactUseCase } from '@modules/contacts/useCases/contacts/subscribeContact'; 5 | 6 | interface IMessage { 7 | user: { 8 | id: string; 9 | email: string; 10 | }; 11 | teams: Array<{ 12 | id: string; 13 | title: string; 14 | }>; 15 | } 16 | 17 | export class AddUserToTeamConsumer { 18 | constructor() { 19 | // this.setupConsumer(); 20 | } 21 | 22 | async setupConsumer(): Promise { 23 | const consumer = kafka.consumer({ 24 | groupId: 'umbriel', 25 | }); 26 | 27 | await consumer.connect(); 28 | await consumer.subscribe({ 29 | topic: 'umbriel.add-user-to-team', 30 | fromBeginning: true, 31 | }); 32 | 33 | await consumer.run({ 34 | async eachMessage({ message }) { 35 | const data: IMessage = JSON.parse(message.value.toString()); 36 | 37 | await subscribeContactUseCase.execute({ 38 | email: data.user.email, 39 | tags: data.teams.map((team) => { 40 | return { 41 | title: team.title, 42 | integrationId: team.id, 43 | }; 44 | }), 45 | }); 46 | }, 47 | }); 48 | 49 | console.log('Listening to messages!'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/infra/prisma/PrismaUserRepo.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | 3 | import { prisma } from '@infra/prisma/client'; 4 | import { User } from '@modules/users/domain/User'; 5 | import { UserEmail } from '@modules/users/domain/UserEmail'; 6 | import { UserMap } from '@modules/users/mappers/UserMap'; 7 | import { IUserRepo } from '@modules/users/repositories/IUserRepo'; 8 | 9 | export class PrismaUserRepo implements IUserRepo { 10 | async findByEmail(email: string | UserEmail): Promise { 11 | const rawUser = await prisma.user.findOne({ 12 | where: { 13 | email: email instanceof UserEmail ? email.value : email, 14 | }, 15 | }); 16 | 17 | if (!rawUser) { 18 | return null; 19 | } 20 | 21 | return UserMap.toDomain(rawUser); 22 | } 23 | 24 | async findById(id: string): Promise { 25 | const rawUser = await prisma.user.findOne({ 26 | where: { id }, 27 | }); 28 | 29 | if (!rawUser) { 30 | return null; 31 | } 32 | 33 | return UserMap.toDomain(rawUser); 34 | } 35 | 36 | async save(user: User): Promise { 37 | const data = await UserMap.toPersistence(user); 38 | 39 | await prisma.user.upsert({ 40 | where: { email: user.email.value }, 41 | update: data, 42 | create: data, 43 | }); 44 | 45 | DomainEvents.dispatchEventsForAggregate(user.id); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/prisma/PrismaTagRepo.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | 3 | import { prisma } from '@infra/prisma/client'; 4 | import { Tag } from '@modules/contacts/domain/Tag'; 5 | import { TagSlug } from '@modules/contacts/domain/TagSlug'; 6 | import { TagMap } from '@modules/contacts/mappers/TagMap'; 7 | import { ITagRepo } from '@modules/contacts/repositories/ITagRepo'; 8 | 9 | export class PrismaTagRepo implements ITagRepo { 10 | async findTagBySlug(slug: TagSlug): Promise { 11 | const tag = await prisma.tag.findOne({ 12 | where: { 13 | slug: slug.value, 14 | }, 15 | }); 16 | 17 | if (!tag) { 18 | return null; 19 | } 20 | 21 | return TagMap.toDomain(tag); 22 | } 23 | 24 | async findTagBySlugBulk(slugs: TagSlug[]): Promise { 25 | const tags = await Promise.all( 26 | slugs.map((slug) => this.findTagBySlug(slug)) 27 | ); 28 | 29 | return tags.filter((tag) => !!tag === true); 30 | } 31 | 32 | async save(tag: Tag): Promise { 33 | const tagData = TagMap.toPersistence(tag); 34 | 35 | await prisma.tag.upsert({ 36 | where: { id: tagData.id }, 37 | update: tagData, 38 | create: tagData, 39 | }); 40 | 41 | DomainEvents.dispatchEventsForAggregate(tag.id); 42 | } 43 | 44 | async saveBulk(tags: Tag[]): Promise { 45 | await Promise.all(tags.map((tag) => this.save(tag))); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/AggregateRoot.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from './Entity'; 2 | import { DomainEvents } from './events/DomainEvents'; 3 | import { IDomainEvent } from './events/IDomainEvent'; 4 | import { UniqueEntityID } from './UniqueEntityID'; 5 | 6 | export abstract class AggregateRoot extends Entity { 7 | private _domainEvents: IDomainEvent[] = []; 8 | 9 | get id(): UniqueEntityID { 10 | return this._id; 11 | } 12 | 13 | get domainEvents(): IDomainEvent[] { 14 | return this._domainEvents; 15 | } 16 | 17 | protected addDomainEvent(domainEvent: IDomainEvent): void { 18 | // Add the domain event to this aggregate's list of domain events 19 | this._domainEvents.push(domainEvent); 20 | // Add this aggregate instance to the domain event's list of aggregates who's 21 | // events it eventually needs to dispatch. 22 | DomainEvents.markAggregateForDispatch(this); 23 | // Log the domain event 24 | this.logDomainEventAdded(domainEvent); 25 | } 26 | 27 | public clearEvents(): void { 28 | this._domainEvents.splice(0, this._domainEvents.length); 29 | } 30 | 31 | private logDomainEventAdded(domainEvent: IDomainEvent): void { 32 | const thisClass = Reflect.getPrototypeOf(this); 33 | const domainEventClass = Reflect.getPrototypeOf(domainEvent); 34 | 35 | console.info( 36 | `[Domain Event Created]:`, 37 | thisClass.constructor.name, 38 | '==>', 39 | domainEventClass.constructor.name 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/SubscribeContactController.ts: -------------------------------------------------------------------------------- 1 | import { BaseController } from '@server/shared/src/core/infra/BaseController'; 2 | 3 | import { ISubscribeContactRequestDTO } from './SubscribeContactDTO'; 4 | import * as SubscribeContactErrors from './SubscribeContactErrors'; 5 | import { SubscribeContactUseCase } from './SubscribeContactUseCase'; 6 | 7 | export class SubscribeContactController extends BaseController { 8 | private useCase: SubscribeContactUseCase; 9 | 10 | constructor(useCase: SubscribeContactUseCase) { 11 | super(); 12 | 13 | this.useCase = useCase; 14 | } 15 | 16 | async executeImpl(): Promise { 17 | try { 18 | const { email, tags } = this.request.body; 19 | 20 | const dto: ISubscribeContactRequestDTO = { 21 | email, 22 | tags: tags.map((tag: string) => { 23 | return { 24 | title: tag, 25 | }; 26 | }), 27 | }; 28 | 29 | const result = await this.useCase.execute(dto); 30 | 31 | if (result.isFailure()) { 32 | const error = result.value; 33 | 34 | switch (error.constructor) { 35 | case SubscribeContactErrors.ContactAlreadyExists: 36 | return this.conflict(error.errorValue().message); 37 | default: 38 | return this.fail(error.errorValue().message); 39 | } 40 | } else { 41 | return this.created(); 42 | } 43 | } catch (err) { 44 | return this.fail(err); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-skylab", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Diego Fernandes ", 7 | "license": "MIT", 8 | "workspaces": [ 9 | "packages/server/*" 10 | ], 11 | "nohoist": ["**/@prisma", "**/.prisma"], 12 | "scripts": { 13 | "build:server": "lerna run build --scope @server/* --stream", 14 | "lint:fix": "eslint --fix packages --ext ts,tsx", 15 | "test": "jest" 16 | }, 17 | "lint-staged": { 18 | "*.ts, *.tsx": [ 19 | "eslint --fix" 20 | ] 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "lint-staged" 25 | } 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.10.1", 29 | "@babel/core": "^7.10.2", 30 | "@babel/node": "^7.10.1", 31 | "@babel/preset-env": "^7.10.2", 32 | "@babel/preset-typescript": "^7.10.1", 33 | "@types/jest": "^25.2.3", 34 | "@typescript-eslint/eslint-plugin": "^3.2.0", 35 | "@typescript-eslint/parser": "^3.2.0", 36 | "babel-plugin-module-resolver": "^4.0.0", 37 | "eslint": "^7.2.0", 38 | "eslint-config-airbnb-base": "^14.1.0", 39 | "eslint-config-prettier": "^6.11.0", 40 | "eslint-import-resolver-typescript": "^2.0.0", 41 | "eslint-plugin-import": "^2.20.1", 42 | "eslint-plugin-import-helpers": "^1.0.2", 43 | "eslint-plugin-prettier": "^3.1.3", 44 | "husky": "^4.2.5", 45 | "jest": "^26.0.1", 46 | "lerna": "^3.21.0", 47 | "lint-staged": "^10.2.9", 48 | "prettier": "^2.0.5", 49 | "ts-jest": "^26.1.0", 50 | "typescript": "^3.9.3" 51 | }, 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/ContactSubscription.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@server/shared/src/core/domain/AggregateRoot'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | interface IContactSubscriptionProps { 7 | contactId: string; 8 | tagId: string; 9 | subscribed?: boolean; 10 | } 11 | 12 | export class ContactSubscription extends AggregateRoot< 13 | IContactSubscriptionProps 14 | > { 15 | get contactId(): string { 16 | return this.props.contactId; 17 | } 18 | 19 | get tagId(): string { 20 | return this.props.tagId; 21 | } 22 | 23 | get subscribed(): boolean { 24 | return this.props.subscribed; 25 | } 26 | 27 | private constructor(props: IContactSubscriptionProps, id?: UniqueEntityID) { 28 | super(props, id); 29 | } 30 | 31 | public static create( 32 | props: IContactSubscriptionProps, 33 | id?: UniqueEntityID 34 | ): Result { 35 | const guardedProps = [ 36 | { argument: props.contactId, argumentName: 'contactId' }, 37 | { argument: props.tagId, argumentName: 'tagId' }, 38 | ]; 39 | 40 | const guardResult = Guard.againstNullOrUndefinedBulk(guardedProps); 41 | 42 | if (!guardResult.succeeded) { 43 | return Result.fail(guardResult.message); 44 | } 45 | 46 | const subscription = new ContactSubscription( 47 | { 48 | ...props, 49 | subscribed: props.subscribed || true, 50 | }, 51 | id 52 | ); 53 | 54 | return Result.ok(subscription); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/UserPassword.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | import { ValueObject } from '@server/shared/src/core/domain/ValueObject'; 4 | import { Guard } from '@server/shared/src/core/logic/Guard'; 5 | import { Result } from '@server/shared/src/core/logic/Result'; 6 | 7 | interface IUserPasswordProps { 8 | value: string; 9 | hashed?: boolean; 10 | } 11 | 12 | export class UserPassword extends ValueObject { 13 | get value(): string { 14 | return this.props.value; 15 | } 16 | 17 | private constructor(props: IUserPasswordProps) { 18 | super(props); 19 | } 20 | 21 | public isAlreadyHashed(): boolean { 22 | return this.props.hashed; 23 | } 24 | 25 | public async comparePassword(plainTextPassword: string): Promise { 26 | let hashed: string; 27 | 28 | if (this.isAlreadyHashed()) { 29 | hashed = this.props.value; 30 | 31 | return bcrypt.compare(plainTextPassword, hashed); 32 | } 33 | 34 | return this.props.value === plainTextPassword; 35 | } 36 | 37 | public async getHashedValue(): Promise { 38 | if (this.isAlreadyHashed()) { 39 | return this.props.value; 40 | } 41 | 42 | return bcrypt.hash(this.props.value, 8); 43 | } 44 | 45 | public static create({ 46 | value, 47 | hashed, 48 | }: IUserPasswordProps): Result { 49 | const propsResult = Guard.againstNullOrUndefined(value, 'password'); 50 | 51 | if (!propsResult.succeeded) { 52 | return Result.fail(propsResult.message); 53 | } 54 | 55 | const userPassword = new UserPassword({ 56 | value, 57 | hashed, 58 | }); 59 | 60 | return Result.ok(userPassword); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | @@map(name: "users") 12 | 13 | id String @default(uuid()) @id 14 | email String @unique 15 | name String 16 | password String 17 | teams Team[] @relation(references: [id]) 18 | addresses UserAddress[] 19 | UserEmailHistory UserEmailHistory[] 20 | } 21 | 22 | model UserAddress { 23 | @@map(name: "user_addresses") 24 | 25 | id String @default(uuid()) @id 26 | country String 27 | foreignCountry Boolean @default(false) 28 | postalCode String 29 | streetName String 30 | number String 31 | complement String 32 | neighborhood String 33 | city String 34 | state String 35 | type String @default("shipping") 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @default(now()) 38 | User User @relation(fields: [userId], references: [id]) 39 | userId String 40 | } 41 | 42 | model UserEmailHistory { 43 | @@map(name: "user_email_history") 44 | 45 | id String @default(uuid()) @id 46 | email String 47 | changedAt DateTime @default(now()) 48 | User User @relation(fields: [userId], references: [id]) 49 | userId String 50 | } 51 | 52 | model Team { 53 | @@map(name: "teams") 54 | 55 | id String @default(uuid()) @id 56 | title String 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @default(now()) 59 | users User[] @relation(references: [id]) 60 | } 61 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200604122553-add_cascade_on_deletes/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | @@map(name: "users") 12 | 13 | id String @default(uuid()) @id 14 | email String @unique 15 | name String 16 | password String 17 | teams Team[] @relation(references: [id]) 18 | addresses UserAddress[] 19 | UserEmailHistory UserEmailHistory[] 20 | } 21 | 22 | model UserAddress { 23 | @@map(name: "user_addresses") 24 | 25 | id String @default(uuid()) @id 26 | country String 27 | foreignCountry Boolean @default(false) 28 | postalCode String 29 | streetName String 30 | number String 31 | complement String 32 | neighborhood String 33 | city String 34 | state String 35 | type String @default("shipping") 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @default(now()) 38 | User User @relation(fields: [userId], references: [id]) 39 | userId String 40 | } 41 | 42 | model UserEmailHistory { 43 | @@map(name: "user_email_history") 44 | 45 | id String @default(uuid()) @id 46 | email String 47 | changedAt DateTime @default(now()) 48 | User User @relation(fields: [userId], references: [id]) 49 | userId String 50 | } 51 | 52 | model Team { 53 | @@map(name: "teams") 54 | 55 | id String @default(uuid()) @id 56 | title String 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @default(now()) 59 | users User[] @relation(references: [id]) 60 | } 61 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603234442-create_user_email_history/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = "***" 4 | } 5 | 6 | generator client { 7 | provider = "prisma-client-js" 8 | } 9 | 10 | model User { 11 | @@map(name: "users") 12 | 13 | id String @default(uuid()) @id 14 | email String @unique 15 | name String 16 | password String 17 | teams Team[] @relation(references: [id]) 18 | addresses UserAddress[] 19 | UserEmailHistory UserEmailHistory[] 20 | } 21 | 22 | model UserAddress { 23 | @@map(name: "user_addresses") 24 | 25 | id String @default(uuid()) @id 26 | country String 27 | foreignCountry Boolean @default(false) 28 | postalCode String 29 | streetName String 30 | number String 31 | complement String 32 | neighborhood String 33 | city String 34 | state String 35 | type String @default("shipping") 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @default(now()) 38 | User User? @relation(fields: [userId], references: [id]) 39 | userId String? 40 | } 41 | 42 | model UserEmailHistory { 43 | @@map(name: "user_email_history") 44 | 45 | id String @default(uuid()) @id 46 | email String 47 | changedAt DateTime @default(now()) 48 | User User? @relation(fields: [userId], references: [id]) 49 | userId String? 50 | } 51 | 52 | model Team { 53 | @@map(name: "teams") 54 | 55 | id String @default(uuid()) @id 56 | title String 57 | createdAt DateTime @default(now()) 58 | updatedAt DateTime @default(now()) 59 | users User[] @relation(references: [id]) 60 | } 61 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/subscribeUserToMailing/SubscribeUserToMailingUseCase.ts: -------------------------------------------------------------------------------- 1 | import { IUseCase } from '@server/shared/src/core/domain/UseCase'; 2 | import * as GenericAppError from '@server/shared/src/core/logic/AppError'; 3 | import { 4 | Result, 5 | failure, 6 | Either, 7 | success, 8 | } from '@server/shared/src/core/logic/Result'; 9 | 10 | import { Team } from '@modules/users/domain/Team'; 11 | import { IUserRepo } from '@modules/users/repositories/IUserRepo'; 12 | import { IUmbrielService } from '@modules/users/services/umbriel/IUmbrielService'; 13 | 14 | import { ISubscribeUserToMailingRequestDTO } from './ISubscribeUserToMailingDTO'; 15 | import * as SubcribeUserToMailingErrors from './SubscribeUserToMailingErrors'; 16 | 17 | type Response = Either>; 18 | 19 | export class SubscribeUserToMailingUseCase 20 | implements IUseCase> { 21 | private userRepo: IUserRepo; 22 | 23 | private umbrielService: IUmbrielService; 24 | 25 | constructor(userRepo: IUserRepo, umbrielService: IUmbrielService) { 26 | this.userRepo = userRepo; 27 | this.umbrielService = umbrielService; 28 | } 29 | 30 | async execute(request: ISubscribeUserToMailingRequestDTO): Promise { 31 | try { 32 | const { userId } = request; 33 | 34 | const user = await this.userRepo.findById(userId); 35 | 36 | const userFound = !!user; 37 | 38 | if (!userFound) { 39 | return failure(new SubcribeUserToMailingErrors.UserNotFoundError()); 40 | } 41 | 42 | const teamOrError = Team.create({ 43 | title: 'Novo time', 44 | }); 45 | 46 | if (teamOrError.isFailure) { 47 | return failure(Result.fail(teamOrError.error)); 48 | } 49 | 50 | const team = teamOrError.getValue(); 51 | 52 | await this.umbrielService.addUserToTeam(user, team); 53 | 54 | return success(Result.ok()); 55 | } catch (err) { 56 | return failure(new GenericAppError.UnexpectedError(err)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/domain/Contact.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@server/shared/src/core/domain/AggregateRoot'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | import { ContactEmail } from './ContactEmail'; 7 | import { ContactSubscription } from './ContactSubscription'; 8 | import { ContactSubscriptions } from './ContactSubscriptions'; 9 | import { Tag } from './Tag'; 10 | 11 | interface IContactProps { 12 | email: ContactEmail; 13 | subscriptions?: ContactSubscriptions; 14 | } 15 | 16 | export class Contact extends AggregateRoot { 17 | get email(): ContactEmail { 18 | return this.props.email; 19 | } 20 | 21 | get subscriptions(): ContactSubscriptions { 22 | return this.props.subscriptions; 23 | } 24 | 25 | private constructor(props: IContactProps, id?: UniqueEntityID) { 26 | super(props, id); 27 | } 28 | 29 | public addSubscription(tag: Tag): Result { 30 | const subscriptionOrError = ContactSubscription.create({ 31 | contactId: this.id.toValue(), 32 | tagId: tag.id.toValue(), 33 | }); 34 | 35 | if (subscriptionOrError.isFailure) { 36 | return Result.fail(subscriptionOrError.error); 37 | } 38 | 39 | this.props.subscriptions.add(subscriptionOrError.getValue()); 40 | 41 | return Result.ok(); 42 | } 43 | 44 | public static create( 45 | props: IContactProps, 46 | id?: UniqueEntityID 47 | ): Result { 48 | const guardedProps = [{ argument: props.email, argumentName: 'email' }]; 49 | 50 | const guardResult = Guard.againstNullOrUndefinedBulk(guardedProps); 51 | 52 | if (!guardResult.succeeded) { 53 | return Result.fail(guardResult.message); 54 | } 55 | 56 | const defaultValues: IContactProps = { 57 | ...props, 58 | subscriptions: props.subscriptions || ContactSubscriptions.create([]), 59 | }; 60 | 61 | const contact = new Contact(defaultValues, id); 62 | 63 | return Result.ok(contact); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/prisma/PrismaContactRepo.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | 3 | import { prisma } from '@infra/prisma/client'; 4 | import { Contact } from '@modules/contacts/domain/Contact'; 5 | import { ContactEmail } from '@modules/contacts/domain/ContactEmail'; 6 | import { ContactSubscriptions } from '@modules/contacts/domain/ContactSubscriptions'; 7 | import { ContactMap } from '@modules/contacts/mappers/ContactMap'; 8 | import { IContactRepo } from '@modules/contacts/repositories/IContactRepo'; 9 | import { IContactSubscriptionsRepo } from '@modules/contacts/repositories/IContactSubscriptionsRepo'; 10 | 11 | export class PrismaContactRepo implements IContactRepo { 12 | private contactSubscriptionsRepo: IContactSubscriptionsRepo; 13 | 14 | constructor(contactSubscriptionRepo: IContactSubscriptionsRepo) { 15 | this.contactSubscriptionsRepo = contactSubscriptionRepo; 16 | } 17 | 18 | private saveSubscriptions( 19 | subscriptions: ContactSubscriptions 20 | ): Promise { 21 | return this.contactSubscriptionsRepo.saveBulk(subscriptions); 22 | } 23 | 24 | async findByEmail(email: string | ContactEmail): Promise { 25 | const rawContact = await prisma.contact.findOne({ 26 | where: { 27 | email: email instanceof ContactEmail ? email.value : email, 28 | }, 29 | }); 30 | 31 | if (!rawContact) { 32 | return null; 33 | } 34 | 35 | return ContactMap.toDomain(rawContact); 36 | } 37 | 38 | async getContacts(): Promise { 39 | const rawContacts = await prisma.contact.findMany(); 40 | 41 | const domainContacts = rawContacts.map((contact) => 42 | ContactMap.toDomain(contact) 43 | ); 44 | 45 | return domainContacts; 46 | } 47 | 48 | async save(contact: Contact): Promise { 49 | const contactData = ContactMap.toPersistence(contact); 50 | 51 | await prisma.contact.upsert({ 52 | where: { email: contact.email.value }, 53 | update: contactData, 54 | create: contactData, 55 | }); 56 | 57 | await this.saveSubscriptions(contact.subscriptions); 58 | 59 | DomainEvents.dispatchEventsForAggregate(contact.id); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200609124237-create_base_templates/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "***" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Contact { 14 | @@map(name: "contacts") 15 | 16 | id String @default(uuid()) @id 17 | email String @unique 18 | tags ContactTags[] 19 | 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @default(now()) 22 | } 23 | 24 | model ContactTags { 25 | @@map(name: "contact_tags") 26 | 27 | id String @default(uuid()) @id 28 | contact Contact @relation(fields: [contactId], references: [id]) 29 | contactId String 30 | tag Tag @relation(fields: [tagId], references: [id]) 31 | tagId String 32 | subscribed Boolean @default(true) 33 | 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @default(now()) 36 | } 37 | 38 | model Tag { 39 | @@map(name: "tags") 40 | 41 | id String @default(uuid()) @id 42 | title String 43 | contacts ContactTags[] 44 | } 45 | 46 | model Message { 47 | @@map(name: "messages") 48 | 49 | id String @default(uuid()) @id 50 | subject String 51 | body String 52 | recipientsCount Int 53 | sentCount Int @default(0) 54 | sentAt DateTime? 55 | template Template @relation(fields: [templateId], references: [id]) 56 | templateId String 57 | sender Sender @relation(fields: [senderId], references: [id]) 58 | senderId String 59 | 60 | createdAt DateTime @default(now()) 61 | updatedAt DateTime @default(now()) 62 | } 63 | 64 | model Template { 65 | @@map(name: "templates") 66 | 67 | id String @default(uuid()) @id 68 | messages Message[] 69 | 70 | title String 71 | content String 72 | } 73 | 74 | model Sender { 75 | @@map(name: "senders") 76 | 77 | id String @default(uuid()) @id 78 | messages Message[] 79 | name String 80 | email String 81 | 82 | createdAt DateTime @default(now()) 83 | updatedAt DateTime @default(now()) 84 | } 85 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = env("DATABASE_URL") 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Contact { 14 | @@map(name: "contacts") 15 | 16 | id String @default(uuid()) @id 17 | email String @unique 18 | tags ContactTags[] 19 | 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @default(now()) 22 | } 23 | 24 | model ContactTags { 25 | @@map(name: "contact_tags") 26 | 27 | id String @default(uuid()) @id 28 | contact Contact @relation(fields: [contactId], references: [id]) 29 | contactId String 30 | tag Tag @relation(fields: [tagId], references: [id]) 31 | tagId String 32 | subscribed Boolean @default(true) 33 | 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @default(now()) 36 | } 37 | 38 | model Tag { 39 | @@map(name: "tags") 40 | 41 | id String @default(uuid()) @id 42 | slug String @unique 43 | title String 44 | contacts ContactTags[] 45 | } 46 | 47 | model Message { 48 | @@map(name: "messages") 49 | 50 | id String @default(uuid()) @id 51 | subject String 52 | body String 53 | recipientsCount Int 54 | sentCount Int @default(0) 55 | sentAt DateTime? 56 | template Template @relation(fields: [templateId], references: [id]) 57 | templateId String 58 | sender Sender @relation(fields: [senderId], references: [id]) 59 | senderId String 60 | 61 | createdAt DateTime @default(now()) 62 | updatedAt DateTime @default(now()) 63 | } 64 | 65 | model Template { 66 | @@map(name: "templates") 67 | 68 | id String @default(uuid()) @id 69 | messages Message[] 70 | 71 | title String 72 | content String 73 | } 74 | 75 | model Sender { 76 | @@map(name: "senders") 77 | 78 | id String @default(uuid()) @id 79 | messages Message[] 80 | name String 81 | email String 82 | 83 | createdAt DateTime @default(now()) 84 | updatedAt DateTime @default(now()) 85 | } 86 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200604122553-add_cascade_on_deletes/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200604122553-add_cascade_on_deletes` 2 | 3 | This migration has been generated by Diego Fernandes at 6/4/2020, 12:25:53 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | ALTER TABLE "public"."user_addresses" DROP CONSTRAINT IF EXiSTS "user_addresses_userId_fkey", 10 | DROP COLUMN "userId", 11 | ADD COLUMN "userId" text NOT NULL ; 12 | 13 | ALTER TABLE "public"."user_email_history" DROP CONSTRAINT IF EXiSTS "user_email_history_userId_fkey", 14 | DROP COLUMN "userId", 15 | ADD COLUMN "userId" text NOT NULL ; 16 | 17 | ALTER TABLE "public"."user_addresses" ADD FOREIGN KEY ("userId")REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE 18 | 19 | ALTER TABLE "public"."user_email_history" ADD FOREIGN KEY ("userId")REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE 20 | ``` 21 | 22 | ## Changes 23 | 24 | ```diff 25 | diff --git schema.prisma schema.prisma 26 | migration 20200603234442-create_user_email_history..20200604122553-add_cascade_on_deletesn 27 | --- datamodel.dml 28 | +++ datamodel.dml 29 | @@ -1,7 +1,7 @@ 30 | datasource db { 31 | provider = "postgresql" 32 | - url = "***" 33 | + url = env("DATABASE_URL") 34 | } 35 | generator client { 36 | provider = "prisma-client-js" 37 | @@ -34,20 +34,20 @@ 38 | state String 39 | type String @default("shipping") 40 | createdAt DateTime @default(now()) 41 | updatedAt DateTime @default(now()) 42 | - User User? @relation(fields: [userId], references: [id]) 43 | - userId String? 44 | + User User @relation(fields: [userId], references: [id]) 45 | + userId String 46 | } 47 | model UserEmailHistory { 48 | @@map(name: "user_email_history") 49 | id String @default(uuid()) @id 50 | email String 51 | changedAt DateTime @default(now()) 52 | - User User? @relation(fields: [userId], references: [id]) 53 | - userId String? 54 | + User User @relation(fields: [userId], references: [id]) 55 | + userId String 56 | } 57 | model Team { 58 | @@map(name: "teams") 59 | ``` 60 | 61 | 62 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/infra/prisma/PrismaContactSubscriptionsRepo.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | 3 | import { prisma } from '@infra/prisma/client'; 4 | import { ContactSubscription } from '@modules/contacts/domain/ContactSubscription'; 5 | import { ContactSubscriptions } from '@modules/contacts/domain/ContactSubscriptions'; 6 | import { ContactSubscriptionMap } from '@modules/contacts/mappers/ContactSubscriptionMap'; 7 | import { IContactSubscriptionsRepo } from '@modules/contacts/repositories/IContactSubscriptionsRepo'; 8 | 9 | export class PrismaContactSubscriptionsRepo 10 | implements IContactSubscriptionsRepo { 11 | async save(subscription: ContactSubscription): Promise { 12 | const subscriptionData = ContactSubscriptionMap.toPersistence(subscription); 13 | 14 | const prismaInput = { 15 | id: subscriptionData.id, 16 | subscribed: subscriptionData.subscribed, 17 | contact: { 18 | connect: { 19 | id: subscriptionData.contactId, 20 | }, 21 | }, 22 | tag: { 23 | connect: { 24 | id: subscriptionData.tagId, 25 | }, 26 | }, 27 | }; 28 | 29 | await prisma.contactTags.upsert({ 30 | where: { id: subscriptionData.id }, 31 | update: prismaInput, 32 | create: prismaInput, 33 | }); 34 | 35 | DomainEvents.dispatchEventsForAggregate(subscription.id); 36 | } 37 | 38 | async saveBulk(subscriptions: ContactSubscriptions): Promise { 39 | const removedItemsPromise = Promise.all( 40 | subscriptions.getRemovedItems().map((subscription) => { 41 | return this.delete(subscription); 42 | }) 43 | ); 44 | 45 | const newItemsPromise = Promise.all( 46 | subscriptions.getNewItems().map((subscription) => { 47 | return this.save(subscription); 48 | }) 49 | ); 50 | 51 | await Promise.all([removedItemsPromise, newItemsPromise]); 52 | } 53 | 54 | async delete(subscription: ContactSubscription): Promise { 55 | const subscriptionData = ContactSubscriptionMap.toPersistence(subscription); 56 | 57 | await prisma.contactTags.delete({ 58 | where: { 59 | id: subscriptionData.id, 60 | }, 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200611222402-add_slug_to_tags/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | datasource db { 5 | provider = "postgresql" 6 | url = "***" 7 | } 8 | 9 | generator client { 10 | provider = "prisma-client-js" 11 | } 12 | 13 | model Contact { 14 | @@map(name: "contacts") 15 | 16 | id String @default(uuid()) @id 17 | email String @unique 18 | tags ContactTags[] 19 | 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @default(now()) 22 | } 23 | 24 | model ContactTags { 25 | @@map(name: "contact_tags") 26 | 27 | id String @default(uuid()) @id 28 | contact Contact @relation(fields: [contactId], references: [id]) 29 | contactId String 30 | tag Tag @relation(fields: [tagId], references: [id]) 31 | tagId String 32 | subscribed Boolean @default(true) 33 | 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @default(now()) 36 | } 37 | 38 | model Tag { 39 | @@map(name: "tags") 40 | 41 | id String @default(uuid()) @id 42 | slug String @unique 43 | title String 44 | contacts ContactTags[] 45 | } 46 | 47 | model Message { 48 | @@map(name: "messages") 49 | 50 | id String @default(uuid()) @id 51 | subject String 52 | body String 53 | recipientsCount Int 54 | sentCount Int @default(0) 55 | sentAt DateTime? 56 | template Template @relation(fields: [templateId], references: [id]) 57 | templateId String 58 | sender Sender @relation(fields: [senderId], references: [id]) 59 | senderId String 60 | 61 | createdAt DateTime @default(now()) 62 | updatedAt DateTime @default(now()) 63 | } 64 | 65 | model Template { 66 | @@map(name: "templates") 67 | 68 | id String @default(uuid()) @id 69 | messages Message[] 70 | 71 | title String 72 | content String 73 | } 74 | 75 | model Sender { 76 | @@map(name: "senders") 77 | 78 | id String @default(uuid()) @id 79 | messages Message[] 80 | name String 81 | email String 82 | 83 | createdAt DateTime @default(now()) 84 | updatedAt DateTime @default(now()) 85 | } 86 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/createUser/CreateUserUseCase.ts: -------------------------------------------------------------------------------- 1 | import { IUseCase } from '@server/shared/src/core/domain/UseCase'; 2 | import * as GenericAppError from '@server/shared/src/core/logic/AppError'; 3 | import { 4 | Result, 5 | failure, 6 | Either, 7 | success, 8 | } from '@server/shared/src/core/logic/Result'; 9 | 10 | import { User } from '@modules/users/domain/User'; 11 | import { UserEmail } from '@modules/users/domain/UserEmail'; 12 | import { UserPassword } from '@modules/users/domain/UserPassword'; 13 | import { IUserRepo } from '@modules/users/repositories/IUserRepo'; 14 | 15 | import * as CreateUserErrors from './CreateUserErrors'; 16 | import { ICreateUserRequestDTO } from './ICreateUserDTO'; 17 | 18 | type Response = Either< 19 | GenericAppError.UnexpectedError | CreateUserErrors.AccountAlreadyExists, 20 | Result 21 | >; 22 | 23 | export class CreateUserUseCase 24 | implements IUseCase> { 25 | private userRepo: IUserRepo; 26 | 27 | constructor(userRepo: IUserRepo) { 28 | this.userRepo = userRepo; 29 | } 30 | 31 | async execute(request: ICreateUserRequestDTO): Promise { 32 | const userEmailOrError = UserEmail.create(request.email); 33 | const userPasswordOrError = UserPassword.create({ 34 | value: request.password, 35 | }); 36 | 37 | const result = Result.combine([userEmailOrError, userPasswordOrError]); 38 | 39 | if (result.isFailure) { 40 | return failure(Result.fail(result.error)); 41 | } 42 | 43 | const userOrError = User.create({ 44 | name: request.name, 45 | email: userEmailOrError.getValue(), 46 | password: userPasswordOrError.getValue(), 47 | }); 48 | 49 | if (userOrError.isFailure) { 50 | return failure(Result.fail(userOrError.error)); 51 | } 52 | 53 | const user = userOrError.getValue(); 54 | 55 | const userAlreadyExists = await this.userRepo.findByEmail(user.email); 56 | 57 | if (userAlreadyExists) { 58 | return failure( 59 | new CreateUserErrors.AccountAlreadyExists(user.email.value) 60 | ); 61 | } 62 | 63 | try { 64 | await this.userRepo.save(user); 65 | } catch (err) { 66 | return failure(new GenericAppError.UnexpectedError(err)); 67 | } 68 | 69 | return success(Result.ok()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603234442-create_user_email_history/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200603234442-create_user_email_history` 2 | 3 | This migration has been generated by Diego Fernandes at 6/3/2020, 11:44:42 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "public"."user_email_history" ( 10 | "changedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" text NOT NULL ,"id" text NOT NULL ,"userId" text , 11 | PRIMARY KEY ("id")) 12 | 13 | ALTER TABLE "public"."user_email_history" ADD FOREIGN KEY ("userId")REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE CASCADE 14 | ``` 15 | 16 | ## Changes 17 | 18 | ```diff 19 | diff --git schema.prisma schema.prisma 20 | migration 20200603231715-create_teams_and_addresses..20200603234442-create_user_email_history 21 | --- datamodel.dml 22 | +++ datamodel.dml 23 | @@ -1,7 +1,7 @@ 24 | datasource db { 25 | provider = "postgresql" 26 | - url = "***" 27 | + url = env("DATABASE_URL") 28 | } 29 | generator client { 30 | provider = "prisma-client-js" 31 | @@ -9,14 +9,15 @@ 32 | model User { 33 | @@map(name: "users") 34 | - id String @default(uuid()) @id 35 | - email String @unique 36 | - name String 37 | - password String 38 | - teams Team[] @relation(references: [id]) 39 | - addresses UserAddress[] 40 | + id String @default(uuid()) @id 41 | + email String @unique 42 | + name String 43 | + password String 44 | + teams Team[] @relation(references: [id]) 45 | + addresses UserAddress[] 46 | + UserEmailHistory UserEmailHistory[] 47 | } 48 | model UserAddress { 49 | @@map(name: "user_addresses") 50 | @@ -37,8 +38,18 @@ 51 | User User? @relation(fields: [userId], references: [id]) 52 | userId String? 53 | } 54 | +model UserEmailHistory { 55 | + @@map(name: "user_email_history") 56 | + 57 | + id String @default(uuid()) @id 58 | + email String 59 | + changedAt DateTime @default(now()) 60 | + User User? @relation(fields: [userId], references: [id]) 61 | + userId String? 62 | +} 63 | + 64 | model Team { 65 | @@map(name: "teams") 66 | id String @default(uuid()) @id 67 | ``` 68 | 69 | 70 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/infra/BaseController.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | export abstract class BaseController { 4 | protected request: express.Request; 5 | 6 | protected response: express.Response; 7 | 8 | protected abstract executeImpl(): Promise; 9 | 10 | public execute(request: express.Request, response: express.Response): void { 11 | this.request = request; 12 | this.response = response; 13 | 14 | this.executeImpl(); 15 | } 16 | 17 | public static jsonResponse( 18 | response: express.Response, 19 | code: number, 20 | message: string 21 | ): express.Response { 22 | return response.status(code).json({ message }); 23 | } 24 | 25 | public ok(dto?: T): express.Response { 26 | if (dto) { 27 | return this.response.status(200).json(dto); 28 | } 29 | 30 | return this.response.sendStatus(200); 31 | } 32 | 33 | public created(): express.Response { 34 | return this.response.sendStatus(201); 35 | } 36 | 37 | public clientError(message?: string): express.Response { 38 | return BaseController.jsonResponse( 39 | this.response, 40 | 400, 41 | message || 'Unauthorized' 42 | ); 43 | } 44 | 45 | public unauthorized(message?: string): express.Response { 46 | return BaseController.jsonResponse( 47 | this.response, 48 | 401, 49 | message || 'Unauthorized' 50 | ); 51 | } 52 | 53 | public forbidden(message?: string): express.Response { 54 | return BaseController.jsonResponse( 55 | this.response, 56 | 403, 57 | message || 'Forbidden' 58 | ); 59 | } 60 | 61 | public notFound(message?: string): express.Response { 62 | return BaseController.jsonResponse( 63 | this.response, 64 | 404, 65 | message || 'Not found' 66 | ); 67 | } 68 | 69 | public conflict(message?: string): express.Response { 70 | return BaseController.jsonResponse( 71 | this.response, 72 | 409, 73 | message || 'Conflict' 74 | ); 75 | } 76 | 77 | public tooMany(message?: string): express.Response { 78 | return BaseController.jsonResponse( 79 | this.response, 80 | 429, 81 | message || 'Too many requests' 82 | ); 83 | } 84 | 85 | public fail(error: Error | string): express.Response { 86 | console.log(error); 87 | 88 | return this.response.status(500).json({ 89 | message: error.toString(), 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "airbnb-base", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint", 11 | "plugin:prettier/recommended" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaVersion": 11, 20 | "sourceType": "module" 21 | }, 22 | "plugins": [ 23 | "@typescript-eslint", 24 | "prettier", 25 | "eslint-plugin-import-helpers" 26 | ], 27 | "rules": { 28 | "no-new": "off", 29 | "no-prototype-builtins": "off", 30 | "no-restricted-syntax": "off", 31 | "max-classes-per-file": "off", 32 | "@typescript-eslint/no-explicit-any": "off", 33 | "no-console": "off", 34 | "import/prefer-default-export": "off", 35 | "@typescript-eslint/explicit-function-return-type": ["off"], 36 | "@typescript-eslint/no-unused-vars": [ 37 | "error", 38 | { 39 | "argsIgnorePattern": "_" 40 | } 41 | ], 42 | "no-useless-constructor": "off", 43 | "@typescript-eslint/naming-convention": [ 44 | "error", 45 | { 46 | "selector": "interface", 47 | "format": ["PascalCase"], 48 | "custom": { 49 | "regex": "^I[A-Z]", 50 | "match": true 51 | } 52 | } 53 | ], 54 | "@typescript-eslint/explicit-module-boundary-types": ["warn", { 55 | "allowArgumentsExplicitlyTypedAsAny": true 56 | }], 57 | "no-underscore-dangle": "off", 58 | "@typescript-eslint/camelcase": "off", 59 | "prettier/prettier": "error", 60 | "class-methods-use-this": "off", 61 | "import/extensions": [ 62 | "error", 63 | "ignorePackages", 64 | { 65 | "ts": "never" 66 | } 67 | ], 68 | "import-helpers/order-imports": [ 69 | "warn", 70 | { 71 | "newlinesBetween": "always", // new line between groups 72 | "groups": [ 73 | "module", 74 | "/^@server\/shared/", 75 | "/^@/", 76 | ["parent", "sibling", "index"] 77 | ], 78 | "alphabetize": { "order": "asc", "ignoreCase": true } 79 | } 80 | ] 81 | }, 82 | "settings": { 83 | "import/resolver": { 84 | "typescript": { 85 | "directory": "packages/server/*/tsconfig.json" 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/logic/Result.ts: -------------------------------------------------------------------------------- 1 | export class Result { 2 | public isSuccess: boolean; 3 | 4 | public isFailure: boolean; 5 | 6 | public error: T | string; 7 | 8 | private _value: T; 9 | 10 | constructor(isSuccess: boolean, error?: T | string, value?: T) { 11 | if (isSuccess && error) { 12 | throw new Error( 13 | 'InvalidOperation: A result cannot be successful and contain an error' 14 | ); 15 | } 16 | if (!isSuccess && !error) { 17 | throw new Error( 18 | 'InvalidOperation: A failing result needs to contain an error message' 19 | ); 20 | } 21 | 22 | this.isSuccess = isSuccess; 23 | this.isFailure = !isSuccess; 24 | this.error = error; 25 | this._value = value; 26 | 27 | Object.freeze(this); 28 | } 29 | 30 | getValue(): T { 31 | if (!this.isSuccess) { 32 | console.log(this.error); 33 | 34 | throw new Error( 35 | "Can't get the value of an error result. Use 'errorValue' instead." 36 | ); 37 | } 38 | 39 | return this._value; 40 | } 41 | 42 | errorValue(): T { 43 | return this.error as T; 44 | } 45 | 46 | static ok(value?: U): Result { 47 | return new Result(true, null, value); 48 | } 49 | 50 | static fail(error: any): Result { 51 | return new Result(false, error); 52 | } 53 | 54 | static combine(results: Result[]): Result { 55 | for (const result of results) { 56 | if (result.isFailure) return result; 57 | } 58 | return Result.ok(); 59 | } 60 | } 61 | 62 | export type Either = Failure | Success; 63 | 64 | export class Failure { 65 | readonly value: L; 66 | 67 | constructor(value: L) { 68 | this.value = value; 69 | } 70 | 71 | isFailure(): this is Failure { 72 | return true; 73 | } 74 | 75 | isSuccess(): this is Success { 76 | return false; 77 | } 78 | } 79 | 80 | export class Success { 81 | readonly value: A; 82 | 83 | constructor(value: A) { 84 | this.value = value; 85 | } 86 | 87 | isFailure(): this is Failure { 88 | return false; 89 | } 90 | 91 | isSuccess(): this is Success { 92 | return true; 93 | } 94 | } 95 | 96 | export const failure = (l: L): Either => { 97 | return new Failure(l); 98 | }; 99 | 100 | export const success = (a: A): Either => { 101 | return new Success(a); 102 | }; 103 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/WatchedList.ts: -------------------------------------------------------------------------------- 1 | export abstract class WatchedList { 2 | public currentItems: T[]; 3 | 4 | private initial: T[]; 5 | 6 | private new: T[]; 7 | 8 | private removed: T[]; 9 | 10 | constructor(initialItems?: T[]) { 11 | this.currentItems = initialItems || []; 12 | this.initial = initialItems || []; 13 | this.new = []; 14 | this.removed = []; 15 | } 16 | 17 | abstract compareItems(a: T, b: T): boolean; 18 | 19 | public getItems(): T[] { 20 | return this.currentItems; 21 | } 22 | 23 | public getNewItems(): T[] { 24 | return this.new; 25 | } 26 | 27 | public getRemovedItems(): T[] { 28 | return this.removed; 29 | } 30 | 31 | private isCurrentItem(item: T): boolean { 32 | return ( 33 | this.currentItems.filter((v: T) => this.compareItems(item, v)).length !== 34 | 0 35 | ); 36 | } 37 | 38 | private isNewItem(item: T): boolean { 39 | return this.new.filter((v: T) => this.compareItems(item, v)).length !== 0; 40 | } 41 | 42 | private isRemovedItem(item: T): boolean { 43 | return ( 44 | this.removed.filter((v: T) => this.compareItems(item, v)).length !== 0 45 | ); 46 | } 47 | 48 | private removeFromNew(item: T): void { 49 | this.new = this.new.filter((v) => !this.compareItems(v, item)); 50 | } 51 | 52 | private removeFromCurrent(item: T): void { 53 | this.currentItems = this.currentItems.filter( 54 | (v) => !this.compareItems(item, v) 55 | ); 56 | } 57 | 58 | private removeFromRemoved(item: T): void { 59 | this.removed = this.removed.filter((v) => !this.compareItems(item, v)); 60 | } 61 | 62 | private wasAddedInitially(item: T): boolean { 63 | return ( 64 | this.initial.filter((v: T) => this.compareItems(item, v)).length !== 0 65 | ); 66 | } 67 | 68 | public exists(item: T): boolean { 69 | return this.isCurrentItem(item); 70 | } 71 | 72 | public add(item: T): void { 73 | if (this.isRemovedItem(item)) { 74 | this.removeFromRemoved(item); 75 | } 76 | 77 | if (!this.isNewItem(item) && !this.wasAddedInitially(item)) { 78 | this.new.push(item); 79 | } 80 | 81 | if (!this.isCurrentItem(item)) { 82 | this.currentItems.push(item); 83 | } 84 | } 85 | 86 | public remove(item: T): void { 87 | this.removeFromCurrent(item); 88 | 89 | if (this.isNewItem(item)) { 90 | this.removeFromNew(item); 91 | return; 92 | } 93 | 94 | if (!this.isRemovedItem(item)) { 95 | this.removed.push(item); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/domain/User.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@server/shared/src/core/domain/AggregateRoot'; 2 | import { UniqueEntityID } from '@server/shared/src/core/domain/UniqueEntityID'; 3 | import { Guard } from '@server/shared/src/core/logic/Guard'; 4 | import { Result } from '@server/shared/src/core/logic/Result'; 5 | 6 | import { UserCreatedEvent } from './events/UserCreatedEvent'; 7 | import { UserEmailChangedEvent } from './events/UserEmailChangedEvent'; 8 | import { UserLoggedInEvent } from './events/UserLoggedInEvent'; 9 | import { UserEmail } from './UserEmail'; 10 | import { UserPassword } from './UserPassword'; 11 | 12 | interface IUserProps { 13 | name: string; 14 | email: UserEmail; 15 | password: UserPassword; 16 | accessToken?: string; 17 | lastLogin?: Date; 18 | } 19 | 20 | export class User extends AggregateRoot { 21 | get name(): string { 22 | return this.props.name; 23 | } 24 | 25 | get email(): UserEmail { 26 | return this.props.email; 27 | } 28 | 29 | get password(): UserPassword { 30 | return this.props.password; 31 | } 32 | 33 | get accessToken(): string { 34 | return this.props.accessToken; 35 | } 36 | 37 | get lastLogin(): Date { 38 | return this.props.lastLogin; 39 | } 40 | 41 | public isLoggedIn(): boolean { 42 | return !!this.props.accessToken; 43 | } 44 | 45 | public setAccessToken(accessToken: string): void { 46 | this.addDomainEvent(new UserLoggedInEvent(this)); 47 | 48 | this.props.accessToken = accessToken; 49 | this.props.lastLogin = new Date(); 50 | } 51 | 52 | public setEmail(email: UserEmail): void { 53 | this.addDomainEvent(new UserEmailChangedEvent(this, this.props.email)); 54 | 55 | this.props.email = email; 56 | } 57 | 58 | private constructor(props: IUserProps, id?: UniqueEntityID) { 59 | super(props, id); 60 | } 61 | 62 | public static create(props: IUserProps, id?: UniqueEntityID): Result { 63 | const guardedProps = [ 64 | { argument: props.name, argumentName: 'name' }, 65 | { argument: props.email, argumentName: 'email' }, 66 | { argument: props.password, argumentName: 'password' }, 67 | ]; 68 | 69 | const guardResult = Guard.againstNullOrUndefinedBulk(guardedProps); 70 | 71 | if (!guardResult.succeeded) { 72 | return Result.fail(guardResult.message); 73 | } 74 | 75 | const user = new User(props, id); 76 | 77 | const idWasProvided = !!id; 78 | 79 | if (!idWasProvided) { 80 | user.addDomainEvent(new UserCreatedEvent(user)); 81 | } 82 | 83 | return Result.ok(user); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/domain/events/DomainEvents.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '../AggregateRoot'; 2 | import { UniqueEntityID } from '../UniqueEntityID'; 3 | import { IDomainEvent } from './IDomainEvent'; 4 | 5 | export class DomainEvents { 6 | private static handlersMap = {}; 7 | 8 | private static markedAggregates: AggregateRoot[] = []; 9 | 10 | /** 11 | * @method markAggregateForDispatch 12 | * @static 13 | * @desc Called by aggregate root objects that have created domain 14 | * events to eventually be dispatched when the infrastructure commits 15 | * the unit of work. 16 | */ 17 | 18 | public static markAggregateForDispatch(aggregate: AggregateRoot): void { 19 | const aggregateFound = !!this.findMarkedAggregateByID(aggregate.id); 20 | 21 | if (!aggregateFound) { 22 | this.markedAggregates.push(aggregate); 23 | } 24 | } 25 | 26 | private static dispatchAggregateEvents(aggregate: AggregateRoot): void { 27 | aggregate.domainEvents.forEach((event: IDomainEvent) => 28 | this.dispatch(event) 29 | ); 30 | } 31 | 32 | private static removeAggregateFromMarkedDispatchList( 33 | aggregate: AggregateRoot 34 | ): void { 35 | const index = this.markedAggregates.findIndex((a) => a.equals(aggregate)); 36 | this.markedAggregates.splice(index, 1); 37 | } 38 | 39 | private static findMarkedAggregateByID( 40 | id: UniqueEntityID 41 | ): AggregateRoot { 42 | let found: AggregateRoot = null; 43 | for (const aggregate of this.markedAggregates) { 44 | if (aggregate.id.equals(id)) { 45 | found = aggregate; 46 | } 47 | } 48 | 49 | return found; 50 | } 51 | 52 | public static dispatchEventsForAggregate(id: UniqueEntityID): void { 53 | const aggregate = this.findMarkedAggregateByID(id); 54 | 55 | if (aggregate) { 56 | this.dispatchAggregateEvents(aggregate); 57 | aggregate.clearEvents(); 58 | this.removeAggregateFromMarkedDispatchList(aggregate); 59 | } 60 | } 61 | 62 | public static register( 63 | callback: (event: IDomainEvent) => void, 64 | eventClassName: string 65 | ): void { 66 | if (!this.handlersMap.hasOwnProperty(eventClassName)) { 67 | this.handlersMap[eventClassName] = []; 68 | } 69 | this.handlersMap[eventClassName].push(callback); 70 | } 71 | 72 | public static clearHandlers(): void { 73 | this.handlersMap = {}; 74 | } 75 | 76 | public static clearMarkedAggregates(): void { 77 | this.markedAggregates = []; 78 | } 79 | 80 | private static dispatch(event: IDomainEvent): void { 81 | const eventClassName: string = event.constructor.name; 82 | 83 | if (this.handlersMap.hasOwnProperty(eventClassName)) { 84 | const handlers: any[] = this.handlersMap[eventClassName]; 85 | for (const handler of handlers) { 86 | handler(event); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/server/atlas/src/modules/users/useCases/login/LoginUseCase.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvents } from '@server/shared/src/core/domain/events/DomainEvents'; 2 | import { IUseCase } from '@server/shared/src/core/domain/UseCase'; 3 | import * as GenericAppError from '@server/shared/src/core/logic/AppError'; 4 | import { 5 | Result, 6 | failure, 7 | Either, 8 | success, 9 | } from '@server/shared/src/core/logic/Result'; 10 | 11 | import { JWT } from '@modules/users/domain/JWT'; 12 | import { UserEmail } from '@modules/users/domain/UserEmail'; 13 | import { UserPassword } from '@modules/users/domain/UserPassword'; 14 | import { IUserRepo } from '@modules/users/repositories/IUserRepo'; 15 | 16 | import { ILoginRequestDTO, ILoginResponseDTO } from './ILoginDTO'; 17 | import * as LoginErrors from './LoginErrors'; 18 | 19 | type Response = Either< 20 | | GenericAppError.UnexpectedError 21 | | LoginErrors.UserNameDoesntExistError 22 | | LoginErrors.PasswordDoesntMatchError, 23 | Result 24 | >; 25 | 26 | export class LoginUseCase 27 | implements IUseCase> { 28 | private userRepo: IUserRepo; 29 | 30 | constructor(userRepo: IUserRepo) { 31 | this.userRepo = userRepo; 32 | } 33 | 34 | async execute(request: ILoginRequestDTO): Promise { 35 | try { 36 | const userEmailOrError = UserEmail.create(request.email); 37 | const userPasswordOrError = UserPassword.create({ 38 | value: request.password, 39 | }); 40 | 41 | const result = Result.combine([userEmailOrError, userPasswordOrError]); 42 | 43 | if (result.isFailure) { 44 | return failure(Result.fail(result.error)); 45 | } 46 | 47 | const email = userEmailOrError.getValue(); 48 | const password = userPasswordOrError.getValue(); 49 | 50 | const user = await this.userRepo.findByEmail(email); 51 | 52 | const userFound = !!user; 53 | 54 | if (!userFound) { 55 | return failure(new LoginErrors.UserNameDoesntExistError()); 56 | } 57 | 58 | const passwordValid = await user.password.comparePassword(password.value); 59 | 60 | if (!passwordValid) { 61 | return failure(new LoginErrors.PasswordDoesntMatchError()); 62 | } 63 | 64 | const accessTokenOrError = JWT.create( 65 | { 66 | sub: user.id.toValue(), 67 | }, 68 | { 69 | name: user.name, 70 | email: user.email.value, 71 | } 72 | ); 73 | 74 | if (accessTokenOrError.isFailure) { 75 | return failure(Result.fail(accessTokenOrError.error)); 76 | } 77 | 78 | const { token } = accessTokenOrError.getValue(); 79 | 80 | user.setAccessToken(token); 81 | 82 | DomainEvents.dispatchEventsForAggregate(user.id); 83 | 84 | return success( 85 | Result.ok({ 86 | token, 87 | }) 88 | ); 89 | } catch (err) { 90 | return failure(new GenericAppError.UnexpectedError(err)); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/server/_shared/src/core/logic/Guard.ts: -------------------------------------------------------------------------------- 1 | export interface IGuardResult { 2 | succeeded: boolean; 3 | message?: string; 4 | } 5 | 6 | export interface IGuardArgument { 7 | argument: any; 8 | argumentName: string; 9 | } 10 | 11 | export type GuardArgumentCollection = IGuardArgument[]; 12 | 13 | export class Guard { 14 | public static combine(guardResults: IGuardResult[]): IGuardResult { 15 | for (const result of guardResults) { 16 | if (result.succeeded === false) { 17 | return result; 18 | } 19 | } 20 | 21 | return { succeeded: true }; 22 | } 23 | 24 | public static againstNullOrUndefined( 25 | argument: any, 26 | argumentName: string 27 | ): IGuardResult { 28 | if (argument === null || argument === undefined) { 29 | return { 30 | succeeded: false, 31 | message: `${argumentName} is null or undefined`, 32 | }; 33 | } 34 | 35 | return { succeeded: true }; 36 | } 37 | 38 | public static againstNullOrUndefinedBulk( 39 | args: GuardArgumentCollection 40 | ): IGuardResult { 41 | for (const arg of args) { 42 | const result = this.againstNullOrUndefined( 43 | arg.argument, 44 | arg.argumentName 45 | ); 46 | 47 | if (!result.succeeded) return result; 48 | } 49 | 50 | return { succeeded: true }; 51 | } 52 | 53 | public static isOneOf( 54 | value: any, 55 | validValues: any[], 56 | argumentName: string 57 | ): IGuardResult { 58 | let isValid = false; 59 | 60 | for (const validValue of validValues) { 61 | if (value === validValue) { 62 | isValid = true; 63 | } 64 | } 65 | 66 | if (isValid) { 67 | return { succeeded: true }; 68 | } 69 | 70 | return { 71 | succeeded: false, 72 | message: `${argumentName} isn't oneOf the correct types in ${JSON.stringify( 73 | validValues 74 | )}. Got "${value}".`, 75 | }; 76 | } 77 | 78 | public static inRange( 79 | num: number, 80 | min: number, 81 | max: number, 82 | argumentName: string 83 | ): IGuardResult { 84 | const isInRange = num >= min && num <= max; 85 | 86 | if (!isInRange) { 87 | return { 88 | succeeded: false, 89 | message: `${argumentName} is not within range ${min} to ${max}.`, 90 | }; 91 | } 92 | 93 | return { succeeded: true }; 94 | } 95 | 96 | public static allInRange( 97 | numbers: number[], 98 | min: number, 99 | max: number, 100 | argumentName: string 101 | ): IGuardResult { 102 | let failingResult = {}; 103 | 104 | for (const num of numbers) { 105 | const numIsInRangeResult = this.inRange(num, min, max, argumentName); 106 | 107 | if (!numIsInRangeResult.succeeded) { 108 | failingResult = numIsInRangeResult; 109 | } 110 | } 111 | 112 | if (failingResult) { 113 | return { 114 | succeeded: false, 115 | message: `${argumentName} is not within the range.`, 116 | }; 117 | } 118 | 119 | return { succeeded: true }; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200515170312-create-users/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateSource", 6 | "source": "db" 7 | }, 8 | { 9 | "tag": "CreateArgument", 10 | "location": { 11 | "tag": "Source", 12 | "source": "db" 13 | }, 14 | "argument": "provider", 15 | "value": "\"postgresql\"" 16 | }, 17 | { 18 | "tag": "CreateArgument", 19 | "location": { 20 | "tag": "Source", 21 | "source": "db" 22 | }, 23 | "argument": "url", 24 | "value": "env(\"DATABASE_URL\")" 25 | }, 26 | { 27 | "tag": "CreateModel", 28 | "model": "User" 29 | }, 30 | { 31 | "tag": "CreateField", 32 | "model": "User", 33 | "field": "id", 34 | "type": "String", 35 | "arity": "Required" 36 | }, 37 | { 38 | "tag": "CreateDirective", 39 | "location": { 40 | "path": { 41 | "tag": "Field", 42 | "model": "User", 43 | "field": "id" 44 | }, 45 | "directive": "default" 46 | } 47 | }, 48 | { 49 | "tag": "CreateArgument", 50 | "location": { 51 | "tag": "Directive", 52 | "path": { 53 | "tag": "Field", 54 | "model": "User", 55 | "field": "id" 56 | }, 57 | "directive": "default" 58 | }, 59 | "argument": "", 60 | "value": "uuid()" 61 | }, 62 | { 63 | "tag": "CreateDirective", 64 | "location": { 65 | "path": { 66 | "tag": "Field", 67 | "model": "User", 68 | "field": "id" 69 | }, 70 | "directive": "id" 71 | } 72 | }, 73 | { 74 | "tag": "CreateField", 75 | "model": "User", 76 | "field": "email", 77 | "type": "String", 78 | "arity": "Required" 79 | }, 80 | { 81 | "tag": "CreateDirective", 82 | "location": { 83 | "path": { 84 | "tag": "Field", 85 | "model": "User", 86 | "field": "email" 87 | }, 88 | "directive": "unique" 89 | } 90 | }, 91 | { 92 | "tag": "CreateField", 93 | "model": "User", 94 | "field": "name", 95 | "type": "String", 96 | "arity": "Required" 97 | }, 98 | { 99 | "tag": "CreateField", 100 | "model": "User", 101 | "field": "password", 102 | "type": "String", 103 | "arity": "Required" 104 | }, 105 | { 106 | "tag": "CreateDirective", 107 | "location": { 108 | "path": { 109 | "tag": "Model", 110 | "model": "User" 111 | }, 112 | "directive": "map" 113 | } 114 | }, 115 | { 116 | "tag": "CreateArgument", 117 | "location": { 118 | "tag": "Directive", 119 | "path": { 120 | "tag": "Model", 121 | "model": "User" 122 | }, 123 | "directive": "map" 124 | }, 125 | "argument": "name", 126 | "value": "\"users\"" 127 | } 128 | ] 129 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | # Application 5 | 6 | app_database: 7 | image: 'bitnami/postgresql' 8 | restart: always 9 | ports: 10 | - '5432:5432' 11 | environment: 12 | - POSTGRESQL_PASSWORD=docker 13 | - POSTGRESQL_DATABASE=skylab 14 | volumes: 15 | - 'postgresql_data:/bitnami/postgresql' 16 | healthcheck: 17 | test: ["CMD", "pg_isready", "-U", "kong"] 18 | interval: 5s 19 | timeout: 5s 20 | retries: 5 21 | 22 | # app_service_atlas: 23 | # build: 24 | # context: ./packages/server/atlas 25 | # ports: 26 | # - '3333:3333' 27 | # command: "yarn dev" 28 | # restart: unless-stopped 29 | # volumes: 30 | # - .:/home/node/api 31 | # - ./node_modules:/home/node/api/node_modules 32 | # links: 33 | # - app_database 34 | # depends_on: 35 | # - app_database 36 | 37 | # Kafka & Zookeeper 38 | 39 | zookeeper: 40 | image: 'bitnami/zookeeper:3' 41 | ports: 42 | - '2181:2181' 43 | volumes: 44 | - 'zookeeper_data:/bitnami' 45 | environment: 46 | - ALLOW_ANONYMOUS_LOGIN=yes 47 | networks: 48 | - app-net 49 | 50 | kafka: 51 | image: 'bitnami/kafka:2' 52 | ports: 53 | - '9092:9092' 54 | volumes: 55 | - 'kafka_data:/bitnami' 56 | environment: 57 | - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 58 | - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 59 | - ALLOW_PLAINTEXT_LISTENER=yes 60 | depends_on: 61 | - zookeeper 62 | networks: 63 | - app-net 64 | 65 | # Kong (API Gateway) 66 | 67 | # kong-database: 68 | # image: 'bitnami/postgresql' 69 | # restart: always 70 | # networks: 71 | # - kong-net 72 | # environment: 73 | # - POSTGRESQL_USERNAME=kong 74 | # - POSTGRESQL_PASSWORD=kong 75 | # - POSTGRESQL_DATABASE=kong 76 | # healthcheck: 77 | # test: ["CMD", "pg_isready", "-U", "kong"] 78 | # interval: 5s 79 | # timeout: 5s 80 | # retries: 5 81 | 82 | # kong-migration: 83 | # image: 'bitnami/kong' 84 | # command: "kong migrations bootstrap" 85 | # networks: 86 | # - kong-net 87 | # restart: on-failure 88 | # environment: 89 | # KONG_DATABASE: postgres 90 | # KONG_PG_HOST: kong-database 91 | # KONG_PG_USER: kong 92 | # KONG_PG_PASSWORD: kong 93 | # links: 94 | # - kong-database 95 | # depends_on: 96 | # - kong-database 97 | 98 | # kong: 99 | # image: 'bitnami/kong' 100 | # restart: always 101 | # networks: 102 | # - kong-net 103 | # environment: 104 | # KONG_DATABASE: postgres 105 | # KONG_PG_HOST: kong-database 106 | # KONG_PG_USER: kong 107 | # KONG_PG_PASSWORD: kong 108 | # KONG_PROXY_LISTEN: 0.0.0.0:8000 109 | # KONG_PROXY_LISTEN_SSL: 0.0.0.0:8443 110 | # KONG_ADMIN_LISTEN: 0.0.0.0:8001 111 | # depends_on: 112 | # - kong-migration 113 | # - kong-database 114 | # healthcheck: 115 | # test: ["CMD", "curl", "-f", "http://kong:8001"] 116 | # interval: 5s 117 | # timeout: 2s 118 | # retries: 15 119 | # ports: 120 | # - "8001:8001" 121 | # - "8000:8000" 122 | 123 | networks: 124 | app-net: 125 | driver: bridge 126 | kong-net: 127 | driver: bridge 128 | 129 | volumes: 130 | zookeeper_data: 131 | driver: local 132 | kafka_data: 133 | driver: local 134 | postgresql_data: 135 | driver: local -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603231715-create_teams_and_addresses/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200603231715-create_teams_and_addresses` 2 | 3 | This migration has been generated by Diego Fernandes at 6/3/2020, 11:17:15 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "public"."user_addresses" ( 10 | "city" text NOT NULL ,"complement" text NOT NULL ,"country" text NOT NULL ,"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"foreignCountry" boolean NOT NULL DEFAULT false,"id" text NOT NULL ,"neighborhood" text NOT NULL ,"number" text NOT NULL ,"postalCode" text NOT NULL ,"state" text NOT NULL ,"streetName" text NOT NULL ,"type" text NOT NULL DEFAULT E'shipping',"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"userId" text , 11 | PRIMARY KEY ("id")) 12 | 13 | CREATE TABLE "public"."teams" ( 14 | "createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"id" text NOT NULL ,"title" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | PRIMARY KEY ("id")) 16 | 17 | CREATE TABLE "public"."_TeamToUser" ( 18 | "A" text NOT NULL ,"B" text NOT NULL ) 19 | 20 | CREATE UNIQUE INDEX "_TeamToUser_AB_unique" ON "public"."_TeamToUser"("A","B") 21 | 22 | CREATE INDEX "_TeamToUser_B_index" ON "public"."_TeamToUser"("B") 23 | 24 | ALTER TABLE "public"."user_addresses" ADD FOREIGN KEY ("userId")REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE CASCADE 25 | 26 | ALTER TABLE "public"."_TeamToUser" ADD FOREIGN KEY ("A")REFERENCES "public"."teams"("id") ON DELETE CASCADE ON UPDATE CASCADE 27 | 28 | ALTER TABLE "public"."_TeamToUser" ADD FOREIGN KEY ("B")REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE 29 | ``` 30 | 31 | ## Changes 32 | 33 | ```diff 34 | diff --git schema.prisma schema.prisma 35 | migration 20200515170312-create-users..20200603231715-create_teams_and_addresses 36 | --- datamodel.dml 37 | +++ datamodel.dml 38 | @@ -1,17 +1,49 @@ 39 | datasource db { 40 | provider = "postgresql" 41 | - url = "***" 42 | + url = env("DATABASE_URL") 43 | } 44 | generator client { 45 | provider = "prisma-client-js" 46 | } 47 | model User { 48 | - id String @default(uuid()) @id 49 | - email String @unique 50 | - name String 51 | - password String 52 | + @@map(name: "users") 53 | - @@map(name: "users") 54 | -} 55 | + id String @default(uuid()) @id 56 | + email String @unique 57 | + name String 58 | + password String 59 | + teams Team[] @relation(references: [id]) 60 | + addresses UserAddress[] 61 | +} 62 | + 63 | +model UserAddress { 64 | + @@map(name: "user_addresses") 65 | + 66 | + id String @default(uuid()) @id 67 | + country String 68 | + foreignCountry Boolean @default(false) 69 | + postalCode String 70 | + streetName String 71 | + number String 72 | + complement String 73 | + neighborhood String 74 | + city String 75 | + state String 76 | + type String @default("shipping") 77 | + createdAt DateTime @default(now()) 78 | + updatedAt DateTime @default(now()) 79 | + User User? @relation(fields: [userId], references: [id]) 80 | + userId String? 81 | +} 82 | + 83 | +model Team { 84 | + @@map(name: "teams") 85 | + 86 | + id String @default(uuid()) @id 87 | + title String 88 | + createdAt DateTime @default(now()) 89 | + updatedAt DateTime @default(now()) 90 | + users User[] @relation(references: [id]) 91 | +} 92 | ``` 93 | 94 | 95 | -------------------------------------------------------------------------------- /packages/server/umbriel/src/modules/contacts/useCases/contacts/subscribeContact/SubscribeContactUseCase.ts: -------------------------------------------------------------------------------- 1 | import { IUseCase } from '@server/shared/src/core/domain/UseCase'; 2 | import * as GenericAppError from '@server/shared/src/core/logic/AppError'; 3 | import { 4 | Result, 5 | failure, 6 | Either, 7 | success, 8 | } from '@server/shared/src/core/logic/Result'; 9 | 10 | import { Contact } from '@modules/contacts/domain/Contact'; 11 | import { ContactEmail } from '@modules/contacts/domain/ContactEmail'; 12 | import { Tag } from '@modules/contacts/domain/Tag'; 13 | import { TagSlug } from '@modules/contacts/domain/TagSlug'; 14 | import { IContactRepo } from '@modules/contacts/repositories/IContactRepo'; 15 | import { ITagRepo } from '@modules/contacts/repositories/ITagRepo'; 16 | 17 | import { ISubscribeContactRequestDTO } from './SubscribeContactDTO'; 18 | import * as SubscribeContactErrors from './SubscribeContactErrors'; 19 | 20 | type Response = Either>; 21 | 22 | export class SubscribeContactUseCase 23 | implements IUseCase> { 24 | private contactRepo: IContactRepo; 25 | 26 | private tagRepo: ITagRepo; 27 | 28 | constructor(contactRepo: IContactRepo, tagRepo: ITagRepo) { 29 | this.contactRepo = contactRepo; 30 | this.tagRepo = tagRepo; 31 | } 32 | 33 | async execute(request: ISubscribeContactRequestDTO): Promise { 34 | try { 35 | const contactAlreadyExists = await this.contactRepo.findByEmail( 36 | request.email 37 | ); 38 | 39 | if (contactAlreadyExists) { 40 | return failure( 41 | new SubscribeContactErrors.ContactAlreadyExists(request.email) 42 | ); 43 | } 44 | 45 | const tagsOrError: Result[] = request.tags.map((tag) => { 46 | const slugOrError = TagSlug.createFromIntegration( 47 | tag.title, 48 | tag.integrationId 49 | ); 50 | 51 | if (slugOrError.isFailure) { 52 | return Result.fail(slugOrError.error); 53 | } 54 | 55 | return Tag.create({ 56 | title: tag.title, 57 | slug: slugOrError.getValue(), 58 | }); 59 | }); 60 | 61 | const tagsResult = Result.combine(tagsOrError); 62 | 63 | if (tagsResult.isFailure) { 64 | return failure(Result.fail(tagsResult.error)); 65 | } 66 | 67 | const tags = tagsOrError.map((tag) => tag.getValue()); 68 | const tagsSlugs = tags.map((tag) => tag.slug); 69 | 70 | const existentTags = await this.tagRepo.findTagBySlugBulk(tagsSlugs); 71 | 72 | const nonExistentTags = tags.filter((tag) => { 73 | return !existentTags.find((existentTag) => { 74 | return existentTag.slug.value === tag.slug.value; 75 | }); 76 | }); 77 | 78 | await this.tagRepo.saveBulk(nonExistentTags); 79 | 80 | const emailOrError = ContactEmail.create(request.email); 81 | 82 | if (emailOrError.isFailure) { 83 | return failure(Result.fail(emailOrError.error)); 84 | } 85 | 86 | const contactOrError = Contact.create({ 87 | email: emailOrError.getValue(), 88 | }); 89 | 90 | if (contactOrError.isFailure) { 91 | return failure(Result.fail(contactOrError.error)); 92 | } 93 | 94 | const contact = contactOrError.getValue(); 95 | 96 | const subscribedTags = [...existentTags, ...nonExistentTags]; 97 | 98 | const subscribedTagsOrErrors = subscribedTags.map((tag) => { 99 | return contact.addSubscription(tag); 100 | }); 101 | 102 | const subscriptionResult = Result.combine(subscribedTagsOrErrors); 103 | 104 | if (subscriptionResult.isFailure) { 105 | return failure(Result.fail(subscriptionResult.error)); 106 | } 107 | 108 | await this.contactRepo.save(contact); 109 | 110 | return success(Result.ok()); 111 | } catch (err) { 112 | return failure(new GenericAppError.UnexpectedError(err)); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603234442-create_user_email_history/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateModel", 6 | "model": "UserEmailHistory" 7 | }, 8 | { 9 | "tag": "CreateField", 10 | "model": "UserEmailHistory", 11 | "field": "id", 12 | "type": "String", 13 | "arity": "Required" 14 | }, 15 | { 16 | "tag": "CreateDirective", 17 | "location": { 18 | "path": { 19 | "tag": "Field", 20 | "model": "UserEmailHistory", 21 | "field": "id" 22 | }, 23 | "directive": "default" 24 | } 25 | }, 26 | { 27 | "tag": "CreateArgument", 28 | "location": { 29 | "tag": "Directive", 30 | "path": { 31 | "tag": "Field", 32 | "model": "UserEmailHistory", 33 | "field": "id" 34 | }, 35 | "directive": "default" 36 | }, 37 | "argument": "", 38 | "value": "uuid()" 39 | }, 40 | { 41 | "tag": "CreateDirective", 42 | "location": { 43 | "path": { 44 | "tag": "Field", 45 | "model": "UserEmailHistory", 46 | "field": "id" 47 | }, 48 | "directive": "id" 49 | } 50 | }, 51 | { 52 | "tag": "CreateField", 53 | "model": "UserEmailHistory", 54 | "field": "email", 55 | "type": "String", 56 | "arity": "Required" 57 | }, 58 | { 59 | "tag": "CreateField", 60 | "model": "UserEmailHistory", 61 | "field": "changedAt", 62 | "type": "DateTime", 63 | "arity": "Required" 64 | }, 65 | { 66 | "tag": "CreateDirective", 67 | "location": { 68 | "path": { 69 | "tag": "Field", 70 | "model": "UserEmailHistory", 71 | "field": "changedAt" 72 | }, 73 | "directive": "default" 74 | } 75 | }, 76 | { 77 | "tag": "CreateArgument", 78 | "location": { 79 | "tag": "Directive", 80 | "path": { 81 | "tag": "Field", 82 | "model": "UserEmailHistory", 83 | "field": "changedAt" 84 | }, 85 | "directive": "default" 86 | }, 87 | "argument": "", 88 | "value": "now()" 89 | }, 90 | { 91 | "tag": "CreateField", 92 | "model": "UserEmailHistory", 93 | "field": "User", 94 | "type": "User", 95 | "arity": "Optional" 96 | }, 97 | { 98 | "tag": "CreateDirective", 99 | "location": { 100 | "path": { 101 | "tag": "Field", 102 | "model": "UserEmailHistory", 103 | "field": "User" 104 | }, 105 | "directive": "relation" 106 | } 107 | }, 108 | { 109 | "tag": "CreateArgument", 110 | "location": { 111 | "tag": "Directive", 112 | "path": { 113 | "tag": "Field", 114 | "model": "UserEmailHistory", 115 | "field": "User" 116 | }, 117 | "directive": "relation" 118 | }, 119 | "argument": "fields", 120 | "value": "[userId]" 121 | }, 122 | { 123 | "tag": "CreateArgument", 124 | "location": { 125 | "tag": "Directive", 126 | "path": { 127 | "tag": "Field", 128 | "model": "UserEmailHistory", 129 | "field": "User" 130 | }, 131 | "directive": "relation" 132 | }, 133 | "argument": "references", 134 | "value": "[id]" 135 | }, 136 | { 137 | "tag": "CreateField", 138 | "model": "UserEmailHistory", 139 | "field": "userId", 140 | "type": "String", 141 | "arity": "Optional" 142 | }, 143 | { 144 | "tag": "CreateDirective", 145 | "location": { 146 | "path": { 147 | "tag": "Model", 148 | "model": "UserEmailHistory" 149 | }, 150 | "directive": "map" 151 | } 152 | }, 153 | { 154 | "tag": "CreateArgument", 155 | "location": { 156 | "tag": "Directive", 157 | "path": { 158 | "tag": "Model", 159 | "model": "UserEmailHistory" 160 | }, 161 | "directive": "map" 162 | }, 163 | "argument": "name", 164 | "value": "\"user_email_history\"" 165 | }, 166 | { 167 | "tag": "CreateField", 168 | "model": "User", 169 | "field": "UserEmailHistory", 170 | "type": "UserEmailHistory", 171 | "arity": "List" 172 | } 173 | ] 174 | } -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200609124237-create_base_templates/README.md: -------------------------------------------------------------------------------- 1 | # Migration `20200609124237-create_base_templates` 2 | 3 | This migration has been generated by Diego Fernandes at 6/9/2020, 12:42:37 PM. 4 | You can check out the [state of the schema](./schema.prisma) after the migration. 5 | 6 | ## Database Steps 7 | 8 | ```sql 9 | CREATE TABLE "public"."contacts" ( 10 | "createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" text NOT NULL ,"id" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | PRIMARY KEY ("id")) 12 | 13 | CREATE TABLE "public"."contact_tags" ( 14 | "contactId" text NOT NULL ,"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"id" text NOT NULL ,"subscribed" boolean NOT NULL DEFAULT true,"tagId" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | PRIMARY KEY ("id")) 16 | 17 | CREATE TABLE "public"."tags" ( 18 | "id" text NOT NULL ,"title" text NOT NULL , 19 | PRIMARY KEY ("id")) 20 | 21 | CREATE TABLE "public"."messages" ( 22 | "body" text NOT NULL ,"createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"id" text NOT NULL ,"recipientsCount" integer NOT NULL ,"senderId" text NOT NULL ,"sentAt" timestamp(3) ,"sentCount" integer NOT NULL DEFAULT 0,"subject" text NOT NULL ,"templateId" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | PRIMARY KEY ("id")) 24 | 25 | CREATE TABLE "public"."templates" ( 26 | "content" text NOT NULL ,"id" text NOT NULL ,"title" text NOT NULL , 27 | PRIMARY KEY ("id")) 28 | 29 | CREATE TABLE "public"."senders" ( 30 | "createdAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" text NOT NULL ,"id" text NOT NULL ,"name" text NOT NULL ,"updatedAt" timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 31 | PRIMARY KEY ("id")) 32 | 33 | CREATE UNIQUE INDEX "contacts.email" ON "public"."contacts"("email") 34 | 35 | ALTER TABLE "public"."contact_tags" ADD FOREIGN KEY ("contactId")REFERENCES "public"."contacts"("id") ON DELETE CASCADE ON UPDATE CASCADE 36 | 37 | ALTER TABLE "public"."contact_tags" ADD FOREIGN KEY ("tagId")REFERENCES "public"."tags"("id") ON DELETE CASCADE ON UPDATE CASCADE 38 | 39 | ALTER TABLE "public"."messages" ADD FOREIGN KEY ("templateId")REFERENCES "public"."templates"("id") ON DELETE CASCADE ON UPDATE CASCADE 40 | 41 | ALTER TABLE "public"."messages" ADD FOREIGN KEY ("senderId")REFERENCES "public"."senders"("id") ON DELETE CASCADE ON UPDATE CASCADE 42 | ``` 43 | 44 | ## Changes 45 | 46 | ```diff 47 | diff --git schema.prisma schema.prisma 48 | migration ..20200609124237-create_base_templates 49 | --- datamodel.dml 50 | +++ datamodel.dml 51 | @@ -1,0 +1,84 @@ 52 | +// This is your Prisma schema file, 53 | +// learn more about it in the docs: https://pris.ly/d/prisma-schema 54 | + 55 | +datasource db { 56 | + provider = "postgresql" 57 | + url = env("DATABASE_URL") 58 | +} 59 | + 60 | +generator client { 61 | + provider = "prisma-client-js" 62 | +} 63 | + 64 | +model Contact { 65 | + @@map(name: "contacts") 66 | + 67 | + id String @default(uuid()) @id 68 | + email String @unique 69 | + tags ContactTags[] 70 | + 71 | + createdAt DateTime @default(now()) 72 | + updatedAt DateTime @default(now()) 73 | +} 74 | + 75 | +model ContactTags { 76 | + @@map(name: "contact_tags") 77 | + 78 | + id String @default(uuid()) @id 79 | + contact Contact @relation(fields: [contactId], references: [id]) 80 | + contactId String 81 | + tag Tag @relation(fields: [tagId], references: [id]) 82 | + tagId String 83 | + subscribed Boolean @default(true) 84 | + 85 | + createdAt DateTime @default(now()) 86 | + updatedAt DateTime @default(now()) 87 | +} 88 | + 89 | +model Tag { 90 | + @@map(name: "tags") 91 | + 92 | + id String @default(uuid()) @id 93 | + title String 94 | + contacts ContactTags[] 95 | +} 96 | + 97 | +model Message { 98 | + @@map(name: "messages") 99 | + 100 | + id String @default(uuid()) @id 101 | + subject String 102 | + body String 103 | + recipientsCount Int 104 | + sentCount Int @default(0) 105 | + sentAt DateTime? 106 | + template Template @relation(fields: [templateId], references: [id]) 107 | + templateId String 108 | + sender Sender @relation(fields: [senderId], references: [id]) 109 | + senderId String 110 | + 111 | + createdAt DateTime @default(now()) 112 | + updatedAt DateTime @default(now()) 113 | +} 114 | + 115 | +model Template { 116 | + @@map(name: "templates") 117 | + 118 | + id String @default(uuid()) @id 119 | + messages Message[] 120 | + 121 | + title String 122 | + content String 123 | +} 124 | + 125 | +model Sender { 126 | + @@map(name: "senders") 127 | + 128 | + id String @default(uuid()) @id 129 | + messages Message[] 130 | + name String 131 | + email String 132 | + 133 | + createdAt DateTime @default(now()) 134 | + updatedAt DateTime @default(now()) 135 | +} 136 | ``` 137 | 138 | 139 | -------------------------------------------------------------------------------- /packages/server/atlas/prisma/migrations/20200603231715-create_teams_and_addresses/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateModel", 6 | "model": "UserAddress" 7 | }, 8 | { 9 | "tag": "CreateField", 10 | "model": "UserAddress", 11 | "field": "id", 12 | "type": "String", 13 | "arity": "Required" 14 | }, 15 | { 16 | "tag": "CreateDirective", 17 | "location": { 18 | "path": { 19 | "tag": "Field", 20 | "model": "UserAddress", 21 | "field": "id" 22 | }, 23 | "directive": "default" 24 | } 25 | }, 26 | { 27 | "tag": "CreateArgument", 28 | "location": { 29 | "tag": "Directive", 30 | "path": { 31 | "tag": "Field", 32 | "model": "UserAddress", 33 | "field": "id" 34 | }, 35 | "directive": "default" 36 | }, 37 | "argument": "", 38 | "value": "uuid()" 39 | }, 40 | { 41 | "tag": "CreateDirective", 42 | "location": { 43 | "path": { 44 | "tag": "Field", 45 | "model": "UserAddress", 46 | "field": "id" 47 | }, 48 | "directive": "id" 49 | } 50 | }, 51 | { 52 | "tag": "CreateField", 53 | "model": "UserAddress", 54 | "field": "country", 55 | "type": "String", 56 | "arity": "Required" 57 | }, 58 | { 59 | "tag": "CreateField", 60 | "model": "UserAddress", 61 | "field": "foreignCountry", 62 | "type": "Boolean", 63 | "arity": "Required" 64 | }, 65 | { 66 | "tag": "CreateDirective", 67 | "location": { 68 | "path": { 69 | "tag": "Field", 70 | "model": "UserAddress", 71 | "field": "foreignCountry" 72 | }, 73 | "directive": "default" 74 | } 75 | }, 76 | { 77 | "tag": "CreateArgument", 78 | "location": { 79 | "tag": "Directive", 80 | "path": { 81 | "tag": "Field", 82 | "model": "UserAddress", 83 | "field": "foreignCountry" 84 | }, 85 | "directive": "default" 86 | }, 87 | "argument": "", 88 | "value": "false" 89 | }, 90 | { 91 | "tag": "CreateField", 92 | "model": "UserAddress", 93 | "field": "postalCode", 94 | "type": "String", 95 | "arity": "Required" 96 | }, 97 | { 98 | "tag": "CreateField", 99 | "model": "UserAddress", 100 | "field": "streetName", 101 | "type": "String", 102 | "arity": "Required" 103 | }, 104 | { 105 | "tag": "CreateField", 106 | "model": "UserAddress", 107 | "field": "number", 108 | "type": "String", 109 | "arity": "Required" 110 | }, 111 | { 112 | "tag": "CreateField", 113 | "model": "UserAddress", 114 | "field": "complement", 115 | "type": "String", 116 | "arity": "Required" 117 | }, 118 | { 119 | "tag": "CreateField", 120 | "model": "UserAddress", 121 | "field": "neighborhood", 122 | "type": "String", 123 | "arity": "Required" 124 | }, 125 | { 126 | "tag": "CreateField", 127 | "model": "UserAddress", 128 | "field": "city", 129 | "type": "String", 130 | "arity": "Required" 131 | }, 132 | { 133 | "tag": "CreateField", 134 | "model": "UserAddress", 135 | "field": "state", 136 | "type": "String", 137 | "arity": "Required" 138 | }, 139 | { 140 | "tag": "CreateField", 141 | "model": "UserAddress", 142 | "field": "type", 143 | "type": "String", 144 | "arity": "Required" 145 | }, 146 | { 147 | "tag": "CreateDirective", 148 | "location": { 149 | "path": { 150 | "tag": "Field", 151 | "model": "UserAddress", 152 | "field": "type" 153 | }, 154 | "directive": "default" 155 | } 156 | }, 157 | { 158 | "tag": "CreateArgument", 159 | "location": { 160 | "tag": "Directive", 161 | "path": { 162 | "tag": "Field", 163 | "model": "UserAddress", 164 | "field": "type" 165 | }, 166 | "directive": "default" 167 | }, 168 | "argument": "", 169 | "value": "\"shipping\"" 170 | }, 171 | { 172 | "tag": "CreateField", 173 | "model": "UserAddress", 174 | "field": "createdAt", 175 | "type": "DateTime", 176 | "arity": "Required" 177 | }, 178 | { 179 | "tag": "CreateDirective", 180 | "location": { 181 | "path": { 182 | "tag": "Field", 183 | "model": "UserAddress", 184 | "field": "createdAt" 185 | }, 186 | "directive": "default" 187 | } 188 | }, 189 | { 190 | "tag": "CreateArgument", 191 | "location": { 192 | "tag": "Directive", 193 | "path": { 194 | "tag": "Field", 195 | "model": "UserAddress", 196 | "field": "createdAt" 197 | }, 198 | "directive": "default" 199 | }, 200 | "argument": "", 201 | "value": "now()" 202 | }, 203 | { 204 | "tag": "CreateField", 205 | "model": "UserAddress", 206 | "field": "updatedAt", 207 | "type": "DateTime", 208 | "arity": "Required" 209 | }, 210 | { 211 | "tag": "CreateDirective", 212 | "location": { 213 | "path": { 214 | "tag": "Field", 215 | "model": "UserAddress", 216 | "field": "updatedAt" 217 | }, 218 | "directive": "default" 219 | } 220 | }, 221 | { 222 | "tag": "CreateArgument", 223 | "location": { 224 | "tag": "Directive", 225 | "path": { 226 | "tag": "Field", 227 | "model": "UserAddress", 228 | "field": "updatedAt" 229 | }, 230 | "directive": "default" 231 | }, 232 | "argument": "", 233 | "value": "now()" 234 | }, 235 | { 236 | "tag": "CreateField", 237 | "model": "UserAddress", 238 | "field": "User", 239 | "type": "User", 240 | "arity": "Optional" 241 | }, 242 | { 243 | "tag": "CreateDirective", 244 | "location": { 245 | "path": { 246 | "tag": "Field", 247 | "model": "UserAddress", 248 | "field": "User" 249 | }, 250 | "directive": "relation" 251 | } 252 | }, 253 | { 254 | "tag": "CreateArgument", 255 | "location": { 256 | "tag": "Directive", 257 | "path": { 258 | "tag": "Field", 259 | "model": "UserAddress", 260 | "field": "User" 261 | }, 262 | "directive": "relation" 263 | }, 264 | "argument": "fields", 265 | "value": "[userId]" 266 | }, 267 | { 268 | "tag": "CreateArgument", 269 | "location": { 270 | "tag": "Directive", 271 | "path": { 272 | "tag": "Field", 273 | "model": "UserAddress", 274 | "field": "User" 275 | }, 276 | "directive": "relation" 277 | }, 278 | "argument": "references", 279 | "value": "[id]" 280 | }, 281 | { 282 | "tag": "CreateField", 283 | "model": "UserAddress", 284 | "field": "userId", 285 | "type": "String", 286 | "arity": "Optional" 287 | }, 288 | { 289 | "tag": "CreateDirective", 290 | "location": { 291 | "path": { 292 | "tag": "Model", 293 | "model": "UserAddress" 294 | }, 295 | "directive": "map" 296 | } 297 | }, 298 | { 299 | "tag": "CreateArgument", 300 | "location": { 301 | "tag": "Directive", 302 | "path": { 303 | "tag": "Model", 304 | "model": "UserAddress" 305 | }, 306 | "directive": "map" 307 | }, 308 | "argument": "name", 309 | "value": "\"user_addresses\"" 310 | }, 311 | { 312 | "tag": "CreateModel", 313 | "model": "Team" 314 | }, 315 | { 316 | "tag": "CreateField", 317 | "model": "Team", 318 | "field": "id", 319 | "type": "String", 320 | "arity": "Required" 321 | }, 322 | { 323 | "tag": "CreateDirective", 324 | "location": { 325 | "path": { 326 | "tag": "Field", 327 | "model": "Team", 328 | "field": "id" 329 | }, 330 | "directive": "default" 331 | } 332 | }, 333 | { 334 | "tag": "CreateArgument", 335 | "location": { 336 | "tag": "Directive", 337 | "path": { 338 | "tag": "Field", 339 | "model": "Team", 340 | "field": "id" 341 | }, 342 | "directive": "default" 343 | }, 344 | "argument": "", 345 | "value": "uuid()" 346 | }, 347 | { 348 | "tag": "CreateDirective", 349 | "location": { 350 | "path": { 351 | "tag": "Field", 352 | "model": "Team", 353 | "field": "id" 354 | }, 355 | "directive": "id" 356 | } 357 | }, 358 | { 359 | "tag": "CreateField", 360 | "model": "Team", 361 | "field": "title", 362 | "type": "String", 363 | "arity": "Required" 364 | }, 365 | { 366 | "tag": "CreateField", 367 | "model": "Team", 368 | "field": "createdAt", 369 | "type": "DateTime", 370 | "arity": "Required" 371 | }, 372 | { 373 | "tag": "CreateDirective", 374 | "location": { 375 | "path": { 376 | "tag": "Field", 377 | "model": "Team", 378 | "field": "createdAt" 379 | }, 380 | "directive": "default" 381 | } 382 | }, 383 | { 384 | "tag": "CreateArgument", 385 | "location": { 386 | "tag": "Directive", 387 | "path": { 388 | "tag": "Field", 389 | "model": "Team", 390 | "field": "createdAt" 391 | }, 392 | "directive": "default" 393 | }, 394 | "argument": "", 395 | "value": "now()" 396 | }, 397 | { 398 | "tag": "CreateField", 399 | "model": "Team", 400 | "field": "updatedAt", 401 | "type": "DateTime", 402 | "arity": "Required" 403 | }, 404 | { 405 | "tag": "CreateDirective", 406 | "location": { 407 | "path": { 408 | "tag": "Field", 409 | "model": "Team", 410 | "field": "updatedAt" 411 | }, 412 | "directive": "default" 413 | } 414 | }, 415 | { 416 | "tag": "CreateArgument", 417 | "location": { 418 | "tag": "Directive", 419 | "path": { 420 | "tag": "Field", 421 | "model": "Team", 422 | "field": "updatedAt" 423 | }, 424 | "directive": "default" 425 | }, 426 | "argument": "", 427 | "value": "now()" 428 | }, 429 | { 430 | "tag": "CreateField", 431 | "model": "Team", 432 | "field": "users", 433 | "type": "User", 434 | "arity": "List" 435 | }, 436 | { 437 | "tag": "CreateDirective", 438 | "location": { 439 | "path": { 440 | "tag": "Field", 441 | "model": "Team", 442 | "field": "users" 443 | }, 444 | "directive": "relation" 445 | } 446 | }, 447 | { 448 | "tag": "CreateArgument", 449 | "location": { 450 | "tag": "Directive", 451 | "path": { 452 | "tag": "Field", 453 | "model": "Team", 454 | "field": "users" 455 | }, 456 | "directive": "relation" 457 | }, 458 | "argument": "references", 459 | "value": "[id]" 460 | }, 461 | { 462 | "tag": "CreateDirective", 463 | "location": { 464 | "path": { 465 | "tag": "Model", 466 | "model": "Team" 467 | }, 468 | "directive": "map" 469 | } 470 | }, 471 | { 472 | "tag": "CreateArgument", 473 | "location": { 474 | "tag": "Directive", 475 | "path": { 476 | "tag": "Model", 477 | "model": "Team" 478 | }, 479 | "directive": "map" 480 | }, 481 | "argument": "name", 482 | "value": "\"teams\"" 483 | }, 484 | { 485 | "tag": "CreateField", 486 | "model": "User", 487 | "field": "teams", 488 | "type": "Team", 489 | "arity": "List" 490 | }, 491 | { 492 | "tag": "CreateDirective", 493 | "location": { 494 | "path": { 495 | "tag": "Field", 496 | "model": "User", 497 | "field": "teams" 498 | }, 499 | "directive": "relation" 500 | } 501 | }, 502 | { 503 | "tag": "CreateArgument", 504 | "location": { 505 | "tag": "Directive", 506 | "path": { 507 | "tag": "Field", 508 | "model": "User", 509 | "field": "teams" 510 | }, 511 | "directive": "relation" 512 | }, 513 | "argument": "references", 514 | "value": "[id]" 515 | }, 516 | { 517 | "tag": "CreateField", 518 | "model": "User", 519 | "field": "addresses", 520 | "type": "UserAddress", 521 | "arity": "List" 522 | } 523 | ] 524 | } -------------------------------------------------------------------------------- /packages/server/umbriel/prisma/migrations/20200609124237-create_base_templates/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.14-fixed", 3 | "steps": [ 4 | { 5 | "tag": "CreateSource", 6 | "source": "db" 7 | }, 8 | { 9 | "tag": "CreateArgument", 10 | "location": { 11 | "tag": "Source", 12 | "source": "db" 13 | }, 14 | "argument": "provider", 15 | "value": "\"postgresql\"" 16 | }, 17 | { 18 | "tag": "CreateArgument", 19 | "location": { 20 | "tag": "Source", 21 | "source": "db" 22 | }, 23 | "argument": "url", 24 | "value": "env(\"DATABASE_URL\")" 25 | }, 26 | { 27 | "tag": "CreateModel", 28 | "model": "Contact" 29 | }, 30 | { 31 | "tag": "CreateField", 32 | "model": "Contact", 33 | "field": "id", 34 | "type": "String", 35 | "arity": "Required" 36 | }, 37 | { 38 | "tag": "CreateDirective", 39 | "location": { 40 | "path": { 41 | "tag": "Field", 42 | "model": "Contact", 43 | "field": "id" 44 | }, 45 | "directive": "default" 46 | } 47 | }, 48 | { 49 | "tag": "CreateArgument", 50 | "location": { 51 | "tag": "Directive", 52 | "path": { 53 | "tag": "Field", 54 | "model": "Contact", 55 | "field": "id" 56 | }, 57 | "directive": "default" 58 | }, 59 | "argument": "", 60 | "value": "uuid()" 61 | }, 62 | { 63 | "tag": "CreateDirective", 64 | "location": { 65 | "path": { 66 | "tag": "Field", 67 | "model": "Contact", 68 | "field": "id" 69 | }, 70 | "directive": "id" 71 | } 72 | }, 73 | { 74 | "tag": "CreateField", 75 | "model": "Contact", 76 | "field": "email", 77 | "type": "String", 78 | "arity": "Required" 79 | }, 80 | { 81 | "tag": "CreateDirective", 82 | "location": { 83 | "path": { 84 | "tag": "Field", 85 | "model": "Contact", 86 | "field": "email" 87 | }, 88 | "directive": "unique" 89 | } 90 | }, 91 | { 92 | "tag": "CreateField", 93 | "model": "Contact", 94 | "field": "tags", 95 | "type": "ContactTags", 96 | "arity": "List" 97 | }, 98 | { 99 | "tag": "CreateField", 100 | "model": "Contact", 101 | "field": "createdAt", 102 | "type": "DateTime", 103 | "arity": "Required" 104 | }, 105 | { 106 | "tag": "CreateDirective", 107 | "location": { 108 | "path": { 109 | "tag": "Field", 110 | "model": "Contact", 111 | "field": "createdAt" 112 | }, 113 | "directive": "default" 114 | } 115 | }, 116 | { 117 | "tag": "CreateArgument", 118 | "location": { 119 | "tag": "Directive", 120 | "path": { 121 | "tag": "Field", 122 | "model": "Contact", 123 | "field": "createdAt" 124 | }, 125 | "directive": "default" 126 | }, 127 | "argument": "", 128 | "value": "now()" 129 | }, 130 | { 131 | "tag": "CreateField", 132 | "model": "Contact", 133 | "field": "updatedAt", 134 | "type": "DateTime", 135 | "arity": "Required" 136 | }, 137 | { 138 | "tag": "CreateDirective", 139 | "location": { 140 | "path": { 141 | "tag": "Field", 142 | "model": "Contact", 143 | "field": "updatedAt" 144 | }, 145 | "directive": "default" 146 | } 147 | }, 148 | { 149 | "tag": "CreateArgument", 150 | "location": { 151 | "tag": "Directive", 152 | "path": { 153 | "tag": "Field", 154 | "model": "Contact", 155 | "field": "updatedAt" 156 | }, 157 | "directive": "default" 158 | }, 159 | "argument": "", 160 | "value": "now()" 161 | }, 162 | { 163 | "tag": "CreateDirective", 164 | "location": { 165 | "path": { 166 | "tag": "Model", 167 | "model": "Contact" 168 | }, 169 | "directive": "map" 170 | } 171 | }, 172 | { 173 | "tag": "CreateArgument", 174 | "location": { 175 | "tag": "Directive", 176 | "path": { 177 | "tag": "Model", 178 | "model": "Contact" 179 | }, 180 | "directive": "map" 181 | }, 182 | "argument": "name", 183 | "value": "\"contacts\"" 184 | }, 185 | { 186 | "tag": "CreateModel", 187 | "model": "ContactTags" 188 | }, 189 | { 190 | "tag": "CreateField", 191 | "model": "ContactTags", 192 | "field": "id", 193 | "type": "String", 194 | "arity": "Required" 195 | }, 196 | { 197 | "tag": "CreateDirective", 198 | "location": { 199 | "path": { 200 | "tag": "Field", 201 | "model": "ContactTags", 202 | "field": "id" 203 | }, 204 | "directive": "default" 205 | } 206 | }, 207 | { 208 | "tag": "CreateArgument", 209 | "location": { 210 | "tag": "Directive", 211 | "path": { 212 | "tag": "Field", 213 | "model": "ContactTags", 214 | "field": "id" 215 | }, 216 | "directive": "default" 217 | }, 218 | "argument": "", 219 | "value": "uuid()" 220 | }, 221 | { 222 | "tag": "CreateDirective", 223 | "location": { 224 | "path": { 225 | "tag": "Field", 226 | "model": "ContactTags", 227 | "field": "id" 228 | }, 229 | "directive": "id" 230 | } 231 | }, 232 | { 233 | "tag": "CreateField", 234 | "model": "ContactTags", 235 | "field": "contact", 236 | "type": "Contact", 237 | "arity": "Required" 238 | }, 239 | { 240 | "tag": "CreateDirective", 241 | "location": { 242 | "path": { 243 | "tag": "Field", 244 | "model": "ContactTags", 245 | "field": "contact" 246 | }, 247 | "directive": "relation" 248 | } 249 | }, 250 | { 251 | "tag": "CreateArgument", 252 | "location": { 253 | "tag": "Directive", 254 | "path": { 255 | "tag": "Field", 256 | "model": "ContactTags", 257 | "field": "contact" 258 | }, 259 | "directive": "relation" 260 | }, 261 | "argument": "fields", 262 | "value": "[contactId]" 263 | }, 264 | { 265 | "tag": "CreateArgument", 266 | "location": { 267 | "tag": "Directive", 268 | "path": { 269 | "tag": "Field", 270 | "model": "ContactTags", 271 | "field": "contact" 272 | }, 273 | "directive": "relation" 274 | }, 275 | "argument": "references", 276 | "value": "[id]" 277 | }, 278 | { 279 | "tag": "CreateField", 280 | "model": "ContactTags", 281 | "field": "contactId", 282 | "type": "String", 283 | "arity": "Required" 284 | }, 285 | { 286 | "tag": "CreateField", 287 | "model": "ContactTags", 288 | "field": "tag", 289 | "type": "Tag", 290 | "arity": "Required" 291 | }, 292 | { 293 | "tag": "CreateDirective", 294 | "location": { 295 | "path": { 296 | "tag": "Field", 297 | "model": "ContactTags", 298 | "field": "tag" 299 | }, 300 | "directive": "relation" 301 | } 302 | }, 303 | { 304 | "tag": "CreateArgument", 305 | "location": { 306 | "tag": "Directive", 307 | "path": { 308 | "tag": "Field", 309 | "model": "ContactTags", 310 | "field": "tag" 311 | }, 312 | "directive": "relation" 313 | }, 314 | "argument": "fields", 315 | "value": "[tagId]" 316 | }, 317 | { 318 | "tag": "CreateArgument", 319 | "location": { 320 | "tag": "Directive", 321 | "path": { 322 | "tag": "Field", 323 | "model": "ContactTags", 324 | "field": "tag" 325 | }, 326 | "directive": "relation" 327 | }, 328 | "argument": "references", 329 | "value": "[id]" 330 | }, 331 | { 332 | "tag": "CreateField", 333 | "model": "ContactTags", 334 | "field": "tagId", 335 | "type": "String", 336 | "arity": "Required" 337 | }, 338 | { 339 | "tag": "CreateField", 340 | "model": "ContactTags", 341 | "field": "subscribed", 342 | "type": "Boolean", 343 | "arity": "Required" 344 | }, 345 | { 346 | "tag": "CreateDirective", 347 | "location": { 348 | "path": { 349 | "tag": "Field", 350 | "model": "ContactTags", 351 | "field": "subscribed" 352 | }, 353 | "directive": "default" 354 | } 355 | }, 356 | { 357 | "tag": "CreateArgument", 358 | "location": { 359 | "tag": "Directive", 360 | "path": { 361 | "tag": "Field", 362 | "model": "ContactTags", 363 | "field": "subscribed" 364 | }, 365 | "directive": "default" 366 | }, 367 | "argument": "", 368 | "value": "true" 369 | }, 370 | { 371 | "tag": "CreateField", 372 | "model": "ContactTags", 373 | "field": "createdAt", 374 | "type": "DateTime", 375 | "arity": "Required" 376 | }, 377 | { 378 | "tag": "CreateDirective", 379 | "location": { 380 | "path": { 381 | "tag": "Field", 382 | "model": "ContactTags", 383 | "field": "createdAt" 384 | }, 385 | "directive": "default" 386 | } 387 | }, 388 | { 389 | "tag": "CreateArgument", 390 | "location": { 391 | "tag": "Directive", 392 | "path": { 393 | "tag": "Field", 394 | "model": "ContactTags", 395 | "field": "createdAt" 396 | }, 397 | "directive": "default" 398 | }, 399 | "argument": "", 400 | "value": "now()" 401 | }, 402 | { 403 | "tag": "CreateField", 404 | "model": "ContactTags", 405 | "field": "updatedAt", 406 | "type": "DateTime", 407 | "arity": "Required" 408 | }, 409 | { 410 | "tag": "CreateDirective", 411 | "location": { 412 | "path": { 413 | "tag": "Field", 414 | "model": "ContactTags", 415 | "field": "updatedAt" 416 | }, 417 | "directive": "default" 418 | } 419 | }, 420 | { 421 | "tag": "CreateArgument", 422 | "location": { 423 | "tag": "Directive", 424 | "path": { 425 | "tag": "Field", 426 | "model": "ContactTags", 427 | "field": "updatedAt" 428 | }, 429 | "directive": "default" 430 | }, 431 | "argument": "", 432 | "value": "now()" 433 | }, 434 | { 435 | "tag": "CreateDirective", 436 | "location": { 437 | "path": { 438 | "tag": "Model", 439 | "model": "ContactTags" 440 | }, 441 | "directive": "map" 442 | } 443 | }, 444 | { 445 | "tag": "CreateArgument", 446 | "location": { 447 | "tag": "Directive", 448 | "path": { 449 | "tag": "Model", 450 | "model": "ContactTags" 451 | }, 452 | "directive": "map" 453 | }, 454 | "argument": "name", 455 | "value": "\"contact_tags\"" 456 | }, 457 | { 458 | "tag": "CreateModel", 459 | "model": "Tag" 460 | }, 461 | { 462 | "tag": "CreateField", 463 | "model": "Tag", 464 | "field": "id", 465 | "type": "String", 466 | "arity": "Required" 467 | }, 468 | { 469 | "tag": "CreateDirective", 470 | "location": { 471 | "path": { 472 | "tag": "Field", 473 | "model": "Tag", 474 | "field": "id" 475 | }, 476 | "directive": "default" 477 | } 478 | }, 479 | { 480 | "tag": "CreateArgument", 481 | "location": { 482 | "tag": "Directive", 483 | "path": { 484 | "tag": "Field", 485 | "model": "Tag", 486 | "field": "id" 487 | }, 488 | "directive": "default" 489 | }, 490 | "argument": "", 491 | "value": "uuid()" 492 | }, 493 | { 494 | "tag": "CreateDirective", 495 | "location": { 496 | "path": { 497 | "tag": "Field", 498 | "model": "Tag", 499 | "field": "id" 500 | }, 501 | "directive": "id" 502 | } 503 | }, 504 | { 505 | "tag": "CreateField", 506 | "model": "Tag", 507 | "field": "title", 508 | "type": "String", 509 | "arity": "Required" 510 | }, 511 | { 512 | "tag": "CreateField", 513 | "model": "Tag", 514 | "field": "contacts", 515 | "type": "ContactTags", 516 | "arity": "List" 517 | }, 518 | { 519 | "tag": "CreateDirective", 520 | "location": { 521 | "path": { 522 | "tag": "Model", 523 | "model": "Tag" 524 | }, 525 | "directive": "map" 526 | } 527 | }, 528 | { 529 | "tag": "CreateArgument", 530 | "location": { 531 | "tag": "Directive", 532 | "path": { 533 | "tag": "Model", 534 | "model": "Tag" 535 | }, 536 | "directive": "map" 537 | }, 538 | "argument": "name", 539 | "value": "\"tags\"" 540 | }, 541 | { 542 | "tag": "CreateModel", 543 | "model": "Message" 544 | }, 545 | { 546 | "tag": "CreateField", 547 | "model": "Message", 548 | "field": "id", 549 | "type": "String", 550 | "arity": "Required" 551 | }, 552 | { 553 | "tag": "CreateDirective", 554 | "location": { 555 | "path": { 556 | "tag": "Field", 557 | "model": "Message", 558 | "field": "id" 559 | }, 560 | "directive": "default" 561 | } 562 | }, 563 | { 564 | "tag": "CreateArgument", 565 | "location": { 566 | "tag": "Directive", 567 | "path": { 568 | "tag": "Field", 569 | "model": "Message", 570 | "field": "id" 571 | }, 572 | "directive": "default" 573 | }, 574 | "argument": "", 575 | "value": "uuid()" 576 | }, 577 | { 578 | "tag": "CreateDirective", 579 | "location": { 580 | "path": { 581 | "tag": "Field", 582 | "model": "Message", 583 | "field": "id" 584 | }, 585 | "directive": "id" 586 | } 587 | }, 588 | { 589 | "tag": "CreateField", 590 | "model": "Message", 591 | "field": "subject", 592 | "type": "String", 593 | "arity": "Required" 594 | }, 595 | { 596 | "tag": "CreateField", 597 | "model": "Message", 598 | "field": "body", 599 | "type": "String", 600 | "arity": "Required" 601 | }, 602 | { 603 | "tag": "CreateField", 604 | "model": "Message", 605 | "field": "recipientsCount", 606 | "type": "Int", 607 | "arity": "Required" 608 | }, 609 | { 610 | "tag": "CreateField", 611 | "model": "Message", 612 | "field": "sentCount", 613 | "type": "Int", 614 | "arity": "Required" 615 | }, 616 | { 617 | "tag": "CreateDirective", 618 | "location": { 619 | "path": { 620 | "tag": "Field", 621 | "model": "Message", 622 | "field": "sentCount" 623 | }, 624 | "directive": "default" 625 | } 626 | }, 627 | { 628 | "tag": "CreateArgument", 629 | "location": { 630 | "tag": "Directive", 631 | "path": { 632 | "tag": "Field", 633 | "model": "Message", 634 | "field": "sentCount" 635 | }, 636 | "directive": "default" 637 | }, 638 | "argument": "", 639 | "value": "0" 640 | }, 641 | { 642 | "tag": "CreateField", 643 | "model": "Message", 644 | "field": "sentAt", 645 | "type": "DateTime", 646 | "arity": "Optional" 647 | }, 648 | { 649 | "tag": "CreateField", 650 | "model": "Message", 651 | "field": "template", 652 | "type": "Template", 653 | "arity": "Required" 654 | }, 655 | { 656 | "tag": "CreateDirective", 657 | "location": { 658 | "path": { 659 | "tag": "Field", 660 | "model": "Message", 661 | "field": "template" 662 | }, 663 | "directive": "relation" 664 | } 665 | }, 666 | { 667 | "tag": "CreateArgument", 668 | "location": { 669 | "tag": "Directive", 670 | "path": { 671 | "tag": "Field", 672 | "model": "Message", 673 | "field": "template" 674 | }, 675 | "directive": "relation" 676 | }, 677 | "argument": "fields", 678 | "value": "[templateId]" 679 | }, 680 | { 681 | "tag": "CreateArgument", 682 | "location": { 683 | "tag": "Directive", 684 | "path": { 685 | "tag": "Field", 686 | "model": "Message", 687 | "field": "template" 688 | }, 689 | "directive": "relation" 690 | }, 691 | "argument": "references", 692 | "value": "[id]" 693 | }, 694 | { 695 | "tag": "CreateField", 696 | "model": "Message", 697 | "field": "templateId", 698 | "type": "String", 699 | "arity": "Required" 700 | }, 701 | { 702 | "tag": "CreateField", 703 | "model": "Message", 704 | "field": "sender", 705 | "type": "Sender", 706 | "arity": "Required" 707 | }, 708 | { 709 | "tag": "CreateDirective", 710 | "location": { 711 | "path": { 712 | "tag": "Field", 713 | "model": "Message", 714 | "field": "sender" 715 | }, 716 | "directive": "relation" 717 | } 718 | }, 719 | { 720 | "tag": "CreateArgument", 721 | "location": { 722 | "tag": "Directive", 723 | "path": { 724 | "tag": "Field", 725 | "model": "Message", 726 | "field": "sender" 727 | }, 728 | "directive": "relation" 729 | }, 730 | "argument": "fields", 731 | "value": "[senderId]" 732 | }, 733 | { 734 | "tag": "CreateArgument", 735 | "location": { 736 | "tag": "Directive", 737 | "path": { 738 | "tag": "Field", 739 | "model": "Message", 740 | "field": "sender" 741 | }, 742 | "directive": "relation" 743 | }, 744 | "argument": "references", 745 | "value": "[id]" 746 | }, 747 | { 748 | "tag": "CreateField", 749 | "model": "Message", 750 | "field": "senderId", 751 | "type": "String", 752 | "arity": "Required" 753 | }, 754 | { 755 | "tag": "CreateField", 756 | "model": "Message", 757 | "field": "createdAt", 758 | "type": "DateTime", 759 | "arity": "Required" 760 | }, 761 | { 762 | "tag": "CreateDirective", 763 | "location": { 764 | "path": { 765 | "tag": "Field", 766 | "model": "Message", 767 | "field": "createdAt" 768 | }, 769 | "directive": "default" 770 | } 771 | }, 772 | { 773 | "tag": "CreateArgument", 774 | "location": { 775 | "tag": "Directive", 776 | "path": { 777 | "tag": "Field", 778 | "model": "Message", 779 | "field": "createdAt" 780 | }, 781 | "directive": "default" 782 | }, 783 | "argument": "", 784 | "value": "now()" 785 | }, 786 | { 787 | "tag": "CreateField", 788 | "model": "Message", 789 | "field": "updatedAt", 790 | "type": "DateTime", 791 | "arity": "Required" 792 | }, 793 | { 794 | "tag": "CreateDirective", 795 | "location": { 796 | "path": { 797 | "tag": "Field", 798 | "model": "Message", 799 | "field": "updatedAt" 800 | }, 801 | "directive": "default" 802 | } 803 | }, 804 | { 805 | "tag": "CreateArgument", 806 | "location": { 807 | "tag": "Directive", 808 | "path": { 809 | "tag": "Field", 810 | "model": "Message", 811 | "field": "updatedAt" 812 | }, 813 | "directive": "default" 814 | }, 815 | "argument": "", 816 | "value": "now()" 817 | }, 818 | { 819 | "tag": "CreateDirective", 820 | "location": { 821 | "path": { 822 | "tag": "Model", 823 | "model": "Message" 824 | }, 825 | "directive": "map" 826 | } 827 | }, 828 | { 829 | "tag": "CreateArgument", 830 | "location": { 831 | "tag": "Directive", 832 | "path": { 833 | "tag": "Model", 834 | "model": "Message" 835 | }, 836 | "directive": "map" 837 | }, 838 | "argument": "name", 839 | "value": "\"messages\"" 840 | }, 841 | { 842 | "tag": "CreateModel", 843 | "model": "Template" 844 | }, 845 | { 846 | "tag": "CreateField", 847 | "model": "Template", 848 | "field": "id", 849 | "type": "String", 850 | "arity": "Required" 851 | }, 852 | { 853 | "tag": "CreateDirective", 854 | "location": { 855 | "path": { 856 | "tag": "Field", 857 | "model": "Template", 858 | "field": "id" 859 | }, 860 | "directive": "default" 861 | } 862 | }, 863 | { 864 | "tag": "CreateArgument", 865 | "location": { 866 | "tag": "Directive", 867 | "path": { 868 | "tag": "Field", 869 | "model": "Template", 870 | "field": "id" 871 | }, 872 | "directive": "default" 873 | }, 874 | "argument": "", 875 | "value": "uuid()" 876 | }, 877 | { 878 | "tag": "CreateDirective", 879 | "location": { 880 | "path": { 881 | "tag": "Field", 882 | "model": "Template", 883 | "field": "id" 884 | }, 885 | "directive": "id" 886 | } 887 | }, 888 | { 889 | "tag": "CreateField", 890 | "model": "Template", 891 | "field": "messages", 892 | "type": "Message", 893 | "arity": "List" 894 | }, 895 | { 896 | "tag": "CreateField", 897 | "model": "Template", 898 | "field": "title", 899 | "type": "String", 900 | "arity": "Required" 901 | }, 902 | { 903 | "tag": "CreateField", 904 | "model": "Template", 905 | "field": "content", 906 | "type": "String", 907 | "arity": "Required" 908 | }, 909 | { 910 | "tag": "CreateDirective", 911 | "location": { 912 | "path": { 913 | "tag": "Model", 914 | "model": "Template" 915 | }, 916 | "directive": "map" 917 | } 918 | }, 919 | { 920 | "tag": "CreateArgument", 921 | "location": { 922 | "tag": "Directive", 923 | "path": { 924 | "tag": "Model", 925 | "model": "Template" 926 | }, 927 | "directive": "map" 928 | }, 929 | "argument": "name", 930 | "value": "\"templates\"" 931 | }, 932 | { 933 | "tag": "CreateModel", 934 | "model": "Sender" 935 | }, 936 | { 937 | "tag": "CreateField", 938 | "model": "Sender", 939 | "field": "id", 940 | "type": "String", 941 | "arity": "Required" 942 | }, 943 | { 944 | "tag": "CreateDirective", 945 | "location": { 946 | "path": { 947 | "tag": "Field", 948 | "model": "Sender", 949 | "field": "id" 950 | }, 951 | "directive": "default" 952 | } 953 | }, 954 | { 955 | "tag": "CreateArgument", 956 | "location": { 957 | "tag": "Directive", 958 | "path": { 959 | "tag": "Field", 960 | "model": "Sender", 961 | "field": "id" 962 | }, 963 | "directive": "default" 964 | }, 965 | "argument": "", 966 | "value": "uuid()" 967 | }, 968 | { 969 | "tag": "CreateDirective", 970 | "location": { 971 | "path": { 972 | "tag": "Field", 973 | "model": "Sender", 974 | "field": "id" 975 | }, 976 | "directive": "id" 977 | } 978 | }, 979 | { 980 | "tag": "CreateField", 981 | "model": "Sender", 982 | "field": "messages", 983 | "type": "Message", 984 | "arity": "List" 985 | }, 986 | { 987 | "tag": "CreateField", 988 | "model": "Sender", 989 | "field": "name", 990 | "type": "String", 991 | "arity": "Required" 992 | }, 993 | { 994 | "tag": "CreateField", 995 | "model": "Sender", 996 | "field": "email", 997 | "type": "String", 998 | "arity": "Required" 999 | }, 1000 | { 1001 | "tag": "CreateField", 1002 | "model": "Sender", 1003 | "field": "createdAt", 1004 | "type": "DateTime", 1005 | "arity": "Required" 1006 | }, 1007 | { 1008 | "tag": "CreateDirective", 1009 | "location": { 1010 | "path": { 1011 | "tag": "Field", 1012 | "model": "Sender", 1013 | "field": "createdAt" 1014 | }, 1015 | "directive": "default" 1016 | } 1017 | }, 1018 | { 1019 | "tag": "CreateArgument", 1020 | "location": { 1021 | "tag": "Directive", 1022 | "path": { 1023 | "tag": "Field", 1024 | "model": "Sender", 1025 | "field": "createdAt" 1026 | }, 1027 | "directive": "default" 1028 | }, 1029 | "argument": "", 1030 | "value": "now()" 1031 | }, 1032 | { 1033 | "tag": "CreateField", 1034 | "model": "Sender", 1035 | "field": "updatedAt", 1036 | "type": "DateTime", 1037 | "arity": "Required" 1038 | }, 1039 | { 1040 | "tag": "CreateDirective", 1041 | "location": { 1042 | "path": { 1043 | "tag": "Field", 1044 | "model": "Sender", 1045 | "field": "updatedAt" 1046 | }, 1047 | "directive": "default" 1048 | } 1049 | }, 1050 | { 1051 | "tag": "CreateArgument", 1052 | "location": { 1053 | "tag": "Directive", 1054 | "path": { 1055 | "tag": "Field", 1056 | "model": "Sender", 1057 | "field": "updatedAt" 1058 | }, 1059 | "directive": "default" 1060 | }, 1061 | "argument": "", 1062 | "value": "now()" 1063 | }, 1064 | { 1065 | "tag": "CreateDirective", 1066 | "location": { 1067 | "path": { 1068 | "tag": "Model", 1069 | "model": "Sender" 1070 | }, 1071 | "directive": "map" 1072 | } 1073 | }, 1074 | { 1075 | "tag": "CreateArgument", 1076 | "location": { 1077 | "tag": "Directive", 1078 | "path": { 1079 | "tag": "Model", 1080 | "model": "Sender" 1081 | }, 1082 | "directive": "map" 1083 | }, 1084 | "argument": "name", 1085 | "value": "\"senders\"" 1086 | } 1087 | ] 1088 | } --------------------------------------------------------------------------------