@frogfish/k2db 3.0.6 → 3.0.8

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.
Files changed (3) hide show
  1. package/db.d.ts +1 -1
  2. package/db.js +178 -31
  3. package/package.json +1 -1
package/db.d.ts CHANGED
@@ -288,7 +288,7 @@ export declare class K2DB {
288
288
  private static normalizeCriteriaIds;
289
289
  /** Uppercase helper for `_uuid` field supporting operators like $in/$nin/$eq/$ne and arrays. */
290
290
  private static normalizeUuidField;
291
- /** Lowercase helper for `_owner` field supporting operators like $in/$nin/$eq/$ne and arrays. */
291
+ /** Trim helper for `_owner` field supporting operators like $in/$nin/$eq/$ne and arrays. */
292
292
  private static normalizeOwnerField;
293
293
  /** Strip any user-provided fields that start with '_' (reserved). */
294
294
  private static stripReservedFields;
package/db.js CHANGED
@@ -398,7 +398,9 @@ export class K2DB {
398
398
  return undefined;
399
399
  if (s === "*")
400
400
  return "*";
401
- return s.toLowerCase();
401
+ // Scope maps to `_owner` (usually another record's `_uuid`), which is case-sensitive.
402
+ // Our `_uuid` is UUIDv7 Crockford Base32 (uppercase), so normalize to that form.
403
+ return K2DB.normalizeId(s);
402
404
  }
403
405
  /**
404
406
  * Apply a scope constraint to criteria for ownership enforcement.
@@ -418,8 +420,13 @@ export class K2DB {
418
420
  // If caller already provided _owner in criteria, ensure it matches the scope to avoid ambiguity/bypass.
419
421
  if (criteria && typeof criteria === "object" && Object.prototype.hasOwnProperty.call(criteria, "_owner")) {
420
422
  const existing = criteria._owner;
421
- if (typeof existing === "string" && existing.trim().toLowerCase() !== normalizedScope) {
422
- throw new K2Error(ServiceError.BAD_REQUEST, "Conflicting _owner in criteria and provided scope", "sys_mdb_scope_conflict");
423
+ if (typeof existing === "string") {
424
+ const ex = existing.trim();
425
+ // Treat any explicit "*" as mismatched here (caller should omit _owner or set scope="*").
426
+ const existingNorm = ex === "*" ? "*" : K2DB.normalizeId(ex);
427
+ if (existingNorm !== normalizedScope) {
428
+ throw new K2Error(ServiceError.BAD_REQUEST, "Conflicting _owner in criteria and provided scope", "sys_mdb_scope_conflict");
429
+ }
423
430
  }
424
431
  // If it matches (or is non-string), prefer the explicit scope value.
425
432
  }
@@ -457,7 +464,23 @@ export class K2DB {
457
464
  const msg = err instanceof Error
458
465
  ? `Failed to connect to MongoDB: ${err.message}`
459
466
  : `Failed to connect to MongoDB: ${String(err)}`;
460
- throw wrap(err, ServiceError.SERVICE_UNAVAILABLE, "sys_mdb_init", msg);
467
+ // Preserve existing K2Error severity if already typed; otherwise map to SERVICE_UNAVAILABLE.
468
+ const sev = err instanceof K2Error ? undefined : ServiceError.SERVICE_UNAVAILABLE;
469
+ const k2 = chain(err, "sys_mdb_init", msg, sev, "k2db.init")
470
+ .setSensitive({
471
+ op: "init",
472
+ db: this.conf?.name,
473
+ // safeConnectUrl is already masked (no credentials)
474
+ connectUrl: safeConnectUrl,
475
+ options: summariseValueShape(options),
476
+ mongo: normaliseMongoError(err),
477
+ });
478
+ // Emit once (deduped per error instance)
479
+ emitDbError(k2, {
480
+ op: "init",
481
+ db: this.conf?.name,
482
+ });
483
+ throw k2;
461
484
  }
462
485
  })().finally(() => {
463
486
  // Allow retry after failure; once initialized, subsequent calls return early.
@@ -540,12 +563,16 @@ export class K2DB {
540
563
  }
541
564
  catch (err) {
542
565
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
543
- throw chain(err, "sys_mdb_gc", `Error getting collection: ${collectionName}`, sev, "k2db.getCollection")
544
- .setSensitive({
566
+ const k2 = chain(err, "sys_mdb_gc", `Error getting collection: ${collectionName}`, sev, "k2db.getCollection").setSensitive({
545
567
  op: "getCollection",
546
568
  collection: collectionName,
547
569
  mongo: normaliseMongoError(err),
548
570
  });
571
+ emitDbError(k2, {
572
+ op: "getCollection",
573
+ collection: collectionName,
574
+ });
575
+ throw k2;
549
576
  }
550
577
  }
551
578
  /**
@@ -557,10 +584,35 @@ export class K2DB {
557
584
  async get(collectionName, uuid, scope) {
558
585
  const id = K2DB.normalizeId(uuid);
559
586
  // Note: findOne() decrypts secure-prefixed fields for single-record reads when encryption is enabled.
560
- const res = await this.findOne(collectionName, {
561
- _uuid: id,
562
- _deleted: { $ne: true },
563
- }, undefined, scope);
587
+ let res;
588
+ try {
589
+ res = await this.findOne(collectionName, {
590
+ _uuid: id,
591
+ _deleted: { $ne: true },
592
+ }, undefined, scope);
593
+ }
594
+ catch (err) {
595
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
596
+ const k2 = chain(err, "sys_mdb_get", "Error getting document", sev, "k2db.get");
597
+ // Merge/attach sensitive diagnostics without stomping any existing sensitive payload.
598
+ const prevSensitive = k2.sensitive;
599
+ const mergedSensitive = prevSensitive && typeof prevSensitive === "object" ? { ...prevSensitive } : {};
600
+ Object.assign(mergedSensitive, {
601
+ op: "get",
602
+ collection: collectionName,
603
+ uuid: id,
604
+ scope,
605
+ mongo: normaliseMongoError(err),
606
+ });
607
+ k2.setSensitive?.(mergedSensitive);
608
+ // Emit once (deduped per error instance)
609
+ emitDbError(k2, {
610
+ op: "get",
611
+ collection: collectionName,
612
+ uuid: id,
613
+ });
614
+ throw k2;
615
+ }
564
616
  if (!res) {
565
617
  throw new K2Error(ServiceError.NOT_FOUND, "Document not found", "sys_mdb_get_not_found");
566
618
  }
@@ -628,8 +680,11 @@ export class K2DB {
628
680
  }
629
681
  catch (err) {
630
682
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
631
- throw chain(err, "sys_mdb_fo", "Error finding document", sev, "k2db.findOne")
632
- .setSensitive({
683
+ const k2 = chain(err, "sys_mdb_fo", "Error finding document", sev, "k2db.findOne");
684
+ // Merge/attach sensitive diagnostics without stomping any existing sensitive payload.
685
+ const prevSensitive = k2.sensitive;
686
+ const mergedSensitive = prevSensitive && typeof prevSensitive === "object" ? { ...prevSensitive } : {};
687
+ Object.assign(mergedSensitive, {
633
688
  op: "findOne",
634
689
  collection: collectionName,
635
690
  scope,
@@ -640,6 +695,12 @@ export class K2DB {
640
695
  projectionPreview: redactShallowSecrets(projection),
641
696
  mongo: normaliseMongoError(err),
642
697
  });
698
+ k2.setSensitive?.(mergedSensitive);
699
+ emitDbError(k2, {
700
+ op: "findOne",
701
+ collection: collectionName,
702
+ });
703
+ throw k2;
643
704
  }
644
705
  }
645
706
  /**
@@ -720,7 +781,7 @@ export class K2DB {
720
781
  }
721
782
  catch (err) {
722
783
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
723
- throw chain(err, "sys_mdb_find_error", "Error executing find query", sev, "k2db.find")
784
+ const k2 = chain(err, "sys_mdb_find_error", "Error executing find query", sev, "k2db.find")
724
785
  .setSensitive({
725
786
  op: "find",
726
787
  collection: collectionName,
@@ -736,6 +797,13 @@ export class K2DB {
736
797
  projectionPreview: redactShallowSecrets(projection),
737
798
  mongo: normaliseMongoError(err),
738
799
  });
800
+ emitDbError(k2, {
801
+ op: "find",
802
+ collection: collectionName,
803
+ skip,
804
+ limit,
805
+ });
806
+ throw k2;
739
807
  }
740
808
  }
741
809
  /**
@@ -1351,13 +1419,15 @@ export class K2DB {
1351
1419
  if (typeof owner !== "string") {
1352
1420
  throw new K2Error(ServiceError.BAD_REQUEST, "Owner must be of a string type", "sys_mdb_crv2");
1353
1421
  }
1354
- const normalizedOwner = owner.trim().toLowerCase();
1355
- if (!normalizedOwner) {
1422
+ const ownerTrimmed = owner.trim();
1423
+ if (!ownerTrimmed) {
1356
1424
  throw new K2Error(ServiceError.BAD_REQUEST, "Owner must be a non-empty string", "sys_mdb_owner_empty");
1357
1425
  }
1358
- if (normalizedOwner === "*") {
1426
+ if (ownerTrimmed === "*") {
1359
1427
  throw new K2Error(ServiceError.BAD_REQUEST, "Owner cannot be '*'", "sys_mdb_owner_star");
1360
1428
  }
1429
+ // `_owner` is typically another record's `_uuid` (case-sensitive). Normalize to the canonical ID form.
1430
+ const normalizedOwner = K2DB.normalizeId(ownerTrimmed);
1361
1431
  const collection = await this.getCollection(collectionName);
1362
1432
  const timestamp = Date.now();
1363
1433
  // Generate a new UUIDv7 encoded as Crockford Base32 with hyphens
@@ -1385,7 +1455,7 @@ export class K2DB {
1385
1455
  throw new K2Error(ServiceError.ALREADY_EXISTS, `A document with _uuid ${document._uuid} already exists.`, "sys_mdb_crv3");
1386
1456
  }
1387
1457
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1388
- throw chain(err, "sys_mdb_sav", "Error saving object to database", sev, "k2db.create")
1458
+ const k2 = chain(err, "sys_mdb_sav", "Error saving object to database", sev, "k2db.create")
1389
1459
  .setSensitive({
1390
1460
  op: "insertOne",
1391
1461
  collection: collectionName,
@@ -1395,6 +1465,12 @@ export class K2DB {
1395
1465
  userFieldPreview: redactShallowSecrets(safeData),
1396
1466
  mongo: normaliseMongoError(err),
1397
1467
  });
1468
+ emitDbError(k2, {
1469
+ op: "create",
1470
+ collection: collectionName,
1471
+ uuid: document._uuid,
1472
+ });
1473
+ throw k2;
1398
1474
  }
1399
1475
  }
1400
1476
  /**
@@ -1438,7 +1514,7 @@ export class K2DB {
1438
1514
  }
1439
1515
  catch (err) {
1440
1516
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1441
- throw chain(err, "sys_mdb_update1", `Error updating ${collectionName}`, sev, "k2db.updateAll")
1517
+ const k2 = chain(err, "sys_mdb_update1", `Error updating ${collectionName}`, sev, "k2db.updateAll")
1442
1518
  .setSensitive({
1443
1519
  op: "updateMany",
1444
1520
  collection: collectionName,
@@ -1450,6 +1526,11 @@ export class K2DB {
1450
1526
  valuesPreview: redactShallowSecrets(values),
1451
1527
  mongo: normaliseMongoError(err),
1452
1528
  });
1529
+ emitDbError(k2, {
1530
+ op: "updateAll",
1531
+ collection: collectionName,
1532
+ });
1533
+ throw k2;
1453
1534
  }
1454
1535
  }
1455
1536
  /**
@@ -1488,6 +1569,11 @@ export class K2DB {
1488
1569
  }, {});
1489
1570
  // Merge the preserved fields into the data
1490
1571
  data = { ...data, ...fieldsToPreserve };
1572
+ // Update _owner if scope is provided
1573
+ const normalizedScope = this.normalizeScope(scope);
1574
+ if (normalizedScope) {
1575
+ data._owner = normalizedScope;
1576
+ }
1491
1577
  data._updated = Date.now();
1492
1578
  // Encrypt secure-prefixed fields at rest (no-op unless encryption is configured)
1493
1579
  data = this.encryptSecureFieldsDeep(data, `k2db|${collectionName}|${id}`);
@@ -1506,7 +1592,7 @@ export class K2DB {
1506
1592
  }
1507
1593
  catch (err) {
1508
1594
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1509
- throw chain(err, "sys_mdb_update_error", `Error updating ${collectionName}`, sev, "k2db.update")
1595
+ const k2 = chain(err, "sys_mdb_update_error", `Error updating ${collectionName}`, sev, "k2db.update")
1510
1596
  .setSensitive({
1511
1597
  op: replace ? "replaceOne" : "updateOne",
1512
1598
  collection: collectionName,
@@ -1517,6 +1603,13 @@ export class K2DB {
1517
1603
  dataPreview: redactShallowSecrets(data),
1518
1604
  mongo: normaliseMongoError(err),
1519
1605
  });
1606
+ emitDbError(k2, {
1607
+ op: "update",
1608
+ collection: collectionName,
1609
+ uuid: id,
1610
+ replace,
1611
+ });
1612
+ throw k2;
1520
1613
  }
1521
1614
  }
1522
1615
  /**
@@ -1535,8 +1628,7 @@ export class K2DB {
1535
1628
  }
1536
1629
  catch (err) {
1537
1630
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1538
- throw chain(err, "sys_mdb_deleteall_update", `Error deleting from ${collectionName}`, sev, "k2db.deleteAll")
1539
- .setSensitive({
1631
+ const k2 = chain(err, "sys_mdb_deleteall_update", `Error deleting from ${collectionName}`, sev, "k2db.deleteAll").setSensitive({
1540
1632
  op: "softDeleteMany",
1541
1633
  collection: collectionName,
1542
1634
  scope,
@@ -1544,6 +1636,11 @@ export class K2DB {
1544
1636
  criteriaPreview: redactShallowSecrets(criteria),
1545
1637
  mongo: normaliseMongoError(err),
1546
1638
  });
1639
+ emitDbError(k2, {
1640
+ op: "deleteAll",
1641
+ collection: collectionName,
1642
+ });
1643
+ throw k2;
1547
1644
  }
1548
1645
  }
1549
1646
  /**
@@ -1573,14 +1670,19 @@ export class K2DB {
1573
1670
  }
1574
1671
  catch (err) {
1575
1672
  const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1576
- throw chain(err, "sys_mdb_remove_upd", "Error removing object from collection", sev, "k2db.delete")
1577
- .setSensitive({
1673
+ const k2 = chain(err, "sys_mdb_remove_upd", "Error removing object from collection", sev, "k2db.delete").setSensitive({
1578
1674
  op: "softDeleteOne",
1579
1675
  collection: collectionName,
1580
1676
  uuid: id,
1581
1677
  scope,
1582
1678
  mongo: normaliseMongoError(err),
1583
1679
  });
1680
+ emitDbError(k2, {
1681
+ op: "delete",
1682
+ collection: collectionName,
1683
+ uuid: id,
1684
+ });
1685
+ throw k2;
1584
1686
  }
1585
1687
  }
1586
1688
  /**
@@ -1641,8 +1743,9 @@ export class K2DB {
1641
1743
  }
1642
1744
  const collection = await this.getCollection(collectionName);
1643
1745
  const cutoff = Date.now() - olderThanMs;
1746
+ let delFilter;
1644
1747
  try {
1645
- const delFilter = this.applyScopeToCriteria({
1748
+ delFilter = this.applyScopeToCriteria({
1646
1749
  _deleted: true,
1647
1750
  _updated: { $lte: cutoff },
1648
1751
  }, scope);
@@ -1650,7 +1753,23 @@ export class K2DB {
1650
1753
  return { purged: res.deletedCount ?? 0 };
1651
1754
  }
1652
1755
  catch (err) {
1653
- throw wrap(err, ServiceError.SYSTEM_ERROR, 'sys_mdb_purge_older', 'Error purging deleted items by age');
1756
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1757
+ const k2 = chain(err, 'sys_mdb_purge_older', 'Error purging deleted items by age', sev, 'k2db.purgeDeletedOlderThan').setSensitive({
1758
+ op: 'purgeDeletedOlderThan',
1759
+ collection: collectionName,
1760
+ scope,
1761
+ olderThanMs,
1762
+ cutoff,
1763
+ delFilterShape: summariseValueShape(delFilter),
1764
+ delFilterPreview: redactShallowSecrets(delFilter),
1765
+ mongo: normaliseMongoError(err),
1766
+ });
1767
+ emitDbError(k2, {
1768
+ op: 'purgeDeletedOlderThan',
1769
+ collection: collectionName,
1770
+ olderThanMs,
1771
+ });
1772
+ throw k2;
1654
1773
  }
1655
1774
  }
1656
1775
  /**
@@ -1672,7 +1791,23 @@ export class K2DB {
1672
1791
  return { status: "restored", modified: res.modifiedCount };
1673
1792
  }
1674
1793
  catch (err) {
1675
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_pres", "Error restoring a deleted item");
1794
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1795
+ const k2 = chain(err, "sys_mdb_pres", "Error restoring a deleted item", sev, "k2db.restore")
1796
+ .setSensitive({
1797
+ op: "restore",
1798
+ collection: collectionName,
1799
+ scope,
1800
+ criteriaShape: summariseValueShape(crit),
1801
+ criteriaPreview: redactShallowSecrets(crit),
1802
+ queryShape: summariseValueShape(query),
1803
+ queryPreview: redactShallowSecrets(query),
1804
+ mongo: normaliseMongoError(err),
1805
+ });
1806
+ emitDbError(k2, {
1807
+ op: "restore",
1808
+ collection: collectionName,
1809
+ });
1810
+ throw k2;
1676
1811
  }
1677
1812
  }
1678
1813
  /**
@@ -1735,7 +1870,19 @@ export class K2DB {
1735
1870
  return { status: "ok" };
1736
1871
  }
1737
1872
  catch (err) {
1738
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_drop", "Error dropping collection");
1873
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1874
+ const k2 = chain(err, "sys_mdb_drop", "Error dropping collection", sev, "k2db.drop").setSensitive({
1875
+ op: "drop",
1876
+ collection: collectionName,
1877
+ scope,
1878
+ normalizedScope,
1879
+ mongo: normaliseMongoError(err),
1880
+ });
1881
+ emitDbError(k2, {
1882
+ op: "drop",
1883
+ collection: collectionName,
1884
+ });
1885
+ throw k2;
1739
1886
  }
1740
1887
  }
1741
1888
  /**
@@ -1803,12 +1950,12 @@ export class K2DB {
1803
1950
  }
1804
1951
  return val;
1805
1952
  }
1806
- /** Lowercase helper for `_owner` field supporting operators like $in/$nin/$eq/$ne and arrays. */
1953
+ /** Trim helper for `_owner` field supporting operators like $in/$nin/$eq/$ne and arrays. */
1807
1954
  static normalizeOwnerField(val) {
1808
1955
  if (typeof val === "string")
1809
- return val.trim().toLowerCase();
1956
+ return val.trim();
1810
1957
  if (Array.isArray(val)) {
1811
- return val.map((x) => (typeof x === "string" ? x.trim().toLowerCase() : x));
1958
+ return val.map((x) => (typeof x === "string" ? x.trim() : x));
1812
1959
  }
1813
1960
  if (val && typeof val === "object") {
1814
1961
  const out = { ...val };
@@ -1839,7 +1986,7 @@ export class K2DB {
1839
1986
  }
1840
1987
  /** Uppercase incoming IDs for case-insensitive lookups. */
1841
1988
  static normalizeId(id) {
1842
- return id.toUpperCase();
1989
+ return id.trim();
1843
1990
  }
1844
1991
  /**
1845
1992
  * Run an async DB operation with timing, slow logging, and hooks.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frogfish/k2db",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "description": "A data handling library for K2 applications.",
5
5
  "type": "module",
6
6
  "main": "data.js",