Skip to content

Commit

Permalink
Merge pull request #26 from icefoganalytics/main
Browse files Browse the repository at this point in the history
Updates from IceFog
  • Loading branch information
datajohnson authored Oct 5, 2024
2 parents 758afb1 + e296e38 commit e43a1e6
Show file tree
Hide file tree
Showing 52 changed files with 2,893 additions and 1,743 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"eslint.workingDirectories": ["./api", "./web"]
"eslint.workingDirectories": ["./api", "./web"],
"eslint.validate": ["javascript", "typescript", "vue"]
}
14 changes: 7 additions & 7 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/express-jwt": "^6.0.4",
"@types/lodash": "^4.14.191",
"@types/lodash": "^4.17.9",
"@types/luxon": "^3.4.2",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.6.0",
Expand Down
11 changes: 11 additions & 0 deletions api/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,16 @@ export const DB_PASS = process.env.DB_PASS || ""
export const DB_NAME = process.env.DB_NAME || ""
export const DB_PORT = parseInt(process.env.DB_PORT || "1433")

export const DB_HEALTH_CHECK_INTERVAL_SECONDS = parseInt(
process.env.DB_HEALTH_CHECK_INTERVAL_SECONDS || "5"
)
export const DB_HEALTH_CHECK_TIMEOUT_SECONDS = parseInt(
process.env.DB_HEALTH_CHECK_TIMEOUT_SECONDS || "10"
)
export const DB_HEALTH_CHECK_RETRIES = parseInt(process.env.DB_HEALTH_CHECK_RETRIES || "3")
export const DB_HEALTH_CHECK_START_PERIOD_SECONDS = parseInt(
process.env.DB_HEALTH_CHECK_START_PERIOD_SECONDS || "5"
)

export const RELEASE_TAG = process.env.RELEASE_TAG || ""
export const GIT_COMMIT_HASH = process.env.GIT_COMMIT_HASH || ""
123 changes: 113 additions & 10 deletions api/src/controllers/base-controller.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,104 @@
import { NextFunction, Request, Response } from "express"
import { Attributes, Model, ScopeOptions, WhereOptions } from "sequelize"
import { isEmpty } from "lodash"

import { AuthorizedRequest } from "@/middleware/authz.middleware"
import User from "@/models/user"

export type Actions = "index" | "show" | "new" | "edit" | "create" | "update" | "destroy"

