@frogfish/k2db 3.0.3 → 3.0.6

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 (2) hide show
  1. package/db.js +445 -49
  2. package/package.json +3 -3
package/db.js CHANGED
@@ -1,14 +1,214 @@
1
1
  // src/db.ts
2
- import { K2Error, ServiceError, wrap } from "@frogfish/k2error"; // Keep the existing error structure
2
+ import { K2Error, ServiceError, wrap, chain } from "@frogfish/k2error"; // Keep the existing error structure
3
3
  import { MongoClient, } from "mongodb";
4
4
  import { randomBytes, createHash, createCipheriv, createDecipheriv } from "crypto";
5
5
  import { Topic } from '@frogfish/ratatouille';
6
6
  import { z } from "zod";
7
7
  // const debug = debugLib("k2:db");
8
- const debug = Topic('k2db#random');
8
+ const debug = Topic('k2db:debug#random');
9
+ const error = Topic('k2db:error#random');
10
+ /**
11
+ * Emit a rich DB error event to Ratatouille.
12
+ * - Never throws (logging must not break request path)
13
+ * - Dedupes per error instance (avoid double logging on rethrow)
14
+ * - INTERNAL: may include sensitive diagnostics
15
+ */
16
+ function emitDbError(err, meta = {}) {
17
+ try {
18
+ if (!error.enabled)
19
+ return;
20
+ // Deduplicate per error instance (non-enumerable marker)
21
+ const MARK = "__k2db_logged";
22
+ if (err && typeof err === "object") {
23
+ const anyErr = err;
24
+ if (anyErr[MARK])
25
+ return;
26
+ try {
27
+ Object.defineProperty(anyErr, MARK, {
28
+ value: true,
29
+ enumerable: false,
30
+ configurable: true,
31
+ });
32
+ }
33
+ catch {
34
+ // ignore marker failures
35
+ }
36
+ }
37
+ const isErr = err instanceof Error;
38
+ const k2 = err instanceof K2Error ? err : undefined;
39
+ // `sensitive` is expected to be non-enumerable on K2Error
40
+ const sensitive = k2 ? k2.sensitive : (isErr ? err.sensitive : undefined);
41
+ const payload = {
42
+ kind: "k2db:error",
43
+ at: Date.now(),
44
+ ...meta,
45
+ k2: k2
46
+ ? {
47
+ error: k2.error,
48
+ code: k2.code,
49
+ error_description: k2.error_description,
50
+ trace: k2.trace,
51
+ chain: k2.chain,
52
+ }
53
+ : undefined,
54
+ name: isErr ? err.name : undefined,
55
+ message: isErr ? err.message : String(err),
56
+ // If we don't have a K2Error yet, still include a compact Mongo-ish normalization
57
+ mongo: k2 ? undefined : normaliseMongoError(err),
58
+ // If K2Error has a cause (often the raw Mongo error), include it normalized too
59
+ cause: k2?.cause ? normaliseMongoError(k2.cause) : undefined,
60
+ sensitive,
61
+ stack: isErr ? err.stack : undefined,
62
+ };
63
+ // strip undefined
64
+ for (const k of Object.keys(payload))
65
+ if (payload[k] === undefined)
66
+ delete payload[k];
67
+ error(payload);
68
+ }
69
+ catch {
70
+ // never throw from logging
71
+ }
72
+ }
9
73
  function _hashSecret(secret) {
10
74
  return createHash("sha256").update(secret).digest("hex");
11
75
  }
