0.1.5Updated 6 months ago
import type { Ledger } from "@ledger/mod.ts";
import type { BodyValidationVerifyFunction } from "@infinity-beyond/utils/handle_body.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 { FeatureFlag } from "@infinity-beyond/modules/features/feature_flag.ts";

export class LedgerRest<EntityName extends string> extends REST_Wrapper {
  instance: Ledger<EntityName, any>

  constructor(instance: Ledger<EntityName, any>) {
    super();

    this.instance = instance;

    // #region POST /

    this.post('/', async (req) => {
      const { body, error_response} = await HandleBody<Ledger.Entry>(req, (verify, entry) => {
        ValidateLedgerKey(verify, instance, entry);
        ValidateLedgerAmount(verify, entry, { min: 1, max: 50 });
        ValidateLedgerSource(verify, entry);
      });
  
      if(error_response) return error_response;
  
      const response = await instance.ConsumeEntries({
        key: body.key,
        amount: body.amount,
        source: body.source,
        description: body.description,
        reference: body.reference,
        request_id: req.state.uuid
      });

      return Response.json(response);
    });

    // #region GET /:key

    this.get('/:key', async (req, ctx) => {
      const { key, valid, reason } = instance.format_and_validate(ctx.params.key);

      if(!valid) return Response.json({errors: [`[key] ${reason || 'invalid format'}`]}, { status: 400 });

      const { page, page_size } = PaginationParams(req);

      let {
        balance,
        entry_count,
        history,
      } = await instance.UserData(key, page_size, page) ?? {};

      balance ??= 0;
      entry_count ??= 0;
      history ??= [];

      return Response.json(Paginate(req, history || [], entry_count, { balance, total_entries: entry_count }));
    });

    // #region POST /:key

    this.post('/:key', async (req, ctx) => {
      const { body, error_response} = await HandleBody<Ledger.Entry>(req, (verify, entry) => {
        entry.key = ctx.params.key;

        ValidateLedgerKey(verify, instance, entry);
        ValidateLedgerAmount(verify, entry, { min: 1, max: 50 });
        ValidateLedgerSource(verify, entry);
      });
  
      const key = instance.format_key(ctx.params.key);

      if(!instance.validate_key(key)) return Response.json('[key] invalid format', { status: 400 });

      if(error_response) return error_response;
  
      const response = await instance.ConsumeEntries({
        key,
        amount: body.amount,
        source: body.source,
        description: body.description,
        reference: body.reference,
        request_id: req.state.uuid
      });

      return Response.json(response);
    });

    // #region GET /:key/entry/:entry_id

    this.get('/:key/entry/:entry_id', async (_req, ctx) => {
      const key = ctx.params.key;
      const entry_id  = Number(ctx.params.entry_id);
      if(!Number.isSafeInteger(entry_id)) return Response.json({}, { status: 404 });

      const entry_data = await instance.EntryData(entry_id);
      if(!entry_data) return Response.json({}, { status: 404 });

      if(entry_data.entry.key !== key) return Response.json({}, { status: 404 });

      return Response.json(entry_data);
    });

    // #region POST /admin/award

    this.post('/admin/award', async (req) => {
      const { body, error_response } = await HandleBody<Ledger.Entry>(req, (verify, entry) => {
        ValidateLedgerKey(verify, instance, entry);
        ValidateLedgerAmount(verify, entry, { min: 1, max: 50 });
        ValidateLedgerSource(verify, entry);
        ValidateLedgerReason(verify, entry);

        verify(FeatureFlag.get('ADMIN_API', 'Enables `/admin` API routes for all data types')).fail('[SYSTEM] Admin API is disabled');
      })

      if(!instance.options?.rest?.enable_admin_api) return Response.json({
        key: body?.key
      }, { status: 403 });

      if(error_response) return error_response;

      const add_response = await instance.AddEntry({
        key: body.key,
        amount: body.amount,
        source: body.source,
        request_id: req.state.uuid,
        description: body.description,
        reason: body.reason,
      })

      if(add_response.added) {
        return Response.json({
          key: body.key,
          ...add_response
        });
      }

      return Response.json({
        key: body.key,
        ...add_response
      }, { status: 400 });
    })

  }
}

const ValidateLedgerKey = (verify: BodyValidationVerifyFunction, instance: Ledger<any>, entry: Ledger.Entry) => {
  verify(!!entry.key).fail('[key] not provided');

  const { valid, reason } = instance.format_and_validate(entry.key);

  verify(valid).fail(reason || '[key] invalid');
}

const ValidateLedgerAmount = (verify: BodyValidationVerifyFunction, entry: Ledger.Entry, options: { min?: number, max?: number, integer?: boolean } = {}) => {
  verify(!!entry.amount).fail('[amount] not provided');
  if(options.integer) verify(Number.isSafeInteger(entry.amount)).fail('[amount] must be an integer');
  if(typeof options.min == 'number') verify(entry.amount >= options.min).fail(`[amount] must be more than or equal to ${options.min}`);
  if(typeof options.max == 'number') verify(entry.amount <= options.max).fail(`[amount] must be less than or equal to ${options.max}`);
}

const ValidateLedgerSource = (verify: BodyValidationVerifyFunction, entry: Ledger.Entry) => {
  verify(typeof entry.source == 'string').fail('[source] must be a string');
  verify(!!entry.source).fail('[source] not provided');
}

const ValidateLedgerReason = (verify: BodyValidationVerifyFunction, entry: Ledger.Entry) => {
  verify(!!entry.reason).fail('[reason] not provided');
  verify(typeof entry.reason == 'string').fail('[reason] must be a string');
}