├── .circleci ├── ansible │ ├── configure-server.yml │ ├── deploy-backend.yml │ ├── inventory.txt │ └── roles │ │ ├── configure-prometheus-node-exporter │ │ ├── files │ │ │ └── node_exporter.service │ │ └── tasks │ │ │ └── main.yml │ │ ├── configure-server │ │ └── tasks │ │ │ └── readme.md │ │ └── deploy │ │ └── tasks │ │ └── readme.md ├── config.yml └── files │ ├── backend.yml │ ├── cloudfront.yml │ └── frontend.yml ├── .github └── workflows │ └── manual.yml ├── .gitignore ├── .theia └── settings.json ├── CODEOWNERS ├── LICENSE.md ├── README.md ├── backend ├── .env.sample ├── .nestcli.json ├── .npmignore ├── .prettierrc ├── README.md ├── development.env ├── nodemon-debug.json ├── package-lock.json ├── package.json ├── src │ ├── main.hmr.ts │ ├── main.ts │ ├── migrations │ │ ├── 1549375960026-AddOrders.ts │ │ ├── 1549398619849-FixProductIdTable.ts │ │ └── 1555722583168-AddEmployee.ts │ └── modules │ │ ├── app │ │ ├── app.logger.ts │ │ ├── app.module.ts │ │ └── app.service.ts │ │ ├── auth │ │ ├── auth.module.ts │ │ ├── currentUser.spec.ts │ │ ├── currentUser.ts │ │ ├── json-object.interface.ts │ │ ├── jwt-payload.interface.ts │ │ ├── jwt.strategy.ts │ │ ├── user.decorator.ts │ │ └── user.interface.ts │ │ ├── common │ │ ├── commands │ │ │ ├── ICommand.ts │ │ │ ├── ICommandHandler.ts │ │ │ ├── baseCommandHandler.ts │ │ │ ├── commandDispatcher.ts │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── syncCommandDispatcher.ts │ │ │ └── validation │ │ │ │ ├── CompositeValidator.spec.ts │ │ │ │ ├── CompositeValidator.ts │ │ │ │ ├── ICommandValidator.ts │ │ │ │ ├── IValidationError.ts │ │ │ │ ├── IValidationResult.ts │ │ │ │ ├── commandValidator.decorator.ts │ │ │ │ ├── commandValidatorOptions.ts │ │ │ │ ├── index.ts │ │ │ │ ├── invalidCommandValidatorException.ts │ │ │ │ ├── joiValidator.ts │ │ │ │ └── validationExplorer.ts │ │ ├── common.module.ts │ │ ├── controllers │ │ │ ├── index.ts │ │ │ ├── paginatedQuery.ts │ │ │ └── query.ts │ │ ├── entities │ │ │ ├── aggregateRoot.spec.ts │ │ │ ├── aggregateRoot.ts │ │ │ ├── baseRepository.spec.ts │ │ │ ├── baseRepository.ts │ │ │ └── index.ts │ │ ├── events │ │ │ ├── domainEvent.ts │ │ │ ├── eventDispatcher.ts │ │ │ ├── eventHandler.ts │ │ │ ├── index.ts │ │ │ └── syncEventDispatcher.ts │ │ ├── index.ts │ │ └── interceptors │ │ │ └── TransformInterceptor.ts │ │ ├── config │ │ ├── about.interface.ts │ │ ├── config.module.ts │ │ └── config.service.ts │ │ ├── domain │ │ ├── employees │ │ │ ├── commands │ │ │ │ ├── activate-employee.command.ts │ │ │ │ ├── create-employee.command.ts │ │ │ │ ├── deactivate-employee.command.ts │ │ │ │ ├── handlers │ │ │ │ │ ├── employee-activator.handler.spec.ts │ │ │ │ │ ├── employee-activator.handler.ts │ │ │ │ │ ├── employee-address-updater.handler.spec.ts │ │ │ │ │ ├── employee-address-updater.handler.ts │ │ │ │ │ ├── employee-birthdate-updater.handler.spec.ts │ │ │ │ │ ├── employee-birthdate-updater.handler.ts │ │ │ │ │ ├── employee-company-email-updater.handler.spec.ts │ │ │ │ │ ├── employee-company-email-updater.handler.ts │ │ │ │ │ ├── employee-creator.handler.spec.ts │ │ │ │ │ ├── employee-creator.handler.ts │ │ │ │ │ ├── employee-deactivator.handler.spec.ts │ │ │ │ │ ├── employee-deactivator.handler.ts │ │ │ │ │ ├── employee-display-name-updater.handler.spec.ts │ │ │ │ │ ├── employee-display-name-updater.handler.ts │ │ │ │ │ ├── employee-effective-date-updater.handler.spec.ts │ │ │ │ │ ├── employee-effective-date-updater.handler.ts │ │ │ │ │ ├── employee-name-updater.handler.spec.ts │ │ │ │ │ ├── employee-name-updater.handler.ts │ │ │ │ │ ├── employee-personal-email-updater.handler.spec.ts │ │ │ │ │ ├── employee-personal-email-updater.handler.ts │ │ │ │ │ ├── employee-phone-number-updater.handler.spec.ts │ │ │ │ │ ├── employee-phone-number-updater.handler.ts │ │ │ │ │ ├── employee-salary-type-updater.handler.spec.ts │ │ │ │ │ ├── employee-salary-type-updater.handler.ts │ │ │ │ │ ├── employee-salary-updater.handler.spec.ts │ │ │ │ │ ├── employee-salary-updater.handler.ts │ │ │ │ │ ├── employee-tags-updater.handler.spec.ts │ │ │ │ │ ├── employee-tags-updater.handler.ts │ │ │ │ │ ├── employee-updater.handler.spec.ts │ │ │ │ │ ├── employee-updater.handler.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── update-employee-address.command.ts │ │ │ │ ├── update-employee-birthdate.command.ts │ │ │ │ ├── update-employee-company-email.command.ts │ │ │ │ ├── update-employee-display-name.command.ts │ │ │ │ ├── update-employee-effective-date.command.ts │ │ │ │ ├── update-employee-name.command.ts │ │ │ │ ├── update-employee-personal-email.command.ts │ │ │ │ ├── update-employee-phone-number.command.ts │ │ │ │ ├── update-employee-salary-type.command.ts │ │ │ │ ├── update-employee-salary.command.ts │ │ │ │ ├── update-employee-tags.command.ts │ │ │ │ ├── update-employee.command.ts │ │ │ │ └── validators │ │ │ │ │ ├── forActivateEmployee │ │ │ │ │ ├── activate-employee-composite.validator.ts │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ └── check-employee-exists.validator.ts │ │ │ │ │ ├── forCreateEmployee │ │ │ │ │ ├── check-properties-value.validator.spec.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ └── create-employee-composite.validator.ts │ │ │ │ │ ├── forDeactivateEmployee │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ └── remove-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeAddress │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeBirthDate │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeCompanyEmail │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeDisplayName │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeEffectiveDate │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeName │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeePersonalEmail │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeePhoneNumber │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeSalary │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeSalaryType │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ ├── forUpdateEmployeeTags │ │ │ │ │ ├── check-employee-exists.validator.spec.ts │ │ │ │ │ ├── check-employee-exists.validator.ts │ │ │ │ │ ├── check-properties-value.validator.ts │ │ │ │ │ ├── check-properties.value.validator.spec.ts │ │ │ │ │ └── update-employee-composite.validator.ts │ │ │ │ │ └── index.ts │ │ │ ├── employee.controller.spec.ts │ │ │ ├── employee.controller.ts │ │ │ ├── employee.module.ts │ │ │ ├── entities │ │ │ │ └── employee.entity.ts │ │ │ ├── events │ │ │ │ ├── employee-created.event.ts │ │ │ │ └── handlers │ │ │ │ │ ├── index.ts │ │ │ │ │ └── notify-employee-created-console.handler.ts │ │ │ ├── repositories │ │ │ │ └── employees.repository.ts │ │ │ └── requests │ │ │ │ ├── create-employee-request.interface.ts │ │ │ │ ├── paginated-employee-query.interface.ts │ │ │ │ └── update-employee-request.interface.ts │ │ └── orders │ │ │ ├── commands │ │ │ ├── createOrder.ts │ │ │ ├── handlers │ │ │ │ ├── index.ts │ │ │ │ ├── orderCreator.spec.ts │ │ │ │ └── orderCreator.ts │ │ │ └── validators │ │ │ │ ├── forCreateOrder │ │ │ │ ├── checkProductExist.spec.ts │ │ │ │ ├── checkProductExist.ts │ │ │ │ ├── checkPropertieValues.spec.ts │ │ │ │ ├── checkPropertiesValue.ts │ │ │ │ └── createOrderCompositeValidator.ts │ │ │ │ └── index.ts │ │ │ ├── entities │ │ │ ├── order.entity.spec.ts │ │ │ ├── order.entity.ts │ │ │ └── product.entity.ts │ │ │ ├── events │ │ │ ├── ProductAddedToOrder.ts │ │ │ └── handlers │ │ │ │ ├── index.ts │ │ │ │ └── notify-product-add-to-order-console.ts │ │ │ ├── orders.controller.spec.ts │ │ │ ├── orders.controller.ts │ │ │ ├── orders.module.ts │ │ │ ├── repositories │ │ │ ├── orderRepository.ts │ │ │ └── productRepository.ts │ │ │ └── requests │ │ │ ├── PaginatedOrderQuery.ts │ │ │ ├── orderRequest.ts │ │ │ └── paginatedProductQuery.ts │ │ ├── errors │ │ └── error.filter.ts │ │ └── status │ │ ├── status.controller.spec.ts │ │ ├── status.controller.ts │ │ └── status.module.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── webpack.config.js ├── frontend ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── src │ ├── __mocks__ │ │ └── fileMock.js │ ├── app │ │ ├── components │ │ │ ├── ActionsMenu │ │ │ │ ├── index.tsx │ │ │ │ └── style.css │ │ │ ├── Breadcrumb │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── Button │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── Empty │ │ │ │ ├── Empty.spec.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── LoadingMessage │ │ │ │ ├── LoadingMessage.spec.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── Navbar │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── NavbarButton │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── OrdersTable │ │ │ │ ├── NoFilteredOrders │ │ │ │ │ ├── NoFilteredOrders.spec.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.local.css │ │ │ │ └── OrdersSearch │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── style.local.css │ │ │ ├── Pagination │ │ │ │ ├── Pagination.spec.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── SearchBar │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── SearchInput │ │ │ │ ├── index.tsx │ │ │ │ └── style.local.css │ │ │ ├── Table │ │ │ │ ├── index.tsx │ │ │ │ └── react-table.css │ │ │ └── index.ts │ │ ├── containers │ │ │ ├── App │ │ │ │ └── index.tsx │ │ │ └── Employee │ │ │ │ ├── actions │ │ │ │ ├── employees.ts │ │ │ │ └── index.ts │ │ │ │ ├── components │ │ │ │ ├── AddEmployee │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── react-tagsinput.css │ │ │ │ ├── EditEmployee │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── inputValidations.ts │ │ │ │ │ └── withAutoSave.tsx │ │ │ │ ├── EmployeeSearch │ │ │ │ │ └── index.tsx │ │ │ │ ├── Employees │ │ │ │ │ └── index.tsx │ │ │ │ └── ViewEmployee │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── models │ │ │ │ ├── EmployeeModel.ts │ │ │ │ └── index.ts │ │ │ │ ├── reducer │ │ │ │ ├── employees.spec.ts │ │ │ │ ├── employees.ts │ │ │ │ ├── sampleData.ts │ │ │ │ └── state.ts │ │ │ │ └── services │ │ │ │ ├── employees.ts │ │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── middleware │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── reducers │ │ │ ├── index.ts │ │ │ └── state.ts │ │ ├── services │ │ │ ├── httpService.ts │ │ │ └── index.ts │ │ ├── store │ │ │ └── index.ts │ │ ├── style.local.css │ │ └── utils │ │ │ ├── FeatureToggler.ts │ │ │ ├── errorLogger.ts │ │ │ └── index.ts │ ├── assets │ │ ├── fonts │ │ │ ├── kiwi.eot │ │ │ ├── kiwi.svg │ │ │ ├── kiwi.ttf │ │ │ └── kiwi.woff │ │ ├── images │ │ │ ├── favicon.png │ │ │ ├── icon-search.svg │ │ │ └── logo-glee.png │ │ ├── img │ │ │ ├── exampleuser.jpg │ │ │ ├── favicon.png │ │ │ ├── gravatar-empty.jpg │ │ │ ├── icon-search.svg │ │ │ └── logo-glee.png │ │ ├── index.ejs │ │ └── translations.ts │ ├── browserHistory.ts │ ├── main.tsx │ ├── setupTests.ts │ ├── toggles.ts │ └── types │ │ └── react-country-region-selector.ts ├── stryker.conf.js ├── tsconfig.json ├── tslint.json ├── types │ ├── global.d.ts │ └── react-redux.d.ts ├── wallaby.config.js └── webpack.config.js ├── instructions ├── 0-selling-cicd.md ├── 1-getting-started.md ├── 2-deploying-trustworthy-code.md ├── 3-configuration-management.md ├── 4-turn-errors-into-sirens.md └── screenshots │ ├── SCREENSHOT01.png │ ├── SCREENSHOT02.png │ ├── SCREENSHOT03.png │ ├── SCREENSHOT04.png │ ├── SCREENSHOT05.png │ ├── SCREENSHOT06.png │ ├── SCREENSHOT07.png │ ├── SCREENSHOT08.png │ ├── SCREENSHOT09.png │ ├── SCREENSHOT10.png │ ├── SCREENSHOT11.png │ ├── SCREENSHOT12.png │ ├── SCREENSHOT__service-discovery.png │ └── readme.md ├── package-lock.json ├── udapeople-pipeline.png ├── udapeople.png └── util └── docker-compose.yml /.circleci/ansible/configure-server.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "configuration play." 4 | hosts: web 5 | user: ubuntu 6 | become: true 7 | become_method: sudo 8 | become_user: root 9 | gather_facts: false 10 | vars: 11 | - ansible_python_interpreter: /usr/bin/python3 12 | - ansible_host_key_checking: false 13 | - ansible_stdout_callback: yaml 14 | 15 | pre_tasks: 16 | - name: "wait 600 seconds for target connection to become reachable/usable." 17 | # Your code here 18 | 19 | - name: "install python for Ansible." 20 | # Your code here 21 | 22 | # Get the environment variables from CircleCI and add to the EC2 instance 23 | environment: 24 | - TYPEORM_CONNECTION: "{{ lookup('env', 'TYPEORM_CONNECTION')}}" 25 | # Add more env vars here 26 | 27 | roles: 28 | # Your code here -------------------------------------------------------------------------------- /.circleci/ansible/deploy-backend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: "configuration play." 4 | hosts: web 5 | user: ubuntu 6 | gather_facts: false 7 | vars: 8 | - ansible_python_interpreter: /usr/bin/python3 9 | - ansible_host_key_checking: false 10 | - ansible_stdout_callback: yaml 11 | roles: 12 | - deploy 13 | -------------------------------------------------------------------------------- /.circleci/ansible/inventory.txt: -------------------------------------------------------------------------------- 1 | [web] 2 | -------------------------------------------------------------------------------- /.circleci/ansible/roles/configure-prometheus-node-exporter/files/node_exporter.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Node Exporter 3 | Wants=network-online.target 4 | After=network-online.target 5 | 6 | [Service] 7 | User=root 8 | Type=simple 9 | ExecStart=/usr/local/bin/node_exporter 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /.circleci/ansible/roles/configure-prometheus-node-exporter/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: "install node exporter." 2 | unarchive: 3 | src: https://github.com/prometheus/node_exporter/releases/download/v0.17.0/node_exporter-0.17.0.linux-amd64.tar.gz 4 | dest: /tmp 5 | remote_src: yes 6 | 7 | - name: "move binary to /usr/local/bin." 8 | become: true 9 | copy: 10 | src: /tmp/node_exporter-0.17.0.linux-amd64/node_exporter 11 | dest: /usr/local/bin/node_exporter 12 | remote_src: yes 13 | mode: '0777' 14 | 15 | - name: "add node exporter configuration." 16 | become: true 17 | copy: 18 | src: node_exporter.service 19 | dest: /etc/systemd/system/ 20 | 21 | - name: "enable node_exporter service" 22 | become: true 23 | systemd: 24 | state: restarted 25 | daemon_reload: yes 26 | name: node_exporter 27 | enabled: yes -------------------------------------------------------------------------------- /.circleci/ansible/roles/configure-server/tasks/readme.md: -------------------------------------------------------------------------------- 1 | ## Back-end server configuration playbook goes here. 2 | -------------------------------------------------------------------------------- /.circleci/ansible/roles/deploy/tasks/readme.md: -------------------------------------------------------------------------------- 1 | ## Deployment playbook goes here. 2 | -------------------------------------------------------------------------------- /.circleci/files/backend.yml: -------------------------------------------------------------------------------- 1 | Description: > 2 | UdaPeople backend stack. 3 | 4 | Parameters: 5 | ID: 6 | Description: Unique identifier. 7 | Type: String 8 | 9 | Resources: 10 | InstanceSecurityGroup: 11 | Type: AWS::EC2::SecurityGroup 12 | Properties: 13 | GroupName: !Sub UdaPeople-${ID} 14 | GroupDescription: Allow port 22 and port 3030. 15 | SecurityGroupIngress: 16 | - IpProtocol: tcp 17 | FromPort: 22 18 | ToPort: 22 19 | CidrIp: 0.0.0.0/0 20 | - IpProtocol: tcp 21 | FromPort: 3030 22 | ToPort: 3030 23 | CidrIp: 0.0.0.0/0 24 | 25 | EC2Instance: 26 | Type: AWS::EC2::Instance 27 | Properties: 28 | InstanceType: t2.micro 29 | SecurityGroups: 30 | - Ref: InstanceSecurityGroup 31 | KeyName: udacity # If you use another key pair name, you should change this value to match. 32 | # If this ami id is not available for you, you can find another (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html) 33 | # Ubuntu 18 or higher works fine 34 | ImageId: ami-068663a3c619dd892 35 | Tags: 36 | - Key: Name 37 | Value: !Sub backend-${ID} 38 | -------------------------------------------------------------------------------- /.circleci/files/cloudfront.yml: -------------------------------------------------------------------------------- 1 | Description: > 2 | Cloudfront distribution for UdaPeople. 3 | 4 | Parameters: 5 | WorkflowID: 6 | Description: Unique identifier. 7 | Type: String 8 | 9 | 10 | Resources: 11 | 12 | CloudFrontOriginAccessIdentity: 13 | Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity" 14 | Properties: 15 | CloudFrontOriginAccessIdentityConfig: 16 | Comment: Origin Access Identity for Serverless Static Website 17 | 18 | WebpageCDN: 19 | Type: AWS::CloudFront::Distribution 20 | Properties: 21 | DistributionConfig: 22 | Origins: 23 | - DomainName: !Sub "udapeople-${WorkflowID}.s3.amazonaws.com" 24 | Id: webpage 25 | S3OriginConfig: 26 | OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}" 27 | Enabled: True 28 | DefaultRootObject: index.html 29 | DefaultCacheBehavior: 30 | ForwardedValues: 31 | QueryString: False 32 | TargetOriginId: webpage 33 | ViewerProtocolPolicy: allow-all 34 | 35 | Outputs: 36 | WorkflowID: 37 | Value: !Sub ${WorkflowID} 38 | Description: URL for website hosted on S3 39 | Export: 40 | Name: WorkflowID 41 | -------------------------------------------------------------------------------- /.circleci/files/frontend.yml: -------------------------------------------------------------------------------- 1 | Description: > 2 | UdaPeople frontend stack. 3 | 4 | Parameters: 5 | ID: 6 | Description: Unique identifier. 7 | Type: String 8 | 9 | Resources: 10 | WebsiteBucket: 11 | Type: AWS::S3::Bucket 12 | Properties: 13 | BucketName: !Sub "udapeople-${ID}" 14 | AccessControl: PublicRead 15 | WebsiteConfiguration: 16 | IndexDocument: index.html 17 | ErrorDocument: 404.html 18 | 19 | WebsiteBucketPolicy: 20 | Type: AWS::S3::BucketPolicy 21 | Properties: 22 | Bucket: !Ref 'WebsiteBucket' 23 | PolicyDocument: 24 | Statement: 25 | - Sid: PublicReadForGetBucketObjects 26 | Effect: Allow 27 | Principal: '*' 28 | Action: s3:GetObject 29 | Resource: !Join ['', ['arn:aws:s3:::', !Ref 'WebsiteBucket', /*]] 30 | 31 | Outputs: 32 | WebsiteURL: 33 | Value: !GetAtt WebsiteBucket.WebsiteURL 34 | Description: URL for website hosted on S3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.sw[mnpcod] 3 | *.log 4 | *.tmp 5 | *.tmp.* 6 | log.txt 7 | *.sublime-project 8 | *.sublime-workspace 9 | .vscode/ 10 | npm-debug.log* 11 | 12 | .idea/ 13 | .ionic/ 14 | .sourcemaps/ 15 | .sass-cache/ 16 | .tmp/ 17 | .versions/ 18 | coverage/ 19 | dist/ 20 | node_modules/ 21 | tmp/ 22 | temp/ 23 | platforms/ 24 | plugins/ 25 | plugins/android.json 26 | plugins/ios.json 27 | $RECYCLE.BIN/ 28 | postgres_dev/ 29 | logfile 30 | 31 | .DS_Store 32 | Thumbs.db 33 | UserInterfaceState.xcuserstate 34 | node_modules 35 | venv/ 36 | *~ 37 | *.sw[mnpcod] 38 | *.log 39 | *.tmp 40 | *.tmp.* 41 | log.txt 42 | *.sublime-project 43 | *.sublime-workspace 44 | .vscode/ 45 | npm-debug.log* 46 | 47 | .idea/ 48 | .ionic/ 49 | .sourcemaps/ 50 | .sass-cache/ 51 | .tmp/ 52 | .versions/ 53 | coverage/ 54 | www/ 55 | node_modules/ 56 | tmp/ 57 | temp/ 58 | platforms/ 59 | plugins/ 60 | plugins/android.json 61 | plugins/ios.json 62 | $RECYCLE.BIN/ 63 | 64 | .DS_Store 65 | Thumbs.db 66 | UserInterfaceState.xcuserstate 67 | node_modules 68 | 69 | .env 70 | dist -------------------------------------------------------------------------------- /.theia/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.autoSave": "on" 3 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @udacity/active-public-content -------------------------------------------------------------------------------- /backend/.env.sample: -------------------------------------------------------------------------------- 1 | NODE_ENV=local 2 | VERSION=1 3 | TYPEORM_CONNECTION=postgres 4 | TYPEORM_MIGRATIONS_DIR=./src/migrations 5 | TYPEORM_ENTITIES=./src/modules/domain/**/*.entity.ts 6 | TYPEORM_MIGRATIONS=./src/migrations/*.ts 7 | 8 | # Use these values for the local PG database from the Docker Compose file 9 | TYPEORM_HOST=localhost 10 | TYPEORM_PORT=5532 11 | TYPEORM_USERNAME=postgres 12 | TYPEORM_PASSWORD=password 13 | TYPEORM_DATABASE=glee -------------------------------------------------------------------------------- /backend/.nestcli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics" 4 | } -------------------------------------------------------------------------------- /backend/.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/backend/.npmignore -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /backend/development.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=local 2 | VERSION=1 3 | TYPEORM_CONNECTION=postgres 4 | TYPEORM_MIGRATIONS_DIR=./src/migrations 5 | TYPEORM_ENTITIES=./src/modules/domain/**/*.entity.ts 6 | TYPEORM_MIGRATIONS=./src/migrations/*.ts 7 | 8 | # Things you can change if you wish... 9 | TYPEORM_HOST=localhost 10 | TYPEORM_PORT=5532 11 | TYPEORM_USERNAME=postgres 12 | TYPEORM_PASSWORD=password 13 | TYPEORM_DATABASE=glee -------------------------------------------------------------------------------- /backend/nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register src/main.ts" 6 | } -------------------------------------------------------------------------------- /backend/src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from 'modules/app/app.module'; 3 | import { ConfigService } from './modules/config/config.service'; 4 | 5 | declare const module: any; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | const config: ConfigService = app.get('ConfigService'); 10 | await app.listen(config.PORT); 11 | 12 | if (module.hot) { 13 | module.hot.accept(); 14 | module.hot.dispose(() => app.close()); 15 | } 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /backend/src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Module, 3 | NestModule, 4 | MiddlewareConsumer, 5 | RequestMethod, 6 | } from '@nestjs/common'; 7 | import { AppService } from './app.service'; 8 | import { StatusModule } from '../status/status.module'; 9 | import { StatusController } from '../status/status.controller'; 10 | import { AppLogger } from './app.logger'; 11 | import { ConfigModule } from '../config/config.module'; 12 | import { TypeOrmModule } from '@nestjs/typeorm'; 13 | import { ConfigService } from '../config/config.service'; 14 | import { OrdersModule } from '../domain/orders/orders.module'; 15 | import { EmployeeModule } from '../domain/employees/employee.module'; 16 | 17 | @Module({ 18 | imports: [ 19 | StatusModule, 20 | ConfigModule, 21 | TypeOrmModule.forRootAsync({ 22 | imports: [ConfigModule], 23 | inject: [ConfigService], 24 | useFactory: (config: ConfigService) => config.TypeOrmDatabase, 25 | }), 26 | OrdersModule, 27 | EmployeeModule, 28 | ], 29 | controllers: [StatusController], 30 | providers: [AppService, AppLogger], 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /backend/src/modules/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | // import { JwtPayload } from '../auth/jwt-payload.interface'; 3 | 4 | @Injectable() 5 | export class AppService { 6 | // root(user: JwtPayload): string { 7 | root(): string { 8 | return `Hello`; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { JwtStrategy } from './jwt.strategy'; 4 | import { CurrentUser } from './currentUser'; 5 | 6 | @Module({ 7 | imports: [PassportModule.register({ defaultStrategy: 'jwt' })], 8 | providers: [JwtStrategy, CurrentUser], 9 | exports: [JwtStrategy, CurrentUser], 10 | }) 11 | export class AuthModule {} 12 | -------------------------------------------------------------------------------- /backend/src/modules/auth/currentUser.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from './user.interface'; 2 | import { CurrentUser } from './currentUser'; 3 | import { Request } from 'express'; 4 | 5 | describe('Current User', () => { 6 | describe('When getting the logged user', () => { 7 | it('should return the user logged in the request', () => { 8 | const mockedLoggedUser: User = { 9 | username: 'TestUserName', 10 | }; 11 | 12 | // @ts-ignore 13 | const MockRequest: Request = jest.fn(() => ({ 14 | user: mockedLoggedUser, 15 | })); 16 | 17 | // arrange 18 | // @ts-ignore 19 | const mockRequest = new MockRequest(); 20 | 21 | const curretUser = new CurrentUser(mockRequest); 22 | 23 | // act 24 | 25 | const user = curretUser.getUser(); 26 | 27 | // assert 28 | expect(mockedLoggedUser).toEqual(user); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /backend/src/modules/auth/currentUser.ts: -------------------------------------------------------------------------------- 1 | import { REQUEST } from '@nestjs/core'; 2 | import { Inject, Injectable, Scope } from '@nestjs/common'; 3 | import { Request } from 'express'; 4 | import { User } from './user.interface'; 5 | 6 | @Injectable({ scope: Scope.REQUEST }) 7 | export class CurrentUser { 8 | constructor(@Inject(REQUEST) private readonly request: Request) {} 9 | 10 | getUser(): User { 11 | return this.request.user; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/modules/auth/json-object.interface.ts: -------------------------------------------------------------------------------- 1 | type JsonValue = string | number | boolean; 2 | 3 | export interface JsonObject { 4 | [k: string]: JsonValue | JsonValue[] | JsonObject; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/modules/auth/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from './json-object.interface'; 2 | 3 | export interface JwtPayload extends JsonObject { 4 | /** Issuer (who created and signed this token) */ 5 | iss?: string; 6 | /** Subject (whom the token refers to) */ 7 | sub?: string; 8 | /** Audience (who or what the token is intended for) */ 9 | aud?: string[]; 10 | /** Issued at (seconds since Unix epoch) */ 11 | iat?: number; 12 | /** Expiration time (seconds since Unix epoch) */ 13 | exp?: number; 14 | /** Authorization party (the party to which this token was issued) */ 15 | azp?: string; 16 | /** Token scope (what the token has access to) */ 17 | scope?: string; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { passportJwtSecret } from 'glee-jwks-rsa'; 5 | import { JwtPayload } from './jwt-payload.interface'; 6 | import { User } from './user.interface'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor() { 11 | const issuer = `https://${process.env.AUTH0_DOMAIN}/`; 12 | const jwksUri = `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`; 13 | const audience = process.env.AUTH0_AUDIENCE; 14 | 15 | super({ 16 | secretOrKeyProvider: passportJwtSecret({ 17 | cache: true, 18 | rateLimit: true, 19 | jwksRequestsPerMinute: 5, 20 | jwksUri, 21 | }), 22 | 23 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 24 | issuer, 25 | aud: audience, 26 | algorithms: ['RS256'], 27 | }); 28 | } 29 | 30 | validate(payload: JwtPayload): User { 31 | if (!payload) { 32 | throw new UnauthorizedException('Invalid JWT Token'); 33 | } 34 | 35 | const user: User = { 36 | username: payload.sub, 37 | }; 38 | return user; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/modules/auth/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | export const Usr = createParamDecorator((_, req) => { 3 | return req.user; 4 | }); 5 | -------------------------------------------------------------------------------- /backend/src/modules/auth/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | username: string; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/ICommand.ts: -------------------------------------------------------------------------------- 1 | import { ICommand as command } from '@nestjs/cqrs'; 2 | 3 | export interface ICommand extends command {} 4 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/ICommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { ICommandHandler as handler } from '@nestjs/cqrs'; 2 | 3 | export interface ICommandHandler extends handler { 4 | handle(command: TCommand): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/baseCommandHandler.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from './ICommand'; 2 | import { ICommandHandler } from './ICommandHandler'; 3 | 4 | export abstract class BaseCommandHandler 5 | implements ICommandHandler { 6 | execute(command: TCommand): Promise { 7 | return this.handle(command); 8 | } 9 | 10 | abstract handle(command: TCommand): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/commandDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | 3 | import { ICommandValidator } from './validation'; 4 | import { ICommand } from './ICommand'; 5 | 6 | export type CommandValidatorMetatype = Type>; 7 | 8 | export interface ICommandDispatcher { 9 | execute(command: TCommand): Promise; 10 | 11 | registerValidators(commandValidator: CommandValidatorMetatype[]); 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/constants.ts: -------------------------------------------------------------------------------- 1 | export const COMMAND_VALIDATOR_METADATA = '__commandValidator__'; 2 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './syncCommandDispatcher'; 2 | export * from './ICommandHandler'; 3 | export * from './ICommandHandler'; 4 | export * from './commandDispatcher'; 5 | export * from './ICommand'; 6 | export * from './validation'; 7 | export * from './baseCommandHandler'; 8 | export { 9 | InvalidCommandValidatorException, 10 | } from './validation/invalidCommandValidatorException'; 11 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/CompositeValidator.ts: -------------------------------------------------------------------------------- 1 | import { ICommandValidator } from './ICommandValidator'; 2 | import { IValidationResult } from './IValidationResult'; 3 | 4 | export class CompositeValidator 5 | implements ICommandValidator { 6 | constructor(private validators: ICommandValidator[]) {} 7 | 8 | async validate(command: TCommand): Promise { 9 | const results: IValidationResult[] = []; 10 | for (const validator of this.validators) { 11 | const result = await validator.validate(command); 12 | if (result.hasError) { 13 | results.push(result); 14 | } 15 | } 16 | 17 | if (results.length > 0) { 18 | const errors = results.map(value => value.errors); 19 | return { 20 | hasError: true, 21 | errors: [].concat.apply([], errors), 22 | }; 23 | } 24 | return { 25 | hasError: false, 26 | errors: [], 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/ICommandValidator.ts: -------------------------------------------------------------------------------- 1 | import { IValidationResult } from './IValidationResult'; 2 | import { ICommand } from '../ICommand'; 3 | 4 | export interface ICommandValidator { 5 | validate(command: TCommand): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/IValidationError.ts: -------------------------------------------------------------------------------- 1 | export interface IValidationError { 2 | message: string; 3 | field: string; 4 | fieldLabel: string; 5 | value: string | number | boolean; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/IValidationResult.ts: -------------------------------------------------------------------------------- 1 | import { IValidationError } from './IValidationError'; 2 | 3 | export interface IValidationResult { 4 | hasError: boolean; 5 | errors: IValidationError[]; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/commandValidator.decorator.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { COMMAND_VALIDATOR_METADATA } from '../constants'; 4 | import { ICommand } from '../ICommand'; 5 | 6 | export const CommandValidator = (command: ICommand): ClassDecorator => { 7 | return (target: object) => { 8 | Reflect.defineMetadata(COMMAND_VALIDATOR_METADATA, command, target); 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/commandValidatorOptions.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ICommandValidator } from './ICommandValidator'; 3 | 4 | export interface CommandValidatorOptions { 5 | validators: Type[]; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './commandValidator.decorator'; 2 | export * from './ICommandValidator'; 3 | export * from './IValidationError'; 4 | export * from './IValidationResult'; 5 | export * from './joiValidator'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/invalidCommandValidatorException.ts: -------------------------------------------------------------------------------- 1 | export class InvalidCommandValidatorException extends Error { 2 | constructor() { 3 | super( 4 | `Invalid command validation exception. Define command validation using @CommandValidator() decorator`, 5 | ); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/modules/common/commands/validation/joiValidator.ts: -------------------------------------------------------------------------------- 1 | import * as joi from '@hapi/joi'; 2 | import { IValidationResult } from './IValidationResult'; 3 | import { ICommandValidator } from './ICommandValidator'; 4 | 5 | export abstract class JoiCommandValidator 6 | implements ICommandValidator { 7 | async validate(command: TCommand): Promise { 8 | const schema = this.getSchema(command); 9 | const valResult = schema.validate(command); 10 | if (valResult.error) { 11 | return { 12 | hasError: true, 13 | errors: valResult.error.details.map(d => ({ 14 | message: d.message, 15 | field: d.context.key, 16 | fieldLabel: d.context.label, 17 | value: command[d.context.key], 18 | })), 19 | }; 20 | } 21 | 22 | return { 23 | hasError: false, 24 | errors: [], 25 | }; 26 | } 27 | 28 | abstract getSchema(command: TCommand); 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/modules/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { SyncCommandDispatcher } from './commands'; 3 | import { SyncEventDispatcher } from './events'; 4 | import { CqrsModule } from '@nestjs/cqrs'; 5 | import { ValidationExplorer } from './commands/validation/validationExplorer'; 6 | 7 | @Module({ 8 | imports: [CqrsModule], 9 | providers: [SyncCommandDispatcher, SyncEventDispatcher, ValidationExplorer], 10 | exports: [SyncCommandDispatcher, SyncEventDispatcher], 11 | }) 12 | export class CommonModule implements OnModuleInit { 13 | constructor() {} 14 | 15 | onModuleInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/modules/common/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paginatedQuery'; 2 | export * from './query'; 3 | -------------------------------------------------------------------------------- /backend/src/modules/common/controllers/paginatedQuery.ts: -------------------------------------------------------------------------------- 1 | import { Query } from './query'; 2 | 3 | export interface PaginatedQuery extends Query { 4 | page: number; 5 | perPage: number; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/common/controllers/query.ts: -------------------------------------------------------------------------------- 1 | export interface Query { 2 | relations: string[]; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/modules/common/entities/aggregateRoot.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from '../events/domainEvent'; 2 | import { Exclude } from 'class-transformer'; 3 | 4 | export interface IAggregateRoot { 5 | id: TId; 6 | 7 | apply(event: DomainEvent): void; 8 | 9 | events(): DomainEvent[]; 10 | 11 | publish(): DomainEvent[]; 12 | } 13 | 14 | export abstract class AggregateRoot implements IAggregateRoot { 15 | @Exclude() 16 | private raisedEvents: DomainEvent[] = []; 17 | 18 | apply(event: DomainEvent) { 19 | this.raisedEvents.push(event); 20 | 21 | this.rehydrate(event); 22 | } 23 | 24 | private rehydrate(event: DomainEvent) { 25 | const handler = this.getEventHandler(event); 26 | handler && handler.call(this, event); 27 | } 28 | 29 | events(): DomainEvent[] { 30 | return this.raisedEvents; 31 | } 32 | 33 | abstract id: TId; 34 | 35 | private getEventHandler(event: DomainEvent): Function | undefined { 36 | const handler = `on${this.getEventName(event)}`; 37 | return this[handler]; 38 | } 39 | 40 | private getEventName(event): string { 41 | const { constructor } = Object.getPrototypeOf(event); 42 | return constructor.name as string; 43 | } 44 | 45 | loadFromHistory(history: DomainEvent[]) { 46 | history.forEach(event => this.rehydrate(event)); 47 | } 48 | 49 | publish(): DomainEvent[] { 50 | return this.raisedEvents.splice(0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/modules/common/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregateRoot'; 2 | export * from './baseRepository'; 3 | -------------------------------------------------------------------------------- /backend/src/modules/common/events/domainEvent.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | export interface DomainEvent extends IEvent {} 3 | -------------------------------------------------------------------------------- /backend/src/modules/common/events/eventDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from './domainEvent'; 2 | 3 | export interface IEventDispatcher { 4 | publish(event: TEvent): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/modules/common/events/eventHandler.ts: -------------------------------------------------------------------------------- 1 | import { IEventHandler as EventHandler } from '@nestjs/cqrs'; 2 | import { DomainEvent } from './domainEvent'; 3 | 4 | export interface IEventHandler 5 | extends EventHandler {} 6 | -------------------------------------------------------------------------------- /backend/src/modules/common/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './domainEvent'; 2 | export * from './eventDispatcher'; 3 | export * from './eventHandler'; 4 | export * from './syncEventDispatcher'; 5 | -------------------------------------------------------------------------------- /backend/src/modules/common/events/syncEventDispatcher.ts: -------------------------------------------------------------------------------- 1 | import { EventBus } from '@nestjs/cqrs'; 2 | import { DomainEvent } from './domainEvent'; 3 | 4 | import { IEventDispatcher } from './eventDispatcher'; 5 | import { ModuleRef } from '@nestjs/core'; 6 | import { Injectable, Type } from '@nestjs/common'; 7 | import { IEventHandler } from './eventHandler'; 8 | 9 | export type EventHandlerMetatype = Type>; 10 | 11 | @Injectable() 12 | export class SyncEventDispatcher implements IEventDispatcher { 13 | constructor(private eventBus: EventBus) {} 14 | publish(event: TEvent): Promise { 15 | this.eventBus.publish(event); 16 | const eb = this.eventBus; 17 | return new Promise(resolve => { 18 | eb.publish(event); 19 | }); 20 | } 21 | 22 | register(handlers: EventHandlerMetatype[]) { 23 | this.eventBus.register(handlers); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './commands'; 2 | export * from './entities'; 3 | export * from './common.module'; 4 | export * from './events'; 5 | export * from './controllers'; 6 | -------------------------------------------------------------------------------- /backend/src/modules/common/interceptors/TransformInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | ExecutionContext, 4 | NestInterceptor, 5 | CallHandler, 6 | } from '@nestjs/common'; 7 | import { Observable } from 'rxjs'; 8 | import { map } from 'rxjs/operators'; 9 | import { classToPlain } from 'class-transformer'; 10 | 11 | @Injectable() 12 | export class TransformInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | return next.handle().pipe(map(data => classToPlain(data))); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/modules/config/about.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AboutInfo { 2 | version: string; 3 | environment: string; 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/modules/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService(), 9 | }, 10 | ], 11 | exports: [ConfigService], 12 | }) 13 | export class ConfigModule {} 14 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/activate-employee.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class ActivateEmployee implements ICommand { 4 | employeeId: number; 5 | isActive: boolean; 6 | 7 | constructor(employeeId: number, isActive: boolean) { 8 | this.employeeId = employeeId; 9 | this.isActive = isActive; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/deactivate-employee.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class DeactivateEmployee implements ICommand { 4 | employeeId: number; 5 | isActive: boolean; 6 | 7 | constructor(employeeId: number, isActive: boolean) { 8 | this.employeeId = employeeId; 9 | this.isActive = isActive; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-activator.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { ActivateEmployee } from '../activate-employee.command'; 3 | import { EmployeeActivator } from './employee-activator.handler'; 4 | 5 | describe('Employee Remover', () => { 6 | describe('when a user activates an employee', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | findById: jest.fn().mockResolvedValue([]), 11 | save: jest.fn(), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should activate the employee from the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeActivator(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 101, //change this to 100 to make the test pass 23 | isActive: false, 24 | }; 25 | 26 | const activateEmployeeCommand = new ActivateEmployee( 27 | params.employeeId, 28 | params.isActive, 29 | ); 30 | 31 | // Act 32 | await handler.handle(activateEmployeeCommand); 33 | 34 | // Assert 35 | expect(employeeRepository.findById).toBeCalledWith(100); 36 | expect(employeeRepository.save).toBeCalled(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-activator.handler.ts: -------------------------------------------------------------------------------- 1 | import { ActivateEmployee } from '../activate-employee.command'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(ActivateEmployee) 8 | @Injectable() 9 | export class EmployeeActivator extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | 14 | async handle(command: ActivateEmployee): Promise { 15 | const { employeeId, isActive } = command; 16 | const employee = await this.employeeRepository.findById(employeeId); 17 | employee.isActive = isActive; 18 | 19 | await this.employeeRepository.save(employee); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-address-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeAddressUpdater } from './employee-address-updater.handler'; 3 | import { UpdateEmployeeAddress } from '../update-employee-address.command'; 4 | 5 | describe('Employee Address Updater', () => { 6 | describe('when an user updates an employee address', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeAddressUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | address: 'San Pedro Sula, Calle 1, Casa 5', 24 | city: 'San Pedro Sula', 25 | country: 'Honduras', 26 | region: 'Cortes', 27 | }; 28 | 29 | const updateEmployee = new UpdateEmployeeAddress(params.employeeId, params.address, 30 | params.country, params.region, params.city); 31 | 32 | // Act 33 | await handler.handle(updateEmployee); 34 | 35 | // Assert 36 | expect(employeeRepository.findById).toBeCalledWith(100); 37 | expect(employeeRepository.save).toBeCalled(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-address-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeAddress } from '../update-employee-address.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeeAddress) 8 | @Injectable() 9 | export class EmployeeAddressUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeeAddress): Promise { 14 | const { 15 | employeeId, 16 | address, 17 | country, 18 | region, 19 | city 20 | } = command; 21 | 22 | const employee = await this.employeeRepository.findById(employeeId); 23 | 24 | employee.address = address; 25 | employee.country = country; 26 | employee.region = region; 27 | employee.city = city; 28 | 29 | await this.employeeRepository.save(employee); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-birthdate-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeBirthDateUpdater } from './employee-birthdate-updater.handler'; 3 | import { UpdateEmployeeBirthdate } from '../update-employee-birthdate.command'; 4 | 5 | describe('Employee Birthdate Updater', () => { 6 | describe('when an user updates an employee birthdate', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeBirthDateUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | birthdate: '2008-09-15T15:53:00', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeBirthdate(params.employeeId, params.birthdate); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-birthdate-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment-timezone'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { UpdateEmployeeBirthdate } from '../update-employee-birthdate.command'; 4 | import { CommandHandler } from '@nestjs/cqrs'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { EmployeeRepository } from '../../repositories/employees.repository'; 7 | 8 | @CommandHandler(UpdateEmployeeBirthdate) 9 | @Injectable() 10 | export class EmployeeBirthDateUpdater extends BaseCommandHandler { 11 | constructor(private readonly employeeRepository: EmployeeRepository) { 12 | super(); 13 | } 14 | async handle(command: UpdateEmployeeBirthdate): Promise { 15 | const { 16 | employeeId, 17 | birthdate 18 | } = command; 19 | 20 | const employee = await this.employeeRepository.findById(employeeId); 21 | 22 | employee.birthdate = moment(birthdate) 23 | .utc() 24 | .format(); 25 | 26 | await this.employeeRepository.save(employee); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-company-email-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeCompanyEmailUpdater } from './employee-company-email-updater.handler'; 3 | import { UpdateEmployeeCompanyEmail } from '../update-employee-company-email.command'; 4 | 5 | describe('Employee Company Email Updater', () => { 6 | describe('when an user updates an employee company email', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeCompanyEmailUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | companyEmail: 'jimmyramos@acklenavenue.com', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeCompanyEmail(params.employeeId, params.companyEmail); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-company-email-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeCompanyEmail } from '../update-employee-company-email.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeeCompanyEmail) 8 | @Injectable() 9 | export class EmployeeCompanyEmailUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeeCompanyEmail): Promise { 14 | const { 15 | employeeId, 16 | companyEmail 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.companyEmail = companyEmail; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-creator.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { CreateEmployee } from '../create-employee.command'; 3 | import { EmployeeRepository } from '../../repositories/employees.repository'; 4 | import { Employee } from '../../entities/employee.entity'; 5 | import { CommandHandler } from '@nestjs/cqrs'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @CommandHandler(CreateEmployee) 9 | @Injectable() 10 | export class EmployeeCreator extends BaseCommandHandler { 11 | constructor(private readonly employeeRepository: EmployeeRepository) { 12 | super(); 13 | } 14 | async handle(command: CreateEmployee): Promise { 15 | 16 | const employee = new Employee(command); 17 | return await this.employeeRepository.save(employee); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-deactivator.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { DeactivateEmployee } from '../deactivate-employee.command'; 3 | import { EmployeeDeactivator } from '../handlers/employee-deactivator.handler'; 4 | 5 | describe('Employee Remover', () => { 6 | describe('when a user removes an employee', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | findById: jest.fn().mockResolvedValue([]), 11 | save: jest.fn(), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should remove the employee from the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeDeactivator(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | isActive: false 24 | }; 25 | 26 | const deactivateEmployeeCommand = new DeactivateEmployee(params.employeeId, params.isActive); 27 | 28 | // Act 29 | await handler.handle(deactivateEmployeeCommand); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-deactivator.handler.ts: -------------------------------------------------------------------------------- 1 | import { DeactivateEmployee } from '../deactivate-employee.command'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(DeactivateEmployee) 8 | @Injectable() 9 | export class EmployeeDeactivator extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | 14 | async handle(command: DeactivateEmployee): Promise { 15 | const { employeeId, isActive } = command; 16 | const employee = await this.employeeRepository.findById(employeeId); 17 | employee.isActive = isActive; 18 | 19 | await this.employeeRepository.save(employee); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-display-name-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeDisplayNameUpdater } from './employee-display-name-updater.handler'; 3 | import { UpdateEmployeeDisplayName } from '../update-employee-display-name.command'; 4 | 5 | describe('Employee Display Name Updater', () => { 6 | describe('when an user updates an employee display name ', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeDisplayNameUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | displayName: 'Test display', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeDisplayName(params.employeeId, params.displayName); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-display-name-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeDisplayName } from '../update-employee-display-name.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeeDisplayName) 8 | @Injectable() 9 | export class EmployeeDisplayNameUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeeDisplayName): Promise { 14 | const { 15 | employeeId, 16 | displayName 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.displayName = displayName; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-effective-date-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeEffectiveDateUpdater } from './employee-effective-date-updater.handler'; 3 | import { UpdateEmployeeEffectiveDate } from '../update-employee-effective-date.command'; 4 | 5 | describe('Employee Effective Date Updater', () => { 6 | describe('when an user updates an employee effective date', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeEffectiveDateUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | effectiveDate: '2008-09-15T15:53:00', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeEffectiveDate(params.employeeId, params.effectiveDate); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-effective-date-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment-timezone'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { UpdateEmployeeEffectiveDate } from '../update-employee-effective-date.command'; 4 | import { CommandHandler } from '@nestjs/cqrs'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { EmployeeRepository } from '../../repositories/employees.repository'; 7 | 8 | @CommandHandler(UpdateEmployeeEffectiveDate) 9 | @Injectable() 10 | export class EmployeeEffectiveDateUpdater extends BaseCommandHandler { 11 | constructor(private readonly employeeRepository: EmployeeRepository) { 12 | super(); 13 | } 14 | async handle(command: UpdateEmployeeEffectiveDate): Promise { 15 | const { 16 | employeeId, 17 | effectiveDate 18 | } = command; 19 | 20 | const employee = await this.employeeRepository.findById(employeeId); 21 | 22 | employee.effectiveDate = moment(effectiveDate) 23 | .utc() 24 | .format(); 25 | 26 | await this.employeeRepository.save(employee); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-name-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeNameUpdater } from './employee-name-updater.handler'; 3 | import { UpdateEmployeeName } from '../update-employee-name.command'; 4 | 5 | describe('Employee Name Updater', () => { 6 | describe('when an user updates an employee name', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeNameUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | firstName: 'Jimmy', 24 | lastName: 'Ramos', 25 | secondLastName: 'Banegas', 26 | middleName: 'test', 27 | }; 28 | 29 | const updateEmployee = new UpdateEmployeeName(params.employeeId, params.firstName, params.middleName, 30 | params.lastName, params.secondLastName); 31 | 32 | // Act 33 | await handler.handle(updateEmployee); 34 | 35 | // Assert 36 | expect(employeeRepository.findById).toBeCalledWith(100); 37 | expect(employeeRepository.save).toBeCalled(); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-name-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment-timezone'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { UpdateEmployeeName } from '../update-employee-name.command'; 4 | import { CommandHandler } from '@nestjs/cqrs'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { EmployeeRepository } from '../../repositories/employees.repository'; 7 | import { SalaryType } from '../../entities/employee.entity'; 8 | 9 | @CommandHandler(UpdateEmployeeName) 10 | @Injectable() 11 | export class EmployeeNameUpdater extends BaseCommandHandler { 12 | constructor(private readonly employeeRepository: EmployeeRepository) { 13 | super(); 14 | } 15 | async handle(command: UpdateEmployeeName): Promise { 16 | const { 17 | employeeId, 18 | firstName, 19 | middleName, 20 | lastName, 21 | secondLastName 22 | } = command; 23 | 24 | const employee = await this.employeeRepository.findById(employeeId); 25 | 26 | employee.firstName = firstName; 27 | employee.middleName = middleName; 28 | employee.lastName = lastName; 29 | employee.secondLastName = secondLastName; 30 | 31 | await this.employeeRepository.save(employee); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-personal-email-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeePersonalEmailUpdater } from './employee-personal-email-updater.handler'; 3 | import { UpdateEmployeePersonalEmail } from '../update-employee-personal-email.command'; 4 | 5 | describe('Employee Personal Email Updater', () => { 6 | describe('when an user updates an employee personal email', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeePersonalEmailUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | personalEmail: 'jimmybanegas93@gmail.com', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeePersonalEmail(params.employeeId, params.personalEmail); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-personal-email-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeePersonalEmail } from '../update-employee-personal-email.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeePersonalEmail) 8 | @Injectable() 9 | export class EmployeePersonalEmailUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeePersonalEmail): Promise { 14 | const { 15 | employeeId, 16 | personalEmail 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.personalEmail = personalEmail; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-phone-number-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeePhoneNumberUpdater } from './employee-phone-number-updater.handler'; 3 | import { UpdateEmployeePhoneNumber } from '../update-employee-phone-number.command'; 4 | 5 | describe('Employee Phone Number Updater', () => { 6 | describe('when an user updates an employee phone number', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeePhoneNumberUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | phoneNumber: '50494621230' 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeePhoneNumber(params.employeeId, params.phoneNumber); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-phone-number-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeePhoneNumber } from '../update-employee-phone-number.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeePhoneNumber) 8 | @Injectable() 9 | export class EmployeePhoneNumberUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeePhoneNumber): Promise { 14 | const { 15 | employeeId, 16 | phoneNumber 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.phoneNumber = phoneNumber; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-salary-type-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeSalaryTypeUpdater } from './employee-salary-type-updater.handler'; 3 | import { UpdateEmployeeSalaryType } from '../update-employee-salary-type.command'; 4 | 5 | describe('Employee Salary Type Updater', () => { 6 | describe('when an user updates an employee salary type', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeSalaryTypeUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | salaryType: 'Montly', 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeSalaryType(params.employeeId,params.salaryType); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-salary-type-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeSalaryType } from '../update-employee-salary-type.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | import { SalaryType } from '../../entities/employee.entity'; 7 | 8 | @CommandHandler(UpdateEmployeeSalaryType) 9 | @Injectable() 10 | export class EmployeeSalaryTypeUpdater extends BaseCommandHandler { 11 | constructor(private readonly employeeRepository: EmployeeRepository) { 12 | super(); 13 | } 14 | async handle(command: UpdateEmployeeSalaryType): Promise { 15 | const { 16 | employeeId, 17 | salaryType 18 | } = command; 19 | const salaryTypeKey = Object.keys(SalaryType).find( 20 | key => SalaryType[key] === salaryType, 21 | ); 22 | const employee = await this.employeeRepository.findById(employeeId); 23 | 24 | employee.salaryType = SalaryType[salaryTypeKey]; 25 | 26 | await this.employeeRepository.save(employee); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-salary-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeSalaryUpdater } from './employee-salary-updater.handler'; 3 | import { UpdateEmployeeSalary } from '../update-employee-salary.command'; 4 | 5 | describe('Employee Salary Updater', () => { 6 | describe('when an user updates an employee salary', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeSalaryUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | salary: 10 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeSalary(params.employeeId, params.salary); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-salary-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeSalary } from '../update-employee-salary.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeeSalary) 8 | @Injectable() 9 | export class EmployeeSalaryUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeeSalary): Promise { 14 | const { 15 | employeeId, 16 | salary 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.salary = salary; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-tags-updater.handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeRepository } from '../../repositories/employees.repository'; 2 | import { EmployeeTagsUpdater } from './employee-tags-updater.handler'; 3 | import { UpdateEmployeeTags } from '../update-employee-tags.command'; 4 | 5 | describe('Employee Tags Updater', () => { 6 | describe('when an user updates an employee tags', () => { 7 | const MockEmployeeRepository = jest.fn( 8 | () => 9 | ({ 10 | save: jest.fn(), 11 | findById: jest.fn().mockResolvedValue([]), 12 | } as any), 13 | ); 14 | 15 | const employeeRepository = new MockEmployeeRepository(); 16 | 17 | it('should get and employee and add it to the repository', async () => { 18 | // Arrange 19 | const handler = new EmployeeTagsUpdater(employeeRepository); 20 | 21 | const params = { 22 | employeeId: 100, 23 | tags: 'Developer' 24 | }; 25 | 26 | const updateEmployee = new UpdateEmployeeTags(params.employeeId, params.tags); 27 | 28 | // Act 29 | await handler.handle(updateEmployee); 30 | 31 | // Assert 32 | expect(employeeRepository.findById).toBeCalledWith(100); 33 | expect(employeeRepository.save).toBeCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/handlers/employee-tags-updater.handler.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommandHandler } from '../../../../common/commands'; 2 | import { UpdateEmployeeTags } from '../update-employee-tags.command'; 3 | import { CommandHandler } from '@nestjs/cqrs'; 4 | import { Injectable } from '@nestjs/common'; 5 | import { EmployeeRepository } from '../../repositories/employees.repository'; 6 | 7 | @CommandHandler(UpdateEmployeeTags) 8 | @Injectable() 9 | export class EmployeeTagsUpdater extends BaseCommandHandler { 10 | constructor(private readonly employeeRepository: EmployeeRepository) { 11 | super(); 12 | } 13 | async handle(command: UpdateEmployeeTags): Promise { 14 | const { 15 | employeeId, 16 | tags 17 | } = command; 18 | 19 | const employee = await this.employeeRepository.findById(employeeId); 20 | 21 | employee.tags = tags; 22 | 23 | await this.employeeRepository.save(employee); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-address.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeAddress implements ICommand { 4 | employeeId: number; 5 | address?: string; 6 | country: string; 7 | region: string; 8 | city: string; 9 | 10 | 11 | constructor(employeeId: number, address: string, country: string, region: string, city: string ) { 12 | this.employeeId = employeeId; 13 | this.address = address; 14 | this.region = region; 15 | this.country = country; 16 | this.city = city; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-birthdate.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeBirthdate implements ICommand { 4 | employeeId: number; 5 | birthdate: string; 6 | 7 | constructor(employeeId: number, birthdate: string) { 8 | this.employeeId = employeeId; 9 | this.birthdate = birthdate; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-company-email.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeCompanyEmail implements ICommand { 4 | employeeId: number; 5 | companyEmail: string; 6 | 7 | constructor(employeeId: number, companyEmail: string) { 8 | this.employeeId = employeeId; 9 | this.companyEmail = companyEmail; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-display-name.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeDisplayName implements ICommand { 4 | employeeId: number; 5 | displayName: string; 6 | 7 | constructor(employeeId: number, displayName: string) { 8 | this.employeeId = employeeId; 9 | this.displayName = displayName; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-effective-date.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeEffectiveDate implements ICommand { 4 | employeeId: number; 5 | effectiveDate: string; 6 | 7 | constructor(employeeId: number, effectiveDate: string) { 8 | this.employeeId = employeeId; 9 | this.effectiveDate = effectiveDate; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-name.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeName implements ICommand { 4 | employeeId: number; 5 | firstName: string; 6 | middleName?: string; 7 | lastName: string; 8 | secondLastName: string; 9 | 10 | 11 | constructor(employeeId: number, firstName: string, middleName: string, lastName: string, secondLastName: string ) { 12 | this.employeeId = employeeId; 13 | this.firstName = firstName; 14 | this.lastName = lastName; 15 | this.middleName = middleName; 16 | this.secondLastName = secondLastName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-personal-email.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeePersonalEmail implements ICommand { 4 | employeeId: number; 5 | personalEmail: string; 6 | 7 | constructor(employeeId: number, personalEmail: string) { 8 | this.employeeId = employeeId; 9 | this.personalEmail = personalEmail; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-phone-number.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeePhoneNumber implements ICommand { 4 | employeeId: number; 5 | phoneNumber: string; 6 | 7 | constructor(employeeId: number, phoneNumber: string) { 8 | this.employeeId = employeeId; 9 | this.phoneNumber = phoneNumber; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-salary-type.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeSalaryType implements ICommand { 4 | employeeId: number; 5 | salaryType: string; 6 | 7 | constructor(employeeId: number, salaryType: string) { 8 | this.employeeId = employeeId; 9 | this.salaryType = salaryType; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-salary.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeSalary implements ICommand { 4 | employeeId: number; 5 | salary: number; 6 | 7 | constructor(employeeId: number, salary: number) { 8 | this.employeeId = employeeId; 9 | this.salary = salary; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/update-employee-tags.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class UpdateEmployeeTags implements ICommand { 4 | employeeId: number; 5 | tags: string; 6 | 7 | constructor(employeeId: number, tags: string) { 8 | this.employeeId = employeeId; 9 | this.tags = tags; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forActivateEmployee/activate-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { ActivateEmployee } from '../../activate-employee.command'; 4 | import { CheckEmployeeExistsOnActivate } from './check-employee-exists.validator'; 5 | 6 | @CommandValidator(ActivateEmployee) 7 | export class ActivateEmployeeCompositeValidator extends CompositeValidator< 8 | ActivateEmployee 9 | > { 10 | constructor(checkEmployeeExists: CheckEmployeeExistsOnActivate) { 11 | super([checkEmployeeExists]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forActivateEmployee/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { ActivateEmployee } from '../../activate-employee.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeExistsOnActivate } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeExistsOnActivate( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | isActive: false 25 | }; 26 | 27 | // Act 28 | const activateEmployee = new ActivateEmployee(params.employeeId, params.isActive); 29 | const result = await employeeValidator.validate(activateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forActivateEmployee/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 6 | import { Injectable } from '@nestjs/common'; 7 | import { ActivateEmployee } from '../../activate-employee.command'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeExistsOnActivate 11 | implements ICommandValidator { 12 | constructor(private readonly employeeRepository: EmployeeRepository) {} 13 | 14 | async validate(command: ActivateEmployee): Promise { 15 | const queryResult = await this.employeeRepository 16 | .where({ id: command.employeeId }) 17 | .get(); 18 | 19 | if (queryResult.length > 0) { 20 | return { 21 | hasError: false, 22 | errors: [], 23 | }; 24 | } 25 | return { 26 | hasError: true, 27 | errors: [ 28 | { 29 | field: 'employeeId', 30 | fieldLabel: 'employeeId', 31 | message: 'The employee does not exist', 32 | value: command.employeeId, 33 | }, 34 | ], 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forCreateEmployee/create-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { CreateEmployee } from '../../create-employee.command'; 4 | import { CheckPropertiesValue } from './check-properties-value.validator'; 5 | 6 | @CommandValidator(CreateEmployee) 7 | export class CreateEmployeeCompositeValidator extends CompositeValidator< 8 | CreateEmployee 9 | > { 10 | constructor(joiValidator: CheckPropertiesValue) { 11 | super([joiValidator]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forDeactivateEmployee/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { DeactivateEmployee } from '../../deactivate-employee.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeExistsOnDeactivate } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeExistsOnDeactivate( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | isActive: false 25 | }; 26 | 27 | // Act 28 | const deactivateEmployee = new DeactivateEmployee(params.employeeId, params.isActive); 29 | const result = await employeeValidator.validate(deactivateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forDeactivateEmployee/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 6 | import { Injectable } from '@nestjs/common'; 7 | import { DeactivateEmployee } from '../../deactivate-employee.command'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeExistsOnDeactivate 11 | implements ICommandValidator { 12 | constructor(private readonly employeeRepository: EmployeeRepository) {} 13 | 14 | async validate(command: DeactivateEmployee): Promise { 15 | const queryResult = await this.employeeRepository 16 | .where({ id: command.employeeId }) 17 | .get(); 18 | 19 | if (queryResult.length > 0) { 20 | return { 21 | hasError: false, 22 | errors: [], 23 | }; 24 | } 25 | return { 26 | hasError: true, 27 | errors: [ 28 | { 29 | field: 'employeeId', 30 | fieldLabel: 'employeeId', 31 | message: 'The employee does not exist', 32 | value: command.employeeId, 33 | }, 34 | ], 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forDeactivateEmployee/remove-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { DeactivateEmployee } from '../../deactivate-employee.command'; 4 | import { CheckEmployeeExistsOnDeactivate } from './check-employee-exists.validator'; 5 | 6 | @CommandValidator(DeactivateEmployee) 7 | export class DeactivateEmployeeCompositeValidator extends CompositeValidator< 8 | DeactivateEmployee 9 | > { 10 | constructor(checkEmployeeExists: CheckEmployeeExistsOnDeactivate) { 11 | super([checkEmployeeExists]); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeAddress/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeAddress } from '../../update-employee-address.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeAddressExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeAddress): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeAddress/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeAddress } from '../../update-employee-address.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateAddressPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeAddress 8 | > { 9 | getSchema(command: UpdateEmployeeAddress) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | address: joi.string().allow('').optional(), 13 | country: joi 14 | .string() 15 | .required(), 16 | city: joi.string().required(), 17 | region: joi.string().required() 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeAddress/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeAddress } from '../../update-employee-address.command'; 2 | import { CheckUpdateAddressPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update address employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateAddressPropertiesValue(); 9 | const params = { 10 | employeeId: 100, 11 | address: 'San Pedro Sula, Calle 1, Casa 5', 12 | city: 'San Pedro Sula', 13 | country: 'Honduras', 14 | region: 'Cortes', 15 | }; 16 | 17 | // Act 18 | const updateEmployee = new UpdateEmployeeAddress(params.employeeId, params.address, 19 | params.country, params.region, params.city); 20 | const result = await updateEmployeeValidator.validate(updateEmployee); 21 | 22 | // Assert 23 | expect(result.hasError).toBeFalsy(); 24 | expect(result.errors.length).toBe(0); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeAddress/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeAddress } from '../../update-employee-address.command'; 4 | import { CheckEmployeeAddressExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateAddressPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeAddress) 8 | export class UpdateEmployeeAddressCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeAddress 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateAddressPropertiesValue, 13 | checkEmployeeExists: CheckEmployeeAddressExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeBirthDate/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeBirthdate } from '../../update-employee-birthdate.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeBirthDateExists } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update birthdate employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeBirthDateExists( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | birthdate: '2008-09-15T15:53:00' 25 | }; 26 | 27 | // Act 28 | const updateEmployee = new UpdateEmployeeBirthdate(params.employeeId, params.birthdate); 29 | const result = await employeeValidator.validate(updateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeBirthDate/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeBirthdate } from '../../update-employee-birthdate.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeBirthDateExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeBirthdate): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeBirthDate/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeBirthdate } from '../../update-employee-birthdate.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateBirthDatePropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeBirthdate 8 | > { 9 | getSchema(command: UpdateEmployeeBirthdate) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | birthdate: joi 13 | .string() 14 | .isoDate() 15 | .required(), 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeBirthDate/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeBirthdate } from '../../update-employee-birthdate.command'; 2 | import { CheckUpdateBirthDatePropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update birthdate employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateBirthDatePropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | birthdate: '2008-09-15T15:53:00', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeBirthdate(params.employeeId, params.birthdate); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeBirthDate/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeBirthdate } from '../../update-employee-birthdate.command'; 4 | import { CheckEmployeeBirthDateExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateBirthDatePropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeBirthdate) 8 | export class UpdateEmployeeBirthDateCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeBirthdate 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateBirthDatePropertiesValue, 13 | checkEmployeeExists: CheckEmployeeBirthDateExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeCompanyEmail/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeCompanyEmail } from '../../update-employee-company-email.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeCompanyEmailExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeCompanyEmail): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeCompanyEmail/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeCompanyEmail } from '../../update-employee-company-email.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateCompanyEmailPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeCompanyEmail 8 | > { 9 | getSchema(command: UpdateEmployeeCompanyEmail) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | companyEmail: joi.string().required(), 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeCompanyEmail/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeCompanyEmail } from '../../update-employee-company-email.command'; 2 | import { CheckUpdateCompanyEmailPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update company email employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateCompanyEmailPropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | companyEmail: 'jimmyramos@acklenavenue.com', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeCompanyEmail(params.employeeId, params.companyEmail); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeCompanyEmail/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeCompanyEmail } from '../../update-employee-company-email.command'; 4 | import { CheckEmployeeCompanyEmailExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateCompanyEmailPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeCompanyEmail) 8 | export class UpdateEmployeeCompanyEmailCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeCompanyEmail 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateCompanyEmailPropertiesValue, 13 | checkEmployeeExists: CheckEmployeeCompanyEmailExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeDisplayName/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeDisplayName } from '../../update-employee-display-name.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeDisplayNameExists } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update display name employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeDisplayNameExists( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | displayName: 'Test display' 25 | }; 26 | 27 | // Act 28 | const updateEmployee = new UpdateEmployeeDisplayName(params.employeeId, params.displayName); 29 | const result = await employeeValidator.validate(updateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeDisplayName/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeDisplayName } from '../../update-employee-display-name.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeDisplayNameExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeDisplayName): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeDisplayName/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeDisplayName } from '../../update-employee-display-name.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateDisplayNamePropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeDisplayName 8 | > { 9 | getSchema(command: UpdateEmployeeDisplayName) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | displayName: joi.string().required(), 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeDisplayName/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeDisplayName } from '../../update-employee-display-name.command'; 2 | import { CheckUpdateDisplayNamePropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update display name employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateDisplayNamePropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | displayName: 'Test display', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeDisplayName(params.employeeId, params.displayName); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeDisplayName/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeDisplayName } from '../../update-employee-display-name.command'; 4 | import { CheckEmployeeDisplayNameExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateDisplayNamePropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeDisplayName) 8 | export class UpdateEmployeeDisplayNameCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeDisplayName 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateDisplayNamePropertiesValue, 13 | checkEmployeeExists: CheckEmployeeDisplayNameExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeEffectiveDate/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeEffectiveDate } from '../../update-employee-effective-date.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeEffectiveDateExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeEffectiveDate): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeEffectiveDate/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeEffectiveDate } from '../../update-employee-effective-date.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateEffectiveDatePropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeEffectiveDate 8 | > { 9 | getSchema(command: UpdateEmployeeEffectiveDate) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | effectiveDate: joi 13 | .string() 14 | .isoDate() 15 | .required(), 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeEffectiveDate/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeEffectiveDate } from '../../update-employee-effective-date.command'; 2 | import { CheckUpdateEffectiveDatePropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update effective date employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateEffectiveDatePropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | effectiveDate: '2008-09-15T15:53:00', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeEffectiveDate(params.employeeId, params.effectiveDate); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeEffectiveDate/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeEffectiveDate } from '../../update-employee-effective-date.command'; 4 | import { CheckEmployeeEffectiveDateExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateEffectiveDatePropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeEffectiveDate) 8 | export class UpdateEmployeeEffectiveDateCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeEffectiveDate 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateEffectiveDatePropertiesValue, 13 | checkEmployeeExists: CheckEmployeeEffectiveDateExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeName/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeName } from '../../update-employee-name.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeNameExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeName): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | 18 | if (queryResult.length > 0) { 19 | return { 20 | hasError: false, 21 | errors: [], 22 | }; 23 | } 24 | return { 25 | hasError: true, 26 | errors: [ 27 | { 28 | field: 'employeeId', 29 | fieldLabel: 'employeeId', 30 | message: 'The employee does not exist', 31 | value: command.employeeId, 32 | }, 33 | ], 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeName/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeName } from '../../update-employee-name.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateNamePropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeName 8 | > { 9 | getSchema(command: UpdateEmployeeName) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | firstName: joi.string().required(), 13 | middleName: joi 14 | .string() 15 | .allow('') 16 | .optional(), 17 | lastName: joi.string().required(), 18 | secondLastName: joi 19 | .string() 20 | .allow('') 21 | .optional() 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeName/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeName } from '../../update-employee-name.command'; 2 | import { CheckUpdateNamePropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update name mployee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateNamePropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | firstName: 'Jimmy', 12 | lastName: 'Ramos', 13 | secondLastName: 'Banegas', 14 | middleName: 'test', 15 | }; 16 | 17 | // Act 18 | const updateEmployee = new UpdateEmployeeName(params.employeeId, params.firstName, params.middleName, 19 | params.lastName, params.secondLastName); 20 | const result = await updateEmployeeValidator.validate(updateEmployee); 21 | 22 | // Assert 23 | expect(result.hasError).toBeFalsy(); 24 | expect(result.errors.length).toBe(0); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeName/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeName } from '../../update-employee-name.command'; 4 | import { CheckEmployeeNameExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateNamePropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeName) 8 | export class UpdateEmployeeNameCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeName 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateNamePropertiesValue, 13 | checkEmployeeExists: CheckEmployeeNameExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePersonalEmail/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeePersonalEmail } from '../../update-employee-personal-email.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeePersonalEmailExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeePersonalEmail): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePersonalEmail/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeePersonalEmail } from '../../update-employee-personal-email.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdatePersonalEmailPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeePersonalEmail 8 | > { 9 | getSchema(command: UpdateEmployeePersonalEmail) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | personalEmail: joi.string().allow('').optional(), 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePersonalEmail/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeePersonalEmail } from '../../update-employee-personal-email.command'; 2 | import { CheckUpdatePersonalEmailPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update personal email employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdatePersonalEmailPropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | personalEmail: 'jimmybanegas93@gmail.com', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeePersonalEmail(params.employeeId, params.personalEmail); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePersonalEmail/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeePersonalEmail } from '../../update-employee-personal-email.command'; 4 | import { CheckEmployeePersonalEmailExists } from './check-employee-exists.validator'; 5 | import { CheckUpdatePersonalEmailPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeePersonalEmail) 8 | export class UpdateEmployeePersonalEmailCompositeValidator extends CompositeValidator< 9 | UpdateEmployeePersonalEmail 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdatePersonalEmailPropertiesValue, 13 | checkEmployeeExists: CheckEmployeePersonalEmailExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePhoneNumber/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeePhoneNumber } from '../../update-employee-phone-number.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeePhoneNumberExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeePhoneNumber): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePhoneNumber/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeePhoneNumber } from '../../update-employee-phone-number.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdatePhoneNumberPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeePhoneNumber 8 | > { 9 | getSchema(command: UpdateEmployeePhoneNumber) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | phoneNumber: joi.string().allow('').optional(), 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePhoneNumber/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeePhoneNumber } from '../../update-employee-phone-number.command'; 2 | import { CheckUpdatePhoneNumberPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update phone number employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdatePhoneNumberPropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | phoneNumber: '50494621230', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeePhoneNumber(params.employeeId, params.phoneNumber); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeePhoneNumber/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeePhoneNumber } from '../../update-employee-phone-number.command'; 4 | import { CheckEmployeePhoneNumberExists } from './check-employee-exists.validator'; 5 | import { CheckUpdatePhoneNumberPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeePhoneNumber) 8 | export class UpdateEmployeePhoneNumberCompositeValidator extends CompositeValidator< 9 | UpdateEmployeePhoneNumber 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdatePhoneNumberPropertiesValue, 13 | checkEmployeeExists: CheckEmployeePhoneNumberExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalary/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalary } from '../../update-employee-salary.command' 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeSalaryExists } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update salary employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeSalaryExists( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | salary: 10, 25 | }; 26 | 27 | // Act 28 | const updateEmployee = new UpdateEmployeeSalary(params.employeeId, params.salary); 29 | const result = await employeeValidator.validate(updateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalary/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeSalary } from '../../update-employee-salary.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeSalaryExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeSalary): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | 18 | if (queryResult.length > 0) { 19 | return { 20 | hasError: false, 21 | errors: [], 22 | }; 23 | } 24 | return { 25 | hasError: true, 26 | errors: [ 27 | { 28 | field: 'employeeId', 29 | fieldLabel: 'employeeId', 30 | message: 'The employee does not exist', 31 | value: command.employeeId, 32 | }, 33 | ], 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalary/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalary } from '../../update-employee-salary.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateSalaryPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeSalary 8 | > { 9 | getSchema(command: UpdateEmployeeSalary) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | salary: joi.number().required() 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalary/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalary } from '../../update-employee-salary.command'; 2 | import { CheckUpdateSalaryPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update salary employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateSalaryPropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | salary: 10 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeSalary(params.employeeId, params.salary); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalary/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeSalary } from '../../update-employee-salary.command'; 4 | import { CheckEmployeeSalaryExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateSalaryPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeSalary) 8 | export class UpdateEmployeeSalaryCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeSalary 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateSalaryPropertiesValue, 13 | checkEmployeeExists: CheckEmployeeSalaryExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalaryType/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalaryType } from '../../update-employee-salary-type.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeSalaryTypeExists } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update salary type employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeSalaryTypeExists( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | salaryType: 'Montly', 25 | }; 26 | 27 | // Act 28 | const updateEmployee = new UpdateEmployeeSalaryType(params.employeeId, params.salaryType); 29 | const result = await employeeValidator.validate(updateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalaryType/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeSalaryType } from '../../update-employee-salary-type.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeSalaryTypeExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeSalaryType): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | 18 | if (queryResult.length > 0) { 19 | return { 20 | hasError: false, 21 | errors: [], 22 | }; 23 | } 24 | return { 25 | hasError: true, 26 | errors: [ 27 | { 28 | field: 'employeeId', 29 | fieldLabel: 'employeeId', 30 | message: 'The employee does not exist', 31 | value: command.employeeId, 32 | }, 33 | ], 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalaryType/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalaryType } from '../../update-employee-salary-type.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateSalaryTypePropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeSalaryType 8 | > { 9 | getSchema(command: UpdateEmployeeSalaryType) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | salaryType: joi.string().required() 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalaryType/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeSalaryType } from '../../update-employee-salary-type.command'; 2 | import { CheckUpdateSalaryTypePropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update salary type employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateSalaryTypePropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | salaryType: 'Montly', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeSalaryType(params.employeeId, params.salaryType); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeSalaryType/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeSalaryType } from '../../update-employee-salary-type.command'; 4 | import { CheckEmployeeSalaryTypeExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateSalaryTypePropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeSalaryType) 8 | export class UpdateEmployeeSalaryTypeCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeSalaryType 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateSalaryTypePropertiesValue, 13 | checkEmployeeExists: CheckEmployeeSalaryTypeExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeTags/check-employee-exists.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeTags } from '../../update-employee-tags.command'; 2 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 3 | import { CheckEmployeeTagsExists } from './check-employee-exists.validator'; 4 | 5 | describe('Check if employee exists', () => { 6 | const MockEmployeeRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching an update tags employee command', () => { 16 | it('should validate that the product exists in the db', async () => { 17 | // Arrange 18 | const employeeValidator = new CheckEmployeeTagsExists( 19 | new MockEmployeeRepository(), 20 | ); 21 | 22 | const params = { 23 | employeeId: 100, 24 | tags: 'Developer', 25 | }; 26 | 27 | // Act 28 | const updateEmployee = new UpdateEmployeeTags(params.employeeId, params.tags); 29 | const result = await employeeValidator.validate(updateEmployee); 30 | 31 | // Assert 32 | expect(result.hasError).toBeTruthy(); 33 | expect(result.errors).toMatchObject([ 34 | { 35 | field: 'employeeId', 36 | fieldLabel: 'employeeId', 37 | message: 'The employee does not exist', 38 | value: 100, 39 | }, 40 | ]); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeTags/check-employee-exists.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands'; 5 | import { UpdateEmployeeTags } from '../../update-employee-tags.command'; 6 | import { EmployeeRepository } from '../../../repositories/employees.repository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckEmployeeTagsExists implements ICommandValidator { 11 | constructor(private readonly employeeRepository: EmployeeRepository) {} 12 | 13 | async validate(command: UpdateEmployeeTags): Promise { 14 | const queryResult = await this.employeeRepository 15 | .where({ id: command.employeeId }) 16 | .get(); 17 | if (queryResult.length > 0) { 18 | return { 19 | hasError: false, 20 | errors: [], 21 | }; 22 | } 23 | return { 24 | hasError: true, 25 | errors: [ 26 | { 27 | field: 'employeeId', 28 | fieldLabel: 'employeeId', 29 | message: 'The employee does not exist', 30 | value: command.employeeId, 31 | }, 32 | ], 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeTags/check-properties-value.validator.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeTags } from '../../update-employee-tags.command'; 2 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 3 | 4 | import * as joi from '@hapi/joi'; 5 | 6 | export class CheckUpdateTagsPropertiesValue extends JoiCommandValidator< 7 | UpdateEmployeeTags 8 | > { 9 | getSchema(command: UpdateEmployeeTags) { 10 | return joi.object({ 11 | employeeId: joi.number().required(), 12 | tags: joi.string().allow('').optional(), 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeTags/check-properties.value.validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { UpdateEmployeeTags } from '../../update-employee-tags.command'; 2 | import { CheckUpdateTagsPropertiesValue } from './check-properties-value.validator'; 3 | 4 | describe('Update Employee Validator', () => { 5 | describe('when sending an update employee command', () => { 6 | it('should pass the validation if the command is correct', async () => { 7 | // Arrange 8 | const updateEmployeeValidator = new CheckUpdateTagsPropertiesValue(); 9 | const params = { 10 | employeeId: 10, 11 | tags: 'Developer', 12 | }; 13 | 14 | // Act 15 | const updateEmployee = new UpdateEmployeeTags(params.employeeId, params.tags); 16 | const result = await updateEmployeeValidator.validate(updateEmployee); 17 | 18 | // Assert 19 | expect(result.hasError).toBeFalsy(); 20 | expect(result.errors.length).toBe(0); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/commands/validators/forUpdateEmployeeTags/update-employee-composite.validator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 3 | import { UpdateEmployeeTags } from '../../update-employee-tags.command'; 4 | import { CheckEmployeeTagsExists } from './check-employee-exists.validator'; 5 | import { CheckUpdateTagsPropertiesValue } from './check-properties-value.validator'; 6 | 7 | @CommandValidator(UpdateEmployeeTags) 8 | export class UpdateEmployeeTagsCompositeValidator extends CompositeValidator< 9 | UpdateEmployeeTags 10 | > { 11 | constructor( 12 | joiValidator: CheckUpdateTagsPropertiesValue, 13 | checkEmployeeExists: CheckEmployeeTagsExists, 14 | ) { 15 | super([joiValidator, checkEmployeeExists]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/employee.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { CommandHandlers } from './commands/handlers'; 3 | import { EventHandlers } from './events/handlers'; 4 | import { CommonModule } from '../../common'; 5 | import { SyncCommandDispatcher } from '../../common'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { AuthModule } from '../../auth/auth.module'; 8 | import { AppLogger } from '../../app/app.logger'; 9 | import { ModuleRef } from '@nestjs/core'; 10 | import { EmployeeRepository } from './repositories/employees.repository'; 11 | import { EmployeeController } from './employee.controller'; 12 | import { Employee } from './entities/employee.entity'; 13 | import { CommandValidators } from './commands/validators'; 14 | import {PassportModule} from "@nestjs/passport"; 15 | 16 | @Module({ 17 | imports: [CommonModule, TypeOrmModule.forFeature([Employee]), AuthModule, PassportModule.register({ defaultStrategy: 'jwt' })], 18 | controllers: [EmployeeController], 19 | providers: [ 20 | EmployeeRepository, 21 | ...CommandHandlers, 22 | ...EventHandlers, 23 | ...CommandValidators, 24 | AppLogger, 25 | ], 26 | }) 27 | export class EmployeeModule implements OnModuleInit { 28 | constructor( 29 | private readonly moduleRef: ModuleRef, 30 | private readonly command$: SyncCommandDispatcher, 31 | ) {} 32 | 33 | onModuleInit() { 34 | this.command$.setModuleRef(this.moduleRef); 35 | this.command$.registerValidators(CommandValidators); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/events/employee-created.event.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from '../../../common/events/domainEvent'; 2 | 3 | export class EmployeeCreated implements DomainEvent { 4 | employeeId: number; 5 | firstName: string; 6 | constructor(employeeId: number, firstName: string) { 7 | this.employeeId = employeeId; 8 | this.firstName = firstName; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/events/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { NotifyEmployeeCreatedConsole } from './notify-employee-created-console.handler'; 2 | 3 | export const EventHandlers = [NotifyEmployeeCreatedConsole]; 4 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/events/handlers/notify-employee-created-console.handler.ts: -------------------------------------------------------------------------------- 1 | import { IEventHandler } from '../../../../common/events'; 2 | import { EventsHandler } from '@nestjs/cqrs'; 3 | import { EmployeeCreated } from '../employee-created.event'; 4 | 5 | @EventsHandler(EmployeeCreated) 6 | export class NotifyEmployeeCreatedConsole 7 | implements IEventHandler { 8 | handle(event: EmployeeCreated): any { 9 | console.log('Employee created'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/repositories/employees.repository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '../../../common/entities'; 2 | import { SyncEventDispatcher } from '../../../common/events'; 3 | import { EntityManager } from 'typeorm'; 4 | import { InjectEntityManager } from '@nestjs/typeorm'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { Employee } from '../entities/employee.entity'; 7 | 8 | @Injectable() 9 | export class EmployeeRepository extends BaseRepository { 10 | constructor( 11 | @InjectEntityManager() 12 | manager: EntityManager, 13 | eventDispatcher: SyncEventDispatcher, 14 | ) { 15 | super(manager.getRepository(Employee), eventDispatcher); 16 | } 17 | 18 | async findByNames(firstName, middleName, lastName, secondLastName) : Promise{ 19 | const emp = await this.repository.createQueryBuilder("employee") 20 | .where("employee.firstName = :firstName AND employee.middleName = :middleName " + 21 | "AND employee.lastName = :lastName AND employee.secondLastName = :secondLastName", 22 | {firstName:firstName,middleName:middleName,lastName:lastName,secondLastName:secondLastName}) 23 | .getOne(); 24 | return emp; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/requests/create-employee-request.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateEmployeeRequest { 2 | firstName: string; 3 | middleName: string; 4 | lastName: string; 5 | secondLastName: string; 6 | displayName: string; 7 | companyEmail: string; 8 | personalEmail: string; 9 | birthdate: string; 10 | startDate: string; 11 | address: string; 12 | phoneNumber: string; 13 | bankName: string; 14 | accountNumber: string; 15 | gender: string; 16 | tags: string; 17 | country: string; 18 | region: string; 19 | city: string; 20 | salary: string; 21 | effectiveDate: string; 22 | salaryType: string; 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/requests/paginated-employee-query.interface.ts: -------------------------------------------------------------------------------- 1 | import { PaginatedQuery } from '../../../common/controllers'; 2 | 3 | export interface PaginatedEmployeeQuery extends PaginatedQuery { 4 | id: number; 5 | pageSize: number; 6 | pageNumber: number; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/modules/domain/employees/requests/update-employee-request.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateEmployeeRequest { 2 | employeeId: number; 3 | firstName: string; 4 | middleName: string; 5 | lastName: string; 6 | secondLastName: string; 7 | displayName: string; 8 | companyEmail: string; 9 | personalEmail: string; 10 | birthdate: string; 11 | address: string; 12 | phoneNumber: string; 13 | bankName: string; 14 | accountNumber: string; 15 | tags: string; 16 | country: string; 17 | region: string; 18 | city: string; 19 | salary: number; 20 | effectiveDate: string; 21 | salaryType: string; 22 | isActive: boolean 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/createOrder.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '../../../common/commands'; 2 | 3 | export class CreateOrder implements ICommand { 4 | productId: string; 5 | productQuantity: number; 6 | userId: string; 7 | constructor(productId: string, productQuantity: number, userId: string) { 8 | this.productId = productId; 9 | this.productQuantity = productQuantity; 10 | this.userId = userId; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { OrderCreator } from './orderCreator'; 2 | 3 | export const CommandHandlers = [OrderCreator]; 4 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/handlers/orderCreator.spec.ts: -------------------------------------------------------------------------------- 1 | import { OrderCreator } from './orderCreator'; 2 | import { CreateOrder } from '../createOrder'; 3 | 4 | import { OrderRepository } from '../../repositories/orderRepository'; 5 | import { ProductRepository } from '../../repositories/productRepository'; 6 | 7 | describe('Order Creator', () => { 8 | const MockOrderRepository = jest.fn( 9 | () => 10 | ({ 11 | save: jest.fn(), 12 | findById: jest.fn(), 13 | } as any), 14 | ); 15 | 16 | const MockProductRepository = jest.fn(() => { 17 | return { 18 | save: jest.fn(), 19 | findById: jest.fn().mockResolvedValue({ id: 'ProductId' }), 20 | } as any; 21 | }); 22 | 23 | const orderRepository = new MockOrderRepository(); 24 | const productRepository = new MockProductRepository(); 25 | 26 | describe('when an user creates an order', () => { 27 | const handler = new OrderCreator(orderRepository, productRepository); 28 | 29 | // @ts-ignore 30 | it('should add order to the repo', async () => { 31 | const creatOrder: CreateOrder = new CreateOrder( 32 | 'productId', 33 | 123, 34 | 'userId', 35 | ); 36 | 37 | await handler.handle(creatOrder); 38 | 39 | expect(productRepository.findById).toBeCalled(); 40 | expect(orderRepository.save).toBeCalled(); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/handlers/orderCreator.ts: -------------------------------------------------------------------------------- 1 | import { CreateOrder } from '../createOrder'; 2 | import { BaseCommandHandler } from '../../../../common/commands'; 3 | import { OrderRepository } from '../../repositories/orderRepository'; 4 | import { ProductRepository } from '../../repositories/productRepository'; 5 | import { Order } from '../../entities/order.entity'; 6 | import { CommandHandler } from '@nestjs/cqrs'; 7 | import { Injectable } from '@nestjs/common'; 8 | import { v4 as uuid } from 'uuid'; 9 | 10 | @CommandHandler(CreateOrder) 11 | @Injectable() 12 | export class OrderCreator extends BaseCommandHandler { 13 | constructor( 14 | private readonly orderRepository: OrderRepository, 15 | private readonly productRepository: ProductRepository, 16 | ) { 17 | super(); 18 | } 19 | 20 | async handle(command: CreateOrder): Promise { 21 | const order = new Order({ id: uuid() }); 22 | await this.orderRepository.save(order); 23 | 24 | const product = await this.productRepository.findById(command.productId); 25 | 26 | order.addProduct(product); 27 | 28 | await this.orderRepository.save(order); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/forCreateOrder/checkProductExist.spec.ts: -------------------------------------------------------------------------------- 1 | import { CheckProductExist } from './checkProductExist'; 2 | import { ProductRepository } from '../../../repositories/productRepository'; 3 | import { CreateOrder } from '../../createOrder'; 4 | 5 | describe('Check if product exists', () => { 6 | const MockRepository = jest.fn( 7 | () => 8 | ({ 9 | where: jest.fn().mockReturnValue({ 10 | get: jest.fn().mockResolvedValue([]), 11 | }), 12 | } as any), 13 | ); 14 | 15 | describe('when dispatching a create order program', () => { 16 | it('it should validate that a the product exist in the db', async () => { 17 | const validator = new CheckProductExist(new MockRepository()); 18 | 19 | const createOrder = new CreateOrder('testProductId', 1, 'TestUserId'); 20 | const result = await validator.validate(createOrder); 21 | 22 | expect(result.hasError).toBeTruthy(); 23 | expect(result.errors).toMatchObject([ 24 | { 25 | field: 'productId', 26 | fieldLabel: 'productId', 27 | message: 'The product is not valid', 28 | value: 'testProductId', 29 | }, 30 | ]); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/forCreateOrder/checkProductExist.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICommandValidator, 3 | IValidationResult, 4 | } from '../../../../../common/commands/validation'; 5 | import { CreateOrder } from '../../createOrder'; 6 | import { ProductRepository } from '../../../repositories/productRepository'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class CheckProductExist implements ICommandValidator { 11 | constructor(private readonly productRepository: ProductRepository) {} 12 | 13 | async validate(command: CreateOrder): Promise { 14 | const queryResult = await this.productRepository 15 | .where({ id: command.productId }) 16 | .get(); 17 | 18 | if (queryResult.length > 0) { 19 | return { 20 | hasError: false, 21 | errors: [], 22 | }; 23 | } 24 | return { 25 | hasError: true, 26 | errors: [ 27 | { 28 | field: 'productId', 29 | fieldLabel: 'productId', 30 | message: 'The product is not valid', 31 | value: command.productId, 32 | }, 33 | ], 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/forCreateOrder/checkPropertieValues.spec.ts: -------------------------------------------------------------------------------- 1 | import { CheckPropertiesValue } from './checkPropertiesValue'; 2 | import { CreateOrder } from '../../createOrder'; 3 | 4 | describe('Create Order Validator', () => { 5 | describe('When sending a in complete save order command', () => { 6 | it('should return an error if the productId is null', async () => { 7 | const createOrderValidator = new CheckPropertiesValue(); 8 | 9 | // @ts-ignore 10 | const createOrder = new CreateOrder(null, 'df', null); 11 | const result = await createOrderValidator.validate(createOrder); 12 | 13 | expect(result.hasError).toBeTruthy(); 14 | expect(result.errors.length).toBeGreaterThan(0); 15 | }); 16 | 17 | it('should pass the validation if the command is correct', async () => { 18 | const createOrderValidator = new CheckPropertiesValue(); 19 | 20 | // @ts-ignore 21 | const createOrder = new CreateOrder( 22 | 'a7ddff7e-ae95-47e8-8bf2-a651541d1dcb', 23 | 1, 24 | 'UserId', 25 | ); 26 | const result = await createOrderValidator.validate(createOrder); 27 | 28 | expect(result.hasError).toBeFalsy(); 29 | expect(result.errors.length).toEqual(0); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/forCreateOrder/checkPropertiesValue.ts: -------------------------------------------------------------------------------- 1 | import { JoiCommandValidator } from '../../../../../common/commands/validation'; 2 | import { CreateOrder } from '../../createOrder'; 3 | import * as joi from '@hapi/joi'; 4 | 5 | export class CheckPropertiesValue extends JoiCommandValidator { 6 | getSchema(command: CreateOrder) { 7 | return joi.object({ 8 | productId: joi 9 | .string() 10 | .uuid({ version: 'uuidv4' }) 11 | .required(), 12 | productQuantity: joi 13 | .number() 14 | .required() 15 | .min(0), 16 | userId: joi.string().required(), 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/forCreateOrder/createOrderCompositeValidator.ts: -------------------------------------------------------------------------------- 1 | import { CommandValidator } from '../../../../../common/commands/validation'; 2 | import { CreateOrder } from '../../createOrder'; 3 | import { CompositeValidator } from '../../../../../common/commands/validation/CompositeValidator'; 4 | import { CheckPropertiesValue } from './checkPropertiesValue'; 5 | import { CheckProductExist } from './checkProductExist'; 6 | 7 | @CommandValidator(CreateOrder) 8 | export class CreateOrderCompositeValidator extends CompositeValidator< 9 | CreateOrder 10 | > { 11 | constructor( 12 | joiValidator: CheckPropertiesValue, 13 | productValidator: CheckProductExist, 14 | ) { 15 | super([joiValidator, productValidator]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/commands/validators/index.ts: -------------------------------------------------------------------------------- 1 | import { CreateOrderCompositeValidator } from './forCreateOrder/createOrderCompositeValidator'; 2 | import { CheckProductExist } from './forCreateOrder/checkProductExist'; 3 | import { CheckPropertiesValue } from './forCreateOrder/checkPropertiesValue'; 4 | 5 | export const CommandValidators = [ 6 | CheckProductExist, 7 | CheckPropertiesValue, 8 | CreateOrderCompositeValidator, 9 | ]; 10 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/entities/order.entity.spec.ts: -------------------------------------------------------------------------------- 1 | import { Order } from './order.entity'; 2 | import { Product } from './product.entity'; 3 | import { ProductAddedToOrder } from '../events/ProductAddedToOrder'; 4 | 5 | describe('Order', () => { 6 | describe('When adding a product', () => { 7 | const order = new Order(); 8 | order.id = 'TestId'; 9 | 10 | const productToAdd = new Product(); 11 | productToAdd.id = 'testId'; 12 | 13 | order.addProduct(productToAdd); 14 | 15 | it('should add the product to the products collection', () => { 16 | expect(order.getProducts()).toContain(productToAdd); 17 | }); 18 | 19 | it('should apply ProductAddedToOrderEvent', () => { 20 | const raisedEvents = order.events(); 21 | 22 | expect(raisedEvents).toMatchObject([ 23 | new ProductAddedToOrder(order.id, productToAdd.id), 24 | ]); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { Product } from './product.entity'; 2 | import { 3 | Column, 4 | Entity, 5 | Generated, 6 | JoinTable, 7 | ManyToMany, 8 | PrimaryColumn, 9 | PrimaryGeneratedColumn, 10 | } from 'typeorm'; 11 | import { AggregateRoot } from '../../../common/entities'; 12 | import { ProductAddedToOrder } from '../events/ProductAddedToOrder'; 13 | 14 | @Entity() 15 | export class Order extends AggregateRoot { 16 | constructor(params: { id: string; products?: Product[] } = {} as Order) { 17 | super(); 18 | 19 | const { id, products } = params; 20 | 21 | this.id = id; 22 | this.products = products; 23 | } 24 | 25 | @PrimaryColumn('uuid') 26 | id: string; 27 | 28 | @ManyToMany(type => Product) 29 | @JoinTable() 30 | products: Product[]; 31 | 32 | addProduct(product: Product) { 33 | if (!this.products) this.products = []; 34 | this.products.push(product); 35 | 36 | this.apply(new ProductAddedToOrder(this.id, product.id)); 37 | } 38 | 39 | getProducts(): Product[] { 40 | return this.products; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'; 2 | import { Order } from './order.entity'; 3 | import { AggregateRoot } from '../../../common/entities'; 4 | 5 | @Entity() 6 | export class Product extends AggregateRoot { 7 | @PrimaryGeneratedColumn('uuid') 8 | id: string; 9 | 10 | @Column() 11 | description: string; 12 | 13 | @ManyToMany(type => Order) 14 | order: Order[]; 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/events/ProductAddedToOrder.ts: -------------------------------------------------------------------------------- 1 | import { DomainEvent } from '../../../common/events/domainEvent'; 2 | 3 | export class ProductAddedToOrder implements DomainEvent { 4 | orderId: string; 5 | productId: string; 6 | constructor(orderId: string, productId: string) { 7 | this.orderId = orderId; 8 | this.productId = productId; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/events/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { NotifyProductAddToOrderConsole } from './notify-product-add-to-order-console'; 2 | 3 | export const EventHandlers = [NotifyProductAddToOrderConsole]; 4 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/events/handlers/notify-product-add-to-order-console.ts: -------------------------------------------------------------------------------- 1 | import { IEventHandler } from '../../../../common/events'; 2 | import { ProductAddedToOrder } from '../ProductAddedToOrder'; 3 | import { EventsHandler } from '@nestjs/cqrs'; 4 | 5 | @EventsHandler(ProductAddedToOrder) 6 | export class NotifyProductAddToOrderConsole 7 | implements IEventHandler { 8 | handle(event: ProductAddedToOrder): any { 9 | console.log('Product added'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/repositories/orderRepository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '../../../common/entities'; 2 | import { Order } from '../entities/order.entity'; 3 | import { EntityManager } from 'typeorm'; 4 | import { InjectEntityManager } from '@nestjs/typeorm'; 5 | import { SyncEventDispatcher } from '../../../common/events'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class OrderRepository extends BaseRepository { 10 | constructor( 11 | @InjectEntityManager() 12 | manager: EntityManager, 13 | eventDispatcher: SyncEventDispatcher, 14 | ) { 15 | super(manager.getRepository(Order), eventDispatcher); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/repositories/productRepository.ts: -------------------------------------------------------------------------------- 1 | import { BaseRepository } from '../../../common/entities'; 2 | import { Product } from '../entities/product.entity'; 3 | import { SyncEventDispatcher } from '../../../common/events'; 4 | import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm'; 5 | 6 | import { EntityManager, Repository } from 'typeorm'; 7 | import { Injectable } from '@nestjs/common'; 8 | 9 | @Injectable() 10 | export class ProductRepository extends BaseRepository { 11 | constructor( 12 | @InjectEntityManager() 13 | manager: EntityManager, 14 | eventDispatcher: SyncEventDispatcher, 15 | ) { 16 | super(manager.getRepository(Product), eventDispatcher); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/requests/PaginatedOrderQuery.ts: -------------------------------------------------------------------------------- 1 | import { PaginatedQuery } from '../../../common/controllers'; 2 | 3 | export interface PaginatedOrderQuery extends PaginatedQuery { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/requests/orderRequest.ts: -------------------------------------------------------------------------------- 1 | export interface OrderRequest { 2 | productId: string; 3 | productQuantity: number; 4 | } 5 | -------------------------------------------------------------------------------- /backend/src/modules/domain/orders/requests/paginatedProductQuery.ts: -------------------------------------------------------------------------------- 1 | import { PaginatedQuery } from '../../../common/controllers'; 2 | 3 | export interface PaginatedProductQuery extends PaginatedQuery { 4 | id: number; 5 | description: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/modules/errors/error.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | HttpException, 5 | ArgumentsHost, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | 9 | @Catch() 10 | export class ErrorFilter implements ExceptionFilter { 11 | catch(error: Error, host: ArgumentsHost) { 12 | const response = host.switchToHttp().getResponse(); 13 | const status = 14 | error instanceof HttpException 15 | ? error.getStatus() 16 | : HttpStatus.INTERNAL_SERVER_ERROR; 17 | 18 | return response.status(status).send( 19 | JSON.stringify({ 20 | statusCode: status, 21 | message: error.message, 22 | stack: error.stack, 23 | }), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/modules/status/status.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { StatusController } from './status.controller'; 3 | import { ConfigService } from '../config/config.service'; 4 | 5 | describe('Status Controller', () => { 6 | let module: TestingModule; 7 | let controller: StatusController; 8 | beforeAll(async () => { 9 | const configServiceMock = jest.fn(() => ({ 10 | about: { version: 'test', environment: 'testing' }, 11 | }))(); 12 | module = await Test.createTestingModule({ 13 | controllers: [StatusController], 14 | providers: [ 15 | { 16 | provide: ConfigService, 17 | useValue: configServiceMock, 18 | }, 19 | ], 20 | }).compile(); 21 | controller = module.get(StatusController); 22 | }); 23 | it('should return ok status', async () => { 24 | const status = await controller.status(); 25 | expect(status).toEqual({ 26 | status: 'ok', 27 | version: 'test', 28 | environment: 'testing', 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /backend/src/modules/status/status.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | import { ConfigService } from '../config/config.service'; 3 | 4 | @Controller('status') 5 | export class StatusController { 6 | constructor(private readonly configService: ConfigService) {} 7 | 8 | @Get() 9 | status() { 10 | const version = this.configService.about.version; 11 | const environment = this.configService.about.environment; 12 | 13 | return { 14 | status: 'ok', 15 | version, 16 | environment, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/modules/status/status.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | @Module({}) 4 | export class StatusModule {} 5 | -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { AppModule } from './../src/app.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./src" 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "**/*.spec.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /backend/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /backend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "eofline": false, 9 | "quotemark": [true, "single"], 10 | "prefer-conditional-expression": false, 11 | "indent": [true, "spaces", 2], 12 | "member-access": [false], 13 | "ordered-imports": [false], 14 | "max-line-length": [true, 200], 15 | "member-ordering": [false], 16 | "curly": false, 17 | "interface-name": [false], 18 | "array-type": [false], 19 | "no-empty-interface": false, 20 | "no-empty": false, 21 | "arrow-parens": false, 22 | "object-literal-sort-keys": false, 23 | "no-unused-expression": false, 24 | "max-classes-per-file": [false], 25 | "variable-name": [false], 26 | "one-line": [false], 27 | "one-variable-per-declaration": [false], 28 | "no-console": [false], 29 | "ban-types": [false] 30 | }, 31 | "rulesDirectory": [] 32 | } 33 | -------------------------------------------------------------------------------- /backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /frontend/src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/__mocks__/fileMock.js -------------------------------------------------------------------------------- /frontend/src/app/components/ActionsMenu/style.css: -------------------------------------------------------------------------------- 1 | .axui-contextmenu { 2 | position: absolute; 3 | background-color: #fff; 4 | box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.14); 5 | padding: 10px; 6 | border-radius: 4px; 7 | z-index: 10; 8 | min-width: 130px; 9 | } 10 | 11 | .axui-contextmenu [data-ctx-item] { 12 | position: relative; 13 | padding: 0.7rem 0.5rem; 14 | line-height: 14px; 15 | white-space: nowrap; 16 | font-size: inherit; 17 | color: inherit; 18 | font-weight: inherit; 19 | display: flex; 20 | flex-direction: row; 21 | justify-content: stretch; 22 | align-items: center; 23 | cursor: pointer; 24 | } 25 | 26 | .axui-contextmenu [data-ctx-item] [data-label] { 27 | flex: 1; 28 | } 29 | 30 | .axui-contextmenu [data-ctx-item] [data-label-icon] { 31 | margin-right: 4px; 32 | } 33 | 34 | .axui-contextmenu [data-ctx-item] [data-checkbox] { 35 | width: 14px; 36 | } 37 | 38 | .axui-contextmenu [data-ctx-item] [data-accelerator] { 39 | font-size: 0.8em; 40 | } 41 | 42 | .axui-contextmenu [data-ctx-item]:hover { 43 | color: #13bc74; 44 | } 45 | 46 | .axui-contextmenu [data-ctx-item][data-opened] { 47 | color: #13bc74; 48 | } 49 | 50 | .axui-contextmenu [data-ctx-item][data-enabled='false'] { 51 | opacity: 0.2; 52 | } 53 | 54 | .axui-contextmenu [data-ctx-item][data-enabled='false']:hover { 55 | background-color: #f6f6f6; 56 | color: #434343; 57 | } 58 | 59 | .axui-contextmenu [data-ctx-separator] { 60 | height: 2px; 61 | background-color: #e5e5e5; 62 | padding: 0; 63 | margin: 5px 0; 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '../../style.local.css'; 3 | import style from '../../style.local.css'; 4 | 5 | export namespace BreadCrumb { 6 | export interface Props { 7 | children?: any; 8 | rootPathName?: string; 9 | isSecondaryPage: boolean; 10 | } 11 | } 12 | 13 | export const Breadcrumb = (props: BreadCrumb.Props) => { 14 | return ( 15 |
16 | 21 |

