import type { Supply } from "@supply/mod.ts";
import { Paginate, PaginationParams } from "@infinity-beyond/modules/pagination.ts";
import { REST_Wrapper } from "@infinity-beyond/classes/rest_wrapper.ts";
import { HandleBody } from "@infinity-beyond/mod.ts";
import type { BodyValidationVerifyFunction } from "@infinity-beyond/utils/handle_body.ts";
import State from "@infinity-beyond/modules/state.ts";
export class SupplyRest<EntityName extends string> extends REST_Wrapper {
constructor(instance: Supply<EntityName, any>) {
super();
this.get('/', async (req, _ctx) => {
const { page, page_size } = PaginationParams(req);
const items = await instance.Items(page_size, page);
const count = await instance.ItemCount();
return Response.json({
...Paginate(req, items, count)
});
})
this.post('/', async (req, _ctx) => {
const { body, error_response} = await HandleBody<Supply.Item.Unserialized>(req, (verify, item) => {
ValidateKey(verify, instance, item.key);
ValidateSupplyItemDates(verify, item);
verify(!!item.name).fail('[name] not provided');
verify(!!item.provider).fail('[provider] not provided');
verify(item.stock_capacity !== undefined).fail('[stock_capacity] not provided');
verify(Number.isSafeInteger(item.stock_capacity)).fail('[stock_capacity] must be an integer');
});
if(error_response) return error_response;
const response = await instance.AddItem(body as Supply.Item);
return Response.json(response, { status: response.added ? 200 : 400 });
});
// #region Pools
this.get('/pools', async (req, _ctx) => {
const { page, page_size } = PaginationParams(req);
const pools = await instance.Pools(page_size, page);
const pool_count = await instance.PoolCount();
return Response.json({
...Paginate(req, pools, pool_count)
});
})
this.post('/pools', async (req, _ctx) => {
const { body, error_response} = await HandleBody<Supply.Pool>(req, (verify, data) => {
verify(!data.id).fail('[id] should not be provided');
verify(!!data.name).fail('[name] not provided');
});
if(error_response) return error_response;
const { rows: [{ id }] } = await State.PostgresClient.query<{ id: number }>(`INSERT INTO ${instance.pools_table} (name) values ($1) returning id`, [body.name]);
return Response.json({
id,
name: body.name
});
})
this.get('/pools/:pool_name', async (_req, ctx) => {
const { rows: [ { count: item_count = 0 } = {}, { count: user_count = 0 } = {} ] } = await State.PostgresClient.query<{ count: number }>(`select count(*)::int as count from ${instance.pool_items_table} PIT where PIT.pool_name = $1 union select count(*)::int as count from ${instance.pool_users_table} PUT where PUT.pool_name = $1`, [ctx.params.pool_name])
return Response.json({
pool: ctx.params.pool_name,
item_count,
user_count
});
})
this.get('/pools/:pool_name/items', async (req, ctx) => {
const { page, page_size } = PaginationParams(req);
const items = await instance.ItemsInPool(ctx.params.pool_name, page_size, page);
const total = await instance.CountItemsInPool(ctx.params.pool_name);
return Response.json({
pool: ctx.params.pool_name,
...Paginate(req, items, total)
});
})
this.get('/pools/:pool_name/users', async (req, ctx) => {
const { page, page_size } = PaginationParams(req);
const items = await instance.UsersInPool(ctx.params.pool_name, page_size, page);
const total = await instance.CountItemsInPool(ctx.params.pool_name);
const { items: users, meta } = Paginate(req, items, total);
return Response.json({
pool: ctx.params.pool_name,
meta,
users,
});
})
// #region Allotments
this.post('/allot', async (req, _ctx) => {
const { body, error_response} = await HandleBody<Supply.Allotment.Unentered>(req, (verify, data) => {
ValidateItemKey(verify, instance, data.item_key);
verify(!!data.user_key).fail('[user_key] not provided');
});
if(error_response) return error_response;
if(instance.options.reservation_expiration) {
return Response.json({ errors: [`Expirations are enabled for ${instance.name}. Please use /reserve instead.`] });
}
const allotment_response = await instance.AllotItem({
item_key: body.item_key,
user_key: body.user_key,
reference: body.reference,
source: body.source
})
return Response.json(allotment_response, { status: allotment_response.allotted ? 200 : 400 });
})
this.post('/reserve', async (req, _ctx) => {
const { body, error_response} = await HandleBody<Supply.Allotment.Unentered>(req, (verify, data) => {
ValidateItemKey(verify, instance, data.item_key);
verify(!!data.user_key).fail('[user_key] not provided');
});
if(error_response) return error_response;
const allotment_response = await instance.AllotItem({
item_key: body.item_key,
user_key: body.user_key,
reserved: true,
reference: body.reference,
source: body.source
})
return Response.json(allotment_response, { status: allotment_response.allotted ? 200 : 400 });
})
this.post('/claim', async (req, _ctx) => {
const { body, error_response} = await HandleBody<{ user_key: string }>(req, (verify, data) => {
verify(!!data.user_key).fail('[user_key] not provided');
});
if(error_response) return error_response;
const claim_response = await instance.ClaimReservedItem(body.user_key);
return Response.json(claim_response, { status: claim_response.claimed ? 200 : 400 });
})
this.post('/cancel', async (req, _ctx) => {
const { body, error_response} = await HandleBody<{ user_key: string }>(req, (verify, data) => {
verify(!!data.user_key).fail('[user_key] not provided');
});
if(error_response) return error_response;
const claim_response = await instance.DeleteReservedItem(body.user_key);
return Response.json(claim_response, { status: claim_response.deleted ? 200 : 400 });
})
// #region Users
this.get('/users', async (req, _ctx) => {
const { page, page_size } = PaginationParams(req);
const users = await instance.Users(page_size, page);
const user_count = await instance.UserCount();
const { meta } = Paginate(req, users, user_count);
return Response.json({
meta,
users
})
});
this.get('/user/:key', async (req, ctx) => {
const { page_size, page } = PaginationParams(req);
const { allotments = [], item_count = 0 } = await instance.UserData(ctx.params.key, page_size, page) ?? {};
const { meta } = Paginate(req, allotments, item_count);
return Response.json({
meta,
allotments
})
});
this.post('/user/:key/add_to_pools', async (req, ctx) => {
const { body, error_response} = await HandleBody<Supply.Pool.API>(req, (verify, item) => {
verify(!!ctx.params.key).fail('[key] not provided');
verify(!!item.pools).fail('[pools] not provided');
verify(typeof item.pools == 'string').fail('[pools] must be a comma-delimited string');
});
const user = await instance.User(ctx.params.key);
if(!user) return Response.json({ errors: [`<${instance.slug}_user:${ctx.params.key}> not found`]});
if(error_response) return error_response;
const pool_list = body!.pools.split(/\s*,\s*/g);
const success = await instance.AddUserToPools(user.key, ...pool_list)
if(success) user.pools.push(...pool_list);
return Response.json({ user }, { status: success ? 200 : 400 });
});
this.post('/user/:key/remove_from_pools', async (req, ctx) => {
const { body, error_response} = await HandleBody<Supply.Pool.API>(req, (verify, item) => {
ValidateKey(verify, instance, ctx.params.key);
verify(typeof item.pools == 'string').fail('[pools] must be a comma-delimited string');
});
const user = await instance.Item(ctx.params.key);
if(!user) return Response.json({ errors: [`<${instance.slug}_user:${ctx.params.key}> not found`]});
if(error_response) return error_response;
const pool_list = body!.pools.split(/\s*,\s*/g);
const success = await instance.RemoveUserFromPools(user.key, ...pool_list)
if(success) user.pools = user.pools.filter(pool => !pool_list.includes(pool));
return Response.json({ item: user }, { status: success ? 200 : 400 });
});
// #region :key
this.get('/:key', async (_req, ctx) => {
const item = await instance.Item(ctx.params.key);
return Response.json(item, { status: item ? 200 : 404 });
});
this.post('/:key/add_to_pools', async (req, ctx) => {
const { body, error_response} = await HandleBody<Supply.Pool.API>(req, (verify, item) => {
ValidateKey(verify, instance, ctx.params.key);
verify(!!item.pools).fail('[pools] not provided');
verify(typeof item.pools == 'string').fail('[pools] must be a comma-delimited string');
});
const item = await instance.Item(ctx.params.key);
if(!item) return Response.json({ errors: [`<${instance.slug}_item:${ctx.params.key}> not found`]});
if(error_response) return error_response;
const pool_list = body!.pools.split(/\s*,\s*/g);
const success = await instance.AddItemToPools(item.key, ...pool_list)
if(success) item.pools.push(...pool_list);
return Response.json({ item }, { status: success ? 200 : 400 });
});
this.post('/:key/remove_from_pools', async (req, ctx) => {
const { body, error_response} = await HandleBody<Supply.Pool.API>(req, (verify, item) => {
ValidateKey(verify, instance, ctx.params.key);
verify(typeof item.pools == 'string').fail('[pools] must be a comma-delimited string');
});
const item = await instance.Item(ctx.params.key);
if(!item) return Response.json({ errors: [`<${instance.slug}_item:${ctx.params.key}> not found`]});
if(error_response) return error_response;
const pool_list = body!.pools.split(/\s*,\s*/g);
const success = await instance.RemoveItemFromPools(item.key, ...pool_list)
if(success) item.pools = item.pools.filter(pool => !pool_list.includes(pool));
return Response.json({ item }, { status: success ? 200 : 400 });
});
}
}
const ValidateKey = (verify: BodyValidationVerifyFunction, instance: Supply<any>, key: string) => {
verify(!!key).fail('[key] not provided');
const { valid, reason } = instance.format_and_validate(key);
verify(valid).fail(reason || '[key] invalid');
}
const ValidateItemKey = (verify: BodyValidationVerifyFunction, instance: Supply<any>, key: string) => {
verify(!!key).fail('[item_key] not provided');
const { valid, reason } = instance.format_and_validate(key);
verify(valid).fail(reason || '[item_key] invalid');
}
const ValidateSupplyItemDates = (verify: BodyValidationVerifyFunction, item: Supply.Item.Unserialized) => {
const from_date = Date.parse(item.available_from_date as string);
const until_date = Date.parse(item.available_until_date as string);
verify(!isNaN(from_date)).fail('[available_from_date] must be a date');
verify(!isNaN(until_date)).fail('[available_until_date] must be a date');
verify(!isNaN(until_date) && (until_date?.valueOf() || 0) > Date.now()).fail('[available_until_date] must be in the future');
item.available_from_date = new Date(from_date);
item.available_until_date = new Date(until_date);
}