@objectstack/plugin-audit 9.7.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.js CHANGED
@@ -1209,6 +1209,34 @@ var SysActivity = import_data2.ObjectSchema.create({
1209
1209
  description: "Display label of the target record at write time",
1210
1210
  group: "Target"
1211
1211
  }),
1212
+ // ── Source pointer (ADR-0052 §5 — ActivityPointer model) ─────────
1213
+ // `object_name`/`record_id` say WHICH record this activity belongs to (the
1214
+ // "regarding" record, e.g. the contact). `source_object`/`source_id` point
1215
+ // to the RICH ENTITY this activity was derived from — the email row in
1216
+ // `sys_email`, the call/meeting in a task object, the `sys_comment` — so the
1217
+ // timeline can drill from a one-line summary to the full record. This is the
1218
+ // queryable, structured equivalent of cramming an id into `metadata`
1219
+ // (cf. Dataverse ActivityPointer → Email/PhoneCall/Appointment subtypes,
1220
+ // Salesforce ActivityTimeline → EmailMessage/Task/Event). Optional: most
1221
+ // CRUD activities have no distinct source (the record IS the source).
1222
+ source_object: import_data2.Field.text({
1223
+ label: "Source Object",
1224
+ required: false,
1225
+ readonly: true,
1226
+ searchable: true,
1227
+ maxLength: 255,
1228
+ 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.',
1229
+ group: "Target"
1230
+ }),
1231
+ source_id: import_data2.Field.text({
1232
+ label: "Source ID",
1233
+ required: false,
1234
+ readonly: true,
1235
+ searchable: true,
1236
+ maxLength: 255,
1237
+ description: "Record id of the rich source entity (paired with source_object) \u2014 lets the timeline drill to the full email/call/meeting record.",
1238
+ group: "Target"
1239
+ }),
1212
1240
  url: import_data2.Field.url({
1213
1241
  label: "URL",
1214
1242
  required: false,
@@ -1437,6 +1465,50 @@ function safeStringify(v) {
1437
1465
  return String(v);
1438
1466
  }
1439
1467
  }
1468
+ function displayFieldValue(field, value) {
1469
+ if (value === null || value === void 0 || value === "") return "\u2205";
1470
+ const options = field?.options;
1471
+ if (Array.isArray(options)) {
1472
+ for (const o of options) {
1473
+ const ov = o && typeof o === "object" ? o.value ?? o.name ?? o.label : o;
1474
+ if (ov === value) {
1475
+ const ol = o && typeof o === "object" ? o.label ?? o.name ?? ov : o;
1476
+ return String(ol);
1477
+ }
1478
+ }
1479
+ }
1480
+ return String(value);
1481
+ }
1482
+ function renderTrackedChangeSummary(fields, oldVals, newVals) {
1483
+ if (!fields || !newVals) return null;
1484
+ const parts = [];
1485
+ for (const key of Object.keys(newVals)) {
1486
+ const field = fields[key];
1487
+ if (!field || field.trackHistory !== true) continue;
1488
+ const label = typeof field.label === "string" && field.label || key;
1489
+ const from = displayFieldValue(field, oldVals ? oldVals[key] : void 0);
1490
+ const to = displayFieldValue(field, newVals[key]);
1491
+ parts.push(`${label}: ${from} \u2192 ${to}`);
1492
+ }
1493
+ return parts.length > 0 ? parts.join("; ") : null;
1494
+ }
1495
+ function matchMilestone(objectDef, fields, before, after) {
1496
+ const milestones = objectDef && Array.isArray(objectDef.activityMilestones) ? objectDef.activityMilestones : null;
1497
+ if (!milestones || !after || !before) return null;
1498
+ for (const m of milestones) {
1499
+ if (!m || typeof m.field !== "string" || typeof m.summary !== "string") continue;
1500
+ if (after[m.field] === m.value && before[m.field] !== m.value) {
1501
+ const summary = m.summary.replace(/\{(\w+)\}/g, (_match, key) => {
1502
+ const v = after[key];
1503
+ if (v === null || v === void 0 || v === "") return "";
1504
+ const field = fields ? fields[key] : void 0;
1505
+ return field ? displayFieldValue(field, v) : String(v);
1506
+ });
1507
+ return { summary, type: typeof m.type === "string" ? m.type : void 0 };
1508
+ }
1509
+ }
1510
+ return null;
1511
+ }
1440
1512
  function installAuditWriters(engine, packageId = "com.objectstack.audit", opts = {}) {
1441
1513
  if (!engine || typeof engine.registerHook !== "function") return;
1442
1514
  const getMessaging = opts.getMessaging ?? (() => void 0);
@@ -1462,6 +1534,30 @@ function installAuditWriters(engine, packageId = "com.objectstack.audit", opts =
1462
1534
  }
1463
1535
  return set != null && set.has(field);
1464
1536
  };
1537
+ const fieldDefsCache = /* @__PURE__ */ new Map();
1538
+ const getFieldDefs = (objectName) => {
1539
+ if (fieldDefsCache.has(objectName)) return fieldDefsCache.get(objectName) ?? null;
1540
+ let defs = null;
1541
+ try {
1542
+ const schema = typeof engine.getSchema === "function" ? engine.getSchema(objectName) : null;
1543
+ const fields = schema?.fields;
1544
+ if (fields && typeof fields === "object" && !Array.isArray(fields)) defs = fields;
1545
+ } catch {
1546
+ }
1547
+ fieldDefsCache.set(objectName, defs);
1548
+ return defs;
1549
+ };
1550
+ const objectDefCache = /* @__PURE__ */ new Map();
1551
+ const getObjectDef = (objectName) => {
1552
+ if (objectDefCache.has(objectName)) return objectDefCache.get(objectName);
1553
+ let def = null;
1554
+ try {
1555
+ def = typeof engine.getSchema === "function" ? engine.getSchema(objectName) : null;
1556
+ } catch {
1557
+ }
1558
+ objectDefCache.set(objectName, def);
1559
+ return def;
1560
+ };
1465
1561
  const captureBefore = async (ctx) => {
1466
1562
  if (SKIP_OBJECTS.has(ctx.object)) return;
1467
1563
  const id = ctx.input?.id;
@@ -1526,9 +1622,23 @@ function installAuditWriters(engine, packageId = "com.objectstack.audit", opts =
1526
1622
  auditRow.organization_id = tenantId ?? null;
1527
1623
  }
1528
1624
  const label = recordLabel(after ?? before, recordId ?? "");
1529
- const summary = action === "create" ? `Created ${ctx.object} "${label}"` : action === "update" ? `Updated ${ctx.object} "${label}"` : `Deleted ${ctx.object} "${label}"`;
1625
+ let summary;
1626
+ let activityType = activityTypeFor(action);
1627
+ if (action === "create") {
1628
+ summary = `Created ${ctx.object} "${label}"`;
1629
+ } else if (action === "delete") {
1630
+ summary = `Deleted ${ctx.object} "${label}"`;
1631
+ } else {
1632
+ const milestone = matchMilestone(getObjectDef(ctx.object), getFieldDefs(ctx.object), before, after);
1633
+ if (milestone) {
1634
+ summary = milestone.summary;
1635
+ if (milestone.type) activityType = milestone.type;
1636
+ } else {
1637
+ summary = renderTrackedChangeSummary(getFieldDefs(ctx.object), oldValue, newValue) ?? `Updated ${ctx.object} "${label}"`;
1638
+ }
1639
+ }
1530
1640
  const activityRow = {
1531
- type: activityTypeFor(action),
1641
+ type: activityType,
1532
1642
  // Explicit ISO timestamp — `defaultValue: 'NOW()'` on the column
1533
1643
  // isn't resolved by every driver and would otherwise leak the
1534
1644
  // literal string "NOW()" into the row.
@@ -1673,7 +1783,7 @@ var AuditPlugin = class {
1673
1783
  scope: "system",
1674
1784
  defaultDatasource: "cloud",
1675
1785
  namespace: "sys",
1676
- objects: [SysAuditLog, SysActivity, SysComment, import_audit.SysAttachment, import_audit.SysNotification],
1786
+ objects: [SysAuditLog, SysActivity, SysComment, import_audit.SysNotification],
1677
1787
  // ADR-0029 D7 — contribute the Audit Logs entry into the Setup app's
1678
1788
  // `group_diagnostics` slot. The plugin owns sys_audit_log (K2).
1679
1789
  navigationContributions: [