Skip to content

Embedded Files and Documents

Daniël Smith edited this page Oct 16, 2026 · 8 revisions

Embedded Files and Documents

A JAZMIN file can carry files alongside its data: PDFs, images, audio, video, HTML, scripts, styles, fonts, or any other bytes. Typical uses:

  • Attachments: the invoice PDF for each statement, the photos taken on a site visit.
  • Branding: a logo per client.
  • A document: a template (HTML, CSS, scripts) that renders the data as a page in the viewer, or as a PDF on a server. One file then holds the data and how to show it.

How they are stored:

  • Once. Identical bytes are stored a single time, however many paths or groups refer to them.
  • In 256 KiB blocks, so part of a large file (a video being sought, a page of a PDF) is read without loading all of it.
  • As they are when already compressed (JPEG, PNG, MP4, PDF); compressed otherwise.
  • Encrypted and checked like everything else: readFile verifies each file against its SHA-256.
  • Per key in access-controlled files: each file lists the groups that may see it, and a key never learns the names of files it can't see.

Adding and reading files

import { JazminKey, append, open, write } from '@smithsoft-studios/jazmin';

const owner = JazminKey.generate();
const clientA = owner.createAccessKey();
const clientB = owner.createAccessKey();

const pdf = Buffer.concat([Buffer.from('%PDF-1.7\n'), Buffer.alloc(600_000, 32)]); // a stand-in for a real 600 KB PDF
write('statements.jzm', [
  { client: 'A', line: 1, amount: 100 },
  { client: 'B', line: 1, amount: 200 },
], {
  key: owner,
  access: {
    partitionBy: 'client',
    grants: [{ key: clientA, rows: ['A'] }, { key: clientB, rows: ['B'] }],
  },
  files: [
    { path: 'index.html', content: '<h1>Statement</h1>' },                  // groups default to '*': everyone
    { path: 'terms.pdf', content: pdf },
    { path: 'invoices/A.pdf', content: pdf, groups: ['A'] },              // the same bytes: stored once
    { path: 'img/logo-B.svg', content: '<svg xmlns="http://www.w3.org/2000/svg"/>', groups: ['B'] },
  ],
  package: { entry: 'index.html', title: 'Statement' },                     // settings for viewers
});

for (const [who, key] of [['owner', owner], ['client A', clientA], ['client B', clientB]]) {
  const r = open('statements.jzm', { key: key === owner ? owner : key.export() });
  console.log(who.padEnd(9), r.files.map((f) => `${f.path} (${f.type}, ${f.size.toLocaleString('en-US')} bytes)`).join('; '));
  r.close();
}

const a = open('statements.jzm', { key: clientA.export() });
console.log(a.readFile('index.html').toString());                 // a whole file, checked against its SHA-256
console.log(a.readFileRange('invoices/A.pdf', 0, 8).toString());  // bytes [0, 8): only the first block is read
a.close();

append('statements.jzm', { key: owner, addFiles: [{ path: 'news.html', content: '<p>New rates</p>' }], removeFiles: ['terms.pdf'] });
console.log(open('statements.jzm', { key: owner }).files.map((f) => f.path));
owner     index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); invoices/A.pdf (application/pdf, 600,009 bytes); img/logo-B.svg (image/svg+xml, 41 bytes)
client A  index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); invoices/A.pdf (application/pdf, 600,009 bytes)
client B  index.html (text/html, 18 bytes); terms.pdf (application/pdf, 600,009 bytes); img/logo-B.svg (image/svg+xml, 41 bytes)
<h1>Statement</h1>
%PDF-1.7
[ 'index.html', 'news.html', 'invoices/A.pdf', 'img/logo-B.svg' ]
Who sees a file groups
Everyone with a key to the file '*' (the default)
Keys for partitions A or B ['A', 'B']
Keys granted a named file group ['template'], with the grant's files: ['template']
  • Reading: files lists what this key can see (path, type, size, sha256; the owner also sees groups). readFile(path) returns the bytes, readFileRange(path, start, end) part of them, and openFile(path) a stream, block by block.
  • Media types come from the extension unless you give type.
  • Changing files: addFiles and removeFiles work with update() and append(). A path that exists is replaced. Appends keep existing files by reference, and a replaced or removed file keeps its space until compact().

