Drivers & SDKs
Integrate DMART with official client libraries.
Python
pydmart Official Python client.
pip install pydmart
Usage
from pydmart import DmartService, QueryRequest, QueryType
dmart = DmartService("http://localhost:8282")
await dmart.login("dmart", "change-me")
resp = await dmart.query(QueryRequest(
type=QueryType.subpath,
space_name="management",
subpath="/users",
search="",
limit=10,
))
for record in resp.records:
print(record.shortname)
C# / .NET
Dmart.Client Async C# client for .NET Standard 2.1, .NET 8 & .NET 10 (AOT-friendly).
dotnet add package Dmart.Client
Usage
using Dmart.Client;
using Dmart.Models.Api;
using Dmart.Models.Enums;
using var client = new DmartClient("http://localhost:8282");
await client.LoginAsync("dmart", "change-me");
var resp = await client.QueryAsync(new Query
{
Type = QueryType.Subpath,
SpaceName = "management",
Subpath = "/users",
Limit = 10,
});
foreach (var record in resp.Records ?? [])
Console.WriteLine(record.Shortname);
Password resets and health checks
- A password reset by code:
PasswordResetRequestAsyncsends the code, andPasswordResetConfirmAsync(otp, password, …)redeems it and sets the new password, naming the same shortname, phone or email. No admin call sets a password (see Managing User Accounts). GetSpaceHealthAsync(space)runs thesofthealth check of a space (/managed/health/soft/{space}, global admins only), andGetSpaceHealthAsync(space, "hard")or"all"adds up to 10 sample shortnames for each check.
Errors and retired calls
- A failed record in a batch. When any record of a
RequestAsync(/managed/request) fails, the call throws aDmartExceptionwhoseError.CodeisSOMETHING_WRONG(430). Each failed record's own code and message are inError.Info[0]["failed"], aserror_codeanderror: a payload that fails its schema isINVALID_DATA(402), "payload failed schema validation", listing each failing constraint. FetchDataAssetAsyncis marked[Obsolete]. It posts to/managed/data-asset, which dmart does not serve, so every call fails. Download the file from the URLGetAttachmentUrlbuilds instead.
Reading whole numbers
A number stored as 10240.0 (the way Python, and a .NET decimal with scale, write it) is an integer to the server's JSON Schema, but System.Text.Json refuses to read it into an int. IntegralInt32Converter and IntegralInt64Converter read it into an int or long, and still refuse a real fraction:
using Dmart.Client.Json;
public sealed class Grant
{
[JsonPropertyName("data")]
[JsonConverter(typeof(IntegralInt32Converter))] // reads 10240.0 as 10240
public int Data { get; set; }
}
More .NET packages
| Package | Targets | What it is |
|---|---|---|
Dmart.Models |
.NET Standard 2.1, .NET 8, .NET 10 | the wire types (Record, Query, Response, the enums); Dmart.Client pulls it in |
Dmart.QueryGrammar |
.NET 8, .NET 10 | the parser for search expressions such as @field:value |
Dmart.SqlAdapter |
.NET 8, .NET 10 | direct reads and writes of a dmart PostgreSQL database, without the server |
Dmart.Client and these three packages are licensed LGPL-3.0-or-later: an application that uses them may be under any license, and the LGPL covers changes to the packages themselves. The dmart server is AGPL-3.0.
Dmart.SqlAdapter works on a database the dmart server has already set up, and suits tools inside the same trust boundary: ETL, bulk loads, internal services.
DmartSqlAdapter.WithRbac(db)checks every call against the roles, ACLs and query policies of theactorit names, and holds each write to the server's rules, so an app can do no more through it than the same caller could through the HTTP API (see Dmart.SqlAdapter under RBAC). Anullactor is the anonymous user.new DmartSqlAdapter(db)is the system context: no permission checks, andactoris ignored.- Either way it leaves out what the server adds on top of the database: schema validation, folder content rules, plugin hooks, the history rows a write records, and the tombstones an incremental export reads deletions from. Under RBAC it reads a ticket's workflow only to refuse what the server refuses. Use
Dmart.Clientwhen you need those. - Concurrent writes. An update, move or delete is checked, inside its transaction, against the revision of the entry it read and judged, so a change another writer made in between is never written over. An update or delete then throws
DmartConflictException, and a move moves nothing and answersfalse: load the entry again and reapply the change. The revision is a column the dmart server adds when it sets up or migrates the database. Until it is there the adapter writes without comparing, and it compares from its next write once it is, with no restart. The revision is not in the JSON the adapter or the API hands out. - Folders with
unique_fields. The adapter refuses a write into such a folder, and a folder save that setsunique_fieldsor changes them, in the system context too: use the HTTP API, which checks uniqueness inside the write's transaction, and checks the records already in a folder against new rules. A folder save that keeps its rules as stored (a new display name, a move) goes ahead. IDmartData:DmartSqlAdapterandDmartClientboth implement it, so code written against the interface takes either.
using Dmart.Models.Enums;
using Dmart.SqlAdapter;
var db = new DmartDb("Host=localhost;Username=dmart;Password=secret;Database=dmart");
var adapter = DmartSqlAdapter.WithRbac(db);
var entry = await adapter.LoadAsync("products", "/catalog", "widget_42",
ResourceType.Content, actor: "alice");
Dmart.SqlAdapter under RBAC
Reads and listings follow the server's permission rules:
-
Owning a row does not list it. A listing follows the grants' resource types and conditions. An
owngrant lists the actor's rows, but not its own user record, whichGetProfileAsyncserves without a grant. -
An ACL that denies
vieworquerykeeps a row or a space out of every listing, whatever grant admits it. The SDK exempts no one from a deny. -
A grant with
filter_fields_valueslists no row, reads no record and admits no write, except on folders and spaces: the SDK does not evaluate row filters. Read and write those rows through the HTTP API. -
__current_user__and__current_user__owner__resolve as on the server. Neither resolves for the anonymous actor or to a name of the form__name__, nor the owner word for an account no one owns, and a subpath that names one then reaches nothing. -
Subpaths. A grant on a space's root lists the whole space; an empty subpaths list (
{"S": []}) reaches nothing. The spaces listing counts only grants that holdvieworqueryand reach something. -
The management space is limited as on the server (see The Management Space), in every check and listing and in the permission map. Give the adapter the server's exemptions and sign-up role, or it limits every account that is not an admin, as a server with no exemptions does:
var adapter = DmartSqlAdapter.WithRbac(db, options: new DmartSqlAdapterOptions { ManagementExemptRoles = ["ops_admin"], // MANAGEMENT_EXEMPT_ROLES ManagementExemptAccounts = ["integration_bot"], // MANAGEMENT_EXEMPT_ACCOUNTS UserCreateDefaultRole = "customer", // USER_CREATE_DEFAULT_ROLE });A
PermissionEnginebuilt by hand takes the same three. An exemption naminganonymous,world,logged_inor the sign-up role throwsArgumentException, as the server refuses to start. -
Accounts. A deleted account holds no grant. The inactive
anonymousrow the server creates resolves its roles, so the anonymous actor gets whatworldopens. -
A read by name answers a record the actor may not read as a missing one:
nullwhere the address admits the actor,DmartPermissionDeniedExceptionwhere it does not.LoadAsync,LoadOrNoneAsync,GetSchemaAsync,FetchSpaceAsync,LoadUserMetaAsyncandLoadUserMetaForAuthAsyncanswer so, andIsEntryExistAsyncjudges a record on its stored type. -
No password hash comes back under RBAC, from
LoadUserMetaForAuthAsyncincluded. A sign-in reads the hash on a system-context adapter. -
History.
QueryHistoryAsyncreads a record's history as far as the record is listed, so aviewgrant alone reads none, and only withhistoryin a grant, withvieworquery, at the record's folder. It answers[]when refused. Its rows carry no request headers, and each diff has its secret-named fields masked at any depth, as the server's history does (seehistory). -
GetUserPermissionsAsynccomputes the mapGET /user/permissionsreturns. Under RBAC it reads the actor's own only: another account answers as a missing one, and a deactivated, deleted or anonymous actor is refused. -
GetLockerAsync(locator, actor)names the holder only to an actor that may read the record; the overload without an actor answers as the anonymous user. -
PermissionFilter.Appendfilters a missing actor as anonymous, and throwsNotSupportedExceptionforattachmentsandhistories.
Writes follow the server's write rules:
- Field limits (
restricted_fields,allowed_fields_values) apply to creates, updates and moves, to every element of an array. Removing a limited field counts as writing it as null. - The ACL, the owner and the collaborators. An update that changes the ACL is judged as an ACL update, and may hand out only actions the writer holds. Changing the owner takes
assignand an owner that exists, and changes nothing else. A create's owner is the caller, and naming another takesassign; so does naming an owner group the caller does not belong to. Changingcollaboratorstakesupdate, under the field limits oncollaboratorsandcollaborators.<key>; the map is written whole. - Times and fixed fields. An update keeps
uuid,created_at,resource_type,owner_group_shortname,last_checksum_historyand the payload'sclient_checksumandcontent_sub_typeas stored, and stampsupdated_at. A JSON payload'schecksumis the SHA-256 of its body as compact JSON: an update that changes the body sets a new one, in the system context too, and one that leaves it keeps the stored value. A create is stamped with the current time, writes noclient_checksumorcontent_sub_type, and gives a JSON body its checksum. A system-context create keeps the times and fields it is given. - Missing and forbidden alike. A caller without the right gets the same answer whether or not the record exists:
UpdateAsync,DeleteAsync,MoveAsync,TryLockAsyncandAppendHistoryAsyncthrow the refusal, andCreateAsynccheckscreatebefore the name conflict. A caller with the right still hears that the record is missing. - Moves take
updateat the source andcreateat the destination, and regenerate the row'squery_policiesso it lists in its new folder. - Deletes and moves carry what hangs off the record, in one transaction and in the system context too: a delete takes the record's attachments, history and lock, a move carries them, and a folder's delete or move takes everything beneath it.
- Tickets. A ticket created on, or put on, a workflow starts at the workflow's initial state.
- Relationships. Every target a create names, or an update adds, must be a record the caller may read: an entry, or a user, role, group, permission or space, looked up in its own table and judged at its place in management (a space at its own root). A global admin reads every target that exists.
- Locks and history. The anonymous actor takes no lock, and
AppendHistoryAsynctakesupdateon the record.
What each refusal throws:
| Refused | Thrown |
|---|---|
| An action the caller does not hold | DmartPermissionDeniedException |
A space, user, role, group, permission or attachment type in entries: written by a create, update, save or move, in the system context too, or deleted under RBAC |
DmartException, NOT_SUPPORTED_TYPE (217) |
A space name, subpath or shortname the server refuses (a / in a shortname, a trailing newline, too long): on create, save and a move's destination in the system context too, and under RBAC on update, delete, a move's source and history append |
DmartValidationException, INVALID_DATA (402) |
| Another user's live lock, on update, save, delete or move | DmartException, LOCKED_ENTRY (31), status 423 |
| An update or delete of an entry another writer changed after the call read it, in the system context too | DmartConflictException, status 409 |
A write into a folder with unique_fields, or a folder save that sets or changes them, in the system context too |
DmartValidationException, NOT_SUPPORTED_TYPE (217) |
An update that changes a workflow ticket's state, is_open, resolution_reason or workflow_shortname (use progress-ticket; a global admin may) |
DmartException, INVALID_TICKET_STATUS (300) |
| A relationship target the caller cannot read, or that is not there | DmartValidationException, INVALID_DATA, "relationship target not found" |
A folder that holds records, deleted without DeleteAsync(locator, force: true, actor), in the system context too; under RBAC, a record another record's relationship points at |
DmartException, CANNT_DELETE (204) |
| A folder moved into itself or beneath it, in the system context too | DmartValidationException, INVALID_DATA |
| An owner that does not exist | DmartNotFoundException |
Trimmed and Native AOT hosts. The rule checks serialize through a source-generated context of the SDK's own, chained before the host's resolver, and need no reflection. Pass JsonSerializerOptions whose resolver covers the Dmart.Models records the adapter reads and writes (Entry, User, Space, Dictionary<string, object>).
TypeScript / JavaScript
@edraj/tsdmart Fully typed client for Node, Deno, Bun, and browsers.
npm install @edraj/tsdmart
Usage
import axios from "axios";
import { Dmart, QueryType } from "@edraj/tsdmart";
Dmart.setAxiosInstance(axios.create({ baseURL: "http://localhost:8282" }));
await Dmart.login("dmart", "change-me");
const resp = await Dmart.query({
type: QueryType.subpath,
space_name: "management",
subpath: "/users",
search: "",
limit: 10,
});
console.log(resp?.records?.map((r) => r.shortname));
Dart / Flutter
dmart Native Dart package for cross-platform apps.
flutter pub add dmart
Usage
import 'package:dmart/dmart.dart';
Dmart.dmartServerUrl = 'http://localhost:8282';
Dmart.initDmart();
await Dmart.login(LoginRequest(shortname: 'dmart', password: 'change-me'));
final (resp, _) = await Dmart.query(QueryRequest(
queryType: QueryType.subpath,
spaceName: 'management',
subpath: '/users',
limit: 10,
));
for (final record in resp?.records ?? []) {
print(record.shortname);
}