76
+ /**
77
+ * Normalise MongoDB driver/server errors into a structured, log-friendly shape.
78
+ *
79
+ * This is intended for INTERNAL diagnostics (e.g. attach to K2Error.sensitive).
80
+ * Keep it compact and resilient to unknown error shapes.
81
+ */
82
+ function normaliseMongoError(err) {
83
+ const MAX_STR = 2000;
84
+ const MAX_KEYS = 50;
85
+ const MAX_ARR = 50;
86
+ const MAX_DEPTH = 4;
87
+ const truncate = (s) => (s.length > MAX_STR ? s.slice(0, MAX_STR) + "…" : s);
88
+ const prune = (val, depth = 0) => {
89
+ if (val === null || val === undefined)
90
+ return val;
91
+ if (depth >= MAX_DEPTH)
92
+ return "[Truncated]";
93
+ const t = typeof val;
94
+ if (t === "string")
95
+ return truncate(val);
96
+ if (t === "number" || t === "boolean" || t === "bigint")
97
+ return val;
98
+ // Avoid serialising functions/symbols
99
+ if (t === "function")
100
+ return "[Function]";
101
+ if (t === "symbol")
102
+ return "[Symbol]";
103
+ if (Array.isArray(val)) {
104
+ const out = val.slice(0, MAX_ARR).map((x) => prune(x, depth + 1));
105
+ if (val.length > MAX_ARR)
106
+ out.push(`[+${val.length - MAX_ARR} more]`);
107
+ return out;
108
+ }
109
+ if (t === "object") {
110
+ const obj = val;
111
+ // Preserve Buffer in a safe way
112
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(obj)) {
113
+ return { type: "Buffer", length: obj.length };
114
+ }
115
+ const keys = Object.keys(obj).slice(0, MAX_KEYS);
116
+ const out = {};
117
+ for (const k of keys) {
118
+ // Basic redaction for common secret-ish keys
119
+ const lk = k.toLowerCase();
120
+ if (lk.includes("password") || lk === "pass" || lk.includes("secret") || lk.includes("token")) {
121
+ out[k] = "[REDACTED]";
122
+ continue;
123
+ }
124
+ try {
125
+ out[k] = prune(obj[k], depth + 1);
126
+ }
127
+ catch (e) {
128
+ out[k] = "[Unserialisable]";
129
+ }
130
+ }
131
+ if (Object.keys(obj).length > MAX_KEYS) {
132
+ out.__truncatedKeys = Object.keys(obj).length - MAX_KEYS;
133
+ }
134
+ return out;
135
+ }
136
+ try {
137
+ return truncate(String(val));
138
+ }
139
+ catch {
140
+ return "[Unknown]";
141
+ }
142
+ };
143
+ // MongoDB driver often provides these fields on thrown errors
144
+ const anyErr = err;
145
+ const base = {
146
+ name: anyErr?.name ?? (err instanceof Error ? err.name : "UnknownError"),
147
+ message: anyErr?.message ?? (err instanceof Error ? err.message : String(err)),
148
+ };
149
+ const out = {
150
+ ...base,
151
+ // common Mongo fields (present on MongoServerError and friends)
152
+ code: typeof anyErr?.code === "number" ? anyErr.code : undefined,
153
+ codeName: typeof anyErr?.codeName === "string" ? anyErr.codeName : undefined,
154
+ errorLabels: Array.isArray(anyErr?.errorLabels) ? anyErr.errorLabels.slice(0, MAX_ARR) : undefined,
155
+ errInfo: anyErr?.errInfo ? prune(anyErr.errInfo, 0) : undefined,
156
+ };
157
+ // Include a compact pruned view of any extra enumerable fields for diagnostics
158
+ if (anyErr && typeof anyErr === "object") {
159
+ out.extra = prune(anyErr, 0);
160
+ }
161
+ // Remove undefined fields for cleanliness
162
+ for (const k of Object.keys(out)) {
163
+ if (out[k] === undefined)
164
+ delete out[k];
165
+ }
166
+ return out;
167
+ }
168
+ /**
169
+ * Produce a compact summary of an arbitrary value for INTERNAL diagnostics.
170
+ * Prefer shapes/keys over full payloads to avoid accidental PII/secret logging.
171
+ */
172
+ function summariseValueShape(val) {
173
+ try {
174
+ if (val === null)
175
+ return { type: "null" };
176
+ if (val === undefined)
177
+ return { type: "undefined" };
178
+ if (Array.isArray(val))
179
+ return { type: "array", length: val.length };
180
+ const t = typeof val;
181
+ if (t !== "object")
182
+ return { type: t };
183
+ const obj = val;
184
+ const keys = Object.keys(obj);
185
+ return { type: "object", keys: keys.slice(0, 50), keyCount: keys.length };
186
+ }
187
+ catch {
188
+ return { type: "unknown" };
189
+ }
190
+ }
191
+ /**
192
+ * Redact common secret-ish keys from a shallow object for INTERNAL diagnostics.
193
+ * (Still treat returned value as sensitive; this is only to reduce obvious footguns.)
194
+ */
195
+ function redactShallowSecrets(obj) {
196
+ if (!obj || typeof obj !== "object" || Array.isArray(obj))
197
+ return obj;
198
+ const out = { ...obj };
199
+ for (const k of Object.keys(out)) {
200
+ const lk = k.toLowerCase();
201
+ if (lk.includes("password") ||
202
+ lk === "pass" ||
203
+ lk.includes("secret") ||
204
+ lk.includes("token") ||
205
+ lk.includes("apikey") ||
206
+ lk.includes("api_key")) {
207
+ out[k] = "[REDACTED]";
208
+ }
209
+ }
210
+ return out;
211
+ }
12
212
  // ---- Shared MongoClient pool (per cluster+auth), reused across DB names ----
