Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/refactor to typescript #775

Open
wants to merge 72 commits into
base: dev
Choose a base branch
from
Open

Conversation

brarsanmol
Copy link
Member

@brarsanmol brarsanmol commented Nov 29, 2021

List of Changes:

  • Migrate from JavaScript to TypeScript for model and service layer.
  • Migrate from MongoDB to Postgres.
    • Migrate from Mongoose to TypeORM.
      • TypeORM supports SQL and Mongo, so we can go back.
  • Upgrade outdated packages
    • express-winston & winston.
  • Create start script for building and testing
  • Adding, removing, and updating members from teams.
  • RBAC seems to be completely broken as the relevant data is not being loaded into the database.
  • Migrate controllers and middleware to TypeScript.
  • Migrate to a decorator based approach to controllers and middleware.
  • Proper dependency injection (we now use TSyringe).

Incomplete Features:

  • Setting's do not load into the database (this is broken by purpose as this should be a configuration file?).
    • The Settings Controller returns the current Date + Time Period for Expiry as of now, but the model and service still needs to be created.

To-Do:

  • Flesh out a finalized database schema and verify that all relations between entities are valid.
  • Change testing kit to support SQL instead of Mongo (simply modifying the identifier's on the dummy objects).
    • Maybe migrate to Jest?

Future Vision:

  • Migrate to a decorator based approach to controllers and middleware.
    • A library already exists for the controller's portion.
      • https://www.npmjs.com/package/@decorators/express
      • It allows us to add middleware as a parameter onto the route but I would prefer if authentication and authorization were their own decorators.
        • Example: @Authorize("Admin,User,...")
        • Example: @Authenticated()

Type of Change:

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • New release
  • This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • Listed change(s) in the Changelog
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have made corresponding changes to the documentation
  • Any dependent changes have been merged and published in downstream modules

Questions:
- Do we need to write unit tests for this?
- Do we need to update/regenerate api docs to reflect the change in response data for the hacker model.
- Drew JSON for testing from the api docs and was getting a validator error, on closer inspection, the spelling of acccommodation was incorrect in the docs, and in my own commit for hacker.validator.js.
- Migrate from JavaScript to TypeScript for model and service layer.
- Migrate from MongoDB to Postgres.
  - Migrate from Mongoose to TypeORM.

- Upgrade outdated packages
  - express-winston & winston.

- Create start script for building and testing.

Incomplete Features:
- Adding, removing, and updating members from teams.
- RBAC seems to be completely broken as the relevant data is not being loaded into the database.
- Setting's do not load into the database (this is broken by purpose as this should be a configuration file?).
- Search Service is currently commented out as the logic needs to be rewritten to support SQL.

To Do (For Team?):
- Flesh out a finalized database schema and verify that all relations between entities are valid.
- Change testing kit to support SQL instead of Mongo (simply modifying the identifier's on the dummy objects).
  - Maybe migrate to Jest?
- Migrate controllers and middleware to TypeScript.
  - The middleware will be the more challenging portion for this.

Further Vision:
- Migrate to a decorator based approach to controllers and middleware.
  - A library already exists for the controller's portion.
    - https://www.npmjs.com/package/@decorators/express
    - It allows us to add middleware as a parameter onto the route but I would prefer if authentication and authorization were their own decorators.
      - Example: @authorize("Admin,User,...")
      - Example: @authenticated()
      - I could be swayed the other way if we find an elegant implementation for both using the inbuilt parameters.
- Proper dependency injection.
  - Find library similar to Guice (TSyringe maybe?).
@krubenok
Copy link
Member

IMO - this should target a branch other than dev since there will probably need to be patches to dev and main before this is ready to be used in prime time. Maybe a refactor branch or something.

Exciting though! cc: @pierreTklein

@krubenok
Copy link
Member

Migrate from MongoDB to Postgres.
Migrate from Mongoose to TypeORM.
TypeORM supports SQL and Mongo, so we can go back.

I explicitly disagree with migrating away from Mongo. I think Mongo is actually better for what the API is trying to do, but we're obviously not using it correctly. Schema-less gives us more flexibility to change things year to year and nest things tidily.

@krubenok
Copy link
Member

Setting's do not load into the database (this is broken by purpose as this should be a configuration file?).

I forget where we left off on this, but we wanted more of what's currently in the config file to be in the database. I think moving back towards a config file is a mistake.

@krubenok
Copy link
Member

Aside: I also think a major refactor like this is a good opportunity to investigate documentation and developer experience changes. We have API docs that are generated, but I think it's worth looking at alternatives to see if there's anything better out there.

Also repo onboarding easier and better documented would be hugely valuable.

@brarsanmol
Copy link
Member Author

brarsanmol commented Nov 30, 2021

IMO - this should target a branch other than dev since there will probably need to be patches to dev and main before this is ready to be used in prime time. Maybe a refactor branch or something.

Exciting though! cc: @pierreTklein

Agreed, I will create a new dedicated branch for this merge to feed into.

@brarsanmol
Copy link
Member Author

brarsanmol commented Nov 30, 2021

Migrate from MongoDB to Postgres.
Migrate from Mongoose to TypeORM.
TypeORM supports SQL and Mongo, so we can go back.

I explicitly disagree with migrating away from Mongo. I think Mongo is actually better for what the API is trying to do, but we're obviously not using it correctly. Schema-less gives us more flexibility to change things year to year and nest things tidily.

Hmm, I see. We would definitely need to come up with a better domain model, a suggestion might be embedding objects? My initial push towards Postgres was because of JSONB, as that would still allow us to store unstructured data, and SQL being a first class citizen with TypeORM.

@brarsanmol
Copy link
Member Author

Setting's do not load into the database (this is broken by purpose as this should be a configuration file?).

I forget where we left off on this, but we wanted more of what's currently in the config file to be in the database. I think moving back towards a config file is a mistake.

Sounds good, I broke this functionality as the default values that were provided in the database were causing issues when launching the application, and I did not have the exact amount of time to figure out why, I will investigate further and reintegrate the functionality.

@brarsanmol
Copy link
Member Author

brarsanmol commented Nov 30, 2021

Aside: I also think a major refactor like this is a good opportunity to investigate documentation and developer experience changes. We have API docs that are generated, but I think it's worth looking at alternatives to see if there's anything better out there.

Also repo onboarding easier and better documented would be hugely valuable.

This will be my next step. Two ideas that I have of the top of my head are:

  • Integrating the stale PR from @meldunn for Postman Collections.
  • Write a web-page or script for developer onboarding.
    • Webpage could be something similar to this from Gitea/Gogs:

Documentation is tricky, there is still a lot more code to rewrite, as the aim was simply to get the codebase functioning with this initial commit. I would suggest that a formal write-up be made for the future of the application architecture and then we slowly build up from there documenting middleware and controllers as necessary. The service layer require's minimum modification other than a few redundant functions that can be removed.

@krubenok
Copy link
Member

Good on the other bits re:onboarding.

Documentation is tricky, there is still a lot more code to rewrite, as the aim was simply to get the codebase functioning with this initial commit. I would suggest that a formal write-up be made for the future of the application architecture and then we slowly build up from there documenting middleware and controllers as necessary. The service layer require's minimum modification other than a few redundant functions that can be removed.

Yeah - I meant more examining other doc generation packages and seeing if we still like the one we're using. For example, the one we're using makes it hard to do that postman generation that @meldunn and I did. Other docstrings and doc generation tools make that a lot easier.

@krubenok
Copy link
Member

Migrate from MongoDB to Postgres.
Migrate from Mongoose to TypeORM.
TypeORM supports SQL and Mongo, so we can go back.

I explicitly disagree with migrating away from Mongo. I think Mongo is actually better for what the API is trying to do, but we're obviously not using it correctly. Schema-less gives us more flexibility to change things year to year and nest things tidily.

Hmm, I see. We would definitely need to come up with a better domain model, a suggestion might be embedding objects? My initial push towards Postgres was because of JSONB, as that would still allow us to store unstructured data, and SQL being a first class citizen with TypeORM.

Yes - We should be nesting objects in Mongo. We've discussed some ideas around this separately too. I'm curious what a now wiser @pierreTklein thinks about this now though...

@pierreTklein
Copy link
Member

Thanks for this! Cool to see the backend converted to TS. I would personally push to separate out each of these bullet points into separate PRs, because they seem unrelated to the typescript migration.

That way, we can focus on each part more closely.

As for the database schema migration, I'm fine with nested schemas. Would be happy to review changes.

@brarsanmol
Copy link
Member Author

brarsanmol commented Nov 30, 2021

Thanks for this! Cool to see the backend converted to TS. I would personally push to separate out each of these bullet points into separate PRs, because they seem unrelated to the typescript migration.

That way, we can focus on each part more closely.

I agree, I will set up the refactor branch sometime today and split up the changes recommended in this PR to separate requests.

As for the database schema migration, I'm fine with nested schemas. Would be happy to review changes.

I would love to set up some time to chat with you and @krubenok further on this. There are still some prickly models that might cause issues, for example one difficult implementation might be Teams, simply nesting the object into each user would be a terrible choice as it would create massive duplication, and using the relational id tag would lead us back down the same road that we are on.

Edit: I had a chat with @krubenok earlier, he suggested an async discussion format on a GitHub issue, I will create this issue later tonight and the relevant discussions can take place there with synchronous discussions being organized later if need be.

@brarsanmol brarsanmol linked an issue Nov 30, 2021 that may be closed by this pull request
@brarsanmol
Copy link
Member Author

brarsanmol commented Dec 8, 2021

I've gotten back on the saddle for a short period again. Here is some of the work that I've done with decorators and how the future service and controller layer may look.

I have already migrated the Database Service, Logger Service, and Environment Service to a similar format, I can commit said code if needed, but as of now I'm not deeming it essential.

Account Controller

@autoInjectable()
@controller("/account")
export class AccountController {
    constructor(private readonly accountService: AccountService) {}

    @action(ActionType.GET, "/")
    @middleware([ensureAuthenticated, ensureAuthorized])
    async get(request: Request, response: Response) {
        const account:
            | Account
            | undefined = await this.accountService.findByEmail(
            request.user?.email
        );

        if (!account) {
            return response.status(400).json({
                message: ErrorConstants.ACCOUNT_404_MESSAGE
            });
        }

        return response.status(200).json({
            message: SuccessConstants.ACCOUNT_READ,
            data: account.toStrippedJSON()
        });
    }
}

Account Service

@autoInjectable()
export class AccountService {
    constructor(
        private readonly userRepository: Repository<Account> = getRepository(
            Account
        ),
        private readonly loggerService: LoggerService
    ) {}

    public async findByIdentifier(
        identifier: number
    ): Promise<Account | undefined> {
        const TAG = `[Account Service # findByIdentifier]:`;
        return await this.userRepository
            .findOne(identifier)
            .then((account: Account) => {
                this.loggerService.queryCallbackFactory(
                    TAG,
                    "account",
                    identifier
                );
                return account;
            });
    }

    public async findByEmail(email: string): Promise<Account | undefined> {
        const TAG = `[Account Service # findByEmail]:`;
        return await this.userRepository
            .findOne({ where: { email: email } })
            .then((account: Account) => {
                this.loggerService.queryCallbackFactory(TAG, "account", email);
                return account;
            });
    }

    public async save(account: Partial<Account>): Promise<Account> {
        const TAG = `[Account Service # addAccount]:`;
        return await this.userRepository.save(account);
    }

    public async update(
        identifier: number,
        account: Object
    ): Promise<UpdateResult> {
        const TAG = `[Account Service # update]:`;
        return await this.userRepository.update(identifier, account);
    }

    public async updatePassword(identifier: number, password: string) {
        return await this.userRepository.update(identifier, {
            password: this.hashPassword(password)
        });
    }

    public async getAccountIfValid(
        email: string,
        password: string
    ): Promise<Account | undefined> {
        const account = await this.findByEmail(email);
        return account && account.comparePassword(password)
            ? account
            : undefined;
    }

    public hashPassword(password: string): string {
        return hashSync(password, 10);
    }
}

Edit: I will create attempt to organize this work soon into multiple PR's or places where we can facilitate discussion once I am free from exams, as of now I see this as fit because I don't see much more work being done over the next 2 weeks on this.

@brarsanmol
Copy link
Member Author

brarsanmol commented Dec 9, 2021

Some further progress has been made. I decided integrate a library that I was using by Lucas Huggler into our codebase as a ways to create a more elegant solution. It is licensed through MIT so I believe it should be okay for us to use it as long as we provide original credit on the router.*.ts files.

This is some really important code, and I don't think I'll be able to get my project compiling for a little bit so any bug's that can be caught would much appreciated!

router.service.ts

type Method = (
    request: Request,
    response: Response,
    next: NextFunction
) => Promise<Response<any, Record<string, any>>>;

export enum ActionType {
    GET = "get",
    POST = "post",
    PUT = "put",
    DELETE = "delete",
    PATCH = "patch",
    ALL = "all"
}

interface ActionMetadata {
    type: ActionType;
    route: string;
}

@autoInjectable()
export class RouterService {
    constructor(
        private readonly router: Router = Router(),
        private readonly controllers: Array<Function>
    ) {
        this.controllers.forEach((controller) =>
            this.addController(controller)
        );
    }

    private addController(controller: Function): void {
        const actions = Object.getOwnPropertyNames(controller.prototype);
        const metadata = Reflect.getMetadata("controller", controller);
        if (!metadata) return;
        for (const action in actions) {
            this.addAction(
                metadata.route,
                action,
                controller.prototype[action]
            );
        }
    }

    private addAction(parent: string, action: string, method: Method) {
        const metadata: ActionMetadata = Reflect.getMetadata("action", action);
        if (!metadata) return;
        this.addMiddleware(parent + metadata.route, action);
        this.addActionUsingAppropriateMethod(parent, metadata, method);
    }

    private addMiddleware(route: string, action: string) {
        const metadata: Array<Method> = Reflect.getMetadata(
            "middleware",
            action
        );
        if (!metadata) return;
        metadata.forEach((middleware) => this.router.use(route, middleware));
    }

    private addActionUsingAppropriateMethod(
        parent: string,
        { type, route }: ActionMetadata,
        method: Method
    ) {
        route = parent + route;
        switch (type) {
            case ActionType.GET:
                this.router.get(route, method);
                break;
            case ActionType.POST:
                this.router.post(route, method);
                break;
            case ActionType.PUT:
                this.router.put(route, method);
                break;
            case ActionType.DELETE:
                this.router.delete(route, method);
                break;
            case ActionType.PATCH:
                this.router.patch(route, method);
                break;
            case ActionType.ALL:
                this.router.all(route, method);
                break;
        }
    }
}

router.decorators.ts

export const controller = (route: string): Function => {
    return (constructor: Function) =>
        Reflect.defineMetadata("controller", route, constructor);
};

export const action = (type: ActionType, route: string): Function => {
    return (target: any, key: string, descriptor: PropertyDescriptor) =>
        Reflect.defineMetadata(
            "action",
            { type, route },
            descriptor.value,
            key
        );
};

export const middleware = (functions: Array<Function>) => {
    return (target: any, key: string, descriptor: PropertyDescriptor) =>
        Reflect.defineMetadata("middleware", functions, descriptor.value, key);
};

A lot has changed since this comment, see issue #781, and the following commit.

…and controllers.

- change main application launch point from bin/www.js to app.ts
- wrap app.ts in an async call to allow database connection. (async/await for #connect()).
- general code cleanup for app.ts

- move the following services to the decorator/class-based model with dependency injection:
  - EnvService
  - DatabaseService
  - LoggerService
  - AccountService

- move the following controllers to the decorator/class-based model with dependency injection:
  - ApplicationController

- create class-based authentication and authorization.
  - EnsureAuthenticated
  - EnsureAuthorized
  - authorization no longer depends on the :self/:all pre-definition in a constants file and instead checks against the authorization level and the identifier provided by an authenticated user to verify permission to access resources.

- move passport authentication strategy to it's own class and combine the user serialization/de-serialization with it.

- refactor LoggerService to provide general purpose logger instead of exporting winston loggers.

- fix EnvService GCP file creation (added a schema).
  - fix small bug where error would be thrown regardless of launch for EnvService as we were checking if a result was not null instead of result.error.

- refactor DatabaseService:
  - genericize database information method (port, host, etc...).
  - remove application callbacks and logging
    - we should add this back in a later update.

- add class-transformer to strip password from JSON response on account model.
- add class-validator for fields on account model.
  - expand the use of this package to all models.

- change the following constant files to typescript:
  - Error Constants
  - General Constants
  - Success Constants

- todos:
  - various todo's have been left throughout the codebase, it would be fruitless to leave a list here, as you can simply query them by searching:
    - TODO
why the change? ->
  - reducing code complexity and supporting all major e-mail clients automatically.
  - ability to use Passport (GUI Application) which allows you to design e-mails and export them to MJML (afaik).
    - allowing dev team to get code directly from design team.

todo ->
  - update social links (facebook group, slack to discord) for mchacks 9.
  - update any remaining mchacks 7 mentions to mchacks 9.
  - as MJML is simply a markup language we still require a templating engine such as handlebars to handle variable injection.
    - in our code where we call the handlebars template compile method we must call the mjml2html method internally.
      - example: compileHandlebarsTemplate(mjml2html(...)).
- remove intellij/jetbrain ide configuration files (no one on our team uses these anymore afaik).
- remove Dockerfile as we no longer deploy on Google Cloud.
- remove Google Cloud service and ingress files for deploy as once again we no longer deploy on Google Cloud.
- remove www.js as the entry point for our app is now app.ts
- remove old route, controller, and service files related to accounts/auth.

- TODO:
  - remove ability to update password from the AccountService#update function.
  - implementing the resending of account confirmation logic on e-mail update.
  - invites:
    - implement the ability to send invites.
    - implement the ability to track who sent the invites.
    - automatic account confirmation logic
    - 100% of the work remains to be done here essentially.

- NOTES:
  - we should maybe remove the ability to update the e-mail from the AccountService#update function as well.
  - maybe following an implementation similar to the password update flow would be better with a unique route and unique service function.
    - route controller function calls the updateEmail function in the service
    - route controller function also resends the mail and all the logic required there.
…rs, and seed files.

- remove old routing files.
  - we use decorator based routing so these centralized files are no longer necessary.
  - some files are for models that we no longer need as RBAC is based on decorators as well
    - roles, role-bindings.

- remove seed files.
  - these were used for role-bindings and RBAC.
  - as we have moved to decorator-based routing and authorization info is stored in the codebase itself instead of the database this is no longer required.
  - the settings seed file is also not required.

- remove old role and role binding service.
  - same reason as mentioned above for RBAC / authorization.

- remove parse-patch service as we use decorator based routing.

- remove email-template model.
  - future plans to implement CRUD interface for sending out e-mails.
    - never got implemented.

- delete route constants file as we have moved to decorator based routing.
- BENEFITS:
  - prevent's vendor lock-in as before we were using sendgrid's proprietary library.
    - this will help prevent incidents such as what occurred on december 1st - 7th.
      - in essence allow's us to use any smtp mailing service as long as we swap the information in the .env file.
  - allow's developers to catch mail using mailcatcher, mailtrap, etc...
    - (no longer having to distribute sendgrid key to devs)
    - no longer have to go to the database to get account confirmation tokens, or password reset tokens.

- TODO:
  - implement a *.mailer.ts pattern.
    - authentication.mailer.ts
      - send forgot password, account confirmation, etc...
    - BENEFITS:
      - will clean up the code in the controller layer.
      - will establish clean boundaries between the mailing layer and controller layer, improving the codebase overall.
@brarsanmol brarsanmol linked an issue Jan 21, 2022 that may be closed by this pull request
@Controller("/settings")
export class SettingsController {
@Get("/")
getAll(@Response() response: ExpressResponse) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should hit the settings service

- add visual studio code launch configurations for tests.
- reconfigure tsconfig.json to ignore tests.
- add initial jest configuration with path/module aliases.
- leverage class-validator to validate against entities.
- retrieve errors and flatmap them to allow for a clean output back
  to the dashboard.
- add ability to query all account confirmation tokens.
- add ability on account creation endpoint
  to have invite token to set role and auto-confirmation.
- add ability to create invites with /invite endpoint.
- add placeholders for setting values in the e-mail template files
- add default seed values for settings
- Add Filter interface to model the basic search filter.
- Add Operation enum to only allow a finite set of SQL operators to be used.
- Remove older way of putting filter's in the query string.
  - Now we require them to be in the request body as an JSON array modeled after the Filter
    interface.
- Remove old Jekyll documentation.
  - Will be replaced in the hackmcgill/documentation repository.
- Remove old api-docs.
  - Find a new library or just re-do them for the new API inside the controllers?
- remove dependabot
- move README from the root to .github/
- remove old bug report and PR templates.
- remove CHANGELOG
- since we have dockerized we don't need a .nvmrc.
- we no longer need the .vscode folder as we removed the runners for the tests.
- remove setupEnv.sh script as we have dockerized.
…commit standards

- add .lintstagedrc to auto-lint files on commit (.husky/pre-commit)
- add .czrc and .commitlintrc.js for enforcing conventional commit standards.
- formalize formatting and linting commands in package.json.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
refactor Code refactors
Projects
None yet
3 participants