dataflow/api/lib/fields.js
Paul Trowbridge 9f164bcd34 Discover feed fields from real data; fix the sync window
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
2026-08-02 02:50:03 -04:00

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 };