Skip to content

ddcore 0.18 ​

0.18.4 — 2026-09-23 ​

Added ​

  • Dialogs answer the keyboard. Escape closes the dialog on top and Enter runs its primary action, wherever the focus is inside it — except in a textarea or rich text, where Enter keeps its new line (Ctrl/Cmd+Enter submits), on a focused button, or while a Link field's options are open. Covers dialog(), confirm() and prompt() from the desk SDK.
  • ddcore.ui.confirm(message, title, { destructive: true }) asks a question whose "Yes" loses something: "Yes" becomes a red danger button and "No" the primary, so Enter and Escape both keep the data. The desk uses it to discard changes, delete a document, an attachment or selected records, cancel a submitted document and revoke an API key.
  • A modal owns the focus while it is open. It takes it on opening (its first field, or the modal itself), keeps Tab inside, and hands it back to what had it on closing — so Escape closes the Import, Share and Assign modals right away, without a click inside them first.
  • GET /api/file-info?url=<file_url> returns an upload's original file_name, file_size, content_type, creation and owner to whoever may read the file. It uses the same rule as /private/files, so a colleague who can read the document gets it even though File itself is owner-only.

Changed ​

  • Attach and Attach Image controls hide the file name. The desk shows the thumbnail, or a file icon for a plain Attach. Either one opens the file and shows its original name, size, type and uploader on hover. The stored name is random, so showing it said nothing. Set showFileName: true on the field to show the name beside it again; it now shows the original name, not the stored one.

