): boolean {
21 | if (object === null || object === undefined) {
22 | return false;
23 | }
24 |
25 | if (this === object) {
26 | return true;
27 | }
28 |
29 | if (!isEntity(object)) {
30 | return false;
31 | }
32 |
33 | return this.id.equals(object.id);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/server/src/competitors/api/competitorSchema.ts:
--------------------------------------------------------------------------------
1 | import Joi from "joi";
2 |
3 | export const socialLinkSchema = Joi.object({
4 | url: Joi.string(),
5 | type: Joi.string().valid("web", "twitter", "facebook", "instagram"),
6 | });
7 |
8 | export const achievementSchema = Joi.object({
9 | eventId: Joi.string(),
10 | categoryId: Joi.string(),
11 | position: Joi.number(),
12 | });
13 |
14 | export const competitorSchema = Joi.object({
15 | id: Joi.string(),
16 | firstName: Joi.string(),
17 | lastName: Joi.string(),
18 | wkfId: Joi.string(),
19 | biography: Joi.string(),
20 | countryId: Joi.string(),
21 | categoryId: Joi.string(),
22 | mainImage: Joi.string(),
23 | isActive: Joi.boolean(),
24 | isLegend: Joi.boolean(),
25 | links: Joi.array().items(socialLinkSchema),
26 | achievements: Joi.array().items(achievementSchema),
27 | });
28 |
--------------------------------------------------------------------------------
/packages/admin/src/common/presentation/layouts/main/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/styles";
3 | import { Typography, Link, Theme } from "@material-ui/core";
4 |
5 | const useStyles = makeStyles((theme: Theme) => ({
6 | root: {
7 | padding: theme.spacing(4),
8 | },
9 | }));
10 |
11 | const Footer: React.FC = () => {
12 | const classes = useStyles();
13 |
14 | return (
15 |
16 |
17 | ©{" "}
18 |
19 | Karate stars
20 | {" "}
21 | 2021
22 |
23 | Created by xurxodev
24 |
25 | );
26 | };
27 |
28 | export default Footer;
29 |
--------------------------------------------------------------------------------
/packages/admin/cypress/integration/login.spec.ts:
--------------------------------------------------------------------------------
1 | const username = Cypress.env("USERNAME");
2 | const password = Cypress.env("PASSWORD");
3 |
4 | describe("Login page", () => {
5 | beforeEach(() => {
6 | cy.visit("/");
7 | cy.contains("Login");
8 | cy.url().should("include", "/login");
9 | })
10 |
11 | it("should realize login", () => {
12 |
13 | cy.findByLabelText("Email").type(username);
14 | // {enter} causes the form to submit
15 | cy.findByLabelText("Password").type(`${password}{enter}`);
16 |
17 | cy.url().should("include", "/dashboard");
18 | });
19 | it("should show invalid credentials if password is wrong", () => {
20 | cy.findByLabelText("Email").type(username);
21 | // {enter} causes the form to submit
22 | cy.findByLabelText("Password").type(`Wrong password{enter}`)
23 |
24 | cy.findByText("Invalid credentials");
25 | });
26 | });
--------------------------------------------------------------------------------
/packages/core/src/value-objects/Password.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from "./ValueObject";
2 | import { Either } from "../types/Either";
3 | import { ValidationErrorKey } from "../types/Errors";
4 | import { validateRequired } from "../utils/validations";
5 |
6 | export interface PasswordProps {
7 | value: string;
8 | }
9 |
10 | export class Password extends ValueObject {
11 | get value(): string {
12 | return this.props.value;
13 | }
14 |
15 | private constructor(props: PasswordProps) {
16 | super(props);
17 | }
18 |
19 | public static create(password: string): Either {
20 | const requiredError = validateRequired(password);
21 |
22 | if (requiredError.length > 0) {
23 | return Either.left(requiredError);
24 | } else {
25 | return Either.right(new Password({ value: password }));
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/core/src/value-objects/__tests__/Password.spec.ts:
--------------------------------------------------------------------------------
1 | import { Password } from "../Password";
2 |
3 | describe("Password", () => {
4 | it("should return success reponse if value argument is valid", () => {
5 | const passwordValue = "info@karatestarsapp.com";
6 | const passwordResult = Password.create(passwordValue);
7 |
8 | passwordResult.fold(
9 | error => fail(error),
10 | email => expect(email.value).toEqual(passwordValue)
11 | );
12 | });
13 | it("should return InvalidEmptyPassword error if value argument is empty", () => {
14 | const passwordResult = Password.create("");
15 |
16 | passwordResult.fold(
17 | errors => {
18 | expect(errors.length).toBe(1);
19 | expect(errors[0]).toBe("field_cannot_be_blank");
20 | },
21 | () => fail("should be fail")
22 | );
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/packages/admin/src/countries/presentation/country-list/__test__/CountryListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { CountryData, Id } from "karate-stars-core";
4 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
5 |
6 | import CountryListPage from "../CountryListPage";
7 |
8 | const verifiableFields: (keyof CountryData)[] = ["id", "name"];
9 |
10 | const dataListCreator = {
11 | givenADataList: (count: number): CountryData[] => {
12 | const dataList = Array.from(Array(count).keys()).map((_, index) => {
13 | const code = ("0" + index).slice(-2);
14 |
15 | return {
16 | id: Id.generateId().value,
17 | name: `name ${code}`,
18 | iso2: code,
19 | };
20 | });
21 |
22 | return dataList;
23 | },
24 | };
25 |
26 | commonListPageTests("countries", verifiableFields, dataListCreator, );
27 |
--------------------------------------------------------------------------------
/packages/admin/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | //
2 | /* global Cypress, cy */
3 |
4 | import "@testing-library/cypress/add-commands";
5 |
6 | const username = Cypress.env("USERNAME");
7 | const password = Cypress.env("PASSWORD");
8 | const apiUrl = Cypress.env("API_URL");
9 |
10 | if (!username) {
11 | throw new Error("CYPRESS_USERNAME not set");
12 | }
13 |
14 | if (!password) {
15 | throw new Error("CYPRESS_PASSWORD not set");
16 | }
17 |
18 | if (!apiUrl) {
19 | throw new Error("CYPRESS_API_URL not set");
20 | }
21 |
22 | Cypress.Commands.add("login", () => {
23 | cy.request({
24 | method: "POST",
25 | url: `${apiUrl}/login`,
26 | body: {
27 | username,
28 | password,
29 | },
30 | }).then(resp => {
31 | cy.log(`Saving apiToken ${resp.headers["authorization"]}`);
32 | window.localStorage.setItem("apiToken", resp.headers["authorization"]);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/packages/server/src/events/domain/usecases/GetEventByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, EventData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import EventRepository from "../boundaries/EventRepository";
5 |
6 | export interface GetEventByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetEventByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetEventByIdUseCase {
13 | constructor(private eventRepository: EventRepository) {}
14 |
15 | public async execute({ id }: GetEventByIdArg): Promise> {
16 | const result = await createIdOrResourceNotFound(id)
17 | .flatMap(id => this.eventRepository.getById(id))
18 | .map(entity => entity.toData())
19 | .run();
20 |
21 | return result;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/server/src/videos/domain/usecases/GetVideoByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, VideoData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import VideoRepository from "../boundaries/VideoRepository";
5 |
6 | export interface GetVideoByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetVideoByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetVideoByIdUseCase {
13 | constructor(private videoRepository: VideoRepository) {}
14 |
15 | public async execute({ id }: GetVideoByIdArg): Promise> {
16 | const result = await createIdOrResourceNotFound(id)
17 | .flatMap(id => this.videoRepository.getById(id))
18 | .map(entity => entity.toData())
19 | .run();
20 |
21 | return result;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/types/Errors.ts:
--------------------------------------------------------------------------------
1 | export type ValidationErrorKey =
2 | | "field_cannot_be_blank"
3 | | "invalid_field"
4 | | "field_number_must_be_greater_than_0"
5 | | "invalid_dependency";
6 |
7 | export const validationErrorMessages: Record string> = {
8 | field_cannot_be_blank: (field: string) => `${capitalize(field)} cannot be blank`,
9 | invalid_field: (field: string) => `Invalid ${field.toLowerCase()}`,
10 | field_number_must_be_greater_than_0: (field: string) => `${capitalize(field)} cannot be 0`,
11 | invalid_dependency: (field: string) => `Invalid dependency ${capitalize(field)}`,
12 | };
13 |
14 | function capitalize(text: string) {
15 | if (typeof text !== "string") return "";
16 | return text.charAt(0).toUpperCase() + text.slice(1);
17 | }
18 |
19 | export type ValidationError = {
20 | type: string;
21 | property: keyof T;
22 | value: unknown;
23 | errors: ValidationErrorKey[];
24 | };
25 |
--------------------------------------------------------------------------------
/packages/admin/src/categories/presentation/category-list/__test__/CategoryListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { CategoryData, Id } from "karate-stars-core";
4 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
5 |
6 | import CategoryListPage from "../CategoryListPage";
7 |
8 | const verifiableFields: (keyof CategoryData)[] = ["id", "name"];
9 |
10 | const dataListCreator = {
11 | givenADataList: (count: number): CategoryData[] => {
12 | const dataList = Array.from(Array(count).keys()).map((_, index) => {
13 | const code = ("0" + index).slice(-2);
14 |
15 | return {
16 | id: Id.generateId().value,
17 | name: `name ${code}`,
18 | typeId: Id.generateId().value,
19 | };
20 | });
21 |
22 | return dataList;
23 | },
24 | };
25 |
26 | commonListPageTests("categories", verifiableFields, dataListCreator, );
27 |
--------------------------------------------------------------------------------
/packages/admin/src/event-types/presentation/event-type-list/__test__/EventListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { EventTypeData, Id } from "karate-stars-core";
4 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
5 |
6 | import EventTypeListPage from "../EventTypeListPage";
7 |
8 | const verifiableFields: (keyof EventTypeData)[] = ["name", "id"];
9 |
10 | const dataListCreator = {
11 | givenADataList: (count: number): EventTypeData[] => {
12 | const dataList = Array.from(Array(count).keys()).map((_, index) => {
13 | const code = ("0" + index).slice(-2);
14 |
15 | return {
16 | id: Id.generateId().value,
17 | name: `name ${code}`,
18 | typeId: Id.generateId().value,
19 | };
20 | });
21 |
22 | return dataList;
23 | },
24 | };
25 |
26 | commonListPageTests("event-types", verifiableFields, dataListCreator, );
27 |
--------------------------------------------------------------------------------
/packages/server/src/newsfeeds/domain/usecases/GetNewsFeedsUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, NewsFeedData } from "karate-stars-core";
2 | import { UnexpectedError } from "../../../common/api/Errors";
3 | import { AdminUseCase, AdminUseCaseArgs } from "../../../common/domain/AdminUseCase";
4 | import UserRepository from "../../../users/domain/boundaries/UserRepository";
5 |
6 | import NewsFeedsRepository from "../boundaries/NewsFeedRepository";
7 |
8 | export class GetNewsFeedsUseCase extends AdminUseCase<
9 | AdminUseCaseArgs,
10 | UnexpectedError,
11 | NewsFeedData[]
12 | > {
13 | constructor(private newsFeedsRepository: NewsFeedsRepository, userRepository: UserRepository) {
14 | super(userRepository);
15 | }
16 |
17 | public async run(_: AdminUseCaseArgs): Promise> {
18 | const newsFeed = await this.newsFeedsRepository.getAll();
19 |
20 | return Either.right(newsFeed.map(newsFeed => newsFeed.toData()));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/packages/server/src/socialnews/SocialNewsDIModule.ts:
--------------------------------------------------------------------------------
1 | import { MongoConector } from "../common/data/MongoConector";
2 | import { di } from "../CompositionRoot";
3 | import SocialNewsController from "./api/SocialNewsController";
4 | import SociaNewsMongoRepository from "./data/SocialNewsMongoRepository";
5 | import GetSocialNewsUseCase from "./domain/usecases/GetSocialNewsUseCase";
6 |
7 | export const socialNewsDIKeys = {
8 | socialNewsRepository: "socialNewsRepository",
9 | };
10 |
11 | export function initializeSocialNews() {
12 | di.bindLazySingleton(socialNewsDIKeys.socialNewsRepository, () => {
13 | return new SociaNewsMongoRepository(di.get(MongoConector));
14 | });
15 |
16 | di.bindLazySingleton(
17 | GetSocialNewsUseCase,
18 | () => new GetSocialNewsUseCase(di.get(socialNewsDIKeys.socialNewsRepository))
19 | );
20 |
21 | di.bindFactory(
22 | SocialNewsController,
23 | () => new SocialNewsController(di.get(GetSocialNewsUseCase))
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/server/src/socialnews/api/SocialNewsRoutes.ts:
--------------------------------------------------------------------------------
1 | import * as hapi from "@hapi/hapi";
2 |
3 | import * as CompositionRoot from "../../CompositionRoot";
4 | import { appDIKeys } from "../../CompositionRoot";
5 | import { JwtAuthenticator } from "../../server";
6 | import SocialNewsController from "./SocialNewsController";
7 |
8 | export default function (apiPrefix: string): hapi.ServerRoute[] {
9 | const jwtAuthenticator = CompositionRoot.di.get(appDIKeys.jwtAuthenticator);
10 |
11 | return [
12 | {
13 | method: "GET",
14 | path: `${apiPrefix}/socialnews`,
15 | options: {
16 | auth: jwtAuthenticator.name,
17 | },
18 | handler: (
19 | request: hapi.Request,
20 | h: hapi.ResponseToolkit
21 | ): hapi.Lifecycle.ReturnValue => {
22 | return CompositionRoot.di.get(SocialNewsController).get(request, h);
23 | },
24 | },
25 | ];
26 | }
27 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | extends: ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
4 | parserOptions: {
5 | ecmaVersion: 2020,
6 | sourceType: "module",
7 | },
8 | rules: {
9 | "no-console": "off",
10 | "@typescript-eslint/explicit-function-return-type": ["off"],
11 | "@typescript-eslint/explicit-module-boundary-types": ["off"],
12 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
13 | "no-unused-expressions": "off",
14 | "no-useless-concat": "off",
15 | "no-useless-constructor": "off",
16 | "default-case": "off",
17 | "@typescript-eslint/no-use-before-define": "off",
18 | "@typescript-eslint/no-explicit-any": "off",
19 | "@typescript-eslint/no-empty-interface": "off",
20 | "@typescript-eslint/ban-ts-ignore": "off",
21 | "@typescript-eslint/no-empty-function": "off",
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/packages/server/src/currentnews/api/CurrentNewsRoutes.ts:
--------------------------------------------------------------------------------
1 | import * as hapi from "@hapi/hapi";
2 |
3 | import * as CompositionRoot from "./../../CompositionRoot";
4 | import CurrentNewsController from "./CurrentNewsController";
5 | import { appDIKeys } from "./../../CompositionRoot";
6 | import { JwtAuthenticator } from "../../server";
7 |
8 | export default function (apiPrefix: string): hapi.ServerRoute[] {
9 | const jwtAuthenticator = CompositionRoot.di.get(appDIKeys.jwtAuthenticator);
10 |
11 | return [
12 | {
13 | method: "GET",
14 | path: `${apiPrefix}/currentnews`,
15 | options: {
16 | auth: jwtAuthenticator.name,
17 | },
18 | handler: (
19 | request: hapi.Request,
20 | h: hapi.ResponseToolkit
21 | ): hapi.Lifecycle.ReturnValue => {
22 | return CompositionRoot.di.get(CurrentNewsController).get(request, h);
23 | },
24 | },
25 | ];
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server/src/countries/domain/usecases/GetCountryByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, CountryData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import CountryRepository from "../boundaries/CountryRepository";
5 |
6 | export interface GetCountryByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetCountryByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetCountryByIdUseCase {
13 | constructor(private countryRepository: CountryRepository) {}
14 |
15 | public async execute({
16 | id,
17 | }: GetCountryByIdArg): Promise> {
18 | const result = await createIdOrResourceNotFound(id)
19 | .flatMap(id => this.countryRepository.getById(id))
20 | .map(entity => entity.toData())
21 | .run();
22 |
23 | return result;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/admin/src/category-types/presentation/category-type-list/__test__/CategoryTypeListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { CategoryTypeData, Id } from "karate-stars-core";
4 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
5 |
6 | import CategoryTypeListPage from "../CategoryTypeListPage";
7 |
8 | const verifiableFields: (keyof CategoryTypeData)[] = ["name", "id"];
9 |
10 | const dataListCreator = {
11 | givenADataList: (count: number): CategoryTypeData[] => {
12 | const dataList = Array.from(Array(count).keys()).map((_, index) => {
13 | const code = ("0" + index).slice(-2);
14 |
15 | return {
16 | id: Id.generateId().value,
17 | name: `name ${code}`,
18 | typeId: Id.generateId().value,
19 | };
20 | });
21 |
22 | return dataList;
23 | },
24 | };
25 |
26 | commonListPageTests("category-types", verifiableFields, dataListCreator, );
27 |
--------------------------------------------------------------------------------
/packages/admin/src/common/presentation/components/add-fab-button/AddFabButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles, Fab, Theme } from "@material-ui/core";
3 | import AddIcon from "@material-ui/icons/Add";
4 | import clsx from "clsx";
5 |
6 | const useStyles = makeStyles((theme: Theme) => ({
7 | root: {
8 | position: "fixed",
9 | bottom: theme.spacing(4),
10 | right: theme.spacing(4),
11 | },
12 | }));
13 |
14 | interface AddFabButtonProps {
15 | action: () => void;
16 | ariaLabel?: string;
17 | className?: string;
18 | }
19 |
20 | const AddFabButton: React.FC = ({ action, className, ariaLabel = "add" }) => {
21 | const classes = useStyles();
22 |
23 | return (
24 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default AddFabButton;
35 |
--------------------------------------------------------------------------------
/packages/server/src/ranking/api/RankingRoutes.ts:
--------------------------------------------------------------------------------
1 | import * as hapi from "@hapi/hapi";
2 |
3 | import * as CompositionRoot from "../../CompositionRoot";
4 | import { appDIKeys } from "../../CompositionRoot";
5 | import { JwtAuthenticator } from "../../server";
6 | import { RankingController } from "./RankingController";
7 |
8 | export const rankingsEndpoint = "rankings";
9 |
10 | export default function (apiPrefix: string): hapi.ServerRoute[] {
11 | const jwtAuthenticator = CompositionRoot.di.get(appDIKeys.jwtAuthenticator);
12 |
13 | return [
14 | {
15 | method: "GET",
16 | path: `${apiPrefix}/${rankingsEndpoint}`,
17 | options: { auth: jwtAuthenticator.name },
18 | handler: (
19 | request: hapi.Request,
20 | h: hapi.ResponseToolkit
21 | ): hapi.Lifecycle.ReturnValue => {
22 | return CompositionRoot.di.get(RankingController).getAll(request, h);
23 | },
24 | },
25 | ];
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server/src/settings/data/SettingsMongoRepository.ts:
--------------------------------------------------------------------------------
1 | import { SettingsDB } from "./SettingsDB";
2 | import SettingsRepository from "../domain/boundaries/SettingsRepository";
3 | import { Id } from "karate-stars-core";
4 | import { MongoConector } from "../../common/data/MongoConector";
5 | import { Settings } from "../domain/entities/Settings";
6 |
7 | export default class SettingsMongoRepository implements SettingsRepository {
8 | constructor(private mongoConector: MongoConector) {}
9 |
10 | async get(): Promise {
11 | const db = await this.mongoConector.db();
12 |
13 | const cursor = db.collection("settings").find({}, {});
14 |
15 | const settingsDB = (await cursor.toArray())[0];
16 |
17 | return this.mapToDomain(settingsDB);
18 | }
19 |
20 | private mapToDomain(settingsDB: SettingsDB): Settings {
21 | return {
22 | ...settingsDB,
23 | identifier: Id.createExisted(settingsDB._id).getOrThrow(),
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server/src/categories/domain/usecases/GetCategoryByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, CategoryData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import CategoryRepository from "../boundaries/CategoryRepository";
5 |
6 | export interface GetCategoryByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetCategoryByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetCategoryByIdUseCase {
13 | constructor(private categoryRepository: CategoryRepository) {}
14 |
15 | public async execute({
16 | id,
17 | }: GetCategoryByIdArg): Promise> {
18 | const result = await createIdOrResourceNotFound(id)
19 | .flatMap(id => this.categoryRepository.getById(id))
20 | .map(entity => entity.toData())
21 | .run();
22 |
23 | return result;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/server/src/event-types/domain/usecases/GetEventTypeByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, EventTypeData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import EventTypeRepository from "../boundaries/EventTypeRepository";
5 |
6 | export interface GetEventTypeByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetEventTypeByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetEventTypeByIdUseCase {
13 | constructor(private EventTypeRepository: EventTypeRepository) {}
14 |
15 | public async execute({
16 | id,
17 | }: GetEventTypeByIdArg): Promise> {
18 | const result = await createIdOrResourceNotFound(id)
19 | .flatMap(id => this.EventTypeRepository.getById(id))
20 | .map(entity => entity.toData())
21 | .run();
22 |
23 | return result;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/server/src/ranking/api/RankingEntriesRoutes.ts:
--------------------------------------------------------------------------------
1 | import * as hapi from "@hapi/hapi";
2 |
3 | import * as CompositionRoot from "../../CompositionRoot";
4 | import { appDIKeys } from "../../CompositionRoot";
5 | import { JwtAuthenticator } from "../../server";
6 | import { RankingEntryController } from "./RankingEntryController";
7 |
8 | export const rankingEntriesEndpoint = "ranking-entries";
9 |
10 | export default function (apiPrefix: string): hapi.ServerRoute[] {
11 | const jwtAuthenticator = CompositionRoot.di.get(appDIKeys.jwtAuthenticator);
12 |
13 | return [
14 | {
15 | method: "GET",
16 | path: `${apiPrefix}/${rankingEntriesEndpoint}`,
17 | options: { auth: jwtAuthenticator.name },
18 | handler: (
19 | request: hapi.Request,
20 | h: hapi.ResponseToolkit
21 | ): hapi.Lifecycle.ReturnValue => {
22 | return CompositionRoot.di.get(RankingEntryController).get(request, h);
23 | },
24 | },
25 | ];
26 | }
27 |
--------------------------------------------------------------------------------
/packages/admin/src/common/data/Base64ImageConverter.ts:
--------------------------------------------------------------------------------
1 | import mime from "mime-types";
2 |
3 | export function base64ImageToFile(base64Image: string, name: string): File {
4 | let byteString;
5 | if (base64Image.split(",")[0].indexOf("base64") >= 0)
6 | byteString = atob(base64Image.split(",")[1]);
7 | else byteString = unescape(base64Image.split(",")[1]);
8 |
9 | // separate out the mime component
10 | const mimeString = base64Image.split(",")[0].split(":")[1].split(";")[0];
11 |
12 | // write the bytes of the string to a typed array
13 | const ia = new Uint8Array(byteString.length);
14 | for (let i = 0; i < byteString.length; i++) {
15 | ia[i] = byteString.charCodeAt(i);
16 | }
17 |
18 | const fileName = `${convertToSlug(name)}.${mime.extension(mimeString)}`;
19 |
20 | return new File([ia], fileName, { type: mimeString });
21 | }
22 |
23 | function convertToSlug(name: string): string {
24 | return name
25 | .trim()
26 | .toLowerCase()
27 | .replace(/ /g, "-")
28 | .replace(/[^\w-]+/g, "");
29 | }
30 |
--------------------------------------------------------------------------------
/packages/server/src/news-import/newsImporterFactory.ts:
--------------------------------------------------------------------------------
1 | import { MongoConector } from "../common/data/MongoConector";
2 | import { CurrentNewsImporter } from "./importers/currentNewsImporter";
3 | import { SocialNewsImporter } from "./importers/socialNewsImporter";
4 | import { NewsImporter } from "./importNews";
5 |
6 | const mongoConnection = process.env.MONGO_DB_CONNECTION;
7 |
8 | if (!mongoConnection) {
9 | throw new Error("Does not exists environment variable for mongo database connection");
10 | }
11 |
12 | const mongoConector = new MongoConector(mongoConnection);
13 |
14 | const strategies: Record NewsImporter> = {
15 | current: () => new CurrentNewsImporter(mongoConector),
16 | social: () => new SocialNewsImporter(mongoConector),
17 | };
18 |
19 | export const newsImporterFactory = {
20 | createStrategies: (importerKeys: string[]) => {
21 | const finalStrategies = Object.keys(strategies)
22 | .filter(key => importerKeys.includes(key))
23 | .map(key => strategies[key]());
24 |
25 | return finalStrategies;
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/packages/server/src/category-types/domain/usecases/GetCategoryTypeByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, CategoryTypeData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
4 | import CategoryTypeRepository from "../boundaries/CategoryTypeRepository";
5 |
6 | export interface GetCategoryTypeByIdArg {
7 | id: string;
8 | }
9 |
10 | type GetCategoryTypeByIdError = ResourceNotFoundError | UnexpectedError;
11 |
12 | export class GetCategoryTypeByIdUseCase {
13 | constructor(private categoryTypeRepository: CategoryTypeRepository) {}
14 |
15 | public async execute({
16 | id,
17 | }: GetCategoryTypeByIdArg): Promise> {
18 | const result = await createIdOrResourceNotFound(id)
19 | .flatMap(id => this.categoryTypeRepository.getById(id))
20 | .map(entity => entity.toData())
21 | .run();
22 |
23 | return result;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # karate-stars-core
4 |
5 | Karate stars core constains common code for all packages.
6 |
7 | ## Setup
8 |
9 | ```
10 | $ yarn install
11 | ```
12 |
13 | ## Development
14 |
15 | Start development server:
16 |
17 | ```
18 | $ yarn start-dev
19 | ```
20 |
21 | This will open the development server at port 8000 or port asigned in process.env.PORT with automaticatic restart the server when a typescript file change.
22 | Use [nodemon](https://github.com/remy/nodemon) to automaticatic restart.
23 |
24 | ## Build
25 |
26 | buid script:
27 |
28 | ```
29 | $ yarn build
30 | ```
31 |
32 | This will open the development server at port 8000 or port asigned in process.env.PORT
33 |
34 | ## Tests
35 |
36 | Run unit tests:
37 |
38 | ```
39 | $ yarn test
40 | ```
41 |
42 | ## Libraries used in this project
43 |
44 | ## License
45 |
46 | Torii Shopping is [GNU GPLv3](https://github.com/xurxodev/torii-shopping-api/blob/master/LICENSE) license.
47 |
--------------------------------------------------------------------------------
/packages/admin/src/common/presentation/bloc/Bloc.ts:
--------------------------------------------------------------------------------
1 | type Subscription = (state: S) => void;
2 |
3 | abstract class Bloc {
4 | protected state: S;
5 | private listeners: Subscription[] = [];
6 |
7 | constructor(initalState: S) {
8 | this.state = initalState;
9 | }
10 |
11 | public get getState(): S {
12 | return this.state;
13 | }
14 |
15 | protected changeState(state: S) {
16 | this.state = state;
17 |
18 | if (this.listeners.length > 0) {
19 | this.listeners.forEach(listener => listener(this.state));
20 | }
21 | }
22 |
23 | subscribe(listener: Subscription) {
24 | this.listeners.push(listener);
25 | }
26 |
27 | unsubscribe(listener: Subscription) {
28 | const index = this.listeners.indexOf(listener);
29 | if (index > -1) {
30 | this.listeners.splice(index, 1);
31 | }
32 | }
33 |
34 | protected dispose() {
35 | //Override on derivated vaclass dispose if
36 | //you need clear subscriptions for example
37 | }
38 | }
39 |
40 | export default Bloc;
41 |
--------------------------------------------------------------------------------
/packages/core/src/value-objects/ImageUrl.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from "./ValueObject";
2 | import { Either } from "../types/Either";
3 | import { validateRequired, validateRegexp } from "../utils/validations";
4 | import { ValidationErrorKey } from "../types/Errors";
5 |
6 | export interface UrlProps {
7 | value: string;
8 | }
9 |
10 | const URL_PATTERN = /(http(s?):)([/|.|\w|\s|-])*\.(?:jpg|png)/;
11 |
12 | export class ImageUrl extends ValueObject {
13 | get value(): string {
14 | return this.props.value;
15 | }
16 |
17 | private constructor(props: UrlProps) {
18 | super(props);
19 | }
20 |
21 | public static create(url: string): Either {
22 | const requiredError = validateRequired(url);
23 | const regexpErrors = validateRegexp(url, URL_PATTERN);
24 |
25 | if (requiredError.length > 0) {
26 | return Either.left(requiredError);
27 | } else if (regexpErrors.length > 0) {
28 | return Either.left(regexpErrors);
29 | } else {
30 | return Either.right(new ImageUrl({ value: url }));
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/server/migrate-mongo-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mongodb: {
3 | // TODO Change (or review) the url to your MongoDB:
4 | url: process.env.MONGO_DB_CONNECTION,
5 |
6 | options: {
7 | useNewUrlParser: true, // removes a deprecation warning when connecting
8 | // connectTimeoutMS: 3600000, // increase connection timeout to 1 hour
9 | // socketTimeoutMS: 3600000, // increase socket timeout to 1 hour
10 | },
11 | },
12 |
13 | // The migrations dir, can be an relative or absolute path. Only edit this when really necessary.
14 | migrationsDir: "migrations",
15 |
16 | // The mongodb collection where the applied changes are stored. Only edit this when really necessary.
17 | changelogCollectionName: "changelog",
18 |
19 | // The file extension to create migrations and search for in migration dir
20 | migrationFileExtension: ".ts",
21 |
22 | // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determin
23 | // if the file should be run. Requires that scripts are coded to be run multiple times.
24 | useFileHash: false,
25 | };
26 |
--------------------------------------------------------------------------------
/packages/server/src/ranking/api/RankingEntryController.ts:
--------------------------------------------------------------------------------
1 | import { JwtAuthenticator } from "../../server";
2 | import { handleFailure } from "../../common/api/AdminController";
3 | import * as hapi from "@hapi/hapi";
4 | import { GetRankingEntriesUseCase } from "../domain/usecases/GetRankingEntriesUseCase";
5 | import * as boom from "@hapi/boom";
6 |
7 | export class RankingEntryController {
8 | constructor(
9 | private _jwtAuthenticator: JwtAuthenticator,
10 | private getRankingEntriesUseCase: GetRankingEntriesUseCase
11 | ) {}
12 |
13 | async get(
14 | request: hapi.Request,
15 | _h: hapi.ResponseToolkit
16 | ): Promise {
17 | const rankingId = request.query.rankingId;
18 | const categoryId = request.query.categoryId;
19 |
20 | if (!rankingId || !categoryId) {
21 | return boom.badRequest("rankingId and categoryId parameters are required");
22 | }
23 |
24 | const result = await this.getRankingEntriesUseCase.execute({ rankingId, categoryId });
25 |
26 | return result.fold(
27 | error => handleFailure(error),
28 | async data => data
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/server/src/currentnews/CurrentNewsDIModule.ts:
--------------------------------------------------------------------------------
1 | import { MongoConector } from "../common/data/MongoConector";
2 | import { di } from "../CompositionRoot";
3 | import { newsFeedDIKeys } from "../newsfeeds/NewsFeedsDIModule";
4 | import CurrentNewsController from "./api/CurrentNewsController";
5 | import CurrentNewsMongoRepository from "./data/CurrentNewsMongoRepository";
6 | import GetCurrentNewsUseCase from "./domain/usecases/GetCurrentNewsUseCase";
7 |
8 | export const currentNewsDIKeys = {
9 | currentNewsRepository: "currentNewsRepository",
10 | };
11 |
12 | export function initializeCurrentNews() {
13 | di.bindLazySingleton(
14 | currentNewsDIKeys.currentNewsRepository,
15 | () => new CurrentNewsMongoRepository(di.get(MongoConector))
16 | );
17 |
18 | di.bindLazySingleton(
19 | GetCurrentNewsUseCase,
20 | () =>
21 | new GetCurrentNewsUseCase(
22 | di.get(currentNewsDIKeys.currentNewsRepository),
23 | di.get(newsFeedDIKeys.newsFeedRepository)
24 | )
25 | );
26 |
27 | di.bindFactory(
28 | CurrentNewsController,
29 | () => new CurrentNewsController(di.get(GetCurrentNewsUseCase))
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/packages/server/src/event-types/data/EventTypeMongoRepository.ts:
--------------------------------------------------------------------------------
1 | import { EventType, EventTypeData } from "karate-stars-core";
2 | import { MongoConector } from "../../common/data/MongoConector";
3 | import { MongoCollection } from "../../common/data/Types";
4 | import EventTypeRepository from "../domain/boundaries/EventTypeRepository";
5 | import MongoRepository from "../../common/data/MongoRepository";
6 | import { renameProp } from "../../common/data/utils";
7 |
8 | type EventTypeDB = Omit & MongoCollection;
9 |
10 | export default class EventTypeMongoRepository
11 | extends MongoRepository
12 | implements EventTypeRepository
13 | {
14 | constructor(mongoConector: MongoConector) {
15 | super(mongoConector, "eventTypes");
16 | }
17 |
18 | protected mapToDomain(eventTypeDB: EventTypeDB): EventType {
19 | return EventType.create({
20 | id: eventTypeDB._id,
21 | name: eventTypeDB.name,
22 | }).get();
23 | }
24 |
25 | protected mapToDB(EventType: EventType): EventTypeDB {
26 | const rawData = EventType.toData();
27 |
28 | return renameProp("id", "_id", rawData) as EventTypeDB;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/server/src/common/api/testUtils/jsonParser.ts:
--------------------------------------------------------------------------------
1 | import request from "supertest";
2 |
3 | export function jsonParser(
4 | res: request.Response,
5 | callback: (err: Error | null, body: any) => void
6 | ) {
7 | res.text = "";
8 | res.setEncoding("utf8");
9 | res.on("data", chunk => {
10 | res.text += chunk;
11 | });
12 | res.on("end", () => {
13 | let body;
14 | let err;
15 | try {
16 | body = res.text && JSON.parse(res.text, reviver);
17 | } catch (err_) {
18 | err = err_;
19 | // issue #675: return the raw response if the response parsing fails
20 | err.rawResponse = res.text || null;
21 | // issue #876: return the http status code if the response parsing fails
22 | err.statusCode = res.status;
23 | } finally {
24 | callback(err, body);
25 | }
26 | });
27 | }
28 |
29 | const dateFormat = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
30 |
31 | function reviver(_key, value: any) {
32 | if (typeof value === "string" && dateFormat.test(value)) {
33 | return new Date(value);
34 | }
35 |
36 | return value;
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/src/videos/domain/usecases/DeleteVideoUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, Id } from "karate-stars-core";
2 | import { ActionResult } from "../../../common/api/ActionResult";
3 | import { AdminUseCase, AdminUseCaseArgs } from "../../../common/domain/AdminUseCase";
4 | import { deleteResource, DeleteResourceError } from "../../../common/domain/DeleteResource";
5 | import UserRepository from "../../../users/domain/boundaries/UserRepository";
6 | import VideoRepository from "../boundaries/VideoRepository";
7 |
8 | export interface DeleteResourceArgs extends AdminUseCaseArgs {
9 | id: string;
10 | }
11 |
12 | export class DeleteVideoUseCase extends AdminUseCase<
13 | DeleteResourceArgs,
14 | DeleteResourceError,
15 | ActionResult
16 | > {
17 | constructor(private videoRepository: VideoRepository, userRepository: UserRepository) {
18 | super(userRepository);
19 | }
20 |
21 | protected run({ id }: DeleteResourceArgs): Promise> {
22 | const getById = (id: Id) => this.videoRepository.getById(id);
23 | const deleteEntity = (id: Id) => this.videoRepository.delete(id);
24 |
25 | return deleteResource(id, getById, deleteEntity);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/server/src/countries/data/CountryMongoRepository.ts:
--------------------------------------------------------------------------------
1 | import { Country, CountryData } from "karate-stars-core";
2 | import { MongoConector } from "../../common/data/MongoConector";
3 | import { MongoCollection } from "../../common/data/Types";
4 | import MongoRepository from "../../common/data/MongoRepository";
5 | import { renameProp } from "../../common/data/utils";
6 | import CountryRepository from "../domain/boundaries/CountryRepository";
7 |
8 | type CountryDB = Omit & MongoCollection;
9 |
10 | export default class CountryMongoRepository
11 | extends MongoRepository
12 | implements CountryRepository
13 | {
14 | constructor(mongoConector: MongoConector) {
15 | super(mongoConector, "countries");
16 | }
17 |
18 | protected mapToDomain(modelDB: CountryDB): Country {
19 | return Country.create({
20 | id: modelDB._id,
21 | name: modelDB.name,
22 | iso2: modelDB.iso2,
23 | image: modelDB.image,
24 | }).get();
25 | }
26 |
27 | protected mapToDB(entity: Country): CountryDB {
28 | const rawData = entity.toData();
29 |
30 | return renameProp("id", "_id", rawData) as CountryDB;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/server/src/videos/domain/usecases/utils.ts:
--------------------------------------------------------------------------------
1 | import { Either, Video, ValidationError, VideoValidationTypes } from "karate-stars-core";
2 | import CompetitorRepository from "../../../competitors/domain/boundaries/CompetitorRepository";
3 |
4 | export async function validateVideoDependencies(
5 | entity: Video,
6 | competitorRepository: CompetitorRepository
7 | ): Promise[], Video>> {
8 | const competitorResults = await Promise.all(
9 | entity.competitors.map(async competitorId => {
10 | return (await competitorRepository.getById(competitorId)).mapLeft(() => [
11 | {
12 | property: "competitors" as const,
13 | errors: ["invalid_dependency"],
14 | type: Video.name,
15 | value: competitorId,
16 | } as ValidationError,
17 | ]);
18 | })
19 | );
20 |
21 | const errorsResults = competitorResults.filter(result => result.isLeft());
22 |
23 | const errorResultsData = errorsResults.map(result => result.getLeft()).flat();
24 |
25 | return errorsResults.length > 0 ? Either.left(errorResultsData) : Either.right(entity);
26 | }
27 |
--------------------------------------------------------------------------------
/packages/server/src/category-types/data/CategoryTypeMongoRepository.ts:
--------------------------------------------------------------------------------
1 | import { CategoryType, CategoryTypeData } from "karate-stars-core";
2 | import { MongoConector } from "../../common/data/MongoConector";
3 | import { MongoCollection } from "../../common/data/Types";
4 | import MongoRepository from "../../common/data/MongoRepository";
5 | import { renameProp } from "../../common/data/utils";
6 | import CategoryTypeRepository from "../domain/boundaries/CategoryTypeRepository";
7 |
8 | type CategoryTypeDB = Omit & MongoCollection;
9 |
10 | export default class CategoryTypeMongoRepository
11 | extends MongoRepository
12 | implements CategoryTypeRepository
13 | {
14 | constructor(mongoConector: MongoConector) {
15 | super(mongoConector, "categoryTypes");
16 | }
17 |
18 | protected mapToDomain(modelDB: CategoryTypeDB): CategoryType {
19 | return CategoryType.create({
20 | id: modelDB._id,
21 | name: modelDB.name,
22 | }).get();
23 | }
24 |
25 | protected mapToDB(entity: CategoryType): CategoryTypeDB {
26 | const rawData = entity.toData();
27 |
28 | return renameProp("id", "_id", rawData) as CategoryTypeDB;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/admin/src/events/presentation/event-list/__test__/EventListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { EventData, Id } from "karate-stars-core";
4 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
5 |
6 | import EventListPage from "../EventListPage";
7 |
8 | const verifiableFields: (keyof EventData)[] = ["id", "name"];
9 |
10 | const dataListCreator = {
11 | givenADataList: (count: number): EventData[] => {
12 | const dataList = Array.from(Array(count).keys())
13 | .map((_, index) => {
14 | const code = ("0" + index).slice(-2);
15 |
16 | return {
17 | id: Id.generateId().value,
18 | name: `name ${code}`,
19 | typeId: Id.generateId().value,
20 | startDate: new Date(+("20" + code), 1, 1),
21 | endDate: new Date(+("20" + code), 1, 1),
22 | url: "http://example.com/" + code,
23 | };
24 | })
25 | .sort((a, b) => b.startDate.getFullYear() - a.startDate.getFullYear());
26 |
27 | return dataList;
28 | },
29 | };
30 |
31 | commonListPageTests("events", verifiableFields, dataListCreator, );
32 |
--------------------------------------------------------------------------------
/packages/admin/src/news/presentation/news-feed-list/__test__/NewsFeedListPage.spec.tsx:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom/extend-expect";
2 |
3 | import { givenAValidAuthenticatedUser } from "../../../../common/testing/scenarios/UserTestScenarios";
4 | import NewsFeedListPage from "../NewsFeedListPage";
5 | import { Id, NewsFeedData, RssType } from "karate-stars-core";
6 | import { commonListPageTests } from "../../../../common/testing/commonListPageTests.spec";
7 |
8 | beforeEach(() => givenAValidAuthenticatedUser());
9 |
10 | const verifiableFields: (keyof NewsFeedData)[] = ["name", "language", "type", "url"];
11 |
12 | const dataListCreator = {
13 | givenADataList: (count: number): NewsFeedData[] => {
14 | const newsFeeds = Array.from(Array(count).keys()).map((_, index) => ({
15 | id: Id.generateId().value,
16 | name: `name ${("0" + index).slice(-2)}`,
17 | language: "en",
18 | type: "rss" as RssType,
19 | image: `https://storage.googleapis.com/karatestars-1261.appspot.com/feeds/${index}.png`,
20 | url: `http://fetchrss.com/rss/${index}.xml`,
21 | }));
22 |
23 | return newsFeeds;
24 | },
25 | };
26 |
27 | commonListPageTests("news-feeds", verifiableFields, dataListCreator, );
28 |
--------------------------------------------------------------------------------
/packages/core/src/value-objects/__tests__/Email.spec.ts:
--------------------------------------------------------------------------------
1 | import { Email } from "../Email";
2 |
3 | describe("Email", () => {
4 | it("should return success reponse if email is valid", () => {
5 | const emailValue = "info@karatestarsapp.com";
6 | const emailResult = Email.create(emailValue);
7 |
8 | emailResult.fold(
9 | error => fail(error),
10 | email => expect(email.value).toEqual(emailValue)
11 | );
12 | });
13 | it("should return InvalidEmptyEmail error if value argument is empty", () => {
14 | const emailResult = Email.create("");
15 |
16 | emailResult.fold(
17 | errors => {
18 | expect(errors.length).toBe(1);
19 | expect(errors[0]).toBe("field_cannot_be_blank");
20 | },
21 | () => fail("should be fail")
22 | );
23 | });
24 | it("should return InvalidId error if value argument is invalid", () => {
25 | const emailResult = Email.create("infokaratestarsapp.com");
26 |
27 | emailResult.fold(
28 | errors => {
29 | expect(errors.length).toBe(1);
30 | expect(errors[0]).toBe("invalid_field");
31 | },
32 | () => fail("should be fail")
33 | );
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | #Ignore build folder
33 | build
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | /.vscode/launch.json
67 |
68 | tsconfig.tsbuildinfo
69 |
70 |
71 | #Cypress
72 | cypress.env.json
73 |
--------------------------------------------------------------------------------
/packages/admin/src/countries/domain/SaveCountryUseCase.ts:
--------------------------------------------------------------------------------
1 | import { CountryRepository } from "./Boundaries";
2 | import { Either, Country, EitherAsync } from "karate-stars-core";
3 | import { DataError } from "../../common/domain/Errors";
4 |
5 | export default class SaveCountryUseCase {
6 | constructor(
7 | private countryRepository: CountryRepository,
8 | private base64ImageToFile: (base64Image: string, name: string) => File
9 | ) {}
10 |
11 | async execute(entity: Country): Promise> {
12 | if (entity.image !== undefined && entity.image.isDataUrl) {
13 | const imageUrl = entity.image.value;
14 | const entityWithoutImage = Country.create({
15 | ...entity.toData(),
16 | image: undefined,
17 | }).get();
18 |
19 | return EitherAsync.fromPromise(this.countryRepository.save(entityWithoutImage))
20 | .flatMap(async () => {
21 | const file = this.base64ImageToFile(imageUrl, entity.name);
22 |
23 | const imageResult = this.countryRepository.saveImage(entity.id, file);
24 |
25 | return imageResult;
26 | })
27 | .run();
28 | } else {
29 | return this.countryRepository.save(entity);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/admin/src/news/domain/SaveNewsFeedUseCase.ts:
--------------------------------------------------------------------------------
1 | import { NewsFeedRepository } from "./Boundaries";
2 | import { Either, EitherAsync, NewsFeed } from "karate-stars-core";
3 | import { DataError } from "../../common/domain/Errors";
4 |
5 | export default class SaveNewsFeedUseCase {
6 | constructor(
7 | private newsFeedRepository: NewsFeedRepository,
8 | private base64ImageToFile: (base64Image: string, name: string) => File
9 | ) {}
10 |
11 | async execute(newsFeed: NewsFeed): Promise> {
12 | if (newsFeed.image !== undefined && newsFeed.image.isDataUrl) {
13 | const imageUrl = newsFeed.image.value;
14 | const newsFeedWithoutImage = NewsFeed.create({
15 | ...newsFeed.toData(),
16 | image: undefined,
17 | }).get();
18 |
19 | return EitherAsync.fromPromise(this.newsFeedRepository.save(newsFeedWithoutImage))
20 | .flatMap(async () => {
21 | const file = this.base64ImageToFile(imageUrl, newsFeed.name);
22 |
23 | const imageResult = this.newsFeedRepository.saveImage(newsFeed.id, file);
24 |
25 | return imageResult;
26 | })
27 | .run();
28 | } else {
29 | return this.newsFeedRepository.save(newsFeed);
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/packages/server/src/authentication/JwtDefaultAuthenticator.ts:
--------------------------------------------------------------------------------
1 | import * as jwt from "jsonwebtoken";
2 | import { JwtAuthenticator, TokenData } from "../server";
3 | import GetUserByIdUseCase from "../users/domain/usecases/GetUserByIdUseCase";
4 |
5 | class JwtDefaultAuthenticator implements JwtAuthenticator {
6 | public readonly name = "jwt Authentication";
7 |
8 | constructor(public secretKey: string, private getUserByIdUseCase: GetUserByIdUseCase) {
9 | if (!secretKey) {
10 | throw new Error("Does not exists environment variable for secretKey");
11 | }
12 | }
13 |
14 | async validateTokenData(tokenData: TokenData): Promise<{ isValid: boolean }> {
15 | const userResult = await this.getUserByIdUseCase.execute(tokenData.userId);
16 |
17 | return userResult.fold<{ isValid: boolean }>(
18 | () => ({ isValid: false }),
19 | () => ({ isValid: true })
20 | );
21 | }
22 |
23 | generateToken(userId: string): string {
24 | const tokenData: TokenData = {
25 | userId: userId,
26 | };
27 |
28 | return jwt.sign(tokenData, this.secretKey, { expiresIn: "24h" });
29 | }
30 |
31 | decodeTokenData(token: string): TokenData {
32 | return jwt.verify(token.replace("Bearer ", ""), this.secretKey) as TokenData;
33 | }
34 | }
35 |
36 | export default JwtDefaultAuthenticator;
37 |
--------------------------------------------------------------------------------
/packages/server/src/users/api/UserRoutes.ts:
--------------------------------------------------------------------------------
1 | import * as hapi from "@hapi/hapi";
2 |
3 | import * as CompositionRoot from "../../CompositionRoot";
4 | import { appDIKeys } from "../../CompositionRoot";
5 | import { JwtAuthenticator } from "../../server";
6 | import UserController from "./UserController";
7 |
8 | export default function (apiPrefix: string): hapi.ServerRoute[] {
9 | const jwtAuthenticator = CompositionRoot.di.get(appDIKeys.jwtAuthenticator);
10 |
11 | return [
12 | {
13 | method: "POST",
14 | path: `${apiPrefix}/login`,
15 | options: { auth: false },
16 | handler: (
17 | request: hapi.Request,
18 | h: hapi.ResponseToolkit
19 | ): hapi.Lifecycle.ReturnValue => {
20 | return CompositionRoot.di.get(UserController).login(request, h);
21 | },
22 | },
23 | {
24 | method: "GET",
25 | path: `${apiPrefix}/me`,
26 | options: {
27 | auth: jwtAuthenticator.name,
28 | },
29 | handler: (
30 | request: hapi.Request,
31 | h: hapi.ResponseToolkit
32 | ): hapi.Lifecycle.ReturnValue => {
33 | return CompositionRoot.di.get(UserController).getCurrentUser(request, h);
34 | },
35 | },
36 | ];
37 | }
38 |
--------------------------------------------------------------------------------
/packages/admin/cypress/integration/eventTypeDetail.spec.ts:
--------------------------------------------------------------------------------
1 | describe("Event type detail page", () => {
2 | describe("New", () => {
3 | beforeEach(() => {
4 | cy.login();
5 | cy.visit("#/event-types/new");
6 |
7 | cy.intercept("POST", "/api/v1/event-types", {
8 | statusCode: 201,
9 | body: {
10 | ok: true,
11 | count: 1,
12 | },
13 | });
14 | });
15 |
16 | it("should create a new item", () => {
17 | typeValidForm();
18 | });
19 | });
20 |
21 | describe("Edit", () => {
22 | beforeEach(() => {
23 | cy.login();
24 | cy.visit("#/event-types/edit/Jr6N73CZWtE");
25 |
26 | cy.intercept("PUT", "/api/v1/event-types/Jr6N73CZWtE", {
27 | statusCode: 200,
28 | body: {
29 | ok: true,
30 | count: 1,
31 | },
32 | });
33 | });
34 | it("should edit an item", () => {
35 | typeValidForm();
36 | });
37 | });
38 |
39 | function typeValidForm() {
40 | cy.findByLabelText("Name (*)").clear().type("World Championships");
41 |
42 | cy.findByRole("button", { name: /accept/i }).click();
43 |
44 | cy.findByText("Event Type saved!");
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/packages/admin/cypress/integration/categoryTypeDetail.spec.ts:
--------------------------------------------------------------------------------
1 | describe("Category type detail page", () => {
2 | describe("New", () => {
3 | beforeEach(() => {
4 | cy.login();
5 | cy.visit("#/category-types/new");
6 |
7 | cy.intercept("POST", "/api/v1/category-types", {
8 | statusCode: 201,
9 | body: {
10 | ok: true,
11 | count: 1,
12 | },
13 | });
14 | });
15 |
16 | it("should create a new item", () => {
17 | typeValidForm();
18 | });
19 | });
20 |
21 | describe("Edit", () => {
22 | beforeEach(() => {
23 | cy.login();
24 | cy.visit("#/category-types/edit/qWPs4i1e78g");
25 |
26 | cy.intercept("PUT", "/api/v1/category-types/qWPs4i1e78g", {
27 | statusCode: 200,
28 | body: {
29 | ok: true,
30 | count: 1,
31 | },
32 | });
33 | });
34 | it("should edit an item", () => {
35 | typeValidForm();
36 | });
37 | });
38 |
39 | function typeValidForm() {
40 | cy.findByLabelText("Name (*)").clear().type("Kata");
41 |
42 | cy.findByRole("button", { name: /accept/i }).click();
43 |
44 | cy.findByText("Category Type saved!");
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/packages/server/src/ranking/domain/usecases/GetRankingEntriesUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, Id, RankingEntryData } from "karate-stars-core";
2 | import { ResourceNotFoundError } from "../../../common/api/Errors";
3 | import RankingEntryRepository from "../boundaries/RankingEntryRepository";
4 |
5 | export interface GetRankingEntriesArgs {
6 | rankingId: string;
7 | categoryId: string;
8 | }
9 |
10 | type GetRankingEntriesdError = ResourceNotFoundError;
11 |
12 | export class GetRankingEntriesUseCase {
13 | constructor(private rankingEntryRepository: RankingEntryRepository) {}
14 |
15 | public async execute({
16 | rankingId,
17 | categoryId,
18 | }: GetRankingEntriesArgs): Promise> {
19 | const ranking = Id.createExisted(rankingId);
20 | const category = Id.createExisted(categoryId);
21 |
22 | if (ranking.isLeft() || category.isLeft()) {
23 | return Either.left({
24 | kind: "ResourceNotFound",
25 | message: `Ranking entry not found for rankingId ${rankingId} and category ${categoryId}`,
26 | } as ResourceNotFoundError);
27 | } else {
28 | const rankings = await this.rankingEntryRepository.get(ranking.get(), category.get());
29 |
30 | return Either.right(rankings.map(ranking => ranking.toData()));
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/admin/src/common/presentation/state/ListState.ts:
--------------------------------------------------------------------------------
1 | export type ListState = {
2 | items: Array;
3 | fields: ListField[];
4 | search?: string;
5 | searchEnable?: boolean;
6 | selectedItems: string[];
7 | pagination?: ListPagination;
8 | sorting?: ListSorting;
9 | actions?: ListAction[];
10 | itemsToDelete?: string[];
11 | };
12 |
13 | export type SortDirection = "asc" | "desc";
14 |
15 | export interface ListSorting {
16 | field: keyof T;
17 | order: SortDirection;
18 | }
19 |
20 | export interface ListAction {
21 | name: string;
22 | text: string;
23 | icon?: string;
24 | multiple?: boolean;
25 | primary?: boolean;
26 | active?: boolean;
27 | }
28 |
29 | export interface ListField {
30 | name: keyof T;
31 | alt?: keyof T;
32 | text: string;
33 | sortable?: boolean;
34 | searchable?: boolean;
35 | type: "text" | "image" | "smallImage" | "avatar" | "url" | "boolean";
36 | hide?: boolean;
37 | }
38 |
39 | export interface ListPagination {
40 | pageSizeOptions?: number[];
41 | pageSize: number;
42 | total: number;
43 | page: number;
44 | }
45 |
46 | export interface ListSorting {
47 | field: keyof T;
48 | order: "asc" | "desc";
49 | }
50 |
51 | export interface IdentifiableObject {
52 | id: string;
53 | }
54 |
--------------------------------------------------------------------------------
/packages/server/src/newsfeeds/data/NewsFeedMongoRepository.ts:
--------------------------------------------------------------------------------
1 | import { NewsFeed, NewsFeedData } from "karate-stars-core";
2 | import { MongoConector } from "../../common/data/MongoConector";
3 | import { MongoCollection } from "../../common/data/Types";
4 | import NewsFeedRepository from "../domain/boundaries/NewsFeedRepository";
5 | import MongoRepository from "../../common/data/MongoRepository";
6 | import { renameProp } from "../../common/data/utils";
7 |
8 | type NewsFeedDB = Omit & MongoCollection;
9 |
10 | export default class NewsFeedMongoRepository
11 | extends MongoRepository
12 | implements NewsFeedRepository
13 | {
14 | constructor(mongoConector: MongoConector) {
15 | super(mongoConector, "newsFeeds");
16 | }
17 |
18 | protected mapToDomain(modelDB: NewsFeedDB): NewsFeed {
19 | const feed = NewsFeed.create({
20 | id: modelDB._id,
21 | name: modelDB.name,
22 | language: modelDB.language,
23 | type: modelDB.type,
24 | image: modelDB.image,
25 | url: modelDB.url,
26 | categories: modelDB.categories,
27 | }).get();
28 |
29 | return feed;
30 | }
31 |
32 | protected mapToDB(entity: NewsFeed): NewsFeedDB {
33 | const rawData = entity.toData();
34 |
35 | return renameProp("id", "_id", rawData) as NewsFeedDB;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/packages/server/src/newsfeeds/domain/usecases/GetNewsFeedByIdUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, NewsFeedData } from "karate-stars-core";
2 | import { ResourceNotFoundError, UnexpectedError } from "../../../common/api/Errors";
3 | import { AdminUseCase, AdminUseCaseArgs } from "../../../common/domain/AdminUseCase";
4 | import { createIdOrResourceNotFound } from "../../../common/domain/utils";
5 | import UserRepository from "../../../users/domain/boundaries/UserRepository";
6 | import NewsFeedsRepository from "../boundaries/NewsFeedRepository";
7 |
8 | export interface GetNewsFeedByIdArg extends AdminUseCaseArgs {
9 | id: string;
10 | }
11 |
12 | type GetNewsFeedByIdUError = ResourceNotFoundError | UnexpectedError;
13 |
14 | export class GetNewsFeedByIdUseCase extends AdminUseCase<
15 | GetNewsFeedByIdArg,
16 | GetNewsFeedByIdUError,
17 | NewsFeedData
18 | > {
19 | constructor(private newsFeedsRepository: NewsFeedsRepository, userRepository: UserRepository) {
20 | super(userRepository);
21 | }
22 |
23 | public async run({
24 | id,
25 | }: GetNewsFeedByIdArg): Promise> {
26 | const result = await createIdOrResourceNotFound(id)
27 | .flatMap(id => this.newsFeedsRepository.getById(id))
28 | .map(newsFeed => newsFeed.toData())
29 | .run();
30 |
31 | return result;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/core/src/value-objects/Email.ts:
--------------------------------------------------------------------------------
1 | import { ValueObject } from "./ValueObject";
2 | import { Either } from "../types/Either";
3 | import { ValidationErrorKey } from "../types/Errors";
4 | import { validateRequired, validateRegexp } from "../utils/validations";
5 |
6 | export interface UserEmailProps {
7 | value: string;
8 | }
9 |
10 | const EMAIL_PATTERN =
11 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
12 |
13 | export class Email extends ValueObject {
14 | public readonly value: string;
15 |
16 | private constructor(props: UserEmailProps) {
17 | super(props);
18 |
19 | this.value = props.value;
20 | }
21 |
22 | public static create(email: string): Either {
23 | const requiredError = validateRequired(email);
24 | const regexpErrors = validateRegexp(email, EMAIL_PATTERN);
25 |
26 | if (requiredError.length > 0) {
27 | return Either.left(requiredError);
28 | } else if (regexpErrors.length > 0) {
29 | return Either.left(regexpErrors);
30 | } else {
31 | return Either.right(new Email({ value: this.format(email) }));
32 | }
33 | }
34 |
35 | private static format(email: string): string {
36 | return email.trim().toLowerCase();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/packages/server/src/countries/domain/usecases/CreateCountryUseCase.ts:
--------------------------------------------------------------------------------
1 | import { Either, CountryData, Country, Id } from "karate-stars-core";
2 | import { ActionResult } from "../../../common/api/ActionResult";
3 | import { AdminUseCase, AdminUseCaseArgs } from "../../../common/domain/AdminUseCase";
4 | import { createResource, CreateResourceError } from "../../../common/domain/CreateResource";
5 | import UserRepository from "../../../users/domain/boundaries/UserRepository";
6 | import CountryRepository from "../boundaries/CountryRepository";
7 |
8 | export interface CreateResourceArgs extends AdminUseCaseArgs {
9 | data: CountryData;
10 | }
11 |
12 | export class CreateCountryUseCase extends AdminUseCase<
13 | CreateResourceArgs,
14 | CreateResourceError,
15 | ActionResult
16 | > {
17 | constructor(private countryRepository: CountryRepository, userRepository: UserRepository) {
18 | super(userRepository);
19 | }
20 |
21 | protected run({
22 | data,
23 | }: CreateResourceArgs): Promise, ActionResult>> {
24 | const createEntity = (data: CountryData) => Country.create(data);
25 | const getById = (id: Id) => this.countryRepository.getById(id);
26 | const saveEntity = (entity: Country) => this.countryRepository.save(entity);
27 |
28 | return createResource(data, createEntity, getById, saveEntity);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/core/src/types/MaybeAsync.ts:
--------------------------------------------------------------------------------
1 | import { Either } from "./Either";
2 | import { Maybe } from "./Maybe";
3 |
4 | export class MaybeAsync {
5 | private constructor(private readonly promiseValue: () => Promise>) {}
6 |
7 | map(fn: (value: Data) => T): MaybeAsync