Fieldia

Data sources

Fieldia has no backend of its own. A page's records and responses travel through a data source: an object your app writes, with up to five methods. Implement only the ones your pages use.

interface DataSource {
  load?(request: LoadRequest): Promise<Values>;              // read a record
  save?(request: SaveRequest): Promise<SaveResult>;          // create or update it
  onchange?(request: OnchangeRequest): Promise<OnchangeResult>;  // recalculate after a change
  search?(request: SearchRequest): Promise<RelatedRecord[]>; // find records to link to
  submit?(request: SubmitRequest): Promise<SubmitResult>;    // store one response
}
MethodCalled whenNeeded for
loadA record page opens with a recordIdEditing an existing record
saveSomeone presses Save (or autosave fires)Record pages
onchangeA value changes, so the backend can fill in othersTotals, defaults that depend on other fields
searchSomeone types in a many2one, many2many or referenceLinks to other records
submitSomeone sends a responses pageSurveys, sign-ups

What each method receives

A slow answer is handled for you: if someone keeps typing, an onchange or search answer that arrives after a newer question is ignored.

What a save sends

changes.values has the plain fields that changed. Tables of lines and many2many links come as operations, named the way Odoo names its x2many commands, so an Odoo or Flectra adapter maps them one to one:

changes.lines['child_ids'] = [
  { op: 'create', key: 'new-1', values: { name: 'Sara' } },
  { op: 'update', id: 7, values: { email: 'sara@example.com' } },
  { op: 'delete', id: 9 },
];
changes.links['tag_ids'] = [{ op: 'link', id: 3 }, { op: 'unlink', id: 4 }];   // or { op: 'set', ids }, { op: 'clear' }

An adapter for a REST API

import type { DataSource } from '@fieldia/core';

export const api: DataSource = {
  async load({ model, id }) {
    return (await fetch(`/api/${model}/${id}`)).json();
  },
  async save({ model, id, values }) {
    const response = await fetch(id === null ? `/api/${model}` : `/api/${model}/${id}`, {
      method: id === null ? 'POST' : 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(values),
    });
    const saved = await response.json();
    return { id: saved.id, values: saved };
  },
  async search({ model, query, limit = 8 }) {
    const rows = await (await fetch(`/api/${model}?q=${encodeURIComponent(query)}&limit=${limit}`)).json();
    return rows.map((row: { id: number; name: string }) => ({ id: row.id, label: row.name }));
  },
  async submit({ pageId, values }) {
    await fetch(`/api/responses/${pageId}`, { method: 'POST', body: JSON.stringify(values) });
    return {};
  },
};

The memory data source

createMemoryDataSource() implements all five methods in memory. It is what the demos and tests use, and a good way to try a page before a backend exists. You can seed it with records, search labels and onchange rules:

import { createMemoryDataSource } from '@fieldia/core';

const dataSource = createMemoryDataSource({
  records: { partner: { 1: { name: 'Nile Towers', is_company: true } }, country: { 63: { name: 'Egypt' } } },
  onchange: { partner: { is_company: (values) => ({ title: values.is_company ? null : values.title }) } },
  delayMs: 300,   // answer slowly, to see loading states
});
dataSource.responses;   // every submitted response, for tests