13
213
  const _clientByKey = new Map();
14
214
  const _connectingByKey = new Map();
@@ -339,7 +539,13 @@ export class K2DB {
339
539
  return collection;
340
540
  }
341
541
  catch (err) {
342
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_gc", `Error getting collection: ${collectionName}`);
542
+ 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({
545
+ op: "getCollection",
546
+ collection: collectionName,
547
+ mongo: normaliseMongoError(err),
548
+ });
343
549
  }
344
550
  }
345
551
  /**
@@ -421,7 +627,19 @@ export class K2DB {
421
627
  return null;
422
628
  }
423
629
  catch (err) {
424
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_fo", "Error finding document");
630
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
631
+ throw chain(err, "sys_mdb_fo", "Error finding document", sev, "k2db.findOne")
632
+ .setSensitive({
633
+ op: "findOne",
634
+ collection: collectionName,
635
+ scope,
636
+ queryShape: summariseValueShape(query),
637
+ projectionShape: summariseValueShape(projection),
638
+ // Queries/projections may contain user identifiers and selectors; treat as sensitive.
639
+ queryPreview: redactShallowSecrets(query),
640
+ projectionPreview: redactShallowSecrets(projection),
641
+ mongo: normaliseMongoError(err),
642
+ });
425
643
  }
426
644
  }
427
645
  /**
@@ -501,7 +719,23 @@ export class K2DB {
501
719
  return result;
502
720
  }
503
721
  catch (err) {
504
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_find_error", "Error executing find query");
722
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
723
+ throw chain(err, "sys_mdb_find_error", "Error executing find query", sev, "k2db.find")
724
+ .setSensitive({
725
+ op: "find",
726
+ collection: collectionName,
727
+ scope,
728
+ skip,
729
+ limit,
730
+ paramsShape: summariseValueShape(params),
731
+ criteriaShape: summariseValueShape(criteria),
732
+ projectionShape: summariseValueShape(projection),
733
+ sortShape: summariseValueShape(sort),
734
+ // Queries/projections may contain user identifiers and selectors; treat as sensitive.
735
+ criteriaPreview: redactShallowSecrets(criteria),
736
+ projectionPreview: redactShallowSecrets(projection),
737
+ mongo: normaliseMongoError(err),
738
+ });
505
739
  }
506
740
  }
507
741
  /**
@@ -513,34 +747,63 @@ export class K2DB {
513
747
  * @param scope - (optional) Owner selector; "*" means all owners.
514
748
  */