In .NET

using System.Text;
using Jazmin;

var columns = new[] { new JazminColumn("id", JazminType.Int) };
var options = new JazminWriteOptions
{
    Files =
    [
        new JazminFileInput("index.html", Encoding.UTF8.GetBytes("<h1>Report</h1>")),
        new JazminFileInput("data/notes.txt", Encoding.UTF8.GetBytes("Quarterly notes")) { Type = "text/plain" },
    ],
    Package = new JazminPackage { Entry = "index.html", Title = "Report" },
};
using (var writer = JazminWriter.Create("report.jzm", columns, options))
    writer.WriteValues(1L);

using var reader = JazminReader.Open("report.jzm");
foreach (var f in reader.Files) Console.WriteLine($"{f.Path} {f.Type} {f.Size} bytes");
Console.WriteLine(Encoding.UTF8.GetString(reader.ReadFile("data/notes.txt")));
using var stream = reader.OpenFile("index.html"); // seekable; decodes one block at a time
Console.WriteLine(new StreamReader(stream).ReadToEnd());
Console.WriteLine($"entry {reader.Package!.Entry}, title {reader.Package.Title}");
data/notes.txt text/plain 15 bytes
index.html text/html 15 bytes
Quarterly notes
<h1>Report</h1>
entry index.html, title Report

JazminFileInput.FromFile(path, filePathOnDisk, groups) embeds a file from disk, read in blocks so it is never wholly in memory.

Documents: a template that shows the data

A package is a set of embedded files with an entry page. The viewer shows it as the file's document, and renderPdf prints it on a server. The page runs in a sandbox with no network access (apart from the package's allowedOrigins), no browser storage, and no sight of the key. It reads the data through window.jazmin:

<!doctype html>
<html>
<head><link rel="stylesheet" href="style.css"></head>
<body>
  <img id="logo" alt="">
  <h1 id="title"></h1>
  <table id="lines"><thead><tr><th>Date</th><th>Description</th><th>Amount</th></tr></thead><tbody></tbody></table>
  <p>Total: <span id="total"></span></p>
  <script>
    (async () => {
      document.getElementById('title').textContent = jazmin.metadata.title ?? 'Statement';
      document.getElementById('logo').src = jazmin.asset('img/logo.svg');     // an embedded file
      const rows = await jazmin.query(null, { orderBy: 'date', limit: 500, select: ['date', 'description', 'amount'] });
      const body = document.querySelector('#lines tbody');
      let total = 0;
      for (const r of rows) {
        const tr = body.insertRow();
        tr.insertCell().textContent = new Date(r.date).toLocaleDateString();
        tr.insertCell().textContent = r.description;
        tr.insertCell().textContent = r.amount;
        total += Number(r.amount);
      }
      document.getElementById('total').textContent = total.toFixed(2);
      jazmin.ready({ rows: rows.length }); // tells renderPdf the page is complete
    })();
  </script>