// See https://guides.rubyonrails.org/routing.html#crud-verbs-and-actions
export class BaseController {
protected request: Request
// See api/node_modules/sequelize/types/model.d.ts -> Model -> scope
export type BaseScopeOptions = string | ScopeOptions

// Keep in sync with web/src/api/base-api.ts
const MAX_PER_PAGE = 1000
const MAX_PER_PAGE_EQUIVALENT = -1
const DEFAULT_PER_PAGE = 10

/**
* See https://guides.rubyonrails.org/routing.html#crud-verbs-and-actions
*
* Usage:
* ```ts
* router
* .route("/api/users")
* .get(UsersController.index)
* .post(UsersController.create)
* route
* .route("/api/users/:userId")
* .get(UsersController.show)
* .patch(UsersController.update)
* .delete(UsersController.destroy)
* ```
* or
* ```ts
* router.get("/api/users", UsersController.index)
* // etc
* ```
*
* ```ts
* router.route("/api/users").get(UsersController.index)
* ```
* maps `/api/users` to `UsersController#index` method.
*/
export class BaseController<TModel extends Model = never> {
protected request: AuthorizedRequest
protected response: Response
protected next: NextFunction

constructor(req: Request, res: Response, next: NextFunction) {
this.request = req
// Assumes authorization has occured first in
// api/src/middlewares/jwt-middleware.ts and api/src/middlewares/authorization-middleware.ts
// At some future point it would make sense to do all that logic as
// controller actions
this.request = req as AuthorizedRequest
this.response = res
this.next = next
}

/**
* Usage:
* ```ts
* router.route("/api/users").get(UsersController.index)
* ```
* or
* ```ts
* router.get("/api/users", UsersController.index)
* ```
*/
static get index() {
return async (req: Request, res: Response, next: NextFunction) => {
const controllerInstance = new this(req, res, next)
return controllerInstance.index().catch(next)
}
}

// Usage app.post("/api/users", UsersController.create)
// maps /api/users to UsersController#create()
/**
* Usage:
* ```ts
* router.route("/api/users").post(UsersController.create)
* ```
* or
* ```ts
* router.post("/api/users", UsersController.create)
* ```
*/
static get create() {
return async (req: Request, res: Response, next: NextFunction) => {
const controllerInstance = new this(req, res, next)
return controllerInstance.create().catch(next)
}
}

/**
* Usage:
* ```ts
* router.route("/api/users/:userId").get(UsersController.show)
* ```
* or
* ```ts
* router.get("/api/users/:userId", UsersController.show)
* ```
*/
static get show() {
return async (req: Request, res: Response, next: NextFunction) => {
const controllerInstance = new this(req, res, next)
Expand Down Expand Up @@ -80,7 +147,7 @@ export class BaseController {
// Child controllers that are on an non-authorizable route should override this method
// and return undefined
get currentUser(): User {
return this.request.user as User
return this.request.user
}

get params() {
Expand All @@ -93,8 +160,8 @@ export class BaseController {

get pagination() {
const page = parseInt(this.query.page?.toString() || "") || 1
const perPage = parseInt(this.query.perPage?.toString() || "") || 10
const limit = perPage === -1 ? 1000 : Math.max(1, Math.min(perPage, 1000))
const perPage = parseInt(this.query.perPage?.toString() || "") || DEFAULT_PER_PAGE
const limit = this.determineLimit(perPage)
const offset = (page - 1) * limit
return {
page,
Expand All @@ -103,6 +170,42 @@ export class BaseController {
offset,
}
}

buildWhere<TModelOverride extends Model = TModel>(
overridableOptions: WhereOptions<Attributes<TModelOverride>> = {},
nonOverridableOptions: WhereOptions<Attributes<TModelOverride>> = {}
): WhereOptions<Attributes<TModelOverride>> {
// TODO: consider if we should add parsing of Sequelize [Op.is] and [Op.not] here
// or in the api/src/utils/enhanced-qs-decoder.ts function
const queryWhere = this.query.where as WhereOptions<Attributes<TModelOverride>>
return {
...overridableOptions,
...queryWhere,
...nonOverridableOptions,
} as WhereOptions<Attributes<TModelOverride>>
}

buildFilterScopes<FilterOptions extends Record<string, unknown>>(
initialScopes: BaseScopeOptions[] = []
): BaseScopeOptions[] {
const filters = this.query.filters as FilterOptions
const scopes = initialScopes
if (!isEmpty(filters)) {
Object.entries(filters).forEach(([key, value]) => {
scopes.push({ method: [key, value] })
})
}

return scopes
}

private determineLimit(perPage: number) {
if (perPage === MAX_PER_PAGE_EQUIVALENT) {
return MAX_PER_PAGE
}

return Math.max(1, Math.min(perPage, MAX_PER_PAGE))
}
}

export default BaseController
13 changes: 8 additions & 5 deletions api/src/controllers/fiscal-periods-controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { WhereOptions } from "sequelize"

import BaseController from "./base-controller"

import { FiscalPeriod } from "@/models"

export class FiscalPeriodsController extends BaseController {
export class FiscalPeriodsController extends BaseController<FiscalPeriod> {
async index() {
const where = this.query.where as WhereOptions<FiscalPeriod>
try {
const fiscalPeriods = await FiscalPeriod.findAll({
const where = this.buildWhere()
const scopes = this.buildFilterScopes()
const scopedFiscalPeriods = FiscalPeriod.scope(scopes)

const totalCount = await scopedFiscalPeriods.count({ where })
const fiscalPeriods = await scopedFiscalPeriods.findAll({
where,
order: [
["dateStart", "ASC"],
Expand All @@ -17,6 +19,7 @@ export class FiscalPeriodsController extends BaseController {
})
return this.response.json({
fiscalPeriods,
totalCount,
})
} catch (error) {
return this.response
Expand Down
Loading

0 comments on commit e43a1e6

Please sign in to comment.