Skip to content

Commit

Permalink
Merge pull request #112 from unblinking/recipe-feature
Browse files Browse the repository at this point in the history
Recipe feature
  • Loading branch information
jmg1138 authored Mar 1, 2023
2 parents c2a6bfc + 1faf9fa commit 47a6e6b
Show file tree
Hide file tree
Showing 32 changed files with 1,371 additions and 8 deletions.
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ build/Release
node_modules/
jspm_packages/

# Typescript declaration and map files
# Typescript declaration and map filesf
typings/
*.d.ts.map

Expand Down Expand Up @@ -128,4 +128,9 @@ build/

# Don't track css files. They are generated pre-build.
*.css
*.css.map
*.css.map

# Svelte-kit
.svelte-kit/
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
1 change: 1 addition & 0 deletions packages/api/controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './src/controllers/responder'
export * from './src/controllers/base-controller'
export * from './src/controllers/account-controller'
export * from './src/controllers/feature-controller'
export * from './src/controllers/recipe-controller'
export * from './src/controllers/role-controller'
export * from './src/controllers/root-controller'
export * from './src/controllers/user-controller'
196 changes: 196 additions & 0 deletions packages/api/src/controllers/recipe-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* The recipe controller and routes.
*
* @author Joshua Gray {@link https:/jmg1138}
* @copyright Copyright (C) 2017-2022
* @license GNU AGPLv3 or later
*
* This file is part of Recipe.Report.
* @see {@link https:/unblinking/recipe-report}
*
* Recipe.Report is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* Recipe.Report is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @module
*/
import type { IBaseController } from '@recipe-report/api/controllers'
import { Responder } from '@recipe-report/api/controllers'
import { SYMBOLS } from '@recipe-report/api/ioc'
import type { RequestWithUser } from '@recipe-report/api/middlewares'
import { fiveHundred, tokenwall } from '@recipe-report/api/middlewares'
import { httpStatus, outcomes } from '@recipe-report/data'
import { Err, errClient, isErrClient } from '@recipe-report/domain/models'
import { RecipeRequest, UuidRequest } from '@recipe-report/domain/services'
import type { IRecipeService } from '@recipe-report/service'
import type { NextFunction, Response } from 'express'
import { Router } from 'express'
import { inject, injectable } from 'inversify'
import 'reflect-metadata'

@injectable()
export class RecipeController implements IBaseController {
private _recipeService: IRecipeService
router: Router = Router()
path: string = `/v1/recipes`

public constructor(@inject(SYMBOLS.IRecipeService) recipeService: IRecipeService) {
this._recipeService = recipeService
this.initRoutes()
}

public initRoutes = (): void => {
this.router.post(`/`, tokenwall, this.create)
this.router.get(`/:id`, tokenwall, this.read)
this.router.put(`/:id`, tokenwall, this.update)
this.router.delete(`/:id`, tokenwall, this.delete)
this.router.use(fiveHundred) // Error handling.
}

private create = async (
req: RequestWithUser,
res: Response,
next: NextFunction,
): Promise<void> => {
try {
const svcReq = RecipeRequest.create({ ...req.body })
const svcRes = await this._recipeService.create(svcReq)
const code = svcRes.statusCode
switch (svcRes.outcome) {
case outcomes.SUCCESS:
Responder.success(res, code, { recipe: svcRes.item })
break
case outcomes.FAIL:
Responder.fail(res, code, svcRes.err?.message, svcRes.err?.name)
break
default:
Responder.error(res, code, svcRes.err?.message, svcRes.err?.name)
break
}
} catch (e) {
// The caught e could be anything. Turn it into an Err.
const err = Err.toErr(e)
// If the error message can be client facing, return BAD_REQUEST.
if (isErrClient(err.name)) {
err.message = `${errClient.RECIPE_CREATE} ${err.message}`
Responder.fail(res, httpStatus.BAD_REQUEST, err.message, err.name)
} else {
next(err)
}
}
}

private read = async (req: RequestWithUser, res: Response, next: NextFunction): Promise<void> => {
try {
if (!req.params['id']) {
throw new Err('UID_INVALID', errClient.UID_INVALID)
}
const svcReq = UuidRequest.create(req.params['id'], req.authorizedId)
const svcRes = await this._recipeService.read(svcReq)
const code = svcRes.statusCode
switch (svcRes.outcome) {
case outcomes.SUCCESS:
Responder.success(res, code, { recipe: svcRes.item })
break
case outcomes.FAIL:
Responder.fail(res, code, svcRes.err?.message, svcRes.err?.name)
break
default:
Responder.error(res, code, svcRes.err?.message, svcRes.err?.name)
break
}
} catch (e) {
// The caught e could be anything. Turn it into an Err.
const err = Err.toErr(e)
// If the error message can be client facing, return BAD_REQUEST.
if (isErrClient(err.name)) {
err.message = `${errClient.RECIPE_READ} ${err.message}`
Responder.fail(res, httpStatus.BAD_REQUEST, err.message, err.name)
} else {
next(err)
}
}
}

private update = async (
req: RequestWithUser,
res: Response,
next: NextFunction,
): Promise<void> => {
try {
if (req.params['id'] !== req.body.id) {
throw new Err(`ID_MISMATCH`, errClient.ID_MISMATCH)
}
const svcReq = RecipeRequest.create({ ...req.body }, req.authorizedId)
const svcRes = await this._recipeService.update(svcReq)
const code = svcRes.statusCode
switch (svcRes.outcome) {
case outcomes.SUCCESS:
Responder.success(res, code, { recipe: svcRes.item })
break
case outcomes.FAIL:
Responder.fail(res, code, svcRes.err?.message, svcRes.err?.name)
break
default:
Responder.error(res, code, svcRes.err?.message, svcRes.err?.name)
break
}
} catch (e) {
// The caught e could be anything. Turn it into an Err.
const err = Err.toErr(e)
// If the error message can be client facing, return BAD_REQUEST.
if (isErrClient(err.name)) {
err.message = `${errClient.RECIPE_UPDATE} ${err.message}`
Responder.fail(res, httpStatus.BAD_REQUEST, err.message, err.name)
} else {
next(err)
}
}
}

private delete = async (
req: RequestWithUser,
res: Response,
next: NextFunction,
): Promise<void> => {
try {
if (!req.params['id']) {
throw new Err('UID_INVALID', errClient.UID_INVALID)
}
const svcReq = UuidRequest.create(req.params['id'], req.authorizedId)
const svcRes = await this._recipeService.delete(svcReq)
const code = svcRes.statusCode
switch (svcRes.outcome) {
case outcomes.SUCCESS:
Responder.success(res, code, { recipe: svcRes.item })
break
case outcomes.FAIL:
Responder.fail(res, code, svcRes.err?.message, svcRes.err?.name)
break
default:
Responder.error(res, code, svcRes.err?.message, svcRes.err?.name)
break
}
} catch (e) {
// The caught e could be anything. Turn it into an Err.
const err = Err.toErr(e)
// If the error message can be client facing, return BAD_REQUEST.
if (isErrClient(err.name)) {
err.message = `${errClient.RECIPE_DELETE} ${err.message}`
Responder.fail(res, httpStatus.BAD_REQUEST, err.message, err.name)
} else {
next(err)
}
}
}
}

