@stamhoofd/backend 2.55.2 → 2.57.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/index.ts +4 -0
- package/package.json +12 -11
- package/src/crons.ts +4 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +150 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +27 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +27 -9
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +17 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -9
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +21 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +4 -306
- package/src/helpers/AdminPermissionChecker.ts +102 -1
- package/src/helpers/AuthenticatedStructures.ts +46 -2
- package/src/helpers/EmailResumer.ts +8 -3
- package/src/seeds/1732117645-move-rrn.ts +77 -0
- package/src/services/AuditLogService.ts +232 -0
- package/src/services/BalanceItemPaymentService.ts +45 -0
- package/src/services/BalanceItemService.ts +88 -0
- package/src/services/GroupService.ts +13 -0
- package/src/services/PaymentService.ts +308 -0
- package/src/services/RegistrationService.ts +78 -0
- package/src/services/explainPatch.ts +639 -0
- package/src/sql-filters/audit-logs.ts +10 -0
- package/src/sql-sorters/audit-logs.ts +35 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import { ArrayDecoder, AutoEncoder, AutoEncoderPatchType, BooleanDecoder, DateDecoder, EnumDecoder, Field, IntegerDecoder, isPatchable, isPatchableArray, isPatchMap, MapDecoder, StringDecoder, SymbolDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { Address, AuditLogPatchItem, AuditLogPatchItemType, AuditLogReplacement, AuditLogReplacementType, BooleanStatus, Image, Parent, ParentTypeHelper, RichText } from '@stamhoofd/structures';
|
|
3
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
4
|
+
|
|
5
|
+
export type PatchExplainer = {
|
|
6
|
+
key: string;
|
|
7
|
+
handler: (oldValue: unknown, value: unknown) => AuditLogPatchItem[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function createStringChangeHandler(key: string) {
|
|
11
|
+
return (oldValue: unknown, value: unknown) => {
|
|
12
|
+
if (oldValue === value) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
// Not altered
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
AuditLogPatchItem.create({
|
|
23
|
+
key: AuditLogReplacement.key(key),
|
|
24
|
+
oldValue: typeof oldValue === 'string' ? AuditLogReplacement.string(oldValue) : undefined,
|
|
25
|
+
value: typeof value === 'string' ? AuditLogReplacement.string(value) : undefined,
|
|
26
|
+
}).autoType(),
|
|
27
|
+
];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createIntegerChangeHandler(key: string) {
|
|
32
|
+
return (oldValue: unknown, value: unknown) => {
|
|
33
|
+
if ((typeof oldValue !== 'number' && oldValue !== null) || (typeof value !== 'number' && value !== null)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
if (oldValue === value) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const formatter: (typeof Formatter.price | typeof Formatter.integer) = key.toLowerCase().includes('price') ? Formatter.price.bind(Formatter) : Formatter.integer.bind(Formatter);
|
|
41
|
+
return [
|
|
42
|
+
AuditLogPatchItem.create({
|
|
43
|
+
key: AuditLogReplacement.key(key),
|
|
44
|
+
oldValue: oldValue !== null ? AuditLogReplacement.string(formatter(oldValue)) : undefined,
|
|
45
|
+
value: value !== null ? AuditLogReplacement.string(formatter(value)) : undefined,
|
|
46
|
+
}).autoType(),
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createDateChangeHandler(key: string) {
|
|
52
|
+
return (oldValue: unknown, value: unknown) => {
|
|
53
|
+
if ((!(oldValue instanceof Date) && oldValue !== null) || (!(value instanceof Date)) && value !== null) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (oldValue?.getTime() === value?.getTime()) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
let dno = oldValue ? Formatter.dateNumber(oldValue, true) : undefined;
|
|
61
|
+
let dn = value ? Formatter.dateNumber(value, true) : undefined;
|
|
62
|
+
|
|
63
|
+
if (dno && dn && (dno === dn || (Formatter.time(oldValue!) !== Formatter.time(value!)))) {
|
|
64
|
+
// Add time
|
|
65
|
+
dno += ' ' + Formatter.time(oldValue!);
|
|
66
|
+
dn += ' ' + Formatter.time(value!);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
AuditLogPatchItem.create({
|
|
71
|
+
key: AuditLogReplacement.key(key),
|
|
72
|
+
oldValue: dno ? AuditLogReplacement.string(dno) : undefined,
|
|
73
|
+
value: dn ? AuditLogReplacement.string(dn) : undefined,
|
|
74
|
+
}).autoType(),
|
|
75
|
+
];
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createBooleanChangeHandler(key: string) {
|
|
80
|
+
return (oldValue: unknown, value: unknown) => {
|
|
81
|
+
if (typeof oldValue !== 'boolean' && oldValue !== null) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof value !== 'boolean' && value !== null) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (oldValue === value) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [
|
|
94
|
+
AuditLogPatchItem.create({
|
|
95
|
+
key: AuditLogReplacement.key(key),
|
|
96
|
+
oldValue: oldValue === true ? AuditLogReplacement.string('Aan') : (oldValue === false ? AuditLogReplacement.string('Uit') : undefined),
|
|
97
|
+
value: value === true ? AuditLogReplacement.string('Aan') : (value === false ? AuditLogReplacement.string('Uit') : undefined),
|
|
98
|
+
}).autoType(),
|
|
99
|
+
];
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getAutoEncoderName(autoEncoder: unknown) {
|
|
104
|
+
if (typeof autoEncoder === 'string') {
|
|
105
|
+
return autoEncoder;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (autoEncoder instanceof Parent) {
|
|
109
|
+
return autoEncoder.name + ` (${ParentTypeHelper.getName(autoEncoder.type)})`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (autoEncoder instanceof Address) {
|
|
113
|
+
return autoEncoder.shortString();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
|
|
117
|
+
return autoEncoder.name;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getAutoEncoderValue(autoEncoder: unknown): AuditLogReplacement | null {
|
|
123
|
+
if (typeof autoEncoder === 'string') {
|
|
124
|
+
return AuditLogReplacement.string(autoEncoder);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (autoEncoder instanceof Parent) {
|
|
128
|
+
return AuditLogReplacement.string(autoEncoder.name + ` (${ParentTypeHelper.getName(autoEncoder.type)})`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (autoEncoder instanceof Address) {
|
|
132
|
+
return AuditLogReplacement.string(autoEncoder.shortString());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
|
|
136
|
+
return AuditLogReplacement.string(autoEncoder.name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (autoEncoder instanceof Image) {
|
|
140
|
+
return AuditLogReplacement.create({
|
|
141
|
+
id: autoEncoder.getPathForSize(undefined, undefined),
|
|
142
|
+
value: autoEncoder.source.name ?? undefined,
|
|
143
|
+
type: AuditLogReplacementType.Image,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (autoEncoder instanceof RichText) {
|
|
148
|
+
return AuditLogReplacement.string(autoEncoder.text);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createArrayChangeHandler(key: string) {
|
|
154
|
+
return (oldValue: unknown, value: unknown) => {
|
|
155
|
+
if (!isPatchableArray(value)) {
|
|
156
|
+
// Not supported
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(oldValue)) {
|
|
160
|
+
// Not supported
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const items: AuditLogPatchItem[] = [];
|
|
165
|
+
const createdIdSet = new Set<string>();
|
|
166
|
+
|
|
167
|
+
const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
168
|
+
|
|
169
|
+
for (const { put } of value.getPuts()) {
|
|
170
|
+
if (!(put instanceof AutoEncoder)) {
|
|
171
|
+
// Not supported
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Little hack: detect PUT/DELETE behaviour:
|
|
176
|
+
let original = 'id' in put ? oldValue.find(v => v.id === put.id) : null;
|
|
177
|
+
if (original && !(original instanceof AutoEncoder)) {
|
|
178
|
+
// Not supported
|
|
179
|
+
original = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Added a new parent
|
|
183
|
+
if (!original) {
|
|
184
|
+
items.push(
|
|
185
|
+
AuditLogPatchItem.create({
|
|
186
|
+
key: AuditLogReplacement.key(keySingular),
|
|
187
|
+
value: getAutoEncoderValue(put) || AuditLogReplacement.string(keySingular),
|
|
188
|
+
type: AuditLogPatchItemType.Added,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if ('id' in put && typeof put.id === 'string') {
|
|
194
|
+
createdIdSet.add(put.id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
items.push(
|
|
198
|
+
...explainPatch(
|
|
199
|
+
original ?? null,
|
|
200
|
+
put,
|
|
201
|
+
).map((i) => {
|
|
202
|
+
const name = getAutoEncoderName(put);
|
|
203
|
+
if (name) {
|
|
204
|
+
i.key = i.key.prepend(AuditLogReplacement.string(name));
|
|
205
|
+
}
|
|
206
|
+
i.key = i.key.prepend(AuditLogReplacement.key(key));
|
|
207
|
+
return i;
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const patch of value.getPatches()) {
|
|
213
|
+
const original = oldValue.find(v => v.id === patch.id);
|
|
214
|
+
if (!original) {
|
|
215
|
+
// Not supported
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!(original instanceof AutoEncoder)) {
|
|
219
|
+
// Not supported
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const l = explainPatch(
|
|
224
|
+
original,
|
|
225
|
+
patch,
|
|
226
|
+
).map((i) => {
|
|
227
|
+
const name = getAutoEncoderName(original);
|
|
228
|
+
if (name) {
|
|
229
|
+
i.key = i.key.prepend(AuditLogReplacement.string(name));
|
|
230
|
+
}
|
|
231
|
+
i.key = i.key.prepend(AuditLogReplacement.key(key));
|
|
232
|
+
return i;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (l.length === 0) {
|
|
236
|
+
items.push(
|
|
237
|
+
AuditLogPatchItem.create({
|
|
238
|
+
key: AuditLogReplacement.key(keySingular),
|
|
239
|
+
value: getAutoEncoderValue(original) || undefined,
|
|
240
|
+
type: AuditLogPatchItemType.Changed,
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
items.push(
|
|
246
|
+
...l,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const id of value.getDeletes()) {
|
|
251
|
+
if (typeof id !== 'string') {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const original = oldValue.find(v => v.id === id);
|
|
255
|
+
if (!original) {
|
|
256
|
+
// Not supported
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!(original instanceof AutoEncoder)) {
|
|
260
|
+
// Not supported
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (createdIdSet.has(id)) {
|
|
265
|
+
// DELETE + PUT happened
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let k = AuditLogReplacement.key(keySingular);
|
|
270
|
+
|
|
271
|
+
const name = getAutoEncoderName(original);
|
|
272
|
+
if (name) {
|
|
273
|
+
k = k.prepend(AuditLogReplacement.string(name));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
items.push(
|
|
277
|
+
AuditLogPatchItem.create({
|
|
278
|
+
key: k,
|
|
279
|
+
type: AuditLogPatchItemType.Removed,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (value.getMoves().length > 0) {
|
|
285
|
+
items.push(
|
|
286
|
+
AuditLogPatchItem.create({
|
|
287
|
+
key: AuditLogReplacement.key(key),
|
|
288
|
+
type: AuditLogPatchItemType.Reordered,
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return items;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function createMapChangeHandler(key: string) {
|
|
297
|
+
return (oldValue: unknown, value: unknown) => {
|
|
298
|
+
if (!isPatchMap(value)) {
|
|
299
|
+
// Not supported
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
if (!(oldValue instanceof Map)) {
|
|
303
|
+
// Not supported
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const items: AuditLogPatchItem[] = [];
|
|
308
|
+
const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
309
|
+
|
|
310
|
+
for (const [k, v] of value.entries()) {
|
|
311
|
+
if (typeof k !== 'string') {
|
|
312
|
+
// Not supported
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const original = oldValue.get(k);
|
|
316
|
+
|
|
317
|
+
if (v === null) {
|
|
318
|
+
// Delete
|
|
319
|
+
if (original) {
|
|
320
|
+
items.push(
|
|
321
|
+
AuditLogPatchItem.create({
|
|
322
|
+
key: AuditLogReplacement.key(keySingular),
|
|
323
|
+
oldValue: getAutoEncoderValue(original as AutoEncoder) || AuditLogReplacement.key(k),
|
|
324
|
+
type: AuditLogPatchItemType.Removed,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!original) {
|
|
332
|
+
// added
|
|
333
|
+
items.push(
|
|
334
|
+
AuditLogPatchItem.create({
|
|
335
|
+
key: AuditLogReplacement.key(keySingular),
|
|
336
|
+
value: getAutoEncoderValue(v as AutoEncoder) || AuditLogReplacement.key(k),
|
|
337
|
+
type: AuditLogPatchItemType.Added,
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const c = explainPatch(
|
|
343
|
+
original,
|
|
344
|
+
v as AutoEncoder,
|
|
345
|
+
).map((i) => {
|
|
346
|
+
const name = getAutoEncoderValue(original as AutoEncoder);
|
|
347
|
+
if (name) {
|
|
348
|
+
i.key = i.key.prepend(name);
|
|
349
|
+
}
|
|
350
|
+
i.key = i.key.prepend(AuditLogReplacement.key(keySingular));
|
|
351
|
+
return i;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (c.length === 0) {
|
|
355
|
+
// Manual log
|
|
356
|
+
items.push(
|
|
357
|
+
AuditLogPatchItem.create({
|
|
358
|
+
key: AuditLogReplacement.key(keySingular).append(getAutoEncoderValue(original as AutoEncoder) || AuditLogReplacement.key(k)),
|
|
359
|
+
type: AuditLogPatchItemType.Changed,
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Changed
|
|
365
|
+
items.push(
|
|
366
|
+
...c,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return items;
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createSimpleArrayChangeHandler(key: string) {
|
|
376
|
+
return (oldValue: unknown, value: unknown) => {
|
|
377
|
+
if (!Array.isArray(oldValue)) {
|
|
378
|
+
// Not supported
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
382
|
+
|
|
383
|
+
if (Array.isArray(value)) {
|
|
384
|
+
if (!value.every(v => typeof v === 'string')) {
|
|
385
|
+
// Not supported
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
if (!oldValue.every(v => typeof v === 'string')) {
|
|
389
|
+
// Not supported
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Simple change
|
|
394
|
+
const valueStr = (value as string[]).join(', ');
|
|
395
|
+
const oldValueStr = (oldValue as string[]).join(', ');
|
|
396
|
+
|
|
397
|
+
if (valueStr === oldValueStr) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return [
|
|
402
|
+
AuditLogPatchItem.create({
|
|
403
|
+
key: AuditLogReplacement.key(keySingular),
|
|
404
|
+
oldValue: oldValue.length ? AuditLogReplacement.string(oldValueStr) : undefined,
|
|
405
|
+
value: value.length ? AuditLogReplacement.string(valueStr) : undefined,
|
|
406
|
+
}).autoType(),
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!isPatchableArray(value)) {
|
|
411
|
+
// Not supported
|
|
412
|
+
return [];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const items: AuditLogPatchItem[] = [];
|
|
416
|
+
const createdIdSet = new Set<string>();
|
|
417
|
+
|
|
418
|
+
for (const { put } of value.getPuts()) {
|
|
419
|
+
if (typeof put !== 'string') {
|
|
420
|
+
// Not supported
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Little hack: detect PUT/DELETE behaviour:
|
|
425
|
+
const original = oldValue.find(v => v === put);
|
|
426
|
+
|
|
427
|
+
// Added a new parent
|
|
428
|
+
if (!original) {
|
|
429
|
+
items.push(
|
|
430
|
+
AuditLogPatchItem.create({
|
|
431
|
+
key: AuditLogReplacement.key(keySingular),
|
|
432
|
+
value: AuditLogReplacement.string(put),
|
|
433
|
+
type: AuditLogPatchItemType.Added,
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
createdIdSet.add(put);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const id of value.getDeletes()) {
|
|
441
|
+
if (typeof id !== 'string') {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (createdIdSet.has(id)) {
|
|
446
|
+
// DELETE + PUT happened
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const original = oldValue.find(v => v === id);
|
|
451
|
+
if (!original || typeof original !== 'string') {
|
|
452
|
+
// Not supported
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
items.push(
|
|
457
|
+
AuditLogPatchItem.create({
|
|
458
|
+
key: AuditLogReplacement.key(keySingular),
|
|
459
|
+
oldValue: AuditLogReplacement.string(original),
|
|
460
|
+
type: AuditLogPatchItemType.Removed,
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (value.getMoves().length > 0) {
|
|
466
|
+
items.push(
|
|
467
|
+
AuditLogPatchItem.create({
|
|
468
|
+
key: AuditLogReplacement.key(key),
|
|
469
|
+
type: AuditLogPatchItemType.Reordered,
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return items;
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getExplainerForField(field: Field<any>) {
|
|
478
|
+
if (field.decoder === StringDecoder || field.decoder instanceof EnumDecoder) {
|
|
479
|
+
return createStringChangeHandler(field.property);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (field.decoder instanceof SymbolDecoder) {
|
|
483
|
+
if (field.decoder.decoder === StringDecoder || field.decoder.decoder instanceof EnumDecoder) {
|
|
484
|
+
return createStringChangeHandler(field.property);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (field.decoder === DateDecoder) {
|
|
489
|
+
return createDateChangeHandler(field.property);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (field.decoder === BooleanDecoder) {
|
|
493
|
+
return createBooleanChangeHandler(field.property);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (field.decoder === IntegerDecoder) {
|
|
497
|
+
return createIntegerChangeHandler(field.property);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (field.decoder instanceof ArrayDecoder && field.decoder.decoder === StringDecoder) {
|
|
501
|
+
return createSimpleArrayChangeHandler(field.property);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (field.decoder instanceof ArrayDecoder) {
|
|
505
|
+
return createArrayChangeHandler(field.property);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (field.decoder instanceof MapDecoder) {
|
|
509
|
+
return createMapChangeHandler(field.property);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (field.decoder === BooleanStatus) {
|
|
513
|
+
return (oldValue: unknown, value: unknown) => {
|
|
514
|
+
if (value === undefined) {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const wasTrueOld = oldValue instanceof BooleanStatus ? oldValue.value : null;
|
|
519
|
+
const isTrue = value instanceof BooleanStatus ? value.value : null;
|
|
520
|
+
|
|
521
|
+
if (wasTrueOld === isTrue) {
|
|
522
|
+
return [];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return [
|
|
526
|
+
AuditLogPatchItem.create({
|
|
527
|
+
key: AuditLogReplacement.key(field.property),
|
|
528
|
+
oldValue: wasTrueOld === true ? AuditLogReplacement.string('Aangevinkt') : (wasTrueOld === false ? AuditLogReplacement.string('Uitgevinkt') : undefined),
|
|
529
|
+
value: isTrue === true ? AuditLogReplacement.string('Aangevinkt') : (isTrue === false ? AuditLogReplacement.string('Uitgevinkt') : undefined),
|
|
530
|
+
}),
|
|
531
|
+
];
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if ((field.decoder as any).prototype instanceof AutoEncoder || field.decoder === AutoEncoder) {
|
|
536
|
+
return (oldValue: unknown, value: unknown) => {
|
|
537
|
+
if (!(value instanceof AutoEncoder) && value !== null) {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (oldValue === value) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (oldValue && value && getAutoEncoderValue(value as AutoEncoder)) {
|
|
546
|
+
// Simplify addition
|
|
547
|
+
return [
|
|
548
|
+
AuditLogPatchItem.create({
|
|
549
|
+
key: AuditLogReplacement.key(field.property),
|
|
550
|
+
value: getAutoEncoderValue(value as AutoEncoder) || AuditLogReplacement.key(field.property),
|
|
551
|
+
oldValue: getAutoEncoderValue(oldValue as AutoEncoder) || AuditLogReplacement.key(field.property),
|
|
552
|
+
type: AuditLogPatchItemType.Changed,
|
|
553
|
+
}),
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (!oldValue && getAutoEncoderValue(value as AutoEncoder)) {
|
|
558
|
+
// Simplify addition
|
|
559
|
+
return [
|
|
560
|
+
AuditLogPatchItem.create({
|
|
561
|
+
key: AuditLogReplacement.key(field.property),
|
|
562
|
+
value: getAutoEncoderValue(value as AutoEncoder) || AuditLogReplacement.key(field.property),
|
|
563
|
+
type: AuditLogPatchItemType.Added,
|
|
564
|
+
}),
|
|
565
|
+
];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (value === null) {
|
|
569
|
+
return [
|
|
570
|
+
AuditLogPatchItem.create({
|
|
571
|
+
key: AuditLogReplacement.key(field.property),
|
|
572
|
+
oldValue: getAutoEncoderValue(oldValue as AutoEncoder) || AuditLogReplacement.key(field.property),
|
|
573
|
+
type: AuditLogPatchItemType.Removed,
|
|
574
|
+
}),
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return explainPatch(oldValue as AutoEncoder | null, value).map((i) => {
|
|
579
|
+
i.key = i.key.prepend(AuditLogReplacement.key(field.property));
|
|
580
|
+
return i;
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Simple addition/delete/change detection
|
|
586
|
+
return (oldValue: unknown, value: unknown) => {
|
|
587
|
+
if (value === undefined) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (oldValue === value) {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return [
|
|
596
|
+
AuditLogPatchItem.create({
|
|
597
|
+
key: AuditLogReplacement.key(field.property),
|
|
598
|
+
type: AuditLogPatchItemType.Changed,
|
|
599
|
+
}),
|
|
600
|
+
];
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function explainPatch<T extends AutoEncoder>(original: T | null, patch: AutoEncoderPatchType<T> | T): AuditLogPatchItem[] {
|
|
605
|
+
if (isPatchableArray(patch)) {
|
|
606
|
+
const b = createArrayChangeHandler('items');
|
|
607
|
+
return b(original, patch);
|
|
608
|
+
}
|
|
609
|
+
if (!(patch instanceof AutoEncoder)) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
if (original && !(original instanceof AutoEncoder)) {
|
|
613
|
+
return [];
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const items: AuditLogPatchItem[] = [];
|
|
617
|
+
|
|
618
|
+
for (const key in patch) {
|
|
619
|
+
const field = original ? original.static.fields.find(f => f.property === key) : patch.static.fields.find(f => f.property === key);
|
|
620
|
+
if (!field) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const oldValue = original?.[key] ?? null;
|
|
625
|
+
const value = patch[key];
|
|
626
|
+
|
|
627
|
+
if (patch.isPut() && key === 'id') {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const handler = getExplainerForField(field);
|
|
632
|
+
if (!handler) {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
items.push(...handler(oldValue, value));
|
|
637
|
+
}
|
|
638
|
+
return items;
|
|
639
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQLFilterDefinitions } from '@stamhoofd/sql';
|
|
2
|
+
|
|
3
|
+
export const auditLogFilterCompilers: SQLFilterDefinitions = {
|
|
4
|
+
...baseSQLFilterCompilers,
|
|
5
|
+
id: createSQLColumnFilterCompiler('id'),
|
|
6
|
+
organizationId: createSQLColumnFilterCompiler('organizationId'),
|
|
7
|
+
type: createSQLColumnFilterCompiler('type'),
|
|
8
|
+
objectId: createSQLColumnFilterCompiler('objectId'),
|
|
9
|
+
createdAt: createSQLColumnFilterCompiler('createdAt'),
|
|
10
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuditLog } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
|
|
4
|
+
export const auditLogSorters: SQLSortDefinitions<AuditLog> = {
|
|
5
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
6
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
7
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
8
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
9
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
10
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
11
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
12
|
+
|
|
13
|
+
id: {
|
|
14
|
+
getValue(a) {
|
|
15
|
+
return a.id;
|
|
16
|
+
},
|
|
17
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
18
|
+
return new SQLOrderBy({
|
|
19
|
+
column: SQL.column('id'),
|
|
20
|
+
direction,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
createdAt: {
|
|
25
|
+
getValue(a) {
|
|
26
|
+
return a.createdAt;
|
|
27
|
+
},
|
|
28
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
29
|
+
return new SQLOrderBy({
|
|
30
|
+
column: SQL.column('createdAt'),
|
|
31
|
+
direction,
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|