ddcore 0.24
0.24.4 — 2026-10-03
Added
poolMaxConnsinddcore.json, orDDCORE_POOL_MAX_CONNS, sizes the database pool requests run on; unset, the DSN'spool_max_connsor pgx's default applies. A site whose requests wait on slow outbound calls holds a connection for each, and may need it larger. Seecli(#67).
Fixed
migratewith"tenancy": trueon a fresh Postgres cluster no longer fails withrole "ddcore_tenant" does not exist (SQLSTATE 22023): the tenant role is created and committed before anything in the migration uses it. Creating the role by hand first is no longer needed, and is harmless if kept. A database restored into a cluster without the role no longer fails every new connection either; the nextmigratecreates it (#66).pool_max_conns(or any otherpool_*setting) in the DSN no longer breaks cross-process cache invalidation: the listener sent it to Postgres, which refused the connection withunrecognized configuration parameter "pool_max_conns", and kept retrying while other processes' changes never reached this one's cache.ddcore backupandddcore restoredrop those settings too before handing the DSN topg_dump/pg_restore, which refuse them (#67).- A
Webhooksubscription created, changed or disabled in another process —ddcore eval --commit,ddcore exec, a worker, another replica — now takes effect on every running process without a restart: the cached subscriptions are dropped through the sameNOTIFYthat clears User Permission scopes, andddcore.db.set_valueon a Webhook clears them too (#71).
0.24.3 — 2026-10-03
Added
- A
Reportfield's cells act on a click like a Table's:grids.<field>.onCellClick.<column>in a form script turns that report column's non-empty cells into buttons, and the handler receives the row as the report returned it. A cell click handler, on a Table or a Report, may now be async; an error it rejects with is shown. See "grids" inform-api(#65).
Changed
- pt-BR: a tenant is now translated as Conta (it was left as Tenant).
Fixed
renamedFromon a tenant DocType no longer failsmigratewithpolicy "ddcore_tenant" … already exists: the renamed table keeps the row-level security policy and key it carried, and the plan no longer creates them again. AbeforeSchemapatch that drops the old table's policy as a workaround is no longer needed, and is harmless if kept (#64).
0.24.2 — 2026-10-02
Added
ddcore.crypto.pfxInfo(pfx, password?)describes the certificate in a PKCS#12 file —{ notBefore, notAfter, subject, issuer, serial, chain }, never its key — andddcore.crypto.certInfo(pem)does the same for a PEM certificate. A wrong password or an unreadable file throws aValidationErrorthat does not repeat the material, so a certificate is checked, and its expiry read, when it is stored. See "ddcore.*" incontroller-api(#63).- A dialog takes a file without uploading it:
fieldtype: "File"inddcore.ui.Dialogandddcore.ui.promptputs{ name, size, type, base64 }invaluesand creates noFile.optionsis the accept list andmaxBytesthe size cap (5 MB by default). It is a dialog field only; a DocType still stores a file withAttach. See "ddcorein the desk" inform-api(#63).
0.24.1 — 2026-10-02
Added
ddcore.vault.get(name, { shared: true })reads a secret of the platform space from any space of a site with tenancy.setanddeltake the option too and are refused inside a tenant: a shared secret is the platform's to change. The read is audited in the space it came from, withdetail.shared. See "With tenancy" invault(#62).
Fixed
- A
Vaultfield on asharedDocType is readable from inside a tenant. Its secret is kept in the platform space, where row-level security hid it from every tenant: the field read as not configured and a required one as missing. Every space now sees it as configured, and reads the value withddcore.vault.get(key, { shared: true }).ddcore tenant adoptno longer moves these secrets into the adopting tenant, and renaming a shared document no longer re-keys a tenant's secret of the same name.isSinglewithsharedis one document for the whole site, now documented. See "What belongs to a tenant" intenancy(#62).
0.24.0 — 2026-10-02
Added
ddcore.datetime.dateDiff(a, b)andddcore.datetime.monthDiff(a, b)on the Desk, with the semantics ofddcore.utils.dateDiffandmonthDiffon the server, so a form script can show a day count the controller computes:dateDiff("2026-05-10", "2026-05-01") === 9. See "Dates and times" inform-api(#58).- The Tree view has a search box. While there is text, the tree shows only the nodes that match — by
id,titleFieldandsearchFields, as a Link search does — and the ancestors that lead to them, every branch open and the matches highlighted; the text is the list's?q=. Nothing to declare in an app. On the API,GET /api/tree/{doctype}?search=answers those nodes, a match flagged"match": true. See "The tree view and/api/tree" intrees(#59). ddcore.httppresents a client certificate withopts.clientCert, for an API that authenticates by mutual TLS (PIX, Open Finance):{ pfx, password? }— a PKCS#12 file, base64-encoded — or{ cert, key }in PEM. The chain goes with the certificate, and calls with the same certificate share connections. A proxy kept only to hold the certificate can go. See "ddcore.*" incontroller-api(#60).ddcore.runAs(user, fn)runs server code under a user's roles and access scopes, in the current transaction. A job, a scheduled method and a guest webhook had no such context: a job ignores permissions, a scheduled method runs asAdminand a webhook arrives asGuest, so a multi-tenant app had to filter every query in those paths by hand. InsiderunAsreads and writes are checked as in a request from that user, andowner,modified_byand audit events record them. The user must exist and be enabled; otherwise the call throws (#40).ddcore.enqueue(method, args, { runAs: user })queues a job whose body,onStartandonFailurerun as that user with permissions enforced. The job keeps who queued it inuserand the user it acts as in the newrun_ascolumn, shown byddcore jobs list|show, the jobs API and thelist_jobs/get_jobtools; a retry keeps it. A job whose user was disabled after it was queued fails instead of running unscoped (#40).- A
schedulerentry can be{ method, runAs }instead of a bare path, to run the method under a user's roles and access scopes rather than asAdmin. A scheduled method that could not be queued is now logged; the error was dropped (#40). - Tenancy: several customers in one database.
"tenancy": trueinddcore.json, thenddcore migrate, gives every tenant its own documents, ids, numbering series,uniquevalues, Singles, users, files, vault secrets, cache keys, jobs, webhooks and realtime events. The wall is the database's: tenant tables are keyed by(tenant, id)and held by Postgres row-level security, so the document API,ddcore.db.sql, SQL reports and background jobs all stay inside the tenant without any of them naming it. Seetenancy. (PRD-08)- Every DocType belongs to a tenant unless it declares
shared: true— reference data the whole site reads and only the operator writes. - The
Site TenantDocType,ddcore tenant create|list|enable|disable|adopt, and--tenant <slug>before any one-shot command (ddcore --tenant acme eval …). ddcore.tenant.current(),.list()and.run(id, fn)for the platform's own code — a scheduled method runs once and fans out — andonTenantCreateindefineApp, which seeds each new tenant.- The operator (
Admin, or a System Manager whose account is in no tenant) enters a tenant from the Desk's user menu, or withX-Tenantand an API key. A tenant's own System Manager administers that tenant and nothing of the site: no MCP, no health report. - It cannot be turned off once migrated, and the servers and workers must be restarted after the migration that turns it on. A site that already has data keeps it in the platform space;
ddcore tenant adopt <slug>moves it into its first tenant. - The site's database role must be able to create a role, or be granted one created by hand and named in
DDCORE_TENANT_ROLE.
- Every DocType belongs to a tenant unless it declares
Changed
- With tenancy off nothing changes: no column, no policy, no new DocType, the same schema.
tenantis a reserved fieldname on a site with tenancy. An app that has a field of that name on a DocType that is notsharedmust rename it (withrenamedFrom) before turning tenancy on.
Fixed
- Events and cache invalidations registered while the server acted as another user are no longer dropped at commit. It affected the writes made on a recipient's behalf by notifications, and would have affected every write under
ddcore.runAs. - A save that unlocks a
readOnlyDependsOnfield may edit it too. The server judged the expression on the stored document alone, so settingstatusback to"Open"and editing in the same save was refused with "… is read-only on this document", although the desk had already unlocked the field. A change is now refused only when the expression holds on the stored document and on the one being saved. See "Field properties" infieldtypes(#61).