22 | {props.isSecondaryPage ? ( 23 | 24 | 25 | 26 | ) : ( 27 | '' 28 | )} 29 | 30 | {props.rootPathName ? props.rootPathName : ''} 31 |

32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/src/app/components/Breadcrumb/style.local.css: -------------------------------------------------------------------------------- 1 | .bread-crumb-container { 2 | display: inherit; 3 | align-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /frontend/src/app/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Component } from 'react'; 3 | import style from './style.local.css'; 4 | import classNames from 'classnames'; 5 | 6 | export namespace Button { 7 | export interface Props { 8 | className?: string; 9 | style?: React.CSSProperties; 10 | onClick?: any; 11 | onMouseLeave?: any; 12 | title?: string; 13 | disabled?: boolean; 14 | type?: any; 15 | } 16 | } 17 | 18 | export class Button extends Component { 19 | getClassNames() { 20 | const propClassName = this.props.className || ''; 21 | return classNames({ 22 | [style.button]: true, 23 | [propClassName]: propClassName, 24 | }); 25 | } 26 | 27 | render() { 28 | return ( 29 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/app/components/Button/style.local.css: -------------------------------------------------------------------------------- 1 | /* .button { 2 | color: #264b96; 3 | border-radius: 4px; 4 | background-color: #ffffff; 5 | border: solid 1px #92a3c5; 6 | margin: 0 5px; 7 | margin-left: 8px; 8 | text-align: center; 9 | transition: all 0.4s ease-in-out; 10 | font-weight: 400; 11 | } 12 | 13 | .button:hover:enabled { 14 | background-color: #e1e6f0; 15 | cursor: pointer; 16 | } 17 | 18 | .button:disabled { 19 | border: solid 1px #e1e6f0; 20 | color: #92a3c5; 21 | } */ 22 | -------------------------------------------------------------------------------- /frontend/src/app/components/Empty/Empty.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Empty } from '.'; 4 | 5 | describe('', () => { 6 | describe('When rendering the component', () => { 7 | it('Should render correctly', () => { 8 | const wrapper = shallow(); 9 | expect(wrapper).toBeDefined(); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /frontend/src/app/components/Empty/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import style from './style.local.css'; 3 | 4 | export namespace Emtpy { 5 | export interface Props { 6 | title: string; 7 | } 8 | } 9 | 10 | export const Empty = ({ title }: Emtpy.Props) => { 11 | return ( 12 |
13 | 14 |

{title}

15 | {/* If you need more information please contact us: 16 | 17 | {process.env.WP_HELP_EMAIL} 18 | */} 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/app/components/Empty/style.local.css: -------------------------------------------------------------------------------- 1 | .no-orders { 2 | display: flex; 3 | padding: 50px; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | } 8 | 9 | .no-orders span { 10 | display: block; 11 | width: 100%; 12 | font-size: 17px; 13 | letter-spacing: -0.1px; 14 | text-align: center; 15 | color: #566076; 16 | } 17 | 18 | .no-orders .title { 19 | font-size: 24px; 20 | font-weight: bold; 21 | letter-spacing: -0.2px; 22 | text-align: center; 23 | color: #566076; 24 | } 25 | 26 | .no-orders a.email { 27 | font-weight: bold; 28 | text-decoration: none; 29 | color: #566076; 30 | } 31 | 32 | .no-orders span.icon img { 33 | width: 312px; 34 | height: 312px; 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app/components/LoadingMessage/LoadingMessage.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { LoadingMessage } from 'app/components/LoadingMessage'; 4 | 5 | describe('', () => { 6 | describe('Props', () => { 7 | describe('message', () => { 8 | it('Should render the props message', () => { 9 | const message = 'Hello!'; 10 | const wrapper = shallow(); 11 | expect(wrapper.contains({message}?)).toBeTruthy(); //remove the question mark to make the test pass 12 | }); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/src/app/components/LoadingMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Component } from 'react'; 3 | import style from './style.local.css'; 4 | 5 | export namespace LoadingMessage { 6 | export interface Props { 7 | message: string; 8 | } 9 | } 10 | 11 | export class LoadingMessage extends Component { 12 | render() { 13 | return ( 14 |
15 | {this.props.message} 16 |
17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/components/LoadingMessage/style.local.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | width: 100%; 3 | height: 100%; 4 | text-align: center; 5 | position: absolute; 6 | } 7 | 8 | .loading span { 9 | position: fixed; 10 | z-index: 999; 11 | height: 2em; 12 | width: 2em; 13 | overflow: show; 14 | margin: auto; 15 | top: 0; 16 | left: 0; 17 | bottom: 0; 18 | right: 0; 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/app/components/Navbar/style.local.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | height: 60px; 3 | background-color: #fff; 4 | padding: 0 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | .logo { 11 | width: 180px; 12 | height: 60%; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/components/NavbarButton/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from '../Button'; 3 | import style from './style.local.css'; 4 | 5 | export namespace NavbarButton { 6 | export interface Props { 7 | title?: string; 8 | children: any; 9 | style?: React.CSSProperties; 10 | className?: string; 11 | onClick?(): void; 12 | } 13 | } 14 | 15 | export const NavbarButton = (props: NavbarButton.Props) => { 16 | return ( 17 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/app/components/NavbarButton/style.local.css: -------------------------------------------------------------------------------- 1 | /* .navbar-button { 2 | background-color: rgba(0, 0, 0, 0); 3 | color: #fff; 4 | border: none; 5 | font-size: 16px; 6 | margin-left: 28px; 7 | } 8 | 9 | .disabled { 10 | color: #92a3c5; 11 | } 12 | 13 | .navbar-button:hover:enabled { 14 | background-color: inherit; 15 | } 16 | 17 | .navbar-button span { 18 | margin-left: 10px; 19 | } */ 20 | -------------------------------------------------------------------------------- /frontend/src/app/components/OrdersTable/NoFilteredOrders/NoFilteredOrders.spec.tsx: -------------------------------------------------------------------------------- 1 | import { ShallowWrapper, shallow } from 'enzyme'; 2 | import { NoFilteredOrders } from '.'; 3 | import * as React from 'react'; 4 | import { Button } from 'app/components/Button'; 5 | 6 | const mockHandleResetOrders = jest.fn(); 7 | describe('', () => { 8 | let wrapper: ShallowWrapper; 9 | beforeEach(() => { 10 | wrapper = shallow( 11 | , 15 | ); 16 | }); 17 | 18 | describe('when reset button is clicked', () => { 19 | it('should called handleResetOrders with empty string', () => { 20 | wrapper.find(Button).simulate('click'); 21 | expect(mockHandleResetOrders).toBeCalledWith(''); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /frontend/src/app/components/OrdersTable/NoFilteredOrders/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Component } from 'react'; 3 | 4 | import style from './style.local.css'; 5 | import { Button } from '../../Button'; 6 | 7 | export namespace NoFilteredOrders { 8 | export interface Props { 9 | searchText: string; 10 | handleResetOrders: (searchText: string) => void; 11 | } 12 | } 13 | 14 | export class NoFilteredOrders extends Component { 15 | render() { 16 | return ( 17 |
18 | 19 |

{`We couldn't find any orders related to "${ 20 | this.props.searchText 21 | }"`}

22 | 23 | Please try with a different order number, 24 |
25 | recipient name or company. 26 |
27 | 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/src/app/components/OrdersTable/NoFilteredOrders/style.local.css: -------------------------------------------------------------------------------- 1 | /* .no-orders { 2 | display: flex; 3 | padding: 50px; 4 | align-items: center; 5 | justify-content: center; 6 | flex-direction: column; 7 | } 8 | 9 | .no-orders span { 10 | display: block; 11 | width: 100%; 12 | font-size: 17px; 13 | letter-spacing: -0.1px; 14 | text-align: center; 15 | color: #566076; 16 | } 17 | 18 | .no-orders .title { 19 | font-size: 24px; 20 | font-weight: bold; 21 | letter-spacing: -0.2px; 22 | text-align: center; 23 | color: #566076; 24 | } 25 | 26 | .no-orders a.email { 27 | font-weight: bold; 28 | text-decoration: none; 29 | color: #566076; 30 | } 31 | 32 | .no-orders span.icon img { 33 | width: 312px; 34 | height: 312px; 35 | } 36 | 37 | .Rectangle { 38 | width: 241px; 39 | height: 42px; 40 | border-radius: 4px; 41 | border: solid 1px #92a3c5; 42 | background-color: #ffffff; 43 | font-size: 16px; 44 | margin-top: 25px; 45 | } */ 46 | -------------------------------------------------------------------------------- /frontend/src/app/components/OrdersTable/OrdersSearch/style.local.css: -------------------------------------------------------------------------------- 1 | /* .searchContainer { 2 | width: 100%; 3 | align-content: center; 4 | padding: 20px 0 20px 0; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .searchContainer input { 11 | width: 430px; 12 | height: 25px; 13 | margin: 0 8px; 14 | padding: 0 5px; 15 | font-family: inherit; 16 | font-size: inherit; 17 | } 18 | 19 | .clear-button { 20 | color: #566076; 21 | float: right; 22 | position: relative; 23 | right: 30px; 24 | cursor: pointer; 25 | } */ 26 | -------------------------------------------------------------------------------- /frontend/src/app/components/Pagination/style.local.css: -------------------------------------------------------------------------------- 1 | /* .Pagination__container { 2 | margin-top: 25px; 3 | padding: 0 8px; 4 | } 5 | 6 | .Pagination__buttons-container { 7 | float: right; 8 | margin-bottom: 15px; 9 | } 10 | 11 | .Pagination__button { 12 | width: 134px; 13 | height: 42px; 14 | font-size: 100%; 15 | } 16 | 17 | .Pagination__input { 18 | width: 45px; 19 | height: 38px; 20 | border-radius: 4px; 21 | border: solid 1px #ced4da; 22 | background-color: #ffffff; 23 | margin: 0 6px; 24 | text-align: center; 25 | font-size: 100%; 26 | } 27 | 28 | .Pagination__button svg { 29 | margin: 0 8px; 30 | } */ 31 | -------------------------------------------------------------------------------- /frontend/src/app/components/SearchBar/style.local.css: -------------------------------------------------------------------------------- 1 | .search-bar { 2 | height: 60px; 3 | background-color: #e1e6f0; 4 | display: flex; 5 | display: -ms-flexbox; 6 | justify-content: space-between; 7 | align-items: center; 8 | padding: 0 25px; 9 | border-bottom: solid 1px #92a3c5; 10 | margin-bottom: 25px; 11 | } 12 | 13 | .bread-crumb-container { 14 | display: inherit; 15 | align-items: center; 16 | } 17 | 18 | .search-bar-container { 19 | display: inherit; 20 | align-items: center; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/app/components/SearchInput/style.local.css: -------------------------------------------------------------------------------- 1 | .searchContainer { 2 | width: 100%; 3 | align-content: center; 4 | padding: 20px 0 20px 0; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | } 9 | 10 | .searchContainer input { 11 | width: 430px; 12 | height: 25px; 13 | margin: 0 8px; 14 | padding: 0 5px; 15 | font-family: inherit; 16 | font-size: inherit; 17 | } 18 | 19 | .clear-button { 20 | color: #566076; 21 | float: right; 22 | position: relative; 23 | right: 30px; 24 | cursor: pointer; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/app/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Table } from './Table'; 2 | export { Empty } from './Empty'; 3 | export { Pagination } from './Pagination'; 4 | export { LoadingMessage } from './LoadingMessage'; 5 | export { Button } from 'app/components/Button'; 6 | export { NavbarButton } from 'app/components/NavbarButton'; 7 | export { SearchBar } from './SearchBar'; 8 | export { SearchInput } from './SearchInput'; 9 | export { Breadcrumb } from './Breadcrumb'; 10 | export { ActionsMenu } from './ActionsMenu'; 11 | -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './employees'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/components/EditEmployee/inputValidations.ts: -------------------------------------------------------------------------------- 1 | export const isInputEmpty = (content: string) => content.length === 0; 2 | 3 | export const doesInputMatchesEmailPattern = (content: string) => /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(content); 4 | 5 | export const isInputGreaterThanOrEqualMinValue = (value: number, minValue: number) => value >= minValue; -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/components/EditEmployee/withAutoSave.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export type Debounce = (inputName: string, callback: () => void) => void; 4 | export const withAutoSave = (Component: React.ComponentClass) => { 5 | return class extends React.Component { 6 | private coolDown = 1000; 7 | private timerDictionary = new Map(); 8 | 9 | debounce: Debounce = (inputName: string, callback: () => void) => { 10 | clearTimeout(this.timerDictionary.get(inputName)); 11 | this.timerDictionary.set(inputName, window.setTimeout(callback, this.coolDown)); 12 | } 13 | 14 | render () { 15 | return 16 | } 17 | } 18 | }; -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/app/containers/Employee/index.tsx -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EmployeeModel'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/reducer/sampleData.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeModel, Gender, SalaryType } from '../models'; 2 | 3 | function generateFakeDate(): Date { 4 | return new Date(Math.random() * 10000); 5 | } 6 | 7 | const employee: EmployeeModel = { 8 | firstName: 'Test', 9 | middleName: 'First', 10 | lastName: 'Super', 11 | secondLastName: 'Second', 12 | displayName: 'Test First 2ND', 13 | companyEmail: 'test@gmail.com', 14 | personalEmail: 'testpersonal@gmail.com', 15 | birthdate: generateFakeDate(), 16 | startDate: generateFakeDate(), 17 | phoneNumber: '99002563', 18 | address: 'Fake Address', 19 | bankName: 'Test First Super Second', 20 | accountNumber: '001-04541-6446', 21 | gender: Gender.MALE, 22 | tags: '{\'hero\', \'acklen\'}', 23 | country: 'Honduras', 24 | region: 'Yoro', 25 | city: 'Morazán', 26 | salary: 15, 27 | salaryType: SalaryType.HOURLY, 28 | effectiveDate: generateFakeDate(), 29 | isActive: true, 30 | }; 31 | 32 | export const employeesSampleData: EmployeeModel[] = [ 33 | employee, 34 | employee, 35 | employee, 36 | employee, 37 | ]; 38 | -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/reducer/state.ts: -------------------------------------------------------------------------------- 1 | import { EmployeeModel } from '../models/EmployeeModel'; 2 | 3 | export interface EmployeeState { 4 | isFetching: boolean; 5 | errorMessage: string; 6 | employees: EmployeeModel[]; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/containers/Employee/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './employees'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Redirect, RouteComponentProps } from 'react-router'; 3 | import App from './containers/App'; 4 | import { hot } from 'react-hot-loader'; 5 | import './style.local.css'; 6 | 7 | export interface Props {} 8 | 9 | export interface State { 10 | showLoader: boolean; 11 | } 12 | export class Root extends React.Component { 13 | state = { 14 | showLoader: true, 15 | }; 16 | 17 | componentDidMount() { 18 | setTimeout(() => this.setState({ showLoader: false }), 1000); 19 | } 20 | render() { 21 | return ( 22 |
23 | 24 | } /> 25 | ) => { 27 | return ; 28 | }} 29 | /> 30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default hot(module)(Root); 37 | -------------------------------------------------------------------------------- /frontend/src/app/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/middleware/logger.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from 'redux'; 2 | 3 | export const logger: Middleware = store => next => action => { 4 | if (process.env.NODE_ENV !== 'production') { 5 | console.info(action); 6 | } 7 | return next(action); 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/app/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { RootState } from './state'; 3 | import { routerReducer, RouterState } from 'react-router-redux'; 4 | import { employeeReducer } from '../containers/Employee/reducer/employees'; 5 | 6 | export { RootState, RouterState }; 7 | 8 | // NOTE: current type definition of Reducer in 'react-router-redux' and 'redux-actions' module 9 | // doesn't go well with redux@4 10 | export const rootReducer = combineReducers({ 11 | router: routerReducer as any, 12 | employees: employeeReducer as any, 13 | }); 14 | -------------------------------------------------------------------------------- /frontend/src/app/reducers/state.ts: -------------------------------------------------------------------------------- 1 | import { RouterState } from 'react-router-redux'; 2 | import { EmployeeState } from 'app/containers/Employee/reducer/state'; 3 | export interface RootState { 4 | router: RouterState; 5 | employees: EmployeeState; 6 | } 7 | 8 | export interface SortedColumn { 9 | id: string; 10 | desc: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './httpService'; 2 | -------------------------------------------------------------------------------- /frontend/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { Store, createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import { History } from 'history'; 4 | import { connectRouter, routerMiddleware } from 'connected-react-router'; 5 | import { logger } from 'app/middleware'; 6 | import { RootState, rootReducer } from 'app/reducers'; 7 | import thunk from 'redux-thunk'; 8 | 9 | export function configureStore( 10 | history: History, 11 | initialState?: RootState, 12 | ): Store { 13 | let middleware = applyMiddleware(logger, thunk, routerMiddleware(history)); 14 | 15 | if (process.env.NODE_ENV !== 'production') { 16 | middleware = composeWithDevTools(middleware); 17 | } 18 | 19 | const store = createStore( 20 | connectRouter(history)(rootReducer) as any, 21 | initialState as any, 22 | middleware, 23 | ) as Store; 24 | 25 | if (module.hot) { 26 | module.hot.accept('app/reducers', () => { 27 | const nextReducer = require('app/reducers'); 28 | store.replaceReducer(nextReducer); 29 | }); 30 | } 31 | 32 | return store; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/utils/FeatureToggler.ts: -------------------------------------------------------------------------------- 1 | import { toggles } from '../../toggles'; 2 | 3 | export function shouldRender(name: string): boolean { 4 | const match = toggles.find(x => { 5 | return x.componentName === name; 6 | }); 7 | if (!match) return true; 8 | const env = process.env.NODE_ENV; 9 | const hasMatch = match.environments.find((x: string) => { 10 | return x === env; 11 | }); 12 | return hasMatch !== undefined; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/utils/errorLogger.ts: -------------------------------------------------------------------------------- 1 | declare namespace _LTracker { 2 | function push(log: any): void; 3 | } 4 | 5 | export function logError(error: Error, info: any) { 6 | try { 7 | _LTracker.push({ error, info }); 8 | } catch (_err) { 9 | return; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/utils/index.ts: -------------------------------------------------------------------------------- 1 | import style from '../style.local.css'; 2 | import Swal from 'sweetalert2'; 3 | import { toast, ToastOptions } from 'react-toastify'; 4 | 5 | export function omit( 6 | target: T, 7 | ...omitKeys: K[] 8 | ): Omit { 9 | return (Object.keys(target) as K[]).reduce( 10 | (res, key) => { 11 | if (!omitKeys.includes(key)) { 12 | res[key] = target[key]; 13 | } 14 | return res; 15 | }, 16 | {} as any, 17 | ); 18 | } 19 | 20 | export const confirmDialog = Swal.mixin({ 21 | confirmButtonClass: `${style.button} ${style.primary}`, 22 | cancelButtonClass: `${style.button} ${style.primary} ${style.hollow}`, 23 | buttonsStyling: false, 24 | }); 25 | 26 | export const showNotification = ( 27 | message: string, options: ToastOptions 28 | ) => toast(message, options); 29 | 30 | export const showErrorNotification = ( 31 | message: string 32 | ) => showNotification(message, { className: style.toaster }); 33 | 34 | export const showSuccessNotification = ( 35 | message: string 36 | ) => showNotification(message, { type: "success" }); -------------------------------------------------------------------------------- /frontend/src/assets/fonts/kiwi.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/fonts/kiwi.eot -------------------------------------------------------------------------------- /frontend/src/assets/fonts/kiwi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/fonts/kiwi.ttf -------------------------------------------------------------------------------- /frontend/src/assets/fonts/kiwi.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/fonts/kiwi.woff -------------------------------------------------------------------------------- /frontend/src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/images/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/images/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/assets/images/logo-glee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/images/logo-glee.png -------------------------------------------------------------------------------- /frontend/src/assets/img/exampleuser.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/img/exampleuser.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/img/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/img/gravatar-empty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/img/gravatar-empty.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/assets/img/logo-glee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/frontend/src/assets/img/logo-glee.png -------------------------------------------------------------------------------- /frontend/src/assets/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.TITLE %> 9 | 10 | 11 | 12 | 13 |
14 | 15 | 18 | <% if (htmlWebpackPlugin.options.LOGGLY_TOKEN) { %> 19 | 27 | <% } %> 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/assets/translations.ts: -------------------------------------------------------------------------------- 1 | export enum Translation { 2 | ADD_EMPLOYEE = 'Add employee', 3 | CANCEL = 'Cancel', 4 | YES = 'Yes', 5 | NO = 'No', 6 | DISCARD_CHANGES = 'Discard changes', 7 | ADDING_EMPLOYEE_SUCCESS = 'New employee has been added successfully!', 8 | ADDING_EMPLOYEE_FAIL = 'An error ocurred while trying to add a new employee.', 9 | CANCEL_CHANGES_QUESTION = 'Are you sure you want to cancel?, all changes will be discarded.', 10 | CHANGES_DISCARD = 'All changes were discarded.', 11 | CHANGES_SAVED = 'All changes have been saved successfully!', 12 | EDIT_EMPLOYEE_FIELD_SUCCESS = 'Employee field has been saved.', 13 | EDIT_EMPLOYEE_FIELD_ERROR = 'There was an error updating the field.', 14 | GENERIC_ERROR = 'Oops!, something went wrong.', 15 | BAD_REQUEST_ERROR = 'Oops!, something went wrong, please check your data.', 16 | SERVER_ERROR = 'Oops!, something went wrong, we could not establish a connection to the server.' 17 | }; 18 | 19 | export default Translation; -------------------------------------------------------------------------------- /frontend/src/browserHistory.ts: -------------------------------------------------------------------------------- 1 | import { createHashHistory } from 'history'; 2 | 3 | const history = createHashHistory({ 4 | hashType: 'slash', 5 | }); 6 | 7 | export default history; 8 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import { configureStore } from 'app/store'; 6 | import Root from './app'; 7 | import history from './browserHistory'; 8 | 9 | // prepare store 10 | const store = configureStore(history); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById('root'), 19 | ); 20 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import * as enzyme from 'enzyme'; 2 | import * as Adapter from 'enzyme-adapter-react-16'; 3 | 4 | enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /frontend/src/toggles.ts: -------------------------------------------------------------------------------- 1 | interface Toggle { 2 | componentName: string; 3 | environments: string[]; 4 | } 5 | 6 | export const toggles: Toggle[] = []; 7 | -------------------------------------------------------------------------------- /frontend/src/types/react-country-region-selector.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-country-region-selector' { 2 | import { InputHTMLAttributes } from 'react'; 3 | 4 | interface Region { 5 | name: string; 6 | shortCode: string; 7 | } 8 | 9 | interface Country { 10 | countryName: string; 11 | countryShortCode: string; 12 | regions: Region[]; 13 | } 14 | 15 | interface ReactCountryDropdownProps extends InputHTMLAttributes { 16 | value: string; 17 | classes?: string; 18 | showDefaultOption?: boolean; 19 | defaultOptionLabel?: string | number; 20 | labelType?: number; 21 | valueType?: 'short' | 'full'; 22 | whitelist?: string[]; 23 | blacklist?: string[]; 24 | } 25 | 26 | interface ReactRegionDropdownProps extends InputHTMLAttributes { 27 | country: string; 28 | value: string | number; 29 | classes?: string; 30 | showDefaultOption?: boolean; 31 | defaultOptionLabel?: string | number; 32 | labelType?: number; 33 | countryValueType?: 'short' | 'full'; 34 | whitelist?: string[]; 35 | blacklist?: string[]; 36 | disableWhenEmpty?: boolean; 37 | } 38 | 39 | class CountryDropdown extends React.Component {} 40 | class RegionDropdown extends React.Component {} 41 | 42 | const CountryRegionData: Country[]; 43 | 44 | export { CountryDropdown, RegionDropdown, CountryRegionData }; 45 | } 46 | -------------------------------------------------------------------------------- /frontend/stryker.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | timeoutMS: 1200000, 4 | files: [ 5 | "**/*", 6 | "!node_modules/**/*" 7 | ], 8 | mutate: [ 9 | "src/**/*.spec.ts" 10 | ], 11 | 12 | mutator: "javascript", 13 | 14 | webpack: { 15 | configFile: "webpack.config.js", 16 | silent: true 17 | }, 18 | 19 | tsconfigFile: "tsconfig.json", 20 | 21 | reporters: [ 22 | "progress", 23 | "clear-text", 24 | "html", 25 | "dashboard" 26 | ], 27 | 28 | coverageAnalysis: "off" 29 | }); 30 | } -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "target": "es5", 5 | "jsx": "react", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "declaration": false, 10 | "removeComments": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "strict": true, 14 | "outDir": "build", 15 | "lib": ["es6", "es7", "dom"], 16 | "baseUrl": "src", 17 | "paths": { 18 | "*": ["types/*"], 19 | "app/*": ["./app/*"], 20 | "assets/*": ["./assets/*"] 21 | }, 22 | "allowSyntheticDefaultImports": true 23 | }, 24 | "exclude": ["dist", "build", "node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "eofline": false, 9 | "quotemark": [true, "single"], 10 | "prefer-conditional-expression": false, 11 | "indent": [true, "spaces", 2], 12 | "member-access": [false], 13 | "ordered-imports": [false], 14 | "max-line-length": [true, 150], 15 | "member-ordering": [false], 16 | "curly": false, 17 | "interface-name": [false], 18 | "array-type": [false], 19 | "no-empty-interface": false, 20 | "no-empty": false, 21 | "no-namespace": false, 22 | "arrow-parens": false, 23 | "object-literal-sort-keys": false, 24 | "no-unused-expression": false, 25 | "max-classes-per-file": [false], 26 | "variable-name": [false], 27 | "one-line": [false], 28 | "one-variable-per-declaration": [false], 29 | "no-console": [true, "log"] 30 | }, 31 | "rulesDirectory": [] 32 | } 33 | -------------------------------------------------------------------------------- /frontend/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /** Global definitions for development **/ 2 | 3 | // for style loader 4 | declare module "*.css" { 5 | const styles: any; 6 | export = styles; 7 | } 8 | 9 | // Omit type https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-377567046 10 | type Omit = Pick>; 11 | type PartialPick = Partial & Pick; 12 | 13 | declare module "*.json" { 14 | const value: any; 15 | export default value; 16 | } 17 | 18 | declare module "*.svg" { 19 | const image: string; 20 | export = image; 21 | } 22 | 23 | declare module "*.png" { 24 | const image: string; 25 | export = image; 26 | } 27 | -------------------------------------------------------------------------------- /frontend/types/react-redux.d.ts: -------------------------------------------------------------------------------- 1 | import 'react-redux'; 2 | import {Component} from 'react'; 3 | 4 | declare module 'react-redux' { 5 | // Add removed inferrable type to support connect as decorator 6 | // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/16652 7 | export interface InferableComponentDecorator { 8 | >(component: T): T; 9 | } 10 | 11 | // overload connect interface to return built-in ClassDecorator 12 | // https://github.com/reactjs/react-redux/pull/541#issuecomment-269197189 13 | export interface Connect { 14 | ( 15 | mapStateToProps: MapStateToPropsParam, 16 | mapDispatchToProps?: MapDispatchToPropsParam, 17 | mergeProps?: MergeProps, 18 | options?: Options 19 | ): InferableComponentDecorator; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/wallaby.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var compilerOptions = require("./tsconfig.json"); 4 | 5 | module.exports = function(wallaby) { 6 | return { 7 | files: [ 8 | "src/**/*.tsx", 9 | "src/**/*.ts", 10 | "package.json", 11 | "!src/**/*.spec.tsx", 12 | "!src/**/*.spec.ts" 13 | ], 14 | tests: ["src/**/*.spec.tsx", "src/**/*.spec.ts"], 15 | env: { 16 | type: "node", 17 | runner: "node" 18 | }, 19 | compilers: { 20 | "**/*.ts?(x)": wallaby.compilers.typeScript(compilerOptions) 21 | }, 22 | testFramework: "jest", 23 | debug: true, 24 | bootstrap: function(wallaby) { 25 | wallaby.testFramework.configure(require("./package.json").jest); 26 | } 27 | }; 28 | }; -------------------------------------------------------------------------------- /instructions/0-selling-cicd.md: -------------------------------------------------------------------------------- 1 | ## Section 1 - Explain the Fundamentals and Benefits of CI/CD to Achieve, Build, and Deploy Automation for Cloud-Based Software Products 2 | 3 | You are leading a team to develop the UdaPeople product, a revolutionary concept in Human Resources which promises to help small businesses care better for their most valuable resource: their people. Before implementing CI/CD for the UdaPeople product, you need to get authorization from the people who write the checks. Create a proposal in document or presentation form that “sells” the concept of CI/CD to non-technical decision-makers in the UdaPeople organization. For this, you will need to step out of your technical world and step into the world of revenue and costs. You will need to translate the benefits of CI/CD from technical language to the values of the business. To appeal to what makes business people tick, you’ll need to focus your attention on benefits that create revenue, protect revenue, control costs or reduce costs. 4 | 5 | The deliverable should be “near-production-quality”, but you should try to time-box your work to about 30 minutes. In other words, it should be good enough to submit to a real boss in a real job. No messy, last-minute submissions. You may use public domain or open source templates and graphics if you’d like. But please make sure the content is your own. Your presentation should be no longer than 5 slides. Your boss likes presentations that are short and sweet! 6 | 7 | Your presentation should be in PDF format named "presentation.pdf" and should be included in your code repository root folder. 8 | -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT01.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT02.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT03.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT04.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT05.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT06.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT07.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT08.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT09.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT10.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT11.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT12.png -------------------------------------------------------------------------------- /instructions/screenshots/SCREENSHOT__service-discovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/instructions/screenshots/SCREENSHOT__service-discovery.png -------------------------------------------------------------------------------- /instructions/screenshots/readme.md: -------------------------------------------------------------------------------- 1 | # Project Solution Screenshots 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /udapeople-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/udapeople-pipeline.png -------------------------------------------------------------------------------- /udapeople.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/cdond-c3-projectstarter/2f425a550a4115c351f05585ece7964e09eb8049/udapeople.png -------------------------------------------------------------------------------- /util/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: "3" 3 | services: 4 | database: 5 | image: "postgres" 6 | environment: 7 | - POSTGRES_USER=postgres 8 | - POSTGRES_PASSWORD=password 9 | - POSTGRES_DB=glee 10 | ports: 11 | - "5532:5432" 12 | --------------------------------------------------------------------------------