</body>
</html>
window.jazmin What it does
metadata, columns, access, rowCount About the file, as the key sees it
await query(filter, { orderBy, offset, limit, select }) A page of rows (orderBy: '-amount' for descending)
await count(filter) Matching rows
rows() Every row, for small data sets (up to 20,000 rows)
asset(path) An embedded file as an in-memory URL, for img.src, fonts, media
await file(path) An embedded file as a Blob
download(name, content, type) Offers the person a file (CSV, say)
print(), navigate(page), ready(info) Print, open another page of the package, say the page is complete
mode, filter (1.4) 'view', or 'print' when rendered as a PDF or image; the rows a PDF was limited to
actions, setActions(changes), savePdf(settings) (1.4) What the viewer offers for this page; turn it off or back on; save as PDF (below)
edit, await saveChanges({ update, add, delete }) (1.4) What the document may change; save changes (below)
table, tables, the table option of query and count (1.6.0) The document's own table; every table of the file ({ name, columns, rowCount }); read any of them by name (Several Tables)
  • Queries run in the viewer, which holds the reader and the key; only the requested rows cross into the sandbox.
  • Use relative paths (img/logo.svg, about.html), plain scripts rather than modules, and window.jazmin rather than fetch(). Links to other .html files in the package open inside the viewer.
  • Allowed origins (package: { allowedOrigins: ['https://fonts.googleapis.com', 'https://fonts.gstatic.com'] }) let the page load scripts, styles, fonts, images and media from those https:// origins only. Each is a trade-off: that site learns when the document is opened, the document needs the network to show it, and its scripts run with the data in view. Files a template needs are better embedded: a font is typically 20–100 KB.
  • WebAssembly is off unless package: { allowWasm: true }.

Documents that need the internet (1.7.0)

A document that loads something from its allowed origins (a chart library from a CDN, a web font) can say so with package.online (.NET JazminPackage.Online, JazminOnline):

  • 'required': it doesn't work offline. The web viewer says the document needs to be online and, while the device is offline, shows a message instead of a half-drawn page, then the document once it is back online.
  • 'optional': it works with less offline (a font that falls back, say). The viewer only says it uses the internet.
import { open, write } from '@smithsoft-studios/jazmin';

write('dashboard.jzm', [{ month: '2026-09', sales: 1250 }], {
  files: [{ path: 'index.html', content: '<canvas id="chart"></canvas>' }],
  package: { entry: 'index.html', allowedOrigins: ['https://cdn.jsdelivr.net'], online: 'required' },
});
console.log(open('dashboard.jzm').package);

try {
  write('broken.jzm', [{ id: 1 }], { files: [{ path: 'index.html', content: '<p>x</p>' }], package: { entry: 'index.html', online: 'required' } });
} catch (e) {
  console.log(e.message);
}
{
  entry: 'index.html',
  allowedOrigins: [ 'https://cdn.jsdelivr.net' ],
  online: 'required'
}
package.online needs allowedOrigins: the https origins the document loads from
  • Pin what you load: an exact version (chart.js@4.4.6, not @4) and an integrity hash on the <script> tag, so the browser refuses a file that changed.
  • Older viewers (1.0 to 1.6) ignore online, and leave it out if they rewrite the file's settings.
  • The demo gallery's Documents page has an online dashboard (dashboard.jzm) that shows its figures as a table when the charts can't load.

Document pages: several pages as tabs (1.7.0)

A document can have several pages. package.pages lists them in order, and the web viewer shows each as a tab on the Document tab. Each page is a stored HTML page that reads the data itself (jazmin.query, jazmin.count), and each can have its own settings:

  • title: the tab's name.
  • table: the table its queries read when they name none (in a file of several tables).
  • online: over the package's. Offline, a page that needs the internet says so in its own tab, and the other tabs still work.
  • pdf and actions (print, pdf, image): over its file's, so a print layout has Print and a list page doesn't. The page's code can still turn an allowed button off and on (jazmin.setActions).

package.files names the document's own files (scripts, styles, fonts: paths, or folders ending in /). Viewers keep them, and the pages, out of the Files tab; the pages load them as before.

import { open, write } from '@smithsoft-studios/jazmin';

