@objectstack/plugin-audit 9.6.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1189,6 +1189,34 @@ var SysActivity = ObjectSchema2.create({
1189
1189
  description: "Display label of the target record at write time",
1190
1190
  group: "Target"
1191
1191
  }),
1192
+ // ── Source pointer (ADR-0052 §5 — ActivityPointer model) ─────────
1193
+ // `object_name`/`record_id` say WHICH record this activity belongs to (the
1194
+ // "regarding" record, e.g. the contact). `source_object`/`source_id` point
1195
+ // to the RICH ENTITY this activity was derived from — the email row in
1196
+ // `sys_email`, the call/meeting in a task object, the `sys_comment` — so the
1197
+ // timeline can drill from a one-line summary to the full record. This is the
1198
+ // queryable, structured equivalent of cramming an id into `metadata`
1199
+ // (cf. Dataverse ActivityPointer → Email/PhoneCall/Appointment subtypes,
1200
+ // Salesforce ActivityTimeline → EmailMessage/Task/Event). Optional: most
1201
+ // CRUD activities have no distinct source (the record IS the source).
1202
+ source_object: Field2.text({
1203
+ label: "Source Object",
1204
+ required: false,
1205
+ readonly: true,
1206
+ searchable: true,
1207
+ maxLength: 255,
1208
+ description: 'Object name of the rich source entity this activity was derived from (e.g. "sys_email"). Null when the activity is about the target record itself.',
1209
+ group: "Target"
1210
+ }),
1211
+ source_id: Field2.text({
1212
+ label: "Source ID",
1213
+ required: false,
1214
+ readonly: true,
1215
+ searchable: true,
1216
+ maxLength: 255,
1217
+ description: "Record id of the rich source entity (paired with source_object) \u2014 lets the timeline drill to the full email/call/meeting record.",
1218
+ group: "Target"
1219
+ }),
1192
1220
  url: Field2.url({
1193
1221
  label: "URL",
1194
1222
  required: false,
@@ -1357,7 +1385,7 @@ var SysComment = ObjectSchema3.create({
1357
1385
  });
1358
1386
 
1359
1387
  // src/audit-plugin.ts
1360
- import { SysNotification, SysAttachment } from "@objectstack/platform-objects/audit";
1388
+ import { SysNotification } from "@objectstack/platform-objects/audit";
1361
1389
 
1362
1390
  // src/audit-writers.ts
1363
1391
  var SKIP_OBJECTS = /* @__PURE__ */ new Set([
@@ -1417,6 +1445,50 @@ function safeStringify(v) {
1417
1445
  return String(v);
1418
1446
  }
1419
1447
  }
1448
+ function displayFieldValue(field, value) {
1449
+ if (value === null || value === void 0 || value === "") return "\u2205";
1450
+ const options = field?.options;
1451
+ if (Array.isArray(options)) {
1452
+ for (const o of options) {
1453
+ const ov = o && typeof o === "object" ? o.value ?? o.name ?? o.label : o;
1454
+ if (ov === value) {
1455
+ const ol = o && typeof o === "object" ? o.label ?? o.name ?? ov : o;
1456
+ return String(ol);
1457
+ }
1458
+ }
1459
+ }
1460
+ return String(value);
1461
+ }
1462
+ function renderTrackedChangeSummary(fields, oldVals, newVals) {
1463
+ if (!fields || !newVals) return null;
1464
+ const parts = [];
1465
+ for (const key of Object.keys(newVals)) {
1466
+ const field = fields[key];
1467
+ if (!field || field.trackHistory !== true) continue;
1468
+ const label = typeof field.label === "string" && field.label || key;
1469
+ const from = displayFieldValue(field, oldVals ? oldVals[key] : void 0);
1470
+ const to = displayFieldValue(field, newVals[key]);
1471
+ parts.push(`${label}: ${from} \u2192 ${to}`);
1472
+ }
1473
+ return parts.length > 0 ? parts.join("; ") : null;
1474
+ }
1475
+ function matchMilestone(objectDef, fields, before, after) {
1476
+ const milestones = objectDef && Array.isArray(objectDef.activityMilestones) ? objectDef.activityMilestones : null;
1477
+ if (!milestones || !after || !before) return null;
1478
+ for (const m of milestones) {
1479
+ if (!m || typeof m.field !== "string" || typeof m.summary !== "string") continue;
1480
+ if (after[m.field] === m.value && before[m.field] !== m.value) {
1481
+ const summary = m.summary.replace(/\{(\w+)\}/g, (_match, key) => {
1482
+ const v = after[key];
1483
+ if (v === null || v === void 0 || v === "") return "";
1484
+ const field = fields ? fields[key] : void 0;
1485
+ return field ? displayFieldValue(field, v) : String(v);
1486
+ });
1487
+ return { summary, type: typeof m.type === "string" ? m.type : void 0 };
1488
+ }
1489
+ }
1490
+ return null;
1491
+ }
1420
1492
  function installAuditWriters(engine, packageId = "com.objectstack.audit", opts = {}) {
1421
1493
  if (!engine || typeof engine.registerHook !== "function") return;
1422
1494
  const getMessaging = opts.getMessaging ?? (() => void 0);
@@ -1442,6 +1514,30 @@ function installAuditWriters(engine, packageId = "com.objectstack.audit", opts =
1442
1514
  }
1443
1515
  return set != null && set.has(field);
1444
1516
  };
1517
+ const fieldDefsCache = /* @__PURE__ */ new Map();
1518
+ const getFieldDefs = (objectName) => {
1519
+ if (fieldDefsCache.has(objectName)) return fieldDefsCache.get(objectName) ?? null;
1520
+ let defs = null;
1521
+ try {
1522
+ const schema = typeof engine.getSchema === "function" ? engine.getSchema(objectName) : null;
1523
+ const fields = schema?.fields;
1524
+ if (fields && typeof fields === "object" && !Array.isArray(fields)) defs = fields;
1525
+ } catch {
1526
+ }
1527
+ fieldDefsCache.set(objectName, defs);
1528
+ return defs;
1529
+ };
1530
+ const objectDefCache = /* @__PURE__ */ new Map();
1531
+ const getObjectDef = (objectName) => {
1532
+ if (objectDefCache.has(objectName)) return objectDefCache.get(objectName);
1533
+ let def = null;
1534
+ try {
1535
+ def = typeof engine.getSchema === "function" ? engine.getSchema(objectName) : null;
1536
+ } catch {
1537
+ }
1538
+ objectDefCache.set(objectName, def);
1539
+ return def;
1540
+ };
1445
1541
  const captureBefore = async (ctx) => {
1446
1542
  if (SKIP_OBJECTS.has(ctx.object)) return;
1447
1543
  const id = ctx.input?.id;
@@ -1506,9 +1602,23 @@ function installAuditWriters(engine, packageId = "com.objectstack.audit", opts =
1506
1602
  auditRow.organization_id = tenantId ?? null;
1507
1603
  }
1508
1604
  const label = recordLabel(after ?? before, recordId ?? "");
1509
- const summary = action === "create" ? `Created ${ctx.object} "${label}"` : action === "update" ? `Updated ${ctx.object} "${label}"` : `Deleted ${ctx.object} "${label}"`;
1605
+ let summary;
1606
+ let activityType = activityTypeFor(action);
1607
+ if (action === "create") {
1608
+ summary = `Created ${ctx.object} "${label}"`;
1609
+ } else if (action === "delete") {
1610
+ summary = `Deleted ${ctx.object} "${label}"`;
1611
+ } else {
1612
+ const milestone = matchMilestone(getObjectDef(ctx.object), getFieldDefs(ctx.object), before, after);
1613
+ if (milestone) {
1614
+ summary = milestone.summary;
1615
+ if (milestone.type) activityType = milestone.type;
1616
+ } else {
1617
+ summary = renderTrackedChangeSummary(getFieldDefs(ctx.object), oldValue, newValue) ?? `Updated ${ctx.object} "${label}"`;
1618
+ }
1619
+ }
1510
1620
  const activityRow = {
1511
- type: activityTypeFor(action),
1621
+ type: activityType,
1512
1622
  // Explicit ISO timestamp — `defaultValue: 'NOW()'` on the column
1513
1623
  // isn't resolved by every driver and would otherwise leak the
1514
1624
  // literal string "NOW()" into the row.
@@ -1653,7 +1763,7 @@ var AuditPlugin = class {
1653
1763
  scope: "system",
1654
1764
  defaultDatasource: "cloud",
1655
1765
  namespace: "sys",
1656
- objects: [SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification],
1766
+ objects: [SysAuditLog, SysActivity, SysComment, SysNotification],
1657
1767
  // ADR-0029 D7 — contribute the Audit Logs entry into the Setup app's
1658
1768
  // `group_diagnostics` slot. The plugin owns sys_audit_log (K2).
1659
1769
  navigationContributions: [