0.1.6Updated a month ago
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);
}