write('bank.jzm', { clients: [{ id: 1, name: 'Thandi' }], payments: [{ id: 1, client: 1, amount: '250.00' }] }, {
  tables: [
    { name: 'clients', columns: [{ name: 'id', type: 'int' }, { name: 'name', type: 'string' }] },
    { name: 'payments', columns: [{ name: 'id', type: 'int' }, { name: 'client', type: 'int' }, { name: 'amount', type: 'decimal' }] },
  ],
  files: [
    { path: 'clients.html', content: '<h1>Clients</h1>' }, { path: 'payments.html', content: '<h1>Payments</h1>' },
    { path: 'summary.html', content: '<canvas></canvas>' }, { path: 'app.js', content: '// shared by the pages' },
    { path: 'terms.pdf', content: 'Terms' },
  ],
  package: {
    pages: [
      { path: 'clients.html', title: 'Clients', table: 'clients', actions: { print: false } },
      { path: 'payments.html', title: 'Payments', table: 'payments' },
      { path: 'summary.html', title: 'Summary', online: 'required' },
    ],
    files: ['app.js'],
    allowedOrigins: ['https://cdn.jsdelivr.net'],
  },
});
const { entry, pages, files } = open('bank.jzm').package;
console.log(entry, pages.map((p) => p.title), files);

try {
  write('broken.jzm', [{ id: 1 }], { files: [{ path: 'a.html', content: 'a' }], package: { pages: [{ path: 'b.html', title: 'B' }] } });
} catch (e) {
  console.log(e.message);
}
clients.html [ 'Clients', 'Payments', 'Summary' ] [ 'app.js' ]
package.pages[0].path 'b.html' is not one of the stored files
  • entry is the page that opens first, one of the pages (the first when you leave it out). Viewers of 1.0 to 1.6 show that page alone and list every file.
  • Pages only some keys see: the package settings are read by every key of a shared file, so the names in pages and files are seen by every key. Mark such a page on its file instead, in its file groups' directories: { path: 'board.html', content, groups: ['directors'], page: { title: 'Board pack', order: 1 } }. Keys that see the group get the tab after the package's pages; others see neither the tab nor the file. Readers give it as files[i].page.
  • .NET: JazminPackage.Pages (JazminPage), JazminPackage.Files, JazminFileInput.Page and JazminEmbeddedFile.Page, with the same checks and messages.

Print-ready documents and file actions

New in 1.4. Versions 1.0 to 1.3 read files with these settings and ignore them, but drop them when they append to or update the file.

Not every page of a package is meant for paper, and not every file is meant to be saved. Each file can say what viewers may do with it (actions, set when it is added), and the package can give page settings for its PDFs:

import { open, write } from '@smithsoft-studios/jazmin';

write('statement.jzm', [{ account: 'A1', amount: '12.50' }], {
  files: [
    { path: 'index.html', content: '<h1>Statement</h1>', actions: { pdf: { format: 'A4', margin: { top: '15mm' } } } },
    { path: 'data.csv', content: 'account,amount\nA1,12.50\n', actions: { open: false, save: false } }, // the page reads it
  ],
  package: { entry: 'index.html', title: 'Statement', pdf: { format: 'A4' } },
});

