Field discovery now samples an account's actual transactions instead of assuming a shape. flatten() passes through every scalar the bridge sends rather than whitelisting eleven keys, so institution-specific fields turn up on their own, and inferFields() — extracted from the CSV suggest route so both paths share it — unions keys across the sample because API feeds omit optional fields entirely. Three bugs the live bridge exposed: - posted=0 on pending transactions became 1970-01-01; falsy epochs are now "no date", with date falling back to transacted_at and posted_date kept separate. - days=0 omitted start-date, which returns only the few most recent transactions rather than everything — 4 instead of 89. A start-date is always sent now, clamped to 89 days (the bridge hard-caps at 90). - Sampling asked for more than 45 days, and the bridge's advisory notice about that surfaced in the UI as an error. Samples use 44 days; the threshold is exclusive. The Sources page can now link an account: a picker in both the create dialog and the detail panel, populated on demand, which fills the field table from the sample and defaults the constraint field to the transaction id with an explanation of why. manage.py option 10 claims a setup token and writes the access URL to .env, replacing the throwaway script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2HFeU5neCKagTnmA6o9Tu
43 lines
1.3 KiB
JavaScript
43 lines
1.3 KiB
JavaScript
/**
|
|
* Field inference
|
|
* Derives a field list and column types from sample records, so a source can be
|
|
* configured from real data rather than an assumed shape. Used by the CSV
|
|
* suggest endpoint and by bank-feed sampling.
|
|
*/
|
|
|
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/;
|
|
|
|
/**
|
|
* @param records array of flat objects
|
|
* @param limit how many rows to look at
|
|
* @returns { fields: [{name, type}], sampleRows }
|
|
*
|
|
* Keys are unioned across the sample, not read off the first row — API feeds
|
|
* omit optional keys entirely on records that lack them.
|
|
*/
|
|
function inferFields(records, limit = 50) {
|
|
const sampleRows = records.slice(0, limit);
|
|
|
|
const keys = [];
|
|
for (const row of sampleRows) {
|
|
for (const key of Object.keys(row)) {
|
|
if (!keys.includes(key)) keys.push(key);
|
|
}
|
|
}
|
|
|
|
const fields = keys.map(key => {
|
|
const vals = sampleRows.map(r => r[key]).filter(v => v !== '' && v != null);
|
|
let type = 'text';
|
|
if (vals.length > 0 && vals.every(v => !isNaN(parseFloat(v)) && isFinite(v) && String(v).charAt(0) !== '0')) {
|
|
type = 'numeric';
|
|
} else if (vals.length > 0 && vals.every(v => ISO_DATE_RE.test(String(v)))) {
|
|
type = 'date';
|
|
}
|
|
return { name: key, type };
|
|
});
|
|
|
|
return { fields, sampleRows };
|
|
}
|
|
|
|
module.exports = { inferFields };
|