diff --git a/server/app.ts b/server/app.ts index efbd18b..b8ef64c 100644 --- a/server/app.ts +++ b/server/app.ts @@ -5,6 +5,7 @@ import responseTime from 'response-time'; import organizationsRouterV2 from 'routers/organizationsRouterV2'; import boundsRouter from './routers/boundsRouter'; import capturesRouter from './routers/capturesRouter'; +import contractsRouter from './routers/contractsRouter'; import countriesRouter from './routers/countriesRouter'; import gisRouter from './routers/gisRouter'; import growerAccountsRouter from './routers/growerAccountsRouter'; @@ -80,6 +81,7 @@ app.use('/v2/growers', growerAccountsRouter); app.use('/v2/trees', treesRouterV2); app.use('/bounds', boundsRouter); app.use('/gis', gisRouter); +app.use('/contract', contractsRouter); // Global error handler app.use(errorHandler); diff --git a/server/infra/database/ContractRepository.ts b/server/infra/database/ContractRepository.ts new file mode 100644 index 0000000..b3b35ee --- /dev/null +++ b/server/infra/database/ContractRepository.ts @@ -0,0 +1,193 @@ +import Contract from 'interfaces/Contract'; +import ContractFilter from 'interfaces/ContractFilter'; +import FilterOptions from 'interfaces/FilterOptions'; +import HttpError from 'utils/HttpError'; +import BaseRepository from './BaseRepository'; +import Session from './Session'; + +export default class ContractRepository extends BaseRepository { + constructor(session: Session) { + super('contract', session); + this.tableName = 'contracts.contract'; + } + + filterWhereBuilder(object, builder) { + const result = builder; + const { + whereNulls = [], + whereNotNulls = [], + whereIns = [], + ...parameters + } = object; + + if (parameters.tokenized === 'true') { + whereNotNulls.push('wallet.token.id'); + } else if (parameters.tokenized === 'false') { + whereNulls.push('wallet.token.id'); + } + delete parameters.tokenized; + + result.whereNot(`${this.tableName}.status`, 'deleted'); + + whereNotNulls.forEach((whereNot) => { + // to map table names to fields for query + switch (true) { + case whereNot === 'tag_id': + result.whereNotNull('treetracker.capture_tag.tag_id'); + break; + default: + result.whereNotNull(whereNot); + } + }); + + whereNulls.forEach((whereNull) => { + // to map table names to fields for query + switch (true) { + case whereNull === 'tag_id': + result.whereNull('treetracker.capture_tag.tag_id'); + break; + default: + result.whereNull(whereNull); + } + }); + + whereIns.forEach((whereIn) => { + result.whereIn(whereIn.field, whereIn.values); + }); + + const filterObject = { ...parameters }; + + if (filterObject.startDate) { + result.where( + `${this.tableName}.captured_at`, + '>=', + filterObject.startDate, + ); + delete filterObject.startDate; + } + if (filterObject.endDate) { + result.where(`${this.tableName}.captured_at`, '<=', filterObject.endDate); + delete filterObject.endDate; + } + + if (filterObject.id) { + result.where(`${this.tableName}.id`, '=', filterObject.id); + delete filterObject.id; + } + + if (filterObject.organization_id) { + result.where(`${this.tableName}.growing_organization_id`, 'in', [ + ...filterObject.organization_id, + ]); + delete filterObject.organization_id; + } + + result.where(filterObject); + } + + async getByFilter(filterCriteria: ContractFilter, options: FilterOptions) { + const knex = this.session.getDB(); + const { sort, ...filter } = filterCriteria; + + let promise = knex + .select( + knex.raw( + ` + ${this.tableName}.id, + ${this.tableName}.status, + ${this.tableName}.notes, + ${this.tableName}.created_at, + ${this.tableName}.updated_at, + ${this.tableName}.signed_at, + ${this.tableName}.closed_at, + ${this.tableName}.listed, + row_to_json(agreement.*) AS agreement, + row_to_json(grower_account.*) AS worker, + row_to_json(stakeholder.*) AS stakeholder + FROM ${this.tableName} + LEFT JOIN contracts.agreement AS agreement + ON agreement.id = ${this.tableName}.agreement_id + LEFT JOIN stakeholder.stakeholder AS stakeholder + ON stakeholder.id = agreement.growing_organization_id + LEFT JOIN treetracker.grower_account AS grower_account + ON grower_account.id = ${this.tableName}.worker_id + `, + ), + ) + .where((builder) => this.filterWhereBuilder(filter, builder)); + + promise = promise.orderBy( + `${this.tableName}.${sort?.order_by}` || `${this.tableName}.id`, + sort?.order || 'desc', + ); + + const { limit, offset } = options; + if (limit) { + promise = promise.limit(limit); + } + if (offset) { + promise = promise.offset(offset); + } + + const captures = await promise; + + return captures; + } + + async getCount(filterCriteria: ContractFilter) { + const knex = this.session.getDB(); + const { ...filter } = filterCriteria; + + const result = await knex + .select( + knex.raw( + `COUNT(*) AS count + FROM ${this.tableName} + LEFT JOIN contracts.agreement AS agreement + ON agreement.id = ${this.tableName}.agreement_id + LEFT JOIN stakeholder.stakeholder AS stakeholder + ON stakeholder.id = agreement.growing_organization_id + LEFT JOIN treetracker.grower_account AS grower_account + ON grower_account.id = ${this.tableName}.worker_id + `, + ), + ) + .where((builder) => this.filterWhereBuilder(filter, builder)); + + return result[0].count; + } + + async getById(id: string) { + const object = await this.session + .getDB() + .select( + this.session.getDB().raw(` + ${this.tableName}.id, + ${this.tableName}.status, + ${this.tableName}.notes, + ${this.tableName}.created_at, + ${this.tableName}.updated_at, + ${this.tableName}.signed_at, + ${this.tableName}.closed_at, + ${this.tableName}.listed, + row_to_json(agreement.*) AS agreement, + row_to_json(grower_account.*) AS worker, + row_to_json(stakeholder.*) AS stakeholder + FROM ${this.tableName} + LEFT JOIN contracts.agreement AS agreement + ON agreement.id = ${this.tableName}.agreement_id + LEFT JOIN stakeholder.stakeholder AS stakeholder + ON stakeholder.id = agreement.growing_organization_id + LEFT JOIN treetracker.grower_account AS grower_account + ON grower_account.id = ${this.tableName}.worker_id + `), + ) + .where(`${this.tableName}.id`, id) + .first(); + + if (!object) { + throw new HttpError(404, `Can not find ${this.tableName} by id:${id}`); + } + return object; + } +} diff --git a/server/infra/database/GrowerAccountRepository.ts b/server/infra/database/GrowerAccountRepository.ts index 68f7ed2..2c31332 100644 --- a/server/infra/database/GrowerAccountRepository.ts +++ b/server/infra/database/GrowerAccountRepository.ts @@ -100,7 +100,11 @@ export default class GrowerAccountRepository extends BaseRepository { .first(); if (!object) { - throw new HttpError(404, `Can not find ${this.tableName} by uuid:${uuid}`); + throw new HttpError( + 404, + `Can not find ${this.tableName} by uuid:${uuid}`, + ); } return object; } - async getByOrganization( organization_id: number, options: FilterOptions, diff --git a/server/interfaces/Contract.ts b/server/interfaces/Contract.ts new file mode 100644 index 0000000..820a82d --- /dev/null +++ b/server/interfaces/Contract.ts @@ -0,0 +1,14 @@ +import DbModel from './DbModel'; + +export default interface Contract extends DbModel { + id: number; + agreement_id: number; + worker_id: number; + status: string; + notes: string; + created_at: string; + updated_at: string; + signed_at: string; + closed_at: string; + listed: true; +} diff --git a/server/interfaces/ContractFilter.ts b/server/interfaces/ContractFilter.ts new file mode 100644 index 0000000..ff72c36 --- /dev/null +++ b/server/interfaces/ContractFilter.ts @@ -0,0 +1,16 @@ +import DbModel from './DbModel'; + +interface ContractFilter extends DbModel { + id?: string | undefined; + agreement_id?: string | undefined; + worker_id?: string | undefined; + status?: string | undefined; + notes?: string | undefined; + created_at?: string | undefined; + updated_at?: string | undefined; + signed_at?: string | undefined; + closed_at?: string | undefined; + listed?: true | false; +} + +export default ContractFilter; diff --git a/server/models/Contract.ts b/server/models/Contract.ts new file mode 100644 index 0000000..8e3afcc --- /dev/null +++ b/server/models/Contract.ts @@ -0,0 +1,29 @@ +import ContractRepository from 'infra/database/ContractRepository'; +import { delegateRepository } from 'infra/database/delegateRepository'; +import Contract from 'interfaces/Contract'; +import ContractFilter from 'interfaces/ContractFilter'; +import FilterOptions from 'interfaces/FilterOptions'; + +function getByFilter( + contractRepository: ContractRepository, +): (filter: ContractFilter, options: FilterOptions) => Promise { + return async function (filter: ContractFilter, options: FilterOptions) { + const contracts = await contractRepository.getByFilter(filter, options); + return contracts; + }; +} + +function getCount( + contractRepository: ContractRepository, +): (filter: ContractFilter) => Promise { + return async function (filter: ContractFilter) { + const count = await contractRepository.getCount(filter); + return count; + }; +} + +export default { + getByFilter, + getCount, + getById: delegateRepository('getById'), +}; diff --git a/server/models/Organization.ts b/server/models/Organization.ts index d011c29..1bd32c2 100644 --- a/server/models/Organization.ts +++ b/server/models/Organization.ts @@ -4,7 +4,7 @@ import Organization from 'interfaces/Organization'; import { delegateRepository } from '../infra/database/delegateRepository'; import OrganizationRepository from '../infra/database/OrganizationRepository'; -type Filter = Partial<{ planter_id: number; organization_id: number}>; +type Filter = Partial<{ planter_id: number; organization_id: number }>; function getByFilter( organizationRepository: OrganizationRepository, diff --git a/server/models/OrganizationV2.ts b/server/models/OrganizationV2.ts index c3ab7ce..ec1eeb3 100644 --- a/server/models/OrganizationV2.ts +++ b/server/models/OrganizationV2.ts @@ -4,7 +4,11 @@ import FilterOptions from 'interfaces/FilterOptions'; import Organization from 'interfaces/Organization'; import { delegateRepository } from '../infra/database/delegateRepository'; -type Filter = Partial<{ planter_id: number; organization_id: number, grower_id:string }>; +type Filter = Partial<{ + planter_id: number; + organization_id: number; + grower_id: string; +}>; function getByFilter( organizationRepository: OrganizationRepositoryV2, @@ -18,7 +22,7 @@ function getByFilter( ); return trees; } - if (filter.grower_id){ + if (filter.grower_id) { log.warn('using grower filter...'); const trees = await organizationRepository.getByGrower( filter.grower_id, @@ -41,14 +45,16 @@ function getOrganizationLinks(organization) { } export default { - getById: delegateRepository('getById'), + getById: delegateRepository( + 'getById', + ), getByMapName: delegateRepository( 'getByMapName', ), getByFilter, getOrganizationLinks, getFeaturedOrganizations: delegateRepository< - OrganizationRepositoryV2, + OrganizationRepositoryV2, Organization >('getFeaturedOrganizations'), -}; \ No newline at end of file +}; diff --git a/server/models/TreeV2.ts b/server/models/TreeV2.ts index f6365e8..e7dd7c6 100644 --- a/server/models/TreeV2.ts +++ b/server/models/TreeV2.ts @@ -116,6 +116,8 @@ function getFeaturedTreeDepricated(treeRepository: TreeRepositoryV2) { export default { getById: delegateRepository('getById'), getByFilter, - getFeaturedTree: delegateRepository('getFeaturedTree'), + getFeaturedTree: delegateRepository( + 'getFeaturedTree', + ), countByFilter, -}; \ No newline at end of file +}; diff --git a/server/routers/contractsRouter.ts b/server/routers/contractsRouter.ts new file mode 100644 index 0000000..a9cd21e --- /dev/null +++ b/server/routers/contractsRouter.ts @@ -0,0 +1,104 @@ +import express from 'express'; +import Joi from 'joi'; +import ContractRepository from 'infra/database/ContractRepository'; +import Session from 'infra/database/Session'; +import { handlerWrapper, queryFormatter } from './utils'; +import ContractModel from '../models/Contract'; + +const router = express.Router(); + +router.get( + '/count', + handlerWrapper(async (req, res) => { + const query = queryFormatter(req); + + Joi.assert( + query, + Joi.object().keys({ + // contract table filters + id: Joi.string().uuid(), + agreement_id: Joi.string().uuid(), + worker_id: Joi.string().uuid(), // grower_account_id? + listed: Joi.boolean(), + // organization_id: Joi.array(), + startDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), + endDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), + // defaults + limit: Joi.number().integer().min(1).max(20000), + offset: Joi.number().integer().min(0), + whereNulls: Joi.array(), + whereNotNulls: Joi.array(), + whereIns: Joi.array(), + }), + ); + const { ...rest } = req.query; + + const repo = new ContractRepository(new Session()); + const count = await ContractModel.getCount(repo)({ ...rest }); + res.send({ + count: Number(count), + }); + res.end(); + }), +); + +router.get( + '/:id', + handlerWrapper(async (req, res) => { + Joi.assert(req.params.id, Joi.string().required()); + const repo = new ContractRepository(new Session()); + const exe = ContractModel.getById(repo); + const result = await exe(req.params.id); + res.send(result); + res.end(); + }), +); + +router.get( + '/', + handlerWrapper(async (req, res) => { + const query = queryFormatter(req); + + Joi.assert( + query, + Joi.object().keys({ + // contract table filters + id: Joi.string().uuid(), + agreement_id: Joi.string().uuid(), + worker_id: Joi.string().uuid(), // grower_account_id? + listed: Joi.boolean(), + // organization_id: Joi.array(), + startDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), + endDate: Joi.string().regex(/^\d{4}-\d{2}-\d{2}$/), + // defaults + limit: Joi.number().integer().min(1).max(20000), + offset: Joi.number().integer().min(0), + whereNulls: Joi.array(), + whereNotNulls: Joi.array(), + whereIns: Joi.array(), + }), + ); + const { + limit = 25, + offset = 0, + order = 'desc', + order_by = 'id', + ...rest + } = query; + + const repo = new ContractRepository(new Session()); + const exe = ContractModel.getByFilter(repo); + const sort = { order, order_by }; + const result = await exe({ ...rest, sort }, { limit, offset }); + const count = await ContractModel.getCount(repo)({ ...rest }); + res.send({ + contracts: result, + total: Number(count), + offset, + limit, + }); + res.end(); + }), +); + +export default router; diff --git a/server/routers/organizationsRouterV2.ts b/server/routers/organizationsRouterV2.ts index dd7cd95..a1e26d6 100644 --- a/server/routers/organizationsRouterV2.ts +++ b/server/routers/organizationsRouterV2.ts @@ -5,7 +5,11 @@ import OrganizationRepositoryV2 from '../infra/database/OrganizationRepositoryV2 import Session from '../infra/database/Session'; import OrganizationModel from '../models/OrganizationV2'; -type Filter = Partial<{ planter_id: number; organization_id: number, grower_id:string }>; +type Filter = Partial<{ + planter_id: number; + organization_id: number; + grower_id: string; +}>; const router = express.Router(); @@ -51,7 +55,7 @@ router.get( req.query, Joi.object().keys({ planter_id: Joi.number().integer().min(0), - grower_id:Joi.string().guid(), + grower_id: Joi.string().guid(), limit: Joi.number().integer().min(1).max(1000), offset: Joi.number().integer().min(0), }), @@ -61,7 +65,7 @@ router.get( const filter: Filter = {}; if (planter_id) { filter.planter_id = planter_id; - } else if(grower_id){ + } else if (grower_id) { filter.grower_id = grower_id; } const result = await OrganizationModel.getByFilter(repo)(filter, {