const reader = open('statement.jzm');
for (const f of reader.files) console.log(f.path, JSON.stringify(f.actions));
console.log(JSON.stringify(reader.package.pdf));
reader.close();
data.csv {"open":false,"save":false}
index.html {"pdf":{"format":"A4","margin":{"top":"15mm"}}}
{"format":"A4"}
Action Viewers offer false for, say
open showing the file a data file only the document reads
save saving (downloading) the file as it is a logo, a script
print printing the page an on-screen dashboard
pdf saving the page as PDF; page settings instead of true set its page a page that isn't print-ready
image saving the page as an image a long statement
list (1.7.0) listing the file among the file's files a script, a font or a data file the page loads
  • Each is allowed when left out. The web viewer hides Download for save: false, and leaves files with list: false out of its Files tab (the page still loads them).
  • They steer viewers; they don't lock the file. A key that sees a file can always read it with the library. To keep a file from a key, use groups or a lock.
  • Page settings (package.pdf for the document, actions.pdf for one page): format (A0–A6, Letter, Legal, Tabloid, Ledger), landscape, margin ({ top, right, bottom, left }: '12mm', '1cm', '0.5in', '20px'), scale (0.1–2), printBackground, and preferCSSPageSize (the page's CSS @page size wins). Writers refuse anything else, so a file never holds settings a browser would reject.
  • In .NET: JazminFileInput.Actions (new JazminFileActions { Save = false, PdfSettings = new JazminPdfSettings { Format = "A4" } }), JazminPackage.Pdf, and reader.Files[i].Actions.

In the page, one template works on screen and on paper:

if (jazmin.mode === 'print') document.body.classList.add('print'); // 'print' when rendered as a PDF or an image
printButton.hidden = !jazmin.actions.print;       // { print, pdf, image }: what the viewer offers for this page
jazmin.setActions({ pdf: { landscape: true } });  // this page, while shown: off, back on, or page settings
pdfButton.onclick = () => jazmin.savePdf();       // the web viewer opens the print dialog (Save as PDF)
  • jazmin.actions combines the page's file settings, what the viewer can do, and the page's own setActions. A page can turn off what its file allows, but can't allow what its file refuses. Another page starts again from its own file's settings.
  • print() and savePdf() throw when the action isn't allowed.
  • On a server, renderPdf uses the page settings: the package's, then the page's file's, then the page's own, then its pdf option. See Server-Side.

Actions by file group (1.7.0)

In an access-controlled file, a file seen by several groups can give some of them their own actions (groupActions, .NET JazminFileInput.GroupActions): downloads for HR, say, and not for staff. A key that sees the file through several groups may do what any of them allows. Each key sees the actions that apply to it; the owner sees each group's (files[i].groupActions, .NET JazminEmbeddedFile.GroupActions).

import { JazminKey, open, write } from '@smithsoft-studios/jazmin';

const owner = JazminKey.generate();
const hr = owner.createAccessKey();
const staff = owner.createAccessKey();
write('payroll.jzm', [{ id: 1 }], {
  key: owner, columns: [{ name: 'id', type: 'int' }],
  access: { grants: [{ key: hr, files: ['hr'] }, { key: staff, files: ['staff'] }] },
  files: [
    { path: 'logo.svg', content: '<svg/>', actions: { list: false } },
    { path: 'payslips.pdf', content: 'Payslips', groups: ['hr', 'staff'], groupActions: { hr: { save: true }, staff: { save: false } } },
  ],
});

for (const [who, key] of [['HR', hr], ['Staff', staff]]) {
  const r = open('payroll.jzm', { key });
  console.log(who, r.files.map((f) => `${f.path} ${JSON.stringify(f.actions ?? {})}`).join(', '));
  r.close();
}
const o = open('payroll.jzm', { key: owner });
console.log('Owner', JSON.stringify(o.files.find((f) => f.path === 'payslips.pdf').groupActions));
o.close();
HR logo.svg {"list":false}, payslips.pdf {"save":true}
Staff logo.svg {"list":false}, payslips.pdf {"save":false}
Owner {"hr":{"save":true},"staff":{"save":false}}

Update, compact and append keep them. Releases 1.0 to 1.6 read such files with the first group's actions, and ignore list; if they rewrite one, every group gets the first group's actions.

Locked files: a password or 2FA as well as the key (1.7.0)

In a shared file (Access Control), a stored file can be locked. A key that sees it learns its name, type and size, but opening it also needs:

  • a password, or
  • an unlock token from your key service, which can ask for a second factor (2FA, such as a code from an authenticator app) first, or
  • both.

So if an access key leaks, its locked files stay shut. One lock can cover several files, and one unlock opens them all. The owner key opens every lock without a password, so the owner can always read, change and rewrite the file.

import { JazminKey, issueFileUnlockToken, open, update, write } from '@smithsoft-studios/jazmin';

const owner = JazminKey.generate();     // keep in your secret manager
const claimant = owner.createAccessKey();
write('claim.jzm', [{ id: 1 }], {
  key: owner, columns: [{ name: 'id', type: 'int' }], access: { grants: [{ key: claimant }] },
  files: [
    { path: 'contract.pdf', content: 'The contract', lock: 'contracts' },
    { path: 'scan.png', content: 'The scan', lock: 'medical' },
  ],
  locks: {
    contracts: { password: 'correct horse battery staple' },
    medical: { online: true },          // an unlock token from your key service; { password, online: true } for both
  },
});

const reader = open('claim.jzm', { key: claimant });
console.log(reader.files.map((f) => `${f.path}: ${JSON.stringify(f.lock)}`).join('\n'));
try {
  reader.readFile('contract.pdf');
} catch (e) {
  console.log(`${e.name}: ${e.message}`);   // e.lock and e.kind say what to ask for
}
reader.unlock('contracts', { password: 'correct horse battery staple' });
console.log(reader.readFile('contract.pdf').toString());

// Your key service does this after its own checks (sign-in, 2FA, the person's grant)
const unlockToken = issueFileUnlockToken('claim.jzm', owner, 'medical');   // 'jzf1-...'
reader.unlock('medical', { unlockToken });
console.log(reader.readFile('scan.png').toString());
reader.close();

// The owner changes a password, and removes a lock (its files stay, unlocked)
update('claim.jzm', { key: owner, locks: { contracts: { password: 'a new, longer passphrase' }, medical: null } });
const again = open('claim.jzm', { key: claimant });
console.log(again.locks);
again.close();
contract.pdf: {"id":"contracts","kind":"password","unlocked":false}
scan.png: {"id":"medical","kind":"online","unlocked":false}
JazminFileLockedError: File 'contract.pdf' is locked (lock 'contracts'): unlock it with its password
The contract
The scan
[ { id: 'contracts', kind: 'password', unlocked: false } ]
using Jazmin;
using System.Text;

var owner = JazminKey.Generate();
var claimant = owner.CreateAccessKey();
using (var writer = JazminWriter.Create("claim.jzm", [new JazminColumn("id", JazminType.Int)], new JazminWriteOptions
{
    Key = owner,
    Access = new JazminAccessOptions { Grants = [new JazminGrant(claimant)] },
    Files = [new JazminFileInput("contract.pdf", "The contract"u8.ToArray()) { Lock = "contracts" }, new JazminFileInput("scan.png", "The scan"u8.ToArray()) { Lock = "medical" }],
    Locks = new Dictionary<string, JazminLockOptions?>
    {
        ["contracts"] = new() { Password = "correct horse battery staple" },
        ["medical"] = new() { Online = true },
    },
}))
{
    writer.WriteValues(1L);
}

using var reader = JazminReader.Open("claim.jzm", new JazminReadOptions { AccessKey = claimant });
foreach (var f in reader.Files) Console.WriteLine($"{f.Path}: lock {f.Lock?.Id} ({f.Lock?.Kind}), unlocked {f.Lock?.Unlocked}");
try
{
    reader.ReadFile("contract.pdf");
}
catch (JazminFileLockedException e)
{
    Console.WriteLine($"{e.GetType().Name}: needs {e.Kind}");
}
reader.Unlock("contracts", password: "correct horse battery staple");
reader.Unlock("medical", unlockToken: JazminFile.IssueFileUnlockToken("claim.jzm", owner, "medical"));   // from your key service
Console.WriteLine($"{Encoding.UTF8.GetString(reader.ReadFile("contract.pdf"))} / {Encoding.UTF8.GetString(reader.ReadFile("scan.png"))}");
contract.pdf: lock contracts (Password), unlocked False
scan.png: lock medical (Online), unlocked False
JazminFileLockedException: needs Password
The contract / The scan
  • Changing locks (update or append with the owner key; .NET JazminUpdate.Locks, JazminAppend.Locks): a new password changes it without storing the files again; { password, online: true } adds a password to an online lock; null removes a lock. Locks you don't name are kept. An online lock keeps its secret, so tokens already issued still work.
  • A wrong password or token throws JazminKeyError (.NET JazminKeyException). An unlock lasts while the reader is open.
  • Only shared files have locks. In a file with one key or password, that key would open every lock, so writers refuse them.
  • Password locks can be guessed offline by whoever has the file and a key that sees the locked file, as fast as PBKDF2 allows (600,000 iterations by default; iterations changes it). Use long passphrases. Online locks can't be guessed this way, because their secret isn't in the file.
  • Once a file is open, its bytes can be copied. A lock decides who can open a file; actions decide what viewers offer.
  • Masked columns use the same locks: one unlock can open locked files and masked columns together.
  • In the browser and the viewer: the browser reader has files() (each with lock), locks() and unlock(id, { password, unlockToken }). The web viewer's Files tab shows a locked file with a padlock and Unlock…, which asks for what the lock needs. A document's page doesn't get a locked file (jazmin.file() gives null) until it is unlocked; the viewer then shows the document again with it.
  • Tokens from a key service with 2FA: see Time-Limited Access.
  • Older libraries (1.0 to 1.6) read the rows and every unlocked file, and refuse a locked one ("has no key"). Don't let them append to a file with locks: they don't check the file's writer feature (file-locks), write the file without its locks, and the locked files can't be opened again. Their update and compact fail without changing the file. 1.7 refuses to change a file with a writer feature it doesn't know.

Documents that change their data

New in 1.4. Versions 1.0 to 1.3 ignore package.edit.

A document can let people change the rows it shows (change them, add some, delete some) when its author allows it in the package: edit: { key, columns, add, delete }. The page calls jazmin.saveChanges(...); the viewer checks the changes, shows them and asks.

  • A file of your own (not shared), opened with its key: the changes are written into it.
  • A shared file: each person works on their own copy. Their changes go back to the owner in a change file, sealed with their submission key, and the owner applies it with applyChanges. A browser can't change a file on disk, so the web viewer always saves a change file.

A change file carries the values the person saw, so a row someone else changed in the meantime is held for the owner instead of overwritten:

import { applyChanges, open, update, write, writeChanges } from '@smithsoft-studios/jazmin';

write('tasks.jzm', [{ id: 1, task: 'Call the client', owner: null }, { id: 2, task: 'Send the quote', owner: null }], {
  columns: [{ name: 'id', type: 'int', nullable: false }, { name: 'task', type: 'string' }, { name: 'owner', type: 'string' }],
  files: [{ path: 'index.html', content: '<h1>Tasks</h1>' }],
  package: { entry: 'index.html', edit: { key: ['id'], columns: ['owner'] } },
});

// What a viewer does when the document saves its changes: a change file, with the values the person saw.
const seen = open('tasks.jzm');
const change = writeChanges(seen, { update: [{ id: 1, owner: 'Ann' }, { id: 2, owner: 'Ann' }] });
seen.close();

// Meanwhile someone else gives task 2 to Ben.
update('tasks.jzm', { upsert: [{ id: 2, task: 'Send the quote', owner: 'Ben' }], keyColumns: ['id'] });

const result = applyChanges('tasks.jzm', change);
console.log(`${result.updated} updated`);
for (const c of result.conflicts) {
  for (const col of c.columns) console.log(`task ${c.key.id} held: ${col.name} was ${col.before}, wanted ${col.wanted}, now ${col.now}`);
}
1 updated
task 2 held: owner was null, wanted Ann, now Ben
  • Checked again when applied: what the document allows now, and in a shared file the sender's access: only its partitions and columns. Changes that aren't allowed are refused, with the reason.
  • Held changes (conflicts): rows changed since the sender's copy, an added row whose key is already there, or a row that's gone. applyChanges(file, change, { overwrite: true }) applies them; dryRun: true shows what would happen without writing.
  • In .NET: JazminPackage.Edit, JazminFile.WriteChanges and JazminFile.ApplyChanges, with the same change files.

Clone this wiki locally