515
749
  async aggregate(collectionName, criteria, skip = 0, limit = 100, scope) {
516
- if (criteria.length === 0) {
517
- throw new K2Error(ServiceError.SYSTEM_ERROR, "Aggregation criteria cannot be empty", "sys_mdb_ag_empty");
518
- }
519
- // Prevent aggregation from reading/deriving values from secure-prefixed fields (when configured)
520
- this.assertNoSecureFieldRefsInPipeline(criteria);
521
- // Validate the aggregation pipeline for allowed/disallowed stages and safety caps
522
- this.validateAggregationPipeline(criteria, skip, limit);
523
- // Enforce soft-delete behavior: never return documents marked as deleted
524
- criteria = K2DB.enforceNoDeletedInPipeline(criteria);
525
- // Enforce ownership scope within the pipeline (and nested pipelines)
526
- criteria = this.enforceScopeInPipeline(criteria, scope);
527
- // Add pagination stages to the aggregation pipeline
528
- if (skip > 0) {
529
- criteria.push({ $skip: skip });
530
- }
531
- if (limit > 0) {
532
- criteria.push({ $limit: limit });
533
- }
534
- debug(`Aggregating with criteria: ${JSON.stringify(criteria, null, 2)}`);
535
- const collection = await this.getCollection(collectionName);
536
- // Sanitize criteria
537
- const sanitizedCriteria = criteria.map((stage) => {
538
- if (stage.$match) {
539
- return K2DB.sanitiseCriteria(stage);
540
- }
541
- return stage;
542
- });
750
+ // --- BEGIN enhanced diagnostics/snapshots ---
751
+ const clonePipeline = (p) => {
752
+ if (!Array.isArray(p))
753
+ return [];
754
+ return p.map((stage) => {
755
+ try {
756
+ return JSON.parse(JSON.stringify(stage));
757
+ }
758
+ catch {
759
+ return stage;
760
+ }
761
+ });
762
+ };
763
+ const inputPipeline = clonePipeline(criteria);
764
+ const inputStageOps = this.collectStageOps(inputPipeline);
765
+ // --- END enhanced diagnostics/snapshots ---
766
+ // These are populated as we build/execute the pipeline so we can attach them on ANY failure.
767
+ let workingPipeline;
768
+ let sanitizedCriteria;
769
+ let finalStageOps;
543
770
  try {
771
+ if (criteria.length === 0) {
772
+ throw new K2Error(ServiceError.SYSTEM_ERROR, "Aggregation criteria cannot be empty", "sys_mdb_ag_empty");
773
+ }
774
+ // Prevent aggregation from reading/deriving values from secure-prefixed fields (when configured)
775
+ this.assertNoSecureFieldRefsInPipeline(criteria);
776
+ // Validate the aggregation pipeline for allowed/disallowed stages and safety caps
777
+ this.validateAggregationPipeline(criteria, skip, limit);
778
+ // Enforce soft-delete behavior: never return documents marked as deleted
779
+ workingPipeline = K2DB.enforceNoDeletedInPipeline(criteria);
780
+ // Enforce ownership scope within the pipeline (and nested pipelines)
781
+ workingPipeline = this.enforceScopeInPipeline(workingPipeline, scope);
782
+ // Add pagination stages to the aggregation pipeline
783
+ if (skip > 0) {
784
+ workingPipeline.push({ $skip: skip });
785
+ }
786
+ if (limit > 0) {
787
+ workingPipeline.push({ $limit: limit });
788
+ }
789
+ debug(`Aggregating with criteria: ${JSON.stringify(workingPipeline, null, 2)}`);
790
+ const collection = await this.getCollection(collectionName);
791
+ // Sanitize criteria (do not mutate the working pipeline)
792
+ sanitizedCriteria = workingPipeline.map((stage) => {
793
+ const clonedStage = (() => {
794
+ try {
795
+ return JSON.parse(JSON.stringify(stage));
796
+ }
797
+ catch {
798
+ return stage;
799
+ }
800
+ })();
801
+ if (clonedStage && clonedStage.$match) {
802
+ return K2DB.sanitiseCriteria(clonedStage);
803
+ }
804
+ return clonedStage;
805
+ });
806
+ finalStageOps = this.collectStageOps(sanitizedCriteria);
544
807
  const data = await this.runTimed("aggregate", { collectionName, pipeline: sanitizedCriteria }, async () => {
545
808
  const mode = this.aggregationMode ?? "loose";
546
809
  const opts = {};
@@ -553,7 +816,54 @@ export class K2DB {
553
816
  return data.map((doc) => this.stripSecureFieldsDeep(doc));
554
817
  }
555
818
  catch (err) {
556
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_ag", "Aggregation failed");
819
+ // If we already have a K2Error (e.g. validation/ownership/secure-field), preserve it.
820
+ // Otherwise chain into a SYSTEM_ERROR with rich internal diagnostics.
821
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
822
+ const k2 = err instanceof K2Error
823
+ ? err
824
+ : chain(err, "sys_mdb_ag", "Aggregation failed", sev, "k2db.aggregate");
825
+ // Merge/attach sensitive diagnostics without losing any existing sensitive payload.
826
+ const prevSensitive = k2.sensitive;
827
+ const mergedSensitive = prevSensitive && typeof prevSensitive === "object" ? { ...prevSensitive } : {};
828
+ // Build a rich diagnostic payload (internal-only)
829
+ Object.assign(mergedSensitive, {
830
+ op: "aggregate",
831
+ collection: collectionName,
832
+ scope,
833
+ skip,
834
+ limit,
835
+ ownershipMode: this.ownershipMode,
836
+ aggregationMode: this.aggregationMode,
837
+ // Caller input vs executed pipeline snapshots
838
+ inputStageOps,
839
+ finalStageOps,
840
+ inputPipelineShape: summariseValueShape(inputPipeline),
841
+ workingPipelineShape: summariseValueShape(workingPipeline),
842
+ finalPipelineShape: summariseValueShape(sanitizedCriteria),
843
+ // Full internal pipelines (may include identifiers/selectors)
844
+ inputPipeline,
845
+ workingPipeline,
846
+ pipeline: sanitizedCriteria,
847
+ // Compact at-a-glance previews (shallow key redaction only)
848
+ inputPipelinePreview: Array.isArray(inputPipeline)
849
+ ? inputPipeline.map((s) => redactShallowSecrets(s))
850
+ : inputPipeline,
851
+ workingPipelinePreview: Array.isArray(workingPipeline)
852
+ ? workingPipeline.map((s) => redactShallowSecrets(s))
853
+ : workingPipeline,
854
+ pipelinePreview: Array.isArray(sanitizedCriteria)
855
+ ? sanitizedCriteria.map((s) => redactShallowSecrets(s))
856
+ : sanitizedCriteria,
857
+ mongo: normaliseMongoError(err),
858
+ });
859
+ // setSensitive is expected to exist on K2Error (non-enumerable sensitive field)
860
+ k2.setSensitive?.(mergedSensitive);
861
+ // Emit once (deduped per error instance)
862
+ emitDbError(k2, {
863
+ op: "aggregate",
864
+ collection: collectionName,
865
+ });
866
+ throw k2;
557
867
  }
558
868
  }
559
869
  /**
@@ -872,8 +1182,10 @@ export class K2DB {
872
1182
  }
873
1183
  else if (lu.localField && lu.foreignField) {
874
1184
  // Convert simple lookup to pipeline lookup so we can enforce owner scope (and deleted) in foreign coll.
875
- const localVar = "__lk";
876
- const ownerVar = "__own";
1185
+ // NOTE: MongoDB aggregation user variables must start with a letter.
1186
+ // Avoid leading underscores (e.g. "__lk") which fail to parse on MongoDB.
1187
+ const localVar = "k2lk";
1188
+ const ownerVar = "k2own";
877
1189
  lu.let = { [localVar]: `$${lu.localField}`, [ownerVar]: normalizedScope };
878
1190
  lu.pipeline = [
879
1191
  {
@@ -966,7 +1278,9 @@ export class K2DB {
966
1278
  }
967
1279
  else if (lu.localField && lu.foreignField) {
968
1280
  // Convert simple lookup to pipeline lookup to filter _deleted
969
- const localVar = "__lk";
1281
+ // NOTE: MongoDB aggregation user variables must start with a letter.
1282
+ // Avoid leading underscores (e.g. "__lk") which fail to parse on MongoDB.
1283
+ const localVar = "k2lk";
970
1284
  lu.let = { [localVar]: `$${lu.localField}` };
971
1285
  lu.pipeline = [
972
1286
  {
@@ -1070,10 +1384,17 @@ export class K2DB {
1070
1384
  if (err.code === 11000 && err.keyPattern && err.keyPattern._uuid) {
1071
1385
  throw new K2Error(ServiceError.ALREADY_EXISTS, `A document with _uuid ${document._uuid} already exists.`, "sys_mdb_crv3");
1072
1386
  }
1073
- // Log the error details for debugging
1074
- debug(`Was trying to insert into collection ${collectionName}, data: ${JSON.stringify(document)}`);
1075
- debug(err);
1076
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_sav", "Error saving object to database");
1387
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1388
+ throw chain(err, "sys_mdb_sav", "Error saving object to database", sev, "k2db.create")
1389
+ .setSensitive({
1390
+ op: "insertOne",
1391
+ collection: collectionName,
1392
+ owner: normalizedOwner,
1393
+ uuid: document._uuid,
1394
+ documentShape: summariseValueShape(document),
1395
+ userFieldPreview: redactShallowSecrets(safeData),
1396
+ mongo: normaliseMongoError(err),
1397
+ });
1077
1398
  }
1078
1399
  }
1079
1400
  /**
@@ -1116,7 +1437,19 @@ export class K2DB {
1116
1437
  };
1117
1438
  }
1118
1439
  catch (err) {
1119
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_update1", `Error updating ${collectionName}`);
1440
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1441
+ throw chain(err, "sys_mdb_update1", `Error updating ${collectionName}`, sev, "k2db.updateAll")
1442
+ .setSensitive({
1443
+ op: "updateMany",
1444
+ collection: collectionName,
1445
+ scope,
1446
+ criteriaShape: summariseValueShape(criteria),
1447
+ valuesShape: summariseValueShape(values),
1448
+ // criteria may contain selectors; treat as sensitive. Keep it compact.
1449
+ criteriaPreview: redactShallowSecrets(criteria),
1450
+ valuesPreview: redactShallowSecrets(values),
1451
+ mongo: normaliseMongoError(err),
1452
+ });
1120
1453
  }
1121
1454
  }
1122
1455
  /**
@@ -1172,7 +1505,18 @@ export class K2DB {
1172
1505
  return { updated: res.modifiedCount };
1173
1506
  }
1174
1507
  catch (err) {
1175
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_update_error", `Error updating ${collectionName}`);
1508
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1509
+ throw chain(err, "sys_mdb_update_error", `Error updating ${collectionName}`, sev, "k2db.update")
1510
+ .setSensitive({
1511
+ op: replace ? "replaceOne" : "updateOne",
1512
+ collection: collectionName,
1513
+ uuid: id,
1514
+ replace,
1515
+ scope,
1516
+ dataShape: summariseValueShape(data),
1517
+ dataPreview: redactShallowSecrets(data),
1518
+ mongo: normaliseMongoError(err),
1519
+ });
1176
1520
  }
1177
1521
  }
1178
1522
  /**
@@ -1190,7 +1534,16 @@ export class K2DB {
1190
1534
  return { deleted: result.updated };
1191
1535
  }
1192
1536
  catch (err) {
1193
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_deleteall_update", `Error updating ${collectionName}`);
1537
+ 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({
1540
+ op: "softDeleteMany",
1541
+ collection: collectionName,
1542
+ scope,
1543
+ criteriaShape: summariseValueShape(criteria),
1544
+ criteriaPreview: redactShallowSecrets(criteria),
1545
+ mongo: normaliseMongoError(err),
1546
+ });
1194
1547
  }
1195
1548
  }
1196
1549
  /**
@@ -1219,7 +1572,15 @@ export class K2DB {
1219
1572
  }
1220
1573
  }
1221
1574
  catch (err) {
1222
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_remove_upd", "Error removing object from collection");
1575
+ 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({
1578
+ op: "softDeleteOne",
1579
+ collection: collectionName,
1580
+ uuid: id,
1581
+ scope,
1582
+ mongo: normaliseMongoError(err),
1583
+ });
1223
1584
  }
1224
1585
  }
1225
1586
  /**
@@ -1231,18 +1592,38 @@ export class K2DB {
1231
1592
  async purge(collectionName, id, scope) {
1232
1593
  id = K2DB.normalizeId(id);
1233
1594
  const collection = await this.getCollection(collectionName);
1595
+ let findFilter;
1596
+ let delFilter;
1234
1597
  try {
1235
- const findFilter = this.applyScopeToCriteria({ _uuid: id, _deleted: true }, scope);
1598
+ findFilter = this.applyScopeToCriteria({ _uuid: id, _deleted: true }, scope);
1236
1599
  const item = await this.runTimed("findOne", { collectionName, ...findFilter }, async () => await collection.findOne(findFilter));
1237
1600
  if (!item) {
1238
1601
  throw new K2Error(ServiceError.SYSTEM_ERROR, "Cannot purge item that is not deleted", "sys_mdb_gcol_pg2");
1239
1602
  }
1240
- const delFilter = this.applyScopeToCriteria({ _uuid: id }, scope);
1603
+ delFilter = this.applyScopeToCriteria({ _uuid: id }, scope);
1241
1604
  await this.runTimed("deleteOne", { collectionName, ...delFilter }, async () => await collection.deleteOne(delFilter));
1242
1605
  return { id };
1243
1606
  }
1244
1607
  catch (err) {
1245
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_pg", `Error purging item with id: ${id}`);
1608
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1609
+ const k2 = chain(err, "sys_mdb_pg", `Error purging item with id: ${id}`, sev, "k2db.purge")
1610
+ .setSensitive({
1611
+ op: "purge",
1612
+ collection: collectionName,
1613
+ uuid: id,
1614
+ scope,
1615
+ findFilterShape: summariseValueShape(findFilter),
1616
+ delFilterShape: summariseValueShape(delFilter),
1617
+ findFilterPreview: redactShallowSecrets(findFilter),
1618
+ delFilterPreview: redactShallowSecrets(delFilter),
1619
+ mongo: normaliseMongoError(err),
1620
+ });
1621
+ emitDbError(k2, {
1622
+ op: "purge",
1623
+ collection: collectionName,
1624
+ uuid: id,
1625
+ });
1626
+ throw k2;
1246
1627
  }
1247
1628
  }
1248
1629
  /**
@@ -1302,9 +1683,10 @@ export class K2DB {
1302
1683
  */
1303
1684
  async count(collectionName, criteria, scope) {
1304
1685
  const collection = await this.getCollection(collectionName);
1686
+ let query;
1305
1687
  try {
1306
1688
  const norm = K2DB.normalizeCriteriaIds(criteria || {});
1307
- let query = {
1689
+ query = {
1308
1690
  ...norm,
1309
1691
  ...(criteria && Object.prototype.hasOwnProperty.call(criteria, "_deleted")
1310
1692
  ? {}
@@ -1315,7 +1697,21 @@ export class K2DB {
1315
1697
  return { count: cnt };
1316
1698
  }
1317
1699
  catch (err) {
1318
- throw wrap(err, ServiceError.SYSTEM_ERROR, "sys_mdb_cn", "Error counting objects with given criteria");
1700
+ const sev = err instanceof K2Error ? undefined : ServiceError.SYSTEM_ERROR;
1701
+ const k2 = chain(err, "sys_mdb_cn", "Error counting objects with given criteria", sev, "k2db.count")
1702
+ .setSensitive({
1703
+ op: "countDocuments",
1704
+ collection: collectionName,
1705
+ scope,
1706
+ queryShape: summariseValueShape(query),
1707
+ queryPreview: redactShallowSecrets(query),
1708
+ mongo: normaliseMongoError(err),
1709
+ });
1710
+ emitDbError(k2, {
1711
+ op: "countDocuments",
1712
+ collection: collectionName,
1713
+ });
1714
+ throw k2;
1319
1715
  }
1320
1716
  }
1321
1717
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frogfish/k2db",
3
- "version": "3.0.3",
3
+ "version": "3.0.6",
4
4
  "description": "A data handling library for K2 applications.",
5
5
  "type": "module",
6
6
  "main": "data.js",
@@ -18,8 +18,8 @@
18
18
  "license": "GPL-3.0-only",
19
19
  "author": "El'Diablo",
20
20
  "dependencies": {
21
- "@frogfish/k2error": "^3.0.1",
22
- "@frogfish/ratatouille": "^0.1.7",
21
+ "@frogfish/k2error": "^3.0.2",
22
+ "@frogfish/ratatouille": "^1.0.0",
23
23
  "debug": "^4.3.7",
24
24
  "mongodb": "^6.9.0",
25
25
  "uuid": "^10.0.0",