import fc, { type Arbitrary } from 'fast-check'; import { schema } from '../server/aql'; import { addDays } from '../shared/months'; export function typeArbitrary(typeDesc, name?) { let arb; switch (typeDesc.type) { case 'id': // uuid shrinking is broken right now, will file an issue arb = fc.hexaString({ minLength: 30, maxLength: 30 }); break; case 'boolean': arb = fc.boolean(); break; case 'integer': arb = fc.integer(); break; case 'float': arb = fc.float(); break; case 'string': arb = fc.string(); break; case 'date': arb = fc.integer({ min: 0, max: 365 * 4 }).map(n => { return addDays('2018-01-01', n); }); break; case 'json': arb = fc.constant(null); break; default: throw new Error('Unknown schema field type: ' + typeDesc.type); } if (!typeDesc.required && name !== 'id') { return fc.option(arb).map(val => { if (val == null) { if (typeDesc.default !== undefined) { return typeof typeDesc.default === 'function' ? typeDesc.default() : typeDesc.default; } else if (typeDesc.type === 'boolean') { return false; } } return val; }); } return arb; } export function flattenSortTransactions(arr) { const flattened = arr.reduce((list, trans) => { const { subtransactions, ...fields } = trans; if (subtransactions.length > 0) { list.push({ ...fields, is_parent: true, is_child: false, parent_id: null, }); subtransactions.forEach(subtrans => { list.push({ ...subtrans, is_parent: false, is_child: true, parent_id: trans.id, date: trans.date, account: trans.account, }); }); } else { list.push({ ...fields, is_parent: false, is_child: false, parent_id: null, }); } return list; }, []); return flattened.sort((t1, t2) => { if (t1.id < t2.id) { return -1; } else if (t1.id > t2.id) { return 1; } return 0; }); } function tableArbitrary< T extends Record<string, { type: string; required?: boolean }>, E extends Record<string, Arbitrary<unknown>>, >( tableSchema: T, extraArbs?: E, requiredKeys: Array<Extract<keyof T | keyof E, string>> = [], ) { const arb = fc.record( { ...Object.fromEntries<T>( Object.entries(tableSchema).map(([name, field]) => { return [name, typeArbitrary(field, name)] as const; }), ), // Override the amount to make it a smaller integer amount: fc.integer({ min: -1000000, max: 1000000 }), ...extraArbs, }, { requiredKeys: [ 'id', ...requiredKeys, ...Object.keys(tableSchema).filter(name => tableSchema[name].required), ], }, ); return arb; } export function makeTransaction({ splitFreq = 1, payeeIds, }: { splitFreq?: number; payeeIds?: string[] } = {}) { const payeeField = payeeIds ? { payee: fc.oneof(...payeeIds.map(id => fc.constant(id))) } : null; const subtrans = tableArbitrary(schema.transactions, payeeField); return tableArbitrary( schema.transactions, { ...payeeField, subtransactions: fc.oneof( { arbitrary: fc.constant([]), weight: 100 }, { arbitrary: fc.array(subtrans), weight: splitFreq * 100 }, ), }, ['subtransactions'], ); } export const makeTransactionArray = ( options: { minLength?; maxLength?; splitFreq?; payeeIds? } = {}, ) => { const { minLength, maxLength, ...transOpts } = options; return fc .array(makeTransaction(transOpts), { minLength, maxLength }) .map(arr => flattenSortTransactions(arr)); }; export const payee = tableArbitrary(schema.payees); export const account = tableArbitrary(schema.accounts); export const category = tableArbitrary(schema.categories); export const category_group = tableArbitrary(schema.category_groups);