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

Add Contract endpoint and queries #307

Merged
merged 6 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 growerAccountsRouter from './routers/growerAccountsRouter';
import organizationsRouter from './routers/organizationsRouter';
Expand Down Expand Up @@ -77,6 +78,7 @@ app.use('/raw-captures', rawCapturesRouter);
app.use('/v2/growers', growerAccountsRouter);
app.use('/v2/trees', treesRouterV2);
app.use('/bounds', boundsRouter);
app.use('/contract', contractsRouter);
// Global error handler
app.use(errorHandler);

Expand Down
193 changes: 193 additions & 0 deletions server/infra/database/ContractRepository.ts
Original file line number Diff line number Diff line change
@@ -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<Contract> {
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 | number) {
gwynndp marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
14 changes: 14 additions & 0 deletions server/interfaces/Contract.ts
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions server/interfaces/ContractFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import DbModel from './DbModel';

interface ContractFilter extends DbModel {
id?: number | undefined;
gwynndp marked this conversation as resolved.
Show resolved Hide resolved
agreement_id?: number | undefined;
worker_id?: number | 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;
29 changes: 29 additions & 0 deletions server/models/Contract.ts
Original file line number Diff line number Diff line change
@@ -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<Contract[]> {
return async function (filter: ContractFilter, options: FilterOptions) {
const contracts = await contractRepository.getByFilter(filter, options);
return contracts;
};
}

function getCount(
contractRepository: ContractRepository,
): (filter: ContractFilter) => Promise<Contract[]> {
return async function (filter: ContractFilter) {
const count = await contractRepository.getCount(filter);
return count;
};
}

export default {
getByFilter,
getCount,
getById: delegateRepository<ContractRepository, Contract>('getById'),
};
104 changes: 104 additions & 0 deletions server/routers/contractsRouter.ts
Original file line number Diff line number Diff line change
@@ -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;