5 changes: 5 additions & 0 deletions packages/api/src/ioc.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import {
AccountController,
FeatureController,
RecipeController,
RoleController,
RootController,
UserController,
Expand All @@ -43,6 +44,7 @@ import type {
IEmailService,
IFeatureService,
IJwtService,
IRecipeService,
IRoleService,
IUserService,
} from '@recipe-report/service'
Expand All @@ -52,6 +54,7 @@ import {
EmailService,
FeatureService,
JwtService,
RecipeService,
RoleService,
UserService,
} from '@recipe-report/service'
Expand All @@ -68,6 +71,7 @@ container.bind<IRecipeReport>(SYMBOLS.IRecipeReport).to(RecipeReport)
// Add the controllers to the container.
container.bind<IBaseController>(SYMBOLS.IBaseController).to(AccountController)
container.bind<IBaseController>(SYMBOLS.IBaseController).to(FeatureController)
container.bind<IBaseController>(SYMBOLS.IBaseController).to(RecipeController)
container.bind<IBaseController>(SYMBOLS.IBaseController).to(RoleController)
container.bind<IBaseController>(SYMBOLS.IBaseController).to(RootController)
container.bind<IBaseController>(SYMBOLS.IBaseController).to(UserController)
Expand All @@ -82,6 +86,7 @@ container.bind<ICryptoService>(SYMBOLS.ICryptoService).to(CryptoService)
container.bind<IEmailService>(SYMBOLS.IEmailService).to(EmailService)
container.bind<IFeatureService>(SYMBOLS.IFeatureService).to(FeatureService)
container.bind<IJwtService>(SYMBOLS.IJwtService).to(JwtService)
container.bind<IRecipeService>(SYMBOLS.IRecipeService).to(RecipeService)
container.bind<IRoleService>(SYMBOLS.IRoleService).to(RoleService)
container.bind<IUserService>(SYMBOLS.IUserService).to(UserService)

Expand Down
1 change: 1 addition & 0 deletions packages/api/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const SYMBOLS = {
IEmailService: Symbol.for('IEmailService'),
IFeatureService: Symbol.for('IFeatureService'),
IJwtService: Symbol.for('IJwtService'),
IRecipeService: Symbol.for('IRecipeService'),
IRoleService: Symbol.for('IRoleService'),
IUserService: Symbol.for('IUserService'),
}
1 change: 1 addition & 0 deletions packages/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
export const dbTables = {
ACCOUNTS: `rr.accounts`,
FEATURES: `rr.features`,
RECIPES: `rr.recipes`,
ROLES: `rr.roles`,
ROLES_TO_FEATURES: `rr.roles_to_features`,
USERS: `rr.users`,
Expand Down
Loading

0 comments on commit 47a6e6b

Please sign in to comment.