Fixed ​

  • A burst of saves no longer floods the list with reloads. The desk ran one full reload per list_update, so a Data Import of ~100 rows in the tree view started over a thousand concurrent /api/tree requests, which the browser refused with "Failed to fetch" toasts. Reloads are now coalesced: while one runs, further updates queue a single follow-up. (#15)

0.18.3 — 2026-09-23 ​

Added ​

  • Job lifecycle callbacks. ddcore.enqueue(method, args, { onStart, onFailure }) takes two method paths, each run in a transaction of its own, committed apart from the job's. onStart runs before the body on every attempt, so a document can show Running while its job runs; onFailure runs after the body rolled back — on an error, a timeout, a cancellation or a dead worker — with job.reason and job.final, so a document can show Failed. Both are called as fn(args, job); the SDK exports JobInfo and JobFailure. ddcore jobs show, /api/jobs and the MCP job tools show them as on_start / on_failure. See docs/agent/ops.md. (#11)
  • An image on each card in the Cards view. defineListView(…, { card: { image: "photo" } }) names an Attach Image (or Attach) field. Each card shows it as a square thumbnail beside the title, loaded lazily. A card whose image is empty or fails to load, or whose field is above the reader's permlevel, shows an avatar with the title's initials, coloured by the title. DocTypes gain imageField, which the card's image defaults to and extendDoctype may set. The meta refuses an imageField that is not an Attach Image or Attach field. See form-api and fieldtypes. (#12)

Changed ​

  • The tree view remembers Expand all / Collapse all. The last of the two buttons clicked on a DocType's tree is kept in the browser (localStorage, per DocType), and the tree opens fully expanded next time when "Expand all" was the last choice.

Fixed ​

  • No more cached plan must not change result type after a migration (#14). A migration that changed a table's columns (a new field, a new fieldtype) left the server's connections holding statements prepared against the old row type. Requests reading that DocType then failed, once per connection, until each one recovered. A migration that ran DDL now closes the pool's connections. A migration run by another process (ddcore migrate, the ddcore mcp server's migrate tool) is caught when a request hits a stale statement: the server resets its pool and runs that request's transaction again, once.

0.18.2 — 2026-09-23 ​

Added ​

  • Tree view labels and order (DAT-07). defineListView's new tree option sets how a node is labelled and in which order a level is listed: tree: { title: "{acronym} - {title}", orderBy: "title asc" }. title is a template, whose placeholders are fetched and whose empty ones take their brackets and end separators with them, or a (row) => string function with fields. orderBy replaces the default order (groups first, then the title). GET /api/tree/{doctype} accepts fields and order_by for this, and returns the fields under each node's values.
  • Read-only external databases (#10). ddcore.externalDb("sql_server").sql(query, params, { timeout }) queries a SQL Server database from server code: a controller, service, report or job. The connection comes from DDCORE_SECRET_<NAME>_HOST, _PORT (default 1433), _DATABASE, _USER, _PASSWORD and the optional _ENCRYPT, so the credentials stay out of every backup. Only SELECT/WITH is accepted, each call runs in a transaction that is always rolled back, parameters are positional (@p1, …), the timeout defaults to 30 s, and rows are normalized like ddcore.db.sql (decimals as numbers, dates as strings, GUIDs in canonical form). ddcore doctor lists each external database and probes it. See external-db.

Fixed ​

  • A report summary card with datatype: "Data" shows its text (#8). The desk formatted every card that was not Currency or Int as a number, so a text value read 0. A card is now formatted like a cell of its datatype, which also accepts "Date" and "Datetime" (shown on the site clock); with no datatype, a number is formatted as a number and anything else as text.
  • i18n extract collects the name of a report with no label (#9). Such a report is shown, and translated, under its name, but the extractor never saw that key, so it stayed in English and --check did not report it. Apps with unlabelled reports get a new key to translate.

0.18.1 — 2026-09-23 ​

Added ​

  • Data Import from CSV and XLSX (DAT-01, the spreadsheet half). A list's toolbar has an Import button. It opens a dialog that:

    • checks a .csv or .xlsx file with a dry run;
    • shows which column goes to which field (each one remappable);
    • lists the rows that would fail and why, with a download of just those rows;
    • then imports, creating records or updating them by ID.

    Each row is an ordinary insert or save as the uploader. Roles, scopes, field levels, hooks, naming, workflows, webhooks and Version all apply, and each row commits on its own.

    Cells are parsed strictly by field type: the locale's decimal separator and date order, Excel date serials, yes/no, Select labels in the user's language, and a Link by its title. An export CSV re-imports unchanged in update mode, with modified guarding against overwriting a later change.

    Also new:

    • the endpoints POST /api/data-import/<DocType> and GET /api/data-import/<DocType>/template;
    • the importMaxRows setting in ddcore.json (default 5000);
    • a data.import audit event.

    See data import.

  • import permission flag on DocType permission rows. It gates Data Import, together with create to insert or write to update. It defaults to off, so no role gains anything until an app grants it.

  • Expand all and Collapse all in the tree view (DAT-07). Expanding fetches one level per round until every group is open; collapsing keeps what was loaded, so reopening a branch costs no request.

Changed ​

  • A Duration field is edited in one box, 1d 2h 30m 0s, in a form and in a grid alike, instead of one number box per unit. ↑/↓ step the unit under the caret (Shift for 10, carrying into the next unit), ←/→ move between units, digits fill the selected unit and move on when it is full, Backspace zeroes it (and unsets an all-zero value), and a pasted 1h 30m or 1:30 is read as before. hideDays/hideSeconds still drop their unit; the stored value is unchanged.

Fixed ​

  • An empty tree view no longer stays on "Loading…" (DAT-07). The tree reloaded itself every time a level arrived, so a hierarchy DocType with no records never left the loading state and kept requesting /api/tree. It now loads once, and again only when a document changes.
  • A tree document's parent picker searches again (DAT-07). It filtered on is_group = 1, which Postgres refuses for a boolean column, so opening the parent field on an existing document failed with a server error. It now filters on true.
  • Tree view leaves line up with their siblings (DAT-07). A leaf's title was pushed to the right edge of the card instead of sitting at its depth.

0.18.0 — 2026-09-22 ​

Breaking ​

  • Text Editor is rich text. Its value is HTML, cleaned on the way in by an allowlist (paragraphs, headings, lists, quotes, code, links and images under /files/); scripts, styles, event handlers, iframes and external image URLs are removed rather than refused. A value written before this release is plain text: it is read as text — so a < b keeps its < — and shown as paragraphs, converted for good on its next save without writing a Version entry for the conversion. An empty editor (<p></p>) stores null, so reqd still holds. Upgrade path: render such a value as HTML (the desk and print already do) or strip it with ddcore.redact-style text extraction, and write plain text through the field as plain text — the server escapes it. ddcore.db.sql writes bypass the cleaning, as they always have.

Added ​

  • Six fieldtypes (DAT-08): Markdown Editor and Code (options is the language) on a text column, Attach Image (an Attach restricted to png/jpg/gif/webp, refused at upload too), Color (normalised to #rrggbb), and Duration (whole seconds; options: ["hideDays", "hideSeconds"] hides a unit on screen) and Rating (0 to options stars, 1–10, default 5; clamped to [0, options] on read) on a bigint. Duration and Rating generate number | null; the rest generate string | null. Converting Text, Small Text or Data to a text type, or Int to Duration/Rating, keeps the same column and needs no convert. See fieldtypes.
  • Tree DocTypes (DAT-07): defineDoctype({ isTree: true }) makes a DocType a hierarchy. It gets a self-referencing Link — parent_<snake(name)>, or the parentField you name — and an is_group Check, both added unless you declare them yourself. The engine keeps the hierarchy honest on every write: the parent must exist and be a group, a document cannot be moved under itself or under one of its own descendants, a group with children stays a group, and deleting a document that still has children is refused with a message that says so (and is not waived by force). dbSet is checked too when it writes either column. Structural writes on one tree DocType are serialised by an advisory lock, so two concurrent moves cannot weave a cycle between them. See trees.
  • Tree filter operators, usable anywhere filters are — getList, REST, reports, the Desk: descendants of, descendants of (inclusive), not descendants of, ancestors of and not ancestors of, over a tree's own id or over a Link pointing at one. They compile to a recursive query and compose with permissions, scopes and shares like any other filter.
  • A User Permission whose allow is a tree DocType now covers the branch below the value it names (SEC-01): "Territory: Brazil" grants Brazil and everything under it, on lists, counts, link search, direct reads and writes. See scopes.
  • GET /api/tree/{doctype}?parent=&limit= returns one level of a hierarchy with each node's readable child count, and the Desk's new Tree view is built on it: it is the default view for a tree DocType, expands a branch at a time, links each node to its form and offers "Add child" on a group. A node whose parent the user cannot read is shown as a root.
  • ddcore import loads a ddcore export directory into a site (DAT-01): plan, run, status and reconcile, with --dry-run, --resume, --batch, --only, --include, --max-batches, --maintenance and --map, plus an import MCP tool. A load keeps ids, owners, timestamps and docstatus (including cancelled documents), runs no controller hook and queues no webhook, notification, email or realtime event; it advances ddcore_series past the ids it writes and marks already-past date notifications as done. Every line leaves a ledger row, so a second run over the same directory writes nothing and an interrupted run resumes at the first line that did not commit. Attachments are verified against the export's checksums before their bytes are written. reconcile compares rows, child rows, docstatus, files and per-docstatus Currency totals, and exits non-zero on any disagreement. A mapping file renames DocTypes and fields, drops, sets constants and remaps ids and users. See import.
  • Print renders a long value as a block of its own instead of a cell in the key/value grid: rich text and Markdown as the markup they are (cleaned by the allowlist), Code as escaped preformatted text and Attach Image as an image. Templates get b.richText(html, title?), b.markdown(source, title?) and b.pre(text, title?); b.raw/b.html remain the unchecked escape hatch. A Duration prints as 1d 2h 30m, a Rating as stars, and both align right in a child table. Relative image paths resolve against the site address in PDFs. See print templates.

Changed ​

  • The desk edits rich text with a toolbar (bold, italic, strikethrough, headings, lists, quote, code, link, image), previews Markdown through the server so the preview matches the printed page, and gets controls for Code (monospace, Tab indents), Duration (one box per unit), Rating (stars, keyboard-operable, clearing stores null), Color (picker and hex) and Attach Image (thumbnail, image files only). A list cell shows rich text as one line of text, a colour as a swatch and an image as a thumbnail, and a version diff compares the text rather than the markup. The editor loads only on a form that has a rich-text field.
  • ddcore export --attachments writes the attachment bytes to <out>/files/public/<file> and <out>/files/private/<file> — their storage key — instead of <out>/files/<file>. A public and a private file sharing a base name no longer overwrite each other. manifest.json records the layout in exportFormat.

Fixed ​

  • Checkbox fields in the Desk now render their description helper text below the label (with proper left alignment), as well as field validation errors and required asterisks.
  • Color control in the Desk now maintains a minimum width for the swatch and hex input so it does not shrink when the "Clear" button is displayed.
  • descendants of was accepted as a filter operator and compiled into =, which answered a hierarchy question with an exact match — a wrong result, with no error. It now walks the tree, and naming it on a field that is neither a tree's id nor a Link to a tree is refused.

MIT Licensed · ddcore (Data Driven Core) · Development documentation (main)