@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.
- package/db.d.ts +1 -1
- package/db.js +178 -31
- 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
|
-
/**
|
|
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
|
-
|
|
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"
|
|
422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
|
1355
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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()
|
|
1956
|
+
return val.trim();
|
|
1810
1957
|
if (Array.isArray(val)) {
|
|
1811
|
-
return val.map((x) => (typeof x === "string" ? x.trim()
|
|
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.
|
|
1989
|
+
return id.trim();
|
|
1843
1990
|
}
|
|
1844
1991
|
/**
|
|
1845
1992
|
* Run an async DB operation with timing, slow logging, and hooks.
|