@lucaapp/service-utils 5.34.1 → 5.36.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.
@@ -1,11 +1,25 @@
1
1
  import type { Model, ModelStatic, Sequelize } from 'sequelize';
2
2
  import { DataTypes } from 'sequelize';
3
+ type AuditOperation = 'create' | 'update' | 'destroy';
3
4
  export interface PartitionTable {
4
5
  tableName: string;
5
6
  timestamp: Date;
6
7
  }
8
+ interface DiffEntry {
9
+ key: string;
10
+ values: {
11
+ old?: unknown;
12
+ new?: unknown;
13
+ };
14
+ }
7
15
  interface AddHistoryOptions {
8
16
  exclude?: string[];
17
+ /**
18
+ * Attributes whose change is recorded, but whose values are not. The diff
19
+ * keeps the key with a placeholder, so the log shows THAT the attribute
20
+ * changed without storing large or sensitive payloads.
21
+ */
22
+ redact?: string[];
9
23
  }
10
24
  interface AtomicAuditLogOptions {
11
25
  attributeRevisionModelTableName?: string;
@@ -18,6 +32,8 @@ export interface PartitionResult {
18
32
  archiveTableName: string;
19
33
  timestamp: Date;
20
34
  }
35
+ export declare const REDACTED_VALUE = "[redacted]";
36
+ export declare const computeDiff: <T extends Model>(instance: T, operation: AuditOperation, exclude: string[], redact: string[]) => DiffEntry[];
21
37
  export declare class AtomicAuditLog {
22
38
  private sequelize;
23
39
  private tableName;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AtomicAuditLog = void 0;
3
+ exports.AtomicAuditLog = exports.computeDiff = exports.REDACTED_VALUE = void 0;
4
4
  const sequelize_1 = require("sequelize");
5
5
  const DEFAULT_EXCLUDE = [
6
6
  'id',
@@ -11,25 +11,32 @@ const DEFAULT_EXCLUDE = [
11
11
  'revision',
12
12
  ];
13
13
  const isValidValue = (value) => value !== undefined && value !== null;
14
- const computeCreateDiff = (currentValues, excludeSet) => {
14
+ exports.REDACTED_VALUE = '[redacted]';
15
+ const computeCreateDiff = (currentValues, excludeSet, redactSet) => {
15
16
  const diff = [];
16
17
  for (const [key, value] of Object.entries(currentValues)) {
17
18
  if (!excludeSet.has(key) && isValidValue(value)) {
18
- diff.push({ key, values: { new: value } });
19
+ diff.push({
20
+ key,
21
+ values: { new: redactSet.has(key) ? exports.REDACTED_VALUE : value },
22
+ });
19
23
  }
20
24
  }
21
25
  return diff;
22
26
  };
23
- const computeDestroyDiff = (previousValues, excludeSet) => {
27
+ const computeDestroyDiff = (previousValues, excludeSet, redactSet) => {
24
28
  const diff = [];
25
29
  for (const [key, value] of Object.entries(previousValues)) {
26
30
  if (!excludeSet.has(key) && isValidValue(value)) {
27
- diff.push({ key, values: { old: value } });
31
+ diff.push({
32
+ key,
33
+ values: { old: redactSet.has(key) ? exports.REDACTED_VALUE : value },
34
+ });
28
35
  }
29
36
  }
30
37
  return diff;
31
38
  };
32
- const computeUpdateDiff = (instance, previousValues, currentValues, excludeSet) => {
39
+ const computeUpdateDiff = (instance, previousValues, currentValues, excludeSet, redactSet) => {
33
40
  const diff = [];
34
41
  const changedKeys = instance.changed();
35
42
  if (!changedKeys || !Array.isArray(changedKeys)) {
@@ -39,24 +46,29 @@ const computeUpdateDiff = (instance, previousValues, currentValues, excludeSet)
39
46
  if (!excludeSet.has(key)) {
40
47
  diff.push({
41
48
  key,
42
- values: { old: previousValues[key], new: currentValues[key] },
49
+ values: redactSet.has(key)
50
+ ? { old: exports.REDACTED_VALUE, new: exports.REDACTED_VALUE }
51
+ : { old: previousValues[key], new: currentValues[key] },
43
52
  });
44
53
  }
45
54
  }
46
55
  return diff;
47
56
  };
48
- const computeDiff = (instance, operation, exclude) => {
57
+ // exported for tests
58
+ const computeDiff = (instance, operation, exclude, redact) => {
49
59
  const previousValues = instance.previous();
50
60
  const currentValues = instance.dataValues;
51
61
  const excludeSet = new Set([...DEFAULT_EXCLUDE, ...exclude]);
62
+ const redactSet = new Set(redact);
52
63
  if (operation === 'create') {
53
- return computeCreateDiff(currentValues, excludeSet);
64
+ return computeCreateDiff(currentValues, excludeSet, redactSet);
54
65
  }
55
66
  if (operation === 'destroy') {
56
- return computeDestroyDiff(previousValues, excludeSet);
67
+ return computeDestroyDiff(previousValues, excludeSet, redactSet);
57
68
  }
58
- return computeUpdateDiff(instance, previousValues, currentValues, excludeSet);
69
+ return computeUpdateDiff(instance, previousValues, currentValues, excludeSet, redactSet);
59
70
  };
71
+ exports.computeDiff = computeDiff;
60
72
  class AtomicAuditLog {
61
73
  constructor(sequelize, options = {}) {
62
74
  this.sequelize = sequelize;
@@ -86,12 +98,12 @@ class AtomicAuditLog {
86
98
  return `${this.tableName}_View`;
87
99
  }
88
100
  addHistory(model, options = {}) {
89
- const { exclude = [] } = options;
101
+ const { exclude = [], redact = [] } = options;
90
102
  const modelName = model.name;
91
103
  const primaryKeyAttribute = model.primaryKeyAttribute || 'uuid';
92
- const createHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'create');
93
- const updateHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'update');
94
- const destroyHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, 'destroy');
104
+ const createHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, redact, 'create');
105
+ const updateHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, redact, 'update');
106
+ const destroyHook = this.createAuditHook(modelName, primaryKeyAttribute, exclude, redact, 'destroy');
95
107
  model.addHook('afterCreate', createHook);
96
108
  model.addHook('afterUpdate', updateHook);
97
109
  model.addHook('afterDestroy', destroyHook);
@@ -144,10 +156,10 @@ class AtomicAuditLog {
144
156
  transaction,
145
157
  });
146
158
  }
147
- createAuditHook(modelName, primaryKeyAttribute, exclude, operation) {
159
+ createAuditHook(modelName, primaryKeyAttribute, exclude, redact, operation) {
148
160
  return async (instance, options) => {
149
161
  try {
150
- const diff = computeDiff(instance, operation, exclude);
162
+ const diff = (0, exports.computeDiff)(instance, operation, exclude, redact);
151
163
  if (diff.length === 0) {
152
164
  return;
153
165
  }
@@ -60,16 +60,28 @@ const ignore = (request) => {
60
60
  false);
61
61
  };
62
62
  exports.ignore = ignore;
63
+ // Sentry is an optional peer dependency: services that do not report to
64
+ // Sentry do not install @sentry/node or pino-sentry-transport.
65
+ const isInstalled = (moduleName) => {
66
+ try {
67
+ require.resolve(moduleName);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ };
63
74
  class LoggerFactory {
64
75
  static getLogger(logLevel, sentryOptions) {
65
76
  const { sentryDsn, releaseVersion, minLevel } = sentryOptions ?? {};
77
+ const withSentry = !!sentryDsn && isInstalled('pino-sentry-transport');
66
78
  const transports = [
67
79
  {
68
80
  target: 'pino/file',
69
81
  options: { destination: 1 },
70
82
  level: logLevel,
71
83
  },
72
- ...(sentryDsn
84
+ ...(withSentry
73
85
  ? [
74
86
  {
75
87
  target: 'pino-sentry-transport',
@@ -89,7 +101,7 @@ class LoggerFactory {
89
101
  ]
90
102
  : []),
91
103
  ];
92
- return (0, pino_1.default)({
104
+ const logger = (0, pino_1.default)({
93
105
  level: logLevel,
94
106
  mixin: requestTracer_1.getRequestTracerMixin,
95
107
  serializers: {
@@ -100,6 +112,10 @@ class LoggerFactory {
100
112
  targets: transports,
101
113
  },
102
114
  });
115
+ if (sentryDsn && !withSentry) {
116
+ logger.warn('sentryDsn is set but pino-sentry-transport is not installed, Sentry reporting is off');
117
+ }
118
+ return logger;
103
119
  }
104
120
  static getHttpLogger(options) {
105
121
  const { requestSerializer, responseSerializer, errorSerializer, defaultHttpLogLevel, customMessage, } = new HttpLoggerHelpers(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucaapp/service-utils",
3
- "version": "5.34.1",
3
+ "version": "5.36.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
@@ -23,7 +23,6 @@
23
23
  "@aws-sdk/client-s3": "^3.1035.0",
24
24
  "@aws-sdk/lib-storage": "^3.1035.0",
25
25
  "@hapi/boom": "^10.0.1",
26
- "@sentry/node": "^9.10.1",
27
26
  "@tsconfig/node24": "24.0.0",
28
27
  "@types/express": "4.17.23",
29
28
  "@types/express-serve-static-core": "^4.17.43",
@@ -47,7 +46,6 @@
47
46
  "moment-timezone": "^0.5.44",
48
47
  "negotiator": "^0.6.4",
49
48
  "pino-http": "9.0.0",
50
- "pino-sentry-transport": "^1.5.1",
51
49
  "prom-client": "14.1.0",
52
50
  "response-time": "2.3.4",
53
51
  "sequelize": "6.37.8",
@@ -61,6 +59,7 @@
61
59
  "pg-boss": "12.14.0"
62
60
  },
63
61
  "devDependencies": {
62
+ "@sentry/node": "^9.10.1",
64
63
  "@types/busboy": "^1.5.4",
65
64
  "@types/lodash": "^4.14.187",
66
65
  "@types/negotiator": "^0.6.4",
@@ -69,20 +68,33 @@
69
68
  "@types/uuid": "^10.0.0",
70
69
  "@typescript-eslint/eslint-plugin": "^7.15.0",
71
70
  "@typescript-eslint/parser": "^7.15.0",
71
+ "@typescript/native": "npm:typescript@7.0.2",
72
72
  "@vitest/coverage-v8": "4.1.1",
73
73
  "conventional-changelog-conventionalcommits": "^8.0.0",
74
74
  "eslint": "8.57.0",
75
75
  "eslint-plugin-prettier": "4.2.1",
76
76
  "eslint-plugin-vitest": "0.4.1",
77
- "yarn-osv-audit": "0.1.8",
77
+ "pino-sentry-transport": "^1.5.1",
78
78
  "prettier": "2.7.1",
79
79
  "semantic-release": "^25.0.3",
80
80
  "semantic-release-monorepo": "8.0.2",
81
81
  "supertest": "6.3.4",
82
- "@typescript/native": "npm:typescript@7.0.2",
83
82
  "typescript": "npm:@typescript/typescript6@6.0.2",
84
83
  "vite-tsconfig-paths": "4.3.2",
85
- "vitest": "4.1.1"
84
+ "vitest": "4.1.1",
85
+ "yarn-osv-audit": "0.1.8"
86
+ },
87
+ "peerDependencies": {
88
+ "@sentry/node": "^9.10.1",
89
+ "pino-sentry-transport": "^1.5.1"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "@sentry/node": {
93
+ "optional": true
94
+ },
95
+ "pino-sentry-transport": {
96
+ "optional": true
97
+ }
86
98
  },
87
99
  "resolutions": {
88
100
  "tar": "7.5.21",