@forestadmin/agent 1.95.2 → 1.97.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/agent.d.ts +4 -0
- package/dist/agent.js +57 -6
- package/dist/audit-trail/action-capture.d.ts +17 -0
- package/dist/audit-trail/action-capture.js +97 -0
- package/dist/audit-trail/index.d.ts +8 -0
- package/dist/audit-trail/index.js +44 -0
- package/dist/audit-trail/instrument.d.ts +12 -0
- package/dist/audit-trail/instrument.js +393 -0
- package/dist/audit-trail/migrations.d.ts +34 -0
- package/dist/audit-trail/migrations.js +213 -0
- package/dist/audit-trail/revert.d.ts +8 -0
- package/dist/audit-trail/revert.js +81 -0
- package/dist/audit-trail/scope.d.ts +20 -0
- package/dist/audit-trail/scope.js +33 -0
- package/dist/audit-trail/sql-store.d.ts +30 -0
- package/dist/audit-trail/sql-store.js +316 -0
- package/dist/audit-trail/types.d.ts +129 -0
- package/dist/audit-trail/types.js +3 -0
- package/dist/routes/access/audit-trail-correlation.d.ts +18 -0
- package/dist/routes/access/audit-trail-correlation.js +84 -0
- package/dist/routes/access/audit-trail.d.ts +23 -0
- package/dist/routes/access/audit-trail.js +293 -0
- package/dist/routes/capabilities.js +5 -1
- package/dist/routes/index.js +14 -1
- package/dist/routes/modification/action/action.d.ts +3 -0
- package/dist/routes/modification/action/action.js +106 -2
- package/dist/types.d.ts +39 -1
- package/dist/types.js +1 -1
- package/dist/utils/correlation-id.d.ts +5 -0
- package/dist/utils/correlation-id.js +26 -0
- package/dist/utils/options-validator.js +19 -1
- package/dist/utils/query-string.d.ts +3 -1
- package/dist/utils/query-string.js +5 -5
- package/package.json +8 -7
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REDACTED = exports.MAX_SNAPSHOT_RECORDS = void 0;
|
|
4
|
+
exports.buildRecorder = buildRecorder;
|
|
5
|
+
exports.default = installAuditTrailHooks;
|
|
6
|
+
const datasource_toolkit_1 = require("@forestadmin/datasource-toolkit");
|
|
7
|
+
const stableKeyOrderReplacer = (key, value) => {
|
|
8
|
+
// JSON.stringify has no native BigInt support and throws on one, regardless of nesting depth or
|
|
9
|
+
// whether it's the root value; the replacer gets a chance to convert it first either way.
|
|
10
|
+
if (typeof value === 'bigint')
|
|
11
|
+
return value.toString();
|
|
12
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
13
|
+
return Object.keys(value)
|
|
14
|
+
.sort()
|
|
15
|
+
.reduce((sorted, name) => {
|
|
16
|
+
sorted[name] = value[name];
|
|
17
|
+
return sorted;
|
|
18
|
+
}, {});
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
// Date/BSON values round-trip through JSON.stringify via their toJSON; the replacer makes
|
|
23
|
+
// plain-object key order irrelevant.
|
|
24
|
+
const deepEquals = (a, b) => JSON.stringify(a, stableKeyOrderReplacer) === JSON.stringify(b, stableKeyOrderReplacer);
|
|
25
|
+
// A value with its own `toJSON` (Date, a BSON wrapper) has a canonical representation that
|
|
26
|
+
// `Object.entries` can't be trusted to reproduce: BSON's `toJSON` is typically on the prototype
|
|
27
|
+
// while the actual data lives in unrelated (and sometimes non-enumerable) own fields, so walking
|
|
28
|
+
// `Object.entries` would silently reconstruct the wrong — or an empty — object instead.
|
|
29
|
+
const hasCanonicalJson = (value) => typeof value.toJSON === 'function';
|
|
30
|
+
// A raw BigInt surviving into a stored `previousValues`/`newValues` would fail later — Sequelize
|
|
31
|
+
// serializing the JSON column, or Koa serializing an HTTP history response — the same way
|
|
32
|
+
// JSON.stringify does above. Recurses through arrays and plain objects (leaving anything with its
|
|
33
|
+
// own `toJSON` untouched, see above) since a BigInt can be nested inside a JSON column's value
|
|
34
|
+
// rather than being the column's value itself — `pickColumns` hands this a whole column value
|
|
35
|
+
// unprocessed by `diff`'s own recursion, and a primitive array is a `diff` leaf kept whole rather
|
|
36
|
+
// than walked.
|
|
37
|
+
const toJsonSafe = (value) => {
|
|
38
|
+
if (typeof value === 'bigint')
|
|
39
|
+
return value.toString();
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map(toJsonSafe);
|
|
42
|
+
if (value && typeof value === 'object' && !hasCanonicalJson(value)) {
|
|
43
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
44
|
+
key,
|
|
45
|
+
toJsonSafe(entry),
|
|
46
|
+
]));
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
// Records touched by a bulk operation, bounded so one write can't hold an unbounded result set in
|
|
51
|
+
// memory (or fan out an unbounded number of pending-row inserts). Truncation is logged explicitly
|
|
52
|
+
// rather than silently auditing only part of the operation. Also the cap a smart action's audited
|
|
53
|
+
// selection is checked against (action.ts) — one constant, so the two can't drift apart the way this
|
|
54
|
+
// value and the Ruby agent's once did.
|
|
55
|
+
exports.MAX_SNAPSHOT_RECORDS = 1000;
|
|
56
|
+
// One caller-supplied `Filter` object can be reused across concurrent (or sequential, e.g. a
|
|
57
|
+
// continue-on-error bulk loop) operations sharing the same template, so each key holds a queue of
|
|
58
|
+
// snapshots rather than a single value — otherwise a second `Before` would overwrite the first
|
|
59
|
+
// snapshot and its `After` would silently emit nothing.
|
|
60
|
+
const snapshotsByFilter = new WeakMap();
|
|
61
|
+
function pushSnapshot(key, entry) {
|
|
62
|
+
const queue = snapshotsByFilter.get(key) ?? [];
|
|
63
|
+
queue.push(entry);
|
|
64
|
+
snapshotsByFilter.set(key, queue);
|
|
65
|
+
}
|
|
66
|
+
// Correlates by `patch` content rather than queue position: if a write throws, its `Before`
|
|
67
|
+
// snapshot is never consumed (there is no matching `After` call), so blindly taking the front of
|
|
68
|
+
// the queue would hand a later, unrelated operation someone else's stale snapshot instead of its
|
|
69
|
+
// own. Delete has no equivalent secondary key, so it falls back to FIFO order — a narrower version
|
|
70
|
+
// of the same risk that remains for deletes sharing one `Filter` reference.
|
|
71
|
+
function takeSnapshot(key, patch) {
|
|
72
|
+
const queue = snapshotsByFilter.get(key);
|
|
73
|
+
if (!queue || queue.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
const index = patch ? queue.findIndex(entry => deepEquals(entry.patch, patch)) : 0;
|
|
76
|
+
const [entry] = queue.splice(index === -1 ? 0 : index, 1);
|
|
77
|
+
if (queue.length === 0)
|
|
78
|
+
snapshotsByFilter.delete(key);
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
// Create has no Filter to key by (there's nothing to select yet), but `context.data` is the exact
|
|
82
|
+
// same array reference in both the Before and After hook for one `.create()` call — verified via
|
|
83
|
+
// `HookBeforeCreateContext`/`HookAfterCreateContext`, which both wrap the literal `data` argument.
|
|
84
|
+
const pendingIdsByData = new WeakMap();
|
|
85
|
+
// Must match IdUtils.packId so recordIds align with the rest of Forest.
|
|
86
|
+
const toPackedRecordId = (record, primaryKeys) => {
|
|
87
|
+
if (primaryKeys.length === 0) {
|
|
88
|
+
throw new Error('Cannot audit a collection with no primary key');
|
|
89
|
+
}
|
|
90
|
+
return primaryKeys.map(pk => String(record[pk])).join('|');
|
|
91
|
+
};
|
|
92
|
+
const pickColumns = (record, columns) => Object.fromEntries(columns.map(column => [column, toJsonSafe(record[column]) ?? null]));
|
|
93
|
+
const isPlainObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date);
|
|
94
|
+
const isObjectArray = (value) => Array.isArray(value) && value.length > 0 && value.every(isPlainObject);
|
|
95
|
+
// Returns only the leaves that changed. Plain objects and object-arrays recurse; everything else
|
|
96
|
+
// (scalars, primitive arrays, Date, BSON) is compared and kept whole. A leaf that did not exist
|
|
97
|
+
// before/after (as opposed to one explicitly set to `null`) is marked `*Absent`, so the caller
|
|
98
|
+
// simply omits that key/index from that side rather than writing a placeholder value: the stored
|
|
99
|
+
// diff itself never carries a marker, it just doesn't have the key.
|
|
100
|
+
const diff = (before, after) => {
|
|
101
|
+
if (deepEquals(before, after))
|
|
102
|
+
return null;
|
|
103
|
+
if (isPlainObject(before) && isPlainObject(after)) {
|
|
104
|
+
const previous = {};
|
|
105
|
+
const next = {};
|
|
106
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
107
|
+
const subDelta = diff(before[key], after[key]);
|
|
108
|
+
if (subDelta) {
|
|
109
|
+
if (!subDelta.previousAbsent)
|
|
110
|
+
previous[key] = subDelta.previous;
|
|
111
|
+
if (!subDelta.nextAbsent)
|
|
112
|
+
next[key] = subDelta.next;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { previous, next, previousAbsent: false, nextAbsent: false };
|
|
116
|
+
}
|
|
117
|
+
if (isObjectArray(before) && isObjectArray(after)) {
|
|
118
|
+
const previous = {};
|
|
119
|
+
const next = {};
|
|
120
|
+
for (let index = 0; index < Math.max(before.length, after.length); index += 1) {
|
|
121
|
+
const subDelta = diff(before[index], after[index]);
|
|
122
|
+
if (subDelta) {
|
|
123
|
+
if (!subDelta.previousAbsent)
|
|
124
|
+
previous[index] = subDelta.previous;
|
|
125
|
+
if (!subDelta.nextAbsent)
|
|
126
|
+
next[index] = subDelta.next;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { previous, next, previousAbsent: false, nextAbsent: false };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
previous: before === undefined ? undefined : toJsonSafe(before) ?? null,
|
|
133
|
+
next: after === undefined ? undefined : toJsonSafe(after) ?? null,
|
|
134
|
+
previousAbsent: before === undefined,
|
|
135
|
+
nextAbsent: after === undefined,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
// Diffs against the record actually persisted by the write (`after`), not the requested patch, so
|
|
139
|
+
// the log reflects normalization/coercion/side effects applied by the datasource or a decorator.
|
|
140
|
+
const changedValues = (before, after, columns) => {
|
|
141
|
+
const previousValues = {};
|
|
142
|
+
const newValues = {};
|
|
143
|
+
for (const column of columns) {
|
|
144
|
+
const delta = diff(before[column], after[column]);
|
|
145
|
+
if (delta) {
|
|
146
|
+
if (!delta.previousAbsent)
|
|
147
|
+
previousValues[column] = delta.previous;
|
|
148
|
+
if (!delta.nextAbsent)
|
|
149
|
+
newValues[column] = delta.next;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { previousValues, newValues };
|
|
153
|
+
};
|
|
154
|
+
exports.REDACTED = '[redacted]';
|
|
155
|
+
const redactValues = (values, redactedFields) => {
|
|
156
|
+
if (!redactedFields.length)
|
|
157
|
+
return values;
|
|
158
|
+
const result = { ...values };
|
|
159
|
+
for (const field of redactedFields)
|
|
160
|
+
if (field in result)
|
|
161
|
+
result[field] = exports.REDACTED;
|
|
162
|
+
return result;
|
|
163
|
+
};
|
|
164
|
+
const consoleSink = record => {
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.info('[audit-trail]', JSON.stringify(record));
|
|
167
|
+
};
|
|
168
|
+
function buildRecorder(store, sink, critical, logger) {
|
|
169
|
+
if (store) {
|
|
170
|
+
return {
|
|
171
|
+
async insertPendingBatch(records) {
|
|
172
|
+
if (records.length === 0)
|
|
173
|
+
return [];
|
|
174
|
+
try {
|
|
175
|
+
return await store.insertPendingBatch(records);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Critical: no unaudited write — refuse the operation before it runs. Non-critical:
|
|
179
|
+
// logged and swallowed, exactly like today's single-shot failures; the write proceeds
|
|
180
|
+
// with nothing to confirm later.
|
|
181
|
+
if (critical)
|
|
182
|
+
throw error;
|
|
183
|
+
logger?.('Error', `[ForestAdmin] Audit trail: unable to record pending entries, continuing: ${error}`);
|
|
184
|
+
return records.map(() => null);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
async confirm(id, patch) {
|
|
188
|
+
if (id === null)
|
|
189
|
+
return;
|
|
190
|
+
try {
|
|
191
|
+
await store.confirm(id, patch);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
// The write has already happened by now; there is nothing left to refuse.
|
|
195
|
+
logger?.('Error', `[ForestAdmin] Audit trail: unable to confirm entry, continuing: ${error}`);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const emit = sink ?? consoleSink;
|
|
201
|
+
const pendingById = new Map();
|
|
202
|
+
let nextId = 1;
|
|
203
|
+
return {
|
|
204
|
+
async insertPendingBatch(records) {
|
|
205
|
+
return records.map(record => {
|
|
206
|
+
const id = nextId;
|
|
207
|
+
nextId += 1;
|
|
208
|
+
pendingById.set(id, record);
|
|
209
|
+
return id;
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
async confirm(id, patch) {
|
|
213
|
+
if (id === null)
|
|
214
|
+
return;
|
|
215
|
+
const pending = pendingById.get(id);
|
|
216
|
+
pendingById.delete(id);
|
|
217
|
+
if (!pending)
|
|
218
|
+
return;
|
|
219
|
+
try {
|
|
220
|
+
await emit({ ...pending, ...patch });
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
// The write has already happened by now; there is nothing left to refuse.
|
|
224
|
+
logger?.('Error', `[ForestAdmin] Audit trail: unable to confirm entry, continuing: ${error}`);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function instrumentCollection(collection, recorder, redactedFields, logger, critical) {
|
|
230
|
+
const { schema } = collection;
|
|
231
|
+
const writableColumns = Object.keys(schema.fields).filter(name => {
|
|
232
|
+
const field = schema.fields[name];
|
|
233
|
+
return field.type === 'Column' && !field.isReadOnly;
|
|
234
|
+
});
|
|
235
|
+
const primaryKeys = datasource_toolkit_1.SchemaUtils.getPrimaryKeys(schema);
|
|
236
|
+
// PKs may be read-only, but they are required to build the packed recordId; the diff itself
|
|
237
|
+
// stays restricted to writableColumns.
|
|
238
|
+
const readProjection = [...new Set([...primaryKeys, ...writableColumns])];
|
|
239
|
+
const { name } = collection;
|
|
240
|
+
const buildRecord = (caller, entry) => ({
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
collection: name,
|
|
243
|
+
userId: caller.id,
|
|
244
|
+
userFirstName: caller.firstName,
|
|
245
|
+
userLastName: caller.lastName,
|
|
246
|
+
userEmail: caller.email,
|
|
247
|
+
actionName: null,
|
|
248
|
+
correlationKey: caller.requestId,
|
|
249
|
+
...entry,
|
|
250
|
+
previousValues: redactValues(entry.previousValues, redactedFields),
|
|
251
|
+
newValues: redactValues(entry.newValues, redactedFields),
|
|
252
|
+
});
|
|
253
|
+
// Requests one more than the cap: fetching exactly `MAX_SNAPSHOT_RECORDS` can never distinguish
|
|
254
|
+
// "matched exactly the cap" (nothing lost) from "matched more than it" (truncated) — the extra
|
|
255
|
+
// row is what makes that distinguishable, and is dropped again below before anything is audited.
|
|
256
|
+
// Constructs a real `PaginatedFilter` (via `.override`-equivalent spread of `context.filter`'s
|
|
257
|
+
// own fields, which is a plain `Filter` for Update/Delete) rather than merging `page` onto a bare
|
|
258
|
+
// object: the latter would depend on `context.collection.list` happening to reconstruct a proper
|
|
259
|
+
// instance from whatever shape it's handed, which isn't part of its contract.
|
|
260
|
+
const withLimit = (filter) => new datasource_toolkit_1.PaginatedFilter({ ...filter, page: new datasource_toolkit_1.Page(0, exports.MAX_SNAPSHOT_RECORDS + 1) });
|
|
261
|
+
// Hitting the cap means the operation may have matched more records than were actually audited.
|
|
262
|
+
// Under `critical: false`, silently dropping the excess would look identical to "everything got
|
|
263
|
+
// audited" — logged instead. Under `critical: true`, that gap is exactly what the option exists
|
|
264
|
+
// to refuse: no unaudited write, so the whole operation is refused rather than let through
|
|
265
|
+
// partially covered.
|
|
266
|
+
const enforceSnapshotCap = (operation, truncated) => {
|
|
267
|
+
if (!truncated)
|
|
268
|
+
return;
|
|
269
|
+
const message = `[ForestAdmin] Audit trail: "${name}" ${operation} matched more than ` +
|
|
270
|
+
`${exports.MAX_SNAPSHOT_RECORDS} records — only the first ${exports.MAX_SNAPSHOT_RECORDS} can be audited.`;
|
|
271
|
+
if (critical) {
|
|
272
|
+
throw new Error(`${message} Refusing the operation because "critical" is enabled.`);
|
|
273
|
+
}
|
|
274
|
+
logger?.('Warn', `${message} The rest were skipped.`);
|
|
275
|
+
};
|
|
276
|
+
collection.addInternalHook('Before', 'Create', async (context) => {
|
|
277
|
+
const pendingRecords = context.data.map(() => buildRecord(context.caller, {
|
|
278
|
+
operation: 'create',
|
|
279
|
+
recordId: null,
|
|
280
|
+
previousValues: {},
|
|
281
|
+
newValues: {},
|
|
282
|
+
}));
|
|
283
|
+
const pendingIds = await recorder.insertPendingBatch(pendingRecords);
|
|
284
|
+
pendingIdsByData.set(context.data, pendingIds);
|
|
285
|
+
});
|
|
286
|
+
collection.addInternalHook('After', 'Create', async (context) => {
|
|
287
|
+
const pendingIds = pendingIdsByData.get(context.data) ?? [];
|
|
288
|
+
pendingIdsByData.delete(context.data);
|
|
289
|
+
await Promise.all(context.records.map((record, index) => recorder.confirm(pendingIds[index] ?? null, {
|
|
290
|
+
operation: 'create',
|
|
291
|
+
recordId: toPackedRecordId(record, primaryKeys),
|
|
292
|
+
previousValues: {},
|
|
293
|
+
newValues: redactValues(pickColumns(record, writableColumns), redactedFields),
|
|
294
|
+
})));
|
|
295
|
+
});
|
|
296
|
+
collection.addInternalHook('Before', 'Update', async (context) => {
|
|
297
|
+
const fetched = (await context.collection.list(
|
|
298
|
+
// `context.filter` is typed as the plain-object `TFilter` shape for customer ergonomics, but
|
|
299
|
+
// is a real (frozen) `Filter` instance at runtime — the same cast the hook decorator itself
|
|
300
|
+
// performs to produce it.
|
|
301
|
+
withLimit(context.filter), readProjection));
|
|
302
|
+
const truncated = fetched.length > exports.MAX_SNAPSHOT_RECORDS;
|
|
303
|
+
enforceSnapshotCap('update', truncated);
|
|
304
|
+
const before = truncated ? fetched.slice(0, exports.MAX_SNAPSHOT_RECORDS) : fetched;
|
|
305
|
+
const pendingRecords = before.map(record => buildRecord(context.caller, {
|
|
306
|
+
operation: 'update',
|
|
307
|
+
recordId: toPackedRecordId(record, primaryKeys),
|
|
308
|
+
previousValues: pickColumns(record, writableColumns),
|
|
309
|
+
newValues: {},
|
|
310
|
+
}));
|
|
311
|
+
const pendingIds = await recorder.insertPendingBatch(pendingRecords);
|
|
312
|
+
pushSnapshot(context.filter, { before, patch: context.patch, pendingIds });
|
|
313
|
+
});
|
|
314
|
+
collection.addInternalHook('After', 'Update', async (context) => {
|
|
315
|
+
const snapshot = takeSnapshot(context.filter, context.patch);
|
|
316
|
+
if (!snapshot || snapshot.before.length === 0)
|
|
317
|
+
return;
|
|
318
|
+
const { before, pendingIds } = snapshot;
|
|
319
|
+
// Re-read each touched record by its own (possibly patch-changed) identity rather than via
|
|
320
|
+
// `context.filter`: if the update changed a field the filter matched on — including a
|
|
321
|
+
// writable primary key — re-querying with that same filter would find nothing, silently
|
|
322
|
+
// dropping the entry instead of filing it under the record's new id.
|
|
323
|
+
const expectedRecords = before.map(record => ({ ...record, ...context.patch }));
|
|
324
|
+
const expectedIds = expectedRecords.map(record => primaryKeys.map(pk => record[pk]));
|
|
325
|
+
const after = (await context.collection.list({ conditionTree: datasource_toolkit_1.ConditionTreeFactory.matchIds(schema, expectedIds) }, readProjection));
|
|
326
|
+
const afterById = new Map(after.map(record => [toPackedRecordId(record, primaryKeys), record]));
|
|
327
|
+
await Promise.all(before.map((record, index) => {
|
|
328
|
+
const updated = afterById.get(toPackedRecordId(expectedRecords[index], primaryKeys));
|
|
329
|
+
const pendingId = pendingIds[index] ?? null;
|
|
330
|
+
if (!updated)
|
|
331
|
+
return undefined;
|
|
332
|
+
const { previousValues, newValues } = changedValues(record, updated, writableColumns);
|
|
333
|
+
// Confirmed even when empty: the write still ran and resolved, so leaving the pending row
|
|
334
|
+
// unconfirmed would misrepresent a completed no-op write as one that never landed.
|
|
335
|
+
// Packed from the post-update values: if the update changed a writable primary key, the
|
|
336
|
+
// entry is filed under the record's new id, matching what later history lookups use.
|
|
337
|
+
return recorder.confirm(pendingId, {
|
|
338
|
+
operation: 'update',
|
|
339
|
+
recordId: toPackedRecordId(updated, primaryKeys),
|
|
340
|
+
previousValues: redactValues(previousValues, redactedFields),
|
|
341
|
+
newValues: redactValues(newValues, redactedFields),
|
|
342
|
+
});
|
|
343
|
+
}));
|
|
344
|
+
});
|
|
345
|
+
collection.addInternalHook('Before', 'Delete', async (context) => {
|
|
346
|
+
const fetched = (await context.collection.list(
|
|
347
|
+
// `context.filter` is typed as the plain-object `TFilter` shape for customer ergonomics, but
|
|
348
|
+
// is a real (frozen) `Filter` instance at runtime — the same cast the hook decorator itself
|
|
349
|
+
// performs to produce it.
|
|
350
|
+
withLimit(context.filter), readProjection));
|
|
351
|
+
const truncated = fetched.length > exports.MAX_SNAPSHOT_RECORDS;
|
|
352
|
+
enforceSnapshotCap('delete', truncated);
|
|
353
|
+
const before = truncated ? fetched.slice(0, exports.MAX_SNAPSHOT_RECORDS) : fetched;
|
|
354
|
+
const pendingRecords = before.map(record => buildRecord(context.caller, {
|
|
355
|
+
operation: 'delete',
|
|
356
|
+
recordId: toPackedRecordId(record, primaryKeys),
|
|
357
|
+
previousValues: pickColumns(record, writableColumns),
|
|
358
|
+
newValues: {},
|
|
359
|
+
}));
|
|
360
|
+
const pendingIds = await recorder.insertPendingBatch(pendingRecords);
|
|
361
|
+
pushSnapshot(context.filter, { before, pendingIds });
|
|
362
|
+
});
|
|
363
|
+
collection.addInternalHook('After', 'Delete', async (context) => {
|
|
364
|
+
const snapshot = takeSnapshot(context.filter);
|
|
365
|
+
if (!snapshot)
|
|
366
|
+
return;
|
|
367
|
+
const { before, pendingIds } = snapshot;
|
|
368
|
+
await Promise.all(before.map((record, index) => recorder.confirm(pendingIds[index] ?? null, {
|
|
369
|
+
operation: 'delete',
|
|
370
|
+
recordId: toPackedRecordId(record, primaryKeys),
|
|
371
|
+
previousValues: redactValues(pickColumns(record, writableColumns), redactedFields),
|
|
372
|
+
newValues: {},
|
|
373
|
+
})));
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function installAuditTrailHooks(dataSourceCustomizer, _collectionCustomizer, options) {
|
|
377
|
+
const { sink, store, redact, critical = false, logger } = options ?? {};
|
|
378
|
+
const recorder = buildRecorder(store, sink, critical, logger);
|
|
379
|
+
for (const collection of dataSourceCustomizer.collections) {
|
|
380
|
+
// toPackedRecordId throws for a collection with no primary key — there is nothing to key an
|
|
381
|
+
// audit entry on, so skip instrumenting it entirely rather than installing hooks that would
|
|
382
|
+
// throw on the collection's first write.
|
|
383
|
+
if (datasource_toolkit_1.SchemaUtils.getPrimaryKeys(collection.schema).length === 0) {
|
|
384
|
+
logger?.('Warn', `[ForestAdmin] Audit trail: skipping "${collection.name}" — it has no primary key.`);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
instrumentCollection(collection, recorder, redact?.[collection.name] ?? [], logger, critical);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Returned so a broken bootstrap fails agent.start() instead of the first write.
|
|
391
|
+
return store?.init?.();
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5zdHJ1bWVudC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hdWRpdC10cmFpbC9pbnN0cnVtZW50LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQW9QQSxzQ0EyRUM7QUFrTkQseUNBd0JDO0FBeGhCRCx3RUFLeUM7QUFFekMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLEdBQVcsRUFBRSxLQUFjLEVBQVcsRUFBRTtJQUN0RSxnR0FBZ0c7SUFDaEcsMEZBQTBGO0lBQzFGLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUTtRQUFFLE9BQU8sS0FBSyxDQUFDLFFBQVEsRUFBRSxDQUFDO0lBRXZELElBQUksS0FBSyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNoRSxPQUFPLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBZ0MsQ0FBQzthQUNqRCxJQUFJLEVBQUU7YUFDTixNQUFNLENBQTBCLENBQUMsTUFBTSxFQUFFLElBQUksRUFBRSxFQUFFO1lBQ2hELE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBSSxLQUFpQyxDQUFDLElBQUksQ0FBQyxDQUFDO1lBRXhELE9BQU8sTUFBTSxDQUFDO1FBQ2hCLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztJQUNYLENBQUM7SUFFRCxPQUFPLEtBQUssQ0FBQztBQUNmLENBQUMsQ0FBQztBQUVGLDBGQUEwRjtBQUMxRixxQ0FBcUM7QUFDckMsTUFBTSxVQUFVLEdBQUcsQ0FBQyxDQUFVLEVBQUUsQ0FBVSxFQUFXLEVBQUUsQ0FDckQsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLEVBQUUsc0JBQXNCLENBQUMsS0FBSyxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxzQkFBc0IsQ0FBQyxDQUFDO0FBRTFGLDJGQUEyRjtBQUMzRixnR0FBZ0c7QUFDaEcsaUdBQWlHO0FBQ2pHLHdGQUF3RjtBQUN4RixNQUFNLGdCQUFnQixHQUFHLENBQUMsS0FBYSxFQUFXLEVBQUUsQ0FDbEQsT0FBUSxLQUE4QixDQUFDLE1BQU0sS0FBSyxVQUFVLENBQUM7QUFFL0QsaUdBQWlHO0FBQ2pHLDBGQUEwRjtBQUMxRixrR0FBa0c7QUFDbEcsK0ZBQStGO0FBQy9GLDhGQUE4RjtBQUM5RixrR0FBa0c7QUFDbEcsZUFBZTtBQUNmLE1BQU0sVUFBVSxHQUFHLENBQUMsS0FBYyxFQUFXLEVBQUU7SUFDN0MsSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRO1FBQUUsT0FBTyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUM7SUFDdkQsSUFBSSxLQUFLLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQztRQUFFLE9BQU8sS0FBSyxDQUFDLEdBQUcsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUV2RCxJQUFJLEtBQUssSUFBSSxPQUFPLEtBQUssS0FBSyxRQUFRLElBQUksQ0FBQyxnQkFBZ0IsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ25FLE9BQU8sTUFBTSxDQUFDLFdBQVcsQ0FDdkIsTUFBTSxDQUFDLE9BQU8sQ0FBQyxLQUFnQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLEVBQUUsRUFBRSxDQUFDO1lBQ3JFLEdBQUc7WUFDSCxVQUFVLENBQUMsS0FBSyxDQUFDO1NBQ2xCLENBQUMsQ0FDSCxDQUFDO0lBQ0osQ0FBQztJQUVELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQyxDQUFDO0FBRUYsa0dBQWtHO0FBQ2xHLGtHQUFrRztBQUNsRyxrR0FBa0c7QUFDbEcscUdBQXFHO0FBQ3JHLHVDQUF1QztBQUMxQixRQUFBLG9CQUFvQixHQUFHLElBQUksQ0FBQztBQVF6Qyw2RkFBNkY7QUFDN0Ysa0dBQWtHO0FBQ2xHLCtGQUErRjtBQUMvRix3REFBd0Q7QUFDeEQsTUFBTSxpQkFBaUIsR0FBRyxJQUFJLE9BQU8sRUFBNkIsQ0FBQztBQUVuRSxTQUFTLFlBQVksQ0FBQyxHQUFXLEVBQUUsS0FBc0I7SUFDdkQsTUFBTSxLQUFLLEdBQUcsaUJBQWlCLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxJQUFJLEVBQUUsQ0FBQztJQUMvQyxLQUFLLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ2xCLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsS0FBSyxDQUFDLENBQUM7QUFDcEMsQ0FBQztBQUVELDRGQUE0RjtBQUM1RixpR0FBaUc7QUFDakcsaUdBQWlHO0FBQ2pHLG1HQUFtRztBQUNuRyw0RUFBNEU7QUFDNUUsU0FBUyxZQUFZLENBQUMsR0FBVyxFQUFFLEtBQWtCO0lBQ25ELE1BQU0sS0FBSyxHQUFHLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUN6QyxJQUFJLENBQUMsS0FBSyxJQUFJLEtBQUssQ0FBQyxNQUFNLEtBQUssQ0FBQztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBRW5ELE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsS0FBSyxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNuRixNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsS0FBSyxDQUFDLE1BQU0sQ0FBQyxLQUFLLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBRTFELElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxDQUFDO1FBQUUsaUJBQWlCLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBRXRELE9BQU8sS0FBSyxDQUFDO0FBQ2YsQ0FBQztBQUVELGtHQUFrRztBQUNsRyxpR0FBaUc7QUFDakcsbUdBQW1HO0FBQ25HLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxPQUFPLEVBQWdDLENBQUM7QUFFckUsd0VBQXdFO0FBQ3hFLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxNQUFrQixFQUFFLFdBQXFCLEVBQVUsRUFBRTtJQUM3RSxJQUFJLFdBQVcsQ0FBQyxNQUFNLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDN0IsTUFBTSxJQUFJLEtBQUssQ0FBQywrQ0FBK0MsQ0FBQyxDQUFDO0lBQ25FLENBQUM7SUFFRCxPQUFPLFdBQVcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUM7QUFDN0QsQ0FBQyxDQUFDO0FBRUYsTUFBTSxXQUFXLEdBQUcsQ0FBQyxNQUFrQixFQUFFLE9BQWlCLEVBQTJCLEVBQUUsQ0FDckYsTUFBTSxDQUFDLFdBQVcsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQyxNQUFNLEVBQUUsVUFBVSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxJQUFJLElBQUksQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUUxRixNQUFNLGFBQWEsR0FBRyxDQUFDLEtBQWMsRUFBb0MsRUFBRSxDQUN6RSxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksT0FBTyxLQUFLLEtBQUssUUFBUSxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsS0FBSyxZQUFZLElBQUksQ0FBQyxDQUFDO0FBRW5HLE1BQU0sYUFBYSxHQUFHLENBQUMsS0FBYyxFQUFzQyxFQUFFLENBQzNFLEtBQUssQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxDQUFDLE1BQU0sR0FBRyxDQUFDLElBQUksS0FBSyxDQUFDLEtBQUssQ0FBQyxhQUFhLENBQUMsQ0FBQztBQUl6RSxpR0FBaUc7QUFDakcsZ0dBQWdHO0FBQ2hHLCtGQUErRjtBQUMvRixpR0FBaUc7QUFDakcsb0VBQW9FO0FBQ3BFLE1BQU0sSUFBSSxHQUFHLENBQUMsTUFBZSxFQUFFLEtBQWMsRUFBZ0IsRUFBRTtJQUM3RCxJQUFJLFVBQVUsQ0FBQyxNQUFNLEVBQUUsS0FBSyxDQUFDO1FBQUUsT0FBTyxJQUFJLENBQUM7SUFFM0MsSUFBSSxhQUFhLENBQUMsTUFBTSxDQUFDLElBQUksYUFBYSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDbEQsTUFBTSxRQUFRLEdBQTRCLEVBQUUsQ0FBQztRQUM3QyxNQUFNLElBQUksR0FBNEIsRUFBRSxDQUFDO1FBRXpDLEtBQUssTUFBTSxHQUFHLElBQUksSUFBSSxHQUFHLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLEVBQUUsR0FBRyxNQUFNLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO1lBQzNFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLEVBQUUsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUM7WUFFL0MsSUFBSSxRQUFRLEVBQUUsQ0FBQztnQkFDYixJQUFJLENBQUMsUUFBUSxDQUFDLGNBQWM7b0JBQUUsUUFBUSxDQUFDLEdBQUcsQ0FBQyxHQUFHLFFBQVEsQ0FBQyxRQUFRLENBQUM7Z0JBQ2hFLElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVTtvQkFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQztZQUN0RCxDQUFDO1FBQ0gsQ0FBQztRQUVELE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLGNBQWMsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLEtBQUssRUFBRSxDQUFDO0lBQ3RFLENBQUM7SUFFRCxJQUFJLGFBQWEsQ0FBQyxNQUFNLENBQUMsSUFBSSxhQUFhLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUNsRCxNQUFNLFFBQVEsR0FBNEIsRUFBRSxDQUFDO1FBQzdDLE1BQU0sSUFBSSxHQUE0QixFQUFFLENBQUM7UUFFekMsS0FBSyxJQUFJLEtBQUssR0FBRyxDQUFDLEVBQUUsS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsTUFBTSxDQUFDLEVBQUUsS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQzlFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLEVBQUUsS0FBSyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUM7WUFFbkQsSUFBSSxRQUFRLEVBQUUsQ0FBQztnQkFDYixJQUFJLENBQUMsUUFBUSxDQUFDLGNBQWM7b0JBQUUsUUFBUSxDQUFDLEtBQUssQ0FBQyxHQUFHLFFBQVEsQ0FBQyxRQUFRLENBQUM7Z0JBQ2xFLElBQUksQ0FBQyxRQUFRLENBQUMsVUFBVTtvQkFBRSxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsUUFBUSxDQUFDLElBQUksQ0FBQztZQUN4RCxDQUFDO1FBQ0gsQ0FBQztRQUVELE9BQU8sRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLGNBQWMsRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLEtBQUssRUFBRSxDQUFDO0lBQ3RFLENBQUM7SUFFRCxPQUFPO1FBQ0wsUUFBUSxFQUFFLE1BQU0sS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxJQUFJLElBQUk7UUFDdkUsSUFBSSxFQUFFLEtBQUssS0FBSyxTQUFTLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxJQUFJLElBQUk7UUFDakUsY0FBYyxFQUFFLE1BQU0sS0FBSyxTQUFTO1FBQ3BDLFVBQVUsRUFBRSxLQUFLLEtBQUssU0FBUztLQUNoQyxDQUFDO0FBQ0osQ0FBQyxDQUFDO0FBRUYsa0dBQWtHO0FBQ2xHLGlHQUFpRztBQUNqRyxNQUFNLGFBQWEsR0FBRyxDQUNwQixNQUFrQixFQUNsQixLQUFpQixFQUNqQixPQUFpQixFQUNnRSxFQUFFO0lBQ25GLE1BQU0sY0FBYyxHQUE0QixFQUFFLENBQUM7SUFDbkQsTUFBTSxTQUFTLEdBQTRCLEVBQUUsQ0FBQztJQUU5QyxLQUFLLE1BQU0sTUFBTSxJQUFJLE9BQU8sRUFBRSxDQUFDO1FBQzdCLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsS0FBSyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7UUFFbEQsSUFBSSxLQUFLLEVBQUUsQ0FBQztZQUNWLElBQUksQ0FBQyxLQUFLLENBQUMsY0FBYztnQkFBRSxjQUFjLENBQUMsTUFBTSxDQUFDLEdBQUcsS0FBSyxDQUFDLFFBQVEsQ0FBQztZQUNuRSxJQUFJLENBQUMsS0FBSyxDQUFDLFVBQVU7Z0JBQUUsU0FBUyxDQUFDLE1BQU0sQ0FBQyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUM7UUFDeEQsQ0FBQztJQUNILENBQUM7SUFFRCxPQUFPLEVBQUUsY0FBYyxFQUFFLFNBQVMsRUFBRSxDQUFDO0FBQ3ZDLENBQUMsQ0FBQztBQUVXLFFBQUEsUUFBUSxHQUFHLFlBQVksQ0FBQztBQUVyQyxNQUFNLFlBQVksR0FBRyxDQUNuQixNQUErQixFQUMvQixjQUF3QixFQUNDLEVBQUU7SUFDM0IsSUFBSSxDQUFDLGNBQWMsQ0FBQyxNQUFNO1FBQUUsT0FBTyxNQUFNLENBQUM7SUFFMUMsTUFBTSxNQUFNLEdBQUcsRUFBRSxHQUFHLE1BQU0sRUFBRSxDQUFDO0lBQzdCLEtBQUssTUFBTSxLQUFLLElBQUksY0FBYztRQUFFLElBQUksS0FBSyxJQUFJLE1BQU07WUFBRSxNQUFNLENBQUMsS0FBSyxDQUFDLEdBQUcsZ0JBQVEsQ0FBQztJQUVsRixPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUM7QUFFRixNQUFNLFdBQVcsR0FBYyxNQUFNLENBQUMsRUFBRTtJQUN0QyxzQ0FBc0M7SUFDdEMsT0FBTyxDQUFDLElBQUksQ0FBQyxlQUFlLEVBQUUsSUFBSSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDO0FBQ3hELENBQUMsQ0FBQztBQWFGLFNBQWdCLGFBQWEsQ0FDM0IsS0FBNkIsRUFDN0IsSUFBMkIsRUFDM0IsUUFBaUIsRUFDakIsTUFBMEI7SUFFMUIsSUFBSSxLQUFLLEVBQUUsQ0FBQztRQUNWLE9BQU87WUFDTCxLQUFLLENBQUMsa0JBQWtCLENBQUMsT0FBTztnQkFDOUIsSUFBSSxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUM7b0JBQUUsT0FBTyxFQUFFLENBQUM7Z0JBRXBDLElBQUksQ0FBQztvQkFDSCxPQUFPLE1BQU0sS0FBSyxDQUFDLGtCQUFrQixDQUFDLE9BQU8sQ0FBQyxDQUFDO2dCQUNqRCxDQUFDO2dCQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7b0JBQ2Ysb0ZBQW9GO29CQUNwRixzRkFBc0Y7b0JBQ3RGLGlDQUFpQztvQkFDakMsSUFBSSxRQUFRO3dCQUFFLE1BQU0sS0FBSyxDQUFDO29CQUUxQixNQUFNLEVBQUUsQ0FDTixPQUFPLEVBQ1AsNEVBQTRFLEtBQUssRUFBRSxDQUNwRixDQUFDO29CQUVGLE9BQU8sT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQztnQkFDakMsQ0FBQztZQUNILENBQUM7WUFDRCxLQUFLLENBQUMsT0FBTyxDQUFDLEVBQUUsRUFBRSxLQUFLO2dCQUNyQixJQUFJLEVBQUUsS0FBSyxJQUFJO29CQUFFLE9BQU87Z0JBRXhCLElBQUksQ0FBQztvQkFDSCxNQUFNLEtBQUssQ0FBQyxPQUFPLENBQUMsRUFBRSxFQUFFLEtBQUssQ0FBQyxDQUFDO2dCQUNqQyxDQUFDO2dCQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7b0JBQ2YsMEVBQTBFO29CQUMxRSxNQUFNLEVBQUUsQ0FDTixPQUFPLEVBQ1AsbUVBQW1FLEtBQUssRUFBRSxDQUMzRSxDQUFDO2dCQUNKLENBQUM7WUFDSCxDQUFDO1NBQ0YsQ0FBQztJQUNKLENBQUM7SUFFRCxNQUFNLElBQUksR0FBRyxJQUFJLElBQUksV0FBVyxDQUFDO0lBQ2pDLE1BQU0sV0FBVyxHQUFHLElBQUksR0FBRyxFQUE4QixDQUFDO0lBQzFELElBQUksTUFBTSxHQUFHLENBQUMsQ0FBQztJQUVmLE9BQU87UUFDTCxLQUFLLENBQUMsa0JBQWtCLENBQUMsT0FBTztZQUM5QixPQUFPLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQzFCLE1BQU0sRUFBRSxHQUFHLE1BQU0sQ0FBQztnQkFDbEIsTUFBTSxJQUFJLENBQUMsQ0FBQztnQkFDWixXQUFXLENBQUMsR0FBRyxDQUFDLEVBQUUsRUFBRSxNQUFNLENBQUMsQ0FBQztnQkFFNUIsT0FBTyxFQUFFLENBQUM7WUFDWixDQUFDLENBQUMsQ0FBQztRQUNMLENBQUM7UUFDRCxLQUFLLENBQUMsT0FBTyxDQUFDLEVBQUUsRUFBRSxLQUFLO1lBQ3JCLElBQUksRUFBRSxLQUFLLElBQUk7Z0JBQUUsT0FBTztZQUV4QixNQUFNLE9BQU8sR0FBRyxXQUFXLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQ3BDLFdBQVcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUFDLENBQUM7WUFDdkIsSUFBSSxDQUFDLE9BQU87Z0JBQUUsT0FBTztZQUVyQixJQUFJLENBQUM7Z0JBQ0gsTUFBTSxJQUFJLENBQUMsRUFBRSxHQUFHLE9BQU8sRUFBRSxHQUFHLEtBQUssRUFBRSxDQUFDLENBQUM7WUFDdkMsQ0FBQztZQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7Z0JBQ2YsMEVBQTBFO2dCQUMxRSxNQUFNLEVBQUUsQ0FDTixPQUFPLEVBQ1AsbUVBQW1FLEtBQUssRUFBRSxDQUMzRSxDQUFDO1lBQ0osQ0FBQztRQUNILENBQUM7S0FDRixDQUFDO0FBQ0osQ0FBQztBQUVELFNBQVMsb0JBQW9CLENBQzNCLFVBQWdDLEVBQ2hDLFFBQWtCLEVBQ2xCLGNBQXdCLEVBQ3hCLE1BQTBCLEVBQzFCLFFBQWlCO0lBRWpCLE1BQU0sRUFBRSxNQUFNLEVBQUUsR0FBRyxVQUFVLENBQUM7SUFDOUIsTUFBTSxlQUFlLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxFQUFFO1FBQy9ELE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7UUFFbEMsT0FBTyxLQUFLLENBQUMsSUFBSSxLQUFLLFFBQVEsSUFBSSxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUM7SUFDdEQsQ0FBQyxDQUFDLENBQUM7SUFDSCxNQUFNLFdBQVcsR0FBRyxnQ0FBVyxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUN2RCw0RkFBNEY7SUFDNUYsdUNBQXVDO0lBQ3ZDLE1BQU0sY0FBYyxHQUFHLENBQUMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLEdBQUcsV0FBVyxFQUFFLEdBQUcsZUFBZSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQzFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsR0FBRyxVQUFVLENBQUM7SUFFNUIsTUFBTSxXQUFXLEdBQUcsQ0FDbEIsTUFBYyxFQUNkLEtBQTBGLEVBQ3RFLEVBQUUsQ0FBQyxDQUFDO1FBQ3hCLFNBQVMsRUFBRSxJQUFJLElBQUksRUFBRSxDQUFDLFdBQVcsRUFBRTtRQUNuQyxVQUFVLEVBQUUsSUFBSTtRQUNoQixNQUFNLEVBQUUsTUFBTSxDQUFDLEVBQUU7UUFDakIsYUFBYSxFQUFFLE1BQU0sQ0FBQyxTQUFTO1FBQy9CLFlBQVksRUFBRSxNQUFNLENBQUMsUUFBUTtRQUM3QixTQUFTLEVBQUUsTUFBTSxDQUFDLEtBQUs7UUFDdkIsVUFBVSxFQUFFLElBQUk7UUFDaEIsY0FBYyxFQUFFLE1BQU0sQ0FBQyxTQUFTO1FBQ2hDLEdBQUcsS0FBSztRQUNSLGNBQWMsRUFBRSxZQUFZLENBQUMsS0FBSyxDQUFDLGNBQWMsRUFBRSxjQUFjLENBQUM7UUFDbEUsU0FBUyxFQUFFLFlBQVksQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUFFLGNBQWMsQ0FBQztLQUN6RCxDQUFDLENBQUM7SUFFSCxnR0FBZ0c7SUFDaEcsK0ZBQStGO0lBQy9GLGlHQUFpRztJQUNqRywrRkFBK0Y7SUFDL0Ysa0dBQWtHO0lBQ2xHLGlHQUFpRztJQUNqRyw4RUFBOEU7SUFDOUUsTUFBTSxTQUFTLEdBQUcsQ0FBQyxNQUFjLEVBQW1CLEVBQUUsQ0FDcEQsSUFBSSxvQ0FBZSxDQUFDLEVBQUUsR0FBRyxNQUFNLEVBQUUsSUFBSSxFQUFFLElBQUkseUJBQUksQ0FBQyxDQUFDLEVBQUUsNEJBQW9CLEdBQUcsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBRWxGLGdHQUFnRztJQUNoRyxnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBQ2hHLDJGQUEyRjtJQUMzRixxQkFBcUI7SUFDckIsTUFBTSxrQkFBa0IsR0FBRyxDQUFDLFNBQThCLEVBQUUsU0FBa0IsRUFBUSxFQUFFO1FBQ3RGLElBQUksQ0FBQyxTQUFTO1lBQUUsT0FBTztRQUV2QixNQUFNLE9BQU8sR0FDWCwrQkFBK0IsSUFBSSxLQUFLLFNBQVMscUJBQXFCO1lBQ3RFLEdBQUcsNEJBQW9CLDZCQUE2Qiw0QkFBb0Isa0JBQWtCLENBQUM7UUFFN0YsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUNiLE1BQU0sSUFBSSxLQUFLLENBQUMsR0FBRyxPQUFPLHdEQUF3RCxDQUFDLENBQUM7UUFDdEYsQ0FBQztRQUVELE1BQU0sRUFBRSxDQUFDLE1BQU0sRUFBRSxHQUFHLE9BQU8seUJBQXlCLENBQUMsQ0FBQztJQUN4RCxDQUFDLENBQUM7SUFFRixVQUFVLENBQUMsZUFBZSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLE9BQWdDLEVBQUUsRUFBRTtRQUN4RixNQUFNLGNBQWMsR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FDM0MsV0FBVyxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUU7WUFDMUIsU0FBUyxFQUFFLFFBQVE7WUFDbkIsUUFBUSxFQUFFLElBQUk7WUFDZCxjQUFjLEVBQUUsRUFBRTtZQUNsQixTQUFTLEVBQUUsRUFBRTtTQUNkLENBQUMsQ0FDSCxDQUFDO1FBQ0YsTUFBTSxVQUFVLEdBQUcsTUFBTSxRQUFRLENBQUMsa0JBQWtCLENBQUMsY0FBYyxDQUFDLENBQUM7UUFFckUsZ0JBQWdCLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxJQUFJLEVBQUUsVUFBVSxDQUFDLENBQUM7SUFDakQsQ0FBQyxDQUFDLENBQUM7SUFFSCxVQUFVLENBQUMsZUFBZSxDQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLE9BQStCLEVBQUUsRUFBRTtRQUN0RixNQUFNLFVBQVUsR0FBRyxnQkFBZ0IsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUM1RCxnQkFBZ0IsQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBRXRDLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FDZixPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUNwQyxRQUFRLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsSUFBSSxJQUFJLEVBQUU7WUFDMUMsU0FBUyxFQUFFLFFBQVE7WUFDbkIsUUFBUSxFQUFFLGdCQUFnQixDQUFDLE1BQU0sRUFBRSxXQUFXLENBQUM7WUFDL0MsY0FBYyxFQUFFLEVBQUU7WUFDbEIsU0FBUyxFQUFFLFlBQVksQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLGVBQWUsQ0FBQyxFQUFFLGNBQWMsQ0FBQztTQUM5RSxDQUFDLENBQ0gsQ0FDRixDQUFDO0lBQ0osQ0FBQyxDQUFDLENBQUM7SUFFSCxVQUFVLENBQUMsZUFBZSxDQUFDLFFBQVEsRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLE9BQWdDLEVBQUUsRUFBRTtRQUN4RixNQUFNLE9BQU8sR0FBRyxDQUFDLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxJQUFJO1FBQzVDLDZGQUE2RjtRQUM3Riw0RkFBNEY7UUFDNUYsMEJBQTBCO1FBQzFCLFNBQVMsQ0FBQyxPQUFPLENBQUMsTUFBMkIsQ0FBVSxFQUN2RCxjQUF5QixDQUMxQixDQUFpQixDQUFDO1FBRW5CLE1BQU0sU0FBUyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEdBQUcsNEJBQW9CLENBQUM7UUFDeEQsa0JBQWtCLENBQUMsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDO1FBQ3hDLE1BQU0sTUFBTSxHQUFHLFNBQVMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsNEJBQW9CLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDO1FBRTVFLE1BQU0sY0FBYyxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FDekMsV0FBVyxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUU7WUFDMUIsU0FBUyxFQUFFLFFBQVE7WUFDbkIsUUFBUSxFQUFFLGdCQUFnQixDQUFDLE1BQU0sRUFBRSxXQUFXLENBQUM7WUFDL0MsY0FBYyxFQUFFLFdBQVcsQ0FBQyxNQUFNLEVBQUUsZUFBZSxDQUFDO1lBQ3BELFNBQVMsRUFBRSxFQUFFO1NBQ2QsQ0FBQyxDQUNILENBQUM7UUFDRixNQUFNLFVBQVUsR0FBRyxNQUFNLFFBQVEsQ0FBQyxrQkFBa0IsQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUVyRSxZQUFZLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsT0FBTyxDQUFDLEtBQUssRUFBRSxVQUFVLEVBQUUsQ0FBQyxDQUFDO0lBQzdFLENBQUMsQ0FBQyxDQUFDO0lBRUgsVUFBVSxDQUFDLGVBQWUsQ0FBQyxPQUFPLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxPQUFnQyxFQUFFLEVBQUU7UUFDdkYsTUFBTSxRQUFRLEdBQUcsWUFBWSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBRTdELElBQUksQ0FBQyxRQUFRLElBQUksUUFBUSxDQUFDLE1BQU0sQ0FBQyxNQUFNLEtBQUssQ0FBQztZQUFFLE9BQU87UUFFdEQsTUFBTSxFQUFFLE1BQU0sRUFBRSxVQUFVLEVBQUUsR0FBRyxRQUFRLENBQUM7UUFFeEMsMkZBQTJGO1FBQzNGLHNGQUFzRjtRQUN0Rix3RkFBd0Y7UUFDeEYscUVBQXFFO1FBQ3JFLE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDLEVBQUUsR0FBRyxNQUFNLEVBQUUsR0FBRyxPQUFPLENBQUMsS0FBSyxFQUFFLENBQUMsQ0FBQyxDQUFDO1FBQ2hGLE1BQU0sV0FBVyxHQUFHLGVBQWUsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxXQUFXLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUNyRixNQUFNLEtBQUssR0FBRyxDQUFDLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQzFDLEVBQUUsYUFBYSxFQUFFLHlDQUFvQixDQUFDLFFBQVEsQ0FBQyxNQUFNLEVBQUUsV0FBVyxDQUFDLEVBQVcsRUFDOUUsY0FBeUIsQ0FDMUIsQ0FBaUIsQ0FBQztRQUNuQixNQUFNLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUMsQ0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLEVBQUUsV0FBVyxDQUFDLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDO1FBRWhHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FDZixNQUFNLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxFQUFFO1lBQzNCLE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLENBQUMsZUFBZSxDQUFDLEtBQUssQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLENBQUM7WUFDckYsTUFBTSxTQUFTLEdBQUcsVUFBVSxDQUFDLEtBQUssQ0FBQyxJQUFJLElBQUksQ0FBQztZQUU1QyxJQUFJLENBQUMsT0FBTztnQkFBRSxPQUFPLFNBQVMsQ0FBQztZQUUvQixNQUFNLEVBQUUsY0FBYyxFQUFFLFNBQVMsRUFBRSxHQUFHLGFBQWEsQ0FBQyxNQUFNLEVBQUUsT0FBTyxFQUFFLGVBQWUsQ0FBQyxDQUFDO1lBRXRGLDBGQUEwRjtZQUMxRixtRkFBbUY7WUFDbkYsd0ZBQXdGO1lBQ3hGLHFGQUFxRjtZQUNyRixPQUFPLFFBQVEsQ0FBQyxPQUFPLENBQUMsU0FBUyxFQUFFO2dCQUNqQyxTQUFTLEVBQUUsUUFBUTtnQkFDbkIsUUFBUSxFQUFFLGdCQUFnQixDQUFDLE9BQU8sRUFBRSxXQUFXLENBQUM7Z0JBQ2hELGNBQWMsRUFBRSxZQUFZLENBQUMsY0FBYyxFQUFFLGNBQWMsQ0FBQztnQkFDNUQsU0FBUyxFQUFFLFlBQVksQ0FBQyxTQUFTLEVBQUUsY0FBYyxDQUFDO2FBQ25ELENBQUMsQ0FBQztRQUNMLENBQUMsQ0FBQyxDQUNILENBQUM7SUFDSixDQUFDLENBQUMsQ0FBQztJQUVILFVBQVUsQ0FBQyxlQUFlLENBQUMsUUFBUSxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsT0FBZ0MsRUFBRSxFQUFFO1FBQ3hGLE1BQU0sT0FBTyxHQUFHLENBQUMsTUFBTSxPQUFPLENBQUMsVUFBVSxDQUFDLElBQUk7UUFDNUMsNkZBQTZGO1FBQzdGLDRGQUE0RjtRQUM1RiwwQkFBMEI7UUFDMUIsU0FBUyxDQUFDLE9BQU8sQ0FBQyxNQUEyQixDQUFVLEVBQ3ZELGNBQXlCLENBQzFCLENBQWlCLENBQUM7UUFFbkIsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLE1BQU0sR0FBRyw0QkFBb0IsQ0FBQztRQUN4RCxrQkFBa0IsQ0FBQyxRQUFRLEVBQUUsU0FBUyxDQUFDLENBQUM7UUFDeEMsTUFBTSxNQUFNLEdBQUcsU0FBUyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSw0QkFBb0IsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFFNUUsTUFBTSxjQUFjLEdBQUcsTUFBTSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxDQUN6QyxXQUFXLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRTtZQUMxQixTQUFTLEVBQUUsUUFBUTtZQUNuQixRQUFRLEVBQUUsZ0JBQWdCLENBQUMsTUFBTSxFQUFFLFdBQVcsQ0FBQztZQUMvQyxjQUFjLEVBQUUsV0FBVyxDQUFDLE1BQU0sRUFBRSxlQUFlLENBQUM7WUFDcEQsU0FBUyxFQUFFLEVBQUU7U0FDZCxDQUFDLENBQ0gsQ0FBQztRQUNGLE1BQU0sVUFBVSxHQUFHLE1BQU0sUUFBUSxDQUFDLGtCQUFrQixDQUFDLGNBQWMsQ0FBQyxDQUFDO1FBRXJFLFlBQVksQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxDQUFDLENBQUM7SUFDdkQsQ0FBQyxDQUFDLENBQUM7SUFFSCxVQUFVLENBQUMsZUFBZSxDQUFDLE9BQU8sRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLE9BQWdDLEVBQUUsRUFBRTtRQUN2RixNQUFNLFFBQVEsR0FBRyxZQUFZLENBQUMsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzlDLElBQUksQ0FBQyxRQUFRO1lBQUUsT0FBTztRQUV0QixNQUFNLEVBQUUsTUFBTSxFQUFFLFVBQVUsRUFBRSxHQUFHLFFBQVEsQ0FBQztRQUV4QyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQ2YsTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsRUFBRSxDQUMzQixRQUFRLENBQUMsT0FBTyxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsSUFBSSxJQUFJLEVBQUU7WUFDMUMsU0FBUyxFQUFFLFFBQVE7WUFDbkIsUUFBUSxFQUFFLGdCQUFnQixDQUFDLE1BQU0sRUFBRSxXQUFXLENBQUM7WUFDL0MsY0FBYyxFQUFFLFlBQVksQ0FBQyxXQUFXLENBQUMsTUFBTSxFQUFFLGVBQWUsQ0FBQyxFQUFFLGNBQWMsQ0FBQztZQUNsRixTQUFTLEVBQUUsRUFBRTtTQUNkLENBQUMsQ0FDSCxDQUNGLENBQUM7SUFDSixDQUFDLENBQUMsQ0FBQztBQUNMLENBQUM7QUFFRCxTQUF3QixzQkFBc0IsQ0FDNUMsb0JBQTBDLEVBQzFDLHFCQUFrRCxFQUNsRCxPQUFxQztJQUVyQyxNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxNQUFNLEVBQUUsUUFBUSxHQUFHLEtBQUssRUFBRSxNQUFNLEVBQUUsR0FBRyxPQUFPLElBQUksRUFBRSxDQUFDO0lBQ3hFLE1BQU0sUUFBUSxHQUFHLGFBQWEsQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxNQUFNLENBQUMsQ0FBQztJQUU5RCxLQUFLLE1BQU0sVUFBVSxJQUFJLG9CQUFvQixDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQzFELDRGQUE0RjtRQUM1Riw0RkFBNEY7UUFDNUYseUNBQXlDO1FBQ3pDLElBQUksZ0NBQVcsQ0FBQyxjQUFjLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sS0FBSyxDQUFDLEVBQUUsQ0FBQztZQUMvRCxNQUFNLEVBQUUsQ0FDTixNQUFNLEVBQ04sd0NBQXdDLFVBQVUsQ0FBQyxJQUFJLDRCQUE0QixDQUNwRixDQUFDO1FBQ0osQ0FBQzthQUFNLENBQUM7WUFDTixvQkFBb0IsQ0FBQyxVQUFVLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLEVBQUUsTUFBTSxFQUFFLFFBQVEsQ0FBQyxDQUFDO1FBQ2hHLENBQUM7SUFDSCxDQUFDO0lBRUQsaUZBQWlGO0lBQ2pGLE9BQU8sS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLENBQUM7QUFDekIsQ0FBQyJ9
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { QueryInterface, Sequelize, Transaction } from 'sequelize';
|
|
2
|
+
import { Umzug } from 'umzug';
|
|
3
|
+
export type MigrationContext = {
|
|
4
|
+
queryInterface: QueryInterface;
|
|
5
|
+
schema?: string;
|
|
6
|
+
tableName: string;
|
|
7
|
+
transaction?: Transaction;
|
|
8
|
+
};
|
|
9
|
+
export declare function buildUmzug(sequelize: Sequelize, options: {
|
|
10
|
+
schema?: string;
|
|
11
|
+
tableName: string;
|
|
12
|
+
transaction?: Transaction;
|
|
13
|
+
}): Umzug<MigrationContext>;
|
|
14
|
+
export declare function isSchemaAlreadyExistsError(error: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Idempotent CREATE SCHEMA. Must run (and commit) on its own before Umzug, because Umzug's storage
|
|
17
|
+
* opens its own connection to create the meta table and would not see a CREATE SCHEMA still
|
|
18
|
+
* pending in a transaction. Not covered by the advisory lock; the existence check + tolerated
|
|
19
|
+
* "already exists" error makes it safe under concurrent boots instead.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensureSchema(sequelize: Sequelize, schema: string | undefined): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Creates the schema then applies every pending audit-table migration. On Postgres the migrations
|
|
24
|
+
* run inside a transaction-scoped advisory lock, and every DDL statement runs on that same
|
|
25
|
+
* transaction, so concurrent agent boots serialize on the DDL and a failure partway through a
|
|
26
|
+
* migration rolls back cleanly instead of leaving partial DDL behind. Other dialects have no
|
|
27
|
+
* equivalent distributed lock; each migration instead checks what already exists before creating
|
|
28
|
+
* it, which closes most — but not all — of the race window on a truly simultaneous boot.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runAuditMigrations(sequelize: Sequelize, options: {
|
|
31
|
+
schema?: string;
|
|
32
|
+
tableName: string;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=migrations.d.ts.map
|