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/infinity.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, ctx) => {
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: ctx.state.request.request_id
});
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: ctx.state.request.request_id
});
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, ctx) => {
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: ctx.state.request.request_id,
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');
}