@stamhoofd/backend 2.55.1 → 2.56.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 +11 -10
- package/src/crons.ts +4 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +150 -0
- 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 +9 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -9
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +5 -3
- 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 +681 -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/sql-filters/audit-logs.ts +10 -0
- package/src/sql-sorters/audit-logs.ts +35 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { ArrayDecoder, AutoEncoder, AutoEncoderPatchType, BooleanDecoder, DateDecoder, EnumDecoder, Field, IntegerDecoder, isPatchableArray, PatchableArray, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { AuditLog, Group, Member, Registration } from '@stamhoofd/models';
|
|
3
|
+
import { Address, AuditLogPatchItem, AuditLogReplacement, AuditLogReplacementType, AuditLogType, BooleanStatus, FinancialSupportSettings, MemberDetails, Parent, ParentTypeHelper, Platform, PlatformConfig } from '@stamhoofd/structures';
|
|
4
|
+
import { Context } from '../helpers/Context';
|
|
5
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
6
|
+
|
|
7
|
+
export type MemberAddedAuditOptions = {
|
|
8
|
+
type: AuditLogType.MemberAdded;
|
|
9
|
+
member: Member;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MemberEditedAuditOptions = {
|
|
13
|
+
type: AuditLogType.MemberEdited;
|
|
14
|
+
member: Member;
|
|
15
|
+
oldMemberDetails: MemberDetails;
|
|
16
|
+
memberDetailsPatch: AutoEncoderPatchType<MemberDetails>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type MemberRegisteredAuditOptions = {
|
|
20
|
+
type: AuditLogType.MemberRegistered | AuditLogType.MemberUnregistered;
|
|
21
|
+
member: Member;
|
|
22
|
+
group: Group;
|
|
23
|
+
registration: Registration;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type PlatformConfigChangeAuditOptions = {
|
|
27
|
+
type: AuditLogType.PlatformSettingChanged;
|
|
28
|
+
oldConfig: PlatformConfig;
|
|
29
|
+
patch: PlatformConfig | AutoEncoderPatchType<PlatformConfig>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AuditLogOptions = MemberAddedAuditOptions | MemberEditedAuditOptions | MemberRegisteredAuditOptions | PlatformConfigChangeAuditOptions;
|
|
33
|
+
|
|
34
|
+
export const AuditLogService = {
|
|
35
|
+
async log(options: AuditLogOptions) {
|
|
36
|
+
const userId = Context.optionalAuth?.user?.id ?? null;
|
|
37
|
+
const organizationId = Context.organization?.id ?? null;
|
|
38
|
+
|
|
39
|
+
const model = new AuditLog();
|
|
40
|
+
|
|
41
|
+
model.type = options.type;
|
|
42
|
+
model.userId = userId;
|
|
43
|
+
model.organizationId = organizationId;
|
|
44
|
+
|
|
45
|
+
if (options.type === AuditLogType.MemberRegistered) {
|
|
46
|
+
this.fillForMemberRegistered(model, options);
|
|
47
|
+
}
|
|
48
|
+
else if (options.type === AuditLogType.MemberUnregistered) {
|
|
49
|
+
this.fillForMemberRegistered(model, options);
|
|
50
|
+
}
|
|
51
|
+
else if (options.type === AuditLogType.MemberEdited) {
|
|
52
|
+
this.fillForMemberEdited(model, options);
|
|
53
|
+
}
|
|
54
|
+
else if (options.type === AuditLogType.MemberAdded) {
|
|
55
|
+
this.fillForMemberAdded(model, options);
|
|
56
|
+
}
|
|
57
|
+
else if (options.type === AuditLogType.PlatformSettingChanged) {
|
|
58
|
+
this.fillForPlatformConfig(model, options);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// In the future we might group these saves together in one query to improve performance
|
|
62
|
+
await model.save();
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
fillForMemberRegistered(model: AuditLog, options: MemberRegisteredAuditOptions) {
|
|
66
|
+
model.objectId = options.member.id;
|
|
67
|
+
model.replacements = new Map([
|
|
68
|
+
['m', AuditLogReplacement.create({
|
|
69
|
+
id: options.member.id,
|
|
70
|
+
value: options.member.details.name,
|
|
71
|
+
type: AuditLogReplacementType.Member,
|
|
72
|
+
})],
|
|
73
|
+
['g', AuditLogReplacement.create({
|
|
74
|
+
id: options.group.id,
|
|
75
|
+
value: options.group.settings.name,
|
|
76
|
+
type: AuditLogReplacementType.Group,
|
|
77
|
+
})],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const registrationStructure = options.registration.setRelation(Registration.group, options.group).getStructure();
|
|
81
|
+
if (registrationStructure.description) {
|
|
82
|
+
model.description = registrationStructure.description;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
fillForMemberEdited(model: AuditLog, options: MemberEditedAuditOptions) {
|
|
87
|
+
model.objectId = options.member.id;
|
|
88
|
+
|
|
89
|
+
model.replacements = new Map([
|
|
90
|
+
['m', AuditLogReplacement.create({
|
|
91
|
+
id: options.member.id,
|
|
92
|
+
value: options.member.details.name,
|
|
93
|
+
type: AuditLogReplacementType.Member,
|
|
94
|
+
})],
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Generate changes list
|
|
98
|
+
model.patchList = explainPatch(options.oldMemberDetails, options.memberDetailsPatch);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
fillForMemberAdded(model: AuditLog, options: MemberAddedAuditOptions) {
|
|
102
|
+
model.objectId = options.member.id;
|
|
103
|
+
|
|
104
|
+
model.replacements = new Map([
|
|
105
|
+
['m', AuditLogReplacement.create({
|
|
106
|
+
id: options.member.id,
|
|
107
|
+
value: options.member.details.name,
|
|
108
|
+
type: AuditLogReplacementType.Member,
|
|
109
|
+
})],
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
// Generate changes list
|
|
113
|
+
model.patchList = explainPatch(null, options.member.details);
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
fillForPlatformConfig(model: AuditLog, options: PlatformConfigChangeAuditOptions) {
|
|
117
|
+
model.objectId = null;
|
|
118
|
+
|
|
119
|
+
let word = 'Platforminstellingen';
|
|
120
|
+
let c = 2;
|
|
121
|
+
let id: string | null = null;
|
|
122
|
+
|
|
123
|
+
const changedProps = Object.keys(options.patch).filter((prop) => {
|
|
124
|
+
return !prop.startsWith('_') && options.patch[prop] && (!(options.patch[prop] instanceof PatchableArray) || options.patch[prop].changes.length > 0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (changedProps.length === 1) {
|
|
128
|
+
const prop = changedProps[0] as keyof PlatformConfig;
|
|
129
|
+
id = prop;
|
|
130
|
+
|
|
131
|
+
switch (prop) {
|
|
132
|
+
case 'financialSupport':
|
|
133
|
+
word = options.oldConfig.financialSupport?.title || FinancialSupportSettings.defaultTitle;
|
|
134
|
+
c = 2;
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'dataPermission':
|
|
138
|
+
word = 'De instellingen voor toestemming gegevensverzameling';
|
|
139
|
+
c = 2;
|
|
140
|
+
break;
|
|
141
|
+
|
|
142
|
+
case 'tags':
|
|
143
|
+
word = 'De hierarchie';
|
|
144
|
+
c = 1;
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'premiseTypes':
|
|
148
|
+
word = 'De soorten lokalen';
|
|
149
|
+
c = 2;
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case 'recordsConfiguration':
|
|
153
|
+
word = 'De persoonsgegevens van leden';
|
|
154
|
+
c = 2;
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'defaultAgeGroups':
|
|
158
|
+
word = 'De standaard leeftijdsgroepen';
|
|
159
|
+
c = 2;
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'responsibilities':
|
|
163
|
+
word = 'De functies van leden';
|
|
164
|
+
c = 2;
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'membershipTypes':
|
|
168
|
+
word = 'De aansluitingen en verzekeringen';
|
|
169
|
+
c = 2;
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case 'eventTypes':
|
|
173
|
+
word = 'De soorten activiteiten';
|
|
174
|
+
c = 2;
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case 'featureFlags':
|
|
178
|
+
word = 'Feature flags';
|
|
179
|
+
c = 2;
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'coverPhoto':
|
|
183
|
+
word = 'De omslagfoto';
|
|
184
|
+
c = 2;
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'expandLogo':
|
|
188
|
+
case 'squareLogo':
|
|
189
|
+
case 'horizontalLogo':
|
|
190
|
+
word = 'Het logo';
|
|
191
|
+
c = 1;
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'squareLogoDark':
|
|
195
|
+
case 'horizontalLogoDark':
|
|
196
|
+
word = 'Het logo in donkere modus';
|
|
197
|
+
c = 1;
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'logoDocuments':
|
|
201
|
+
word = 'Het logo op documenten';
|
|
202
|
+
c = 1;
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case 'privacy':
|
|
206
|
+
word = 'Privacyinstellingen';
|
|
207
|
+
c = 2;
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case 'color':
|
|
211
|
+
word = 'De huisstijkleur';
|
|
212
|
+
c = 1;
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case 'name':
|
|
216
|
+
word = 'De naam van het platform';
|
|
217
|
+
c = 1;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('changedProps', changedProps, options.patch);
|
|
223
|
+
|
|
224
|
+
model.replacements = new Map([
|
|
225
|
+
['o', AuditLogReplacement.create({
|
|
226
|
+
id: id ?? undefined,
|
|
227
|
+
value: word,
|
|
228
|
+
count: c,
|
|
229
|
+
})],
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
// Generate changes list
|
|
233
|
+
model.patchList = explainPatch(options.oldConfig, options.patch);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export type PatchExplainer = {
|
|
238
|
+
key: string;
|
|
239
|
+
handler: (oldValue: unknown, value: unknown) => AuditLogPatchItem[];
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function createStringChangeHandler(key: string) {
|
|
243
|
+
return (oldValue: unknown, value: unknown) => {
|
|
244
|
+
if ((typeof oldValue !== 'string' && oldValue !== null) || (typeof value !== 'string' && value !== null)) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
if (oldValue === value) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
return [
|
|
251
|
+
AuditLogPatchItem.create({
|
|
252
|
+
key: key,
|
|
253
|
+
oldValue: oldValue ?? undefined,
|
|
254
|
+
value: value ?? undefined,
|
|
255
|
+
}),
|
|
256
|
+
];
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function createIntegerChangeHandler(key: string) {
|
|
261
|
+
return (oldValue: unknown, value: unknown) => {
|
|
262
|
+
if ((typeof oldValue !== 'number' && oldValue !== null) || (typeof value !== 'number' && value !== null)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
if (oldValue === value) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
return [
|
|
269
|
+
AuditLogPatchItem.create({
|
|
270
|
+
key: key,
|
|
271
|
+
oldValue: oldValue !== null ? Formatter.integer(oldValue) : undefined,
|
|
272
|
+
value: value !== null ? Formatter.integer(value) : undefined,
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function createDateChangeHandler(key: string) {
|
|
279
|
+
return (oldValue: unknown, value: unknown) => {
|
|
280
|
+
if ((!(oldValue instanceof Date) && oldValue !== null) || (!(value instanceof Date)) && value !== null) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (oldValue?.getTime() === value?.getTime()) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
AuditLogPatchItem.create({
|
|
289
|
+
key: key,
|
|
290
|
+
oldValue: oldValue ? Formatter.dateNumber(oldValue, true) : undefined,
|
|
291
|
+
value: value ? Formatter.dateNumber(value, true) : undefined,
|
|
292
|
+
}),
|
|
293
|
+
];
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createBooleanChangeHandler(key: string) {
|
|
298
|
+
return (oldValue: unknown, value: unknown) => {
|
|
299
|
+
if (typeof oldValue !== 'boolean' && oldValue !== null) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (typeof value !== 'boolean' && value !== null) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (oldValue === value) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return [
|
|
312
|
+
AuditLogPatchItem.create({
|
|
313
|
+
key: key,
|
|
314
|
+
oldValue: oldValue === true ? 'Aangevinkt' : (oldValue === false ? 'Uitgevinkt' : undefined),
|
|
315
|
+
value: value === true ? 'Aangevinkt' : (value === false ? 'Uitgevinkt' : undefined),
|
|
316
|
+
}),
|
|
317
|
+
];
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getAutoEncoderName(autoEncoder: unknown) {
|
|
322
|
+
if (typeof autoEncoder === 'string') {
|
|
323
|
+
return autoEncoder;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (autoEncoder instanceof Parent) {
|
|
327
|
+
return autoEncoder.name + ` (${ParentTypeHelper.getName(autoEncoder.type)})`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (autoEncoder instanceof Address) {
|
|
331
|
+
return autoEncoder.shortString();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
|
|
335
|
+
return autoEncoder.name;
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function createArrayChangeHandler(key: string) {
|
|
341
|
+
return (oldValue: unknown, value: unknown) => {
|
|
342
|
+
if (!isPatchableArray(value)) {
|
|
343
|
+
// Not supported
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(oldValue)) {
|
|
347
|
+
// Not supported
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const items: AuditLogPatchItem[] = [];
|
|
352
|
+
const createdIdSet = new Set<string>();
|
|
353
|
+
|
|
354
|
+
const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
355
|
+
|
|
356
|
+
for (const { put } of value.getPuts()) {
|
|
357
|
+
if (!(put instanceof AutoEncoder)) {
|
|
358
|
+
// Not supported
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Little hack: detect PUT/DELETE behaviour:
|
|
363
|
+
let original = 'id' in put ? oldValue.find(v => v.id === put.id) : null;
|
|
364
|
+
if (original && !(original instanceof AutoEncoder)) {
|
|
365
|
+
// Not supported
|
|
366
|
+
original = null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Added a new parent
|
|
370
|
+
if (!original) {
|
|
371
|
+
items.push(
|
|
372
|
+
AuditLogPatchItem.create({
|
|
373
|
+
key: keySingular,
|
|
374
|
+
value: getAutoEncoderName(put) || keySingular,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if ('id' in put && typeof put.id === 'string') {
|
|
380
|
+
createdIdSet.add(put.id);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
items.push(
|
|
384
|
+
...explainPatch(
|
|
385
|
+
original ?? null,
|
|
386
|
+
put,
|
|
387
|
+
).map((i) => {
|
|
388
|
+
i.name = getAutoEncoderName(put) || keySingular;
|
|
389
|
+
return i;
|
|
390
|
+
}),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
for (const patch of value.getPatches()) {
|
|
395
|
+
const original = oldValue.find(v => v.id === patch.id);
|
|
396
|
+
if (!original) {
|
|
397
|
+
// Not supported
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!(original instanceof AutoEncoder)) {
|
|
401
|
+
// Not supported
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
items.push(
|
|
406
|
+
...explainPatch(
|
|
407
|
+
original,
|
|
408
|
+
patch,
|
|
409
|
+
).map((i) => {
|
|
410
|
+
i.name = getAutoEncoderName(original) || keySingular;
|
|
411
|
+
return i;
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
for (const id of value.getDeletes()) {
|
|
417
|
+
if (typeof id !== 'string') {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const original = oldValue.find(v => v.id === id);
|
|
421
|
+
if (!original) {
|
|
422
|
+
// Not supported
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!(original instanceof AutoEncoder)) {
|
|
426
|
+
// Not supported
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (createdIdSet.has(id)) {
|
|
431
|
+
// DELETE + PUT happened
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
items.push(
|
|
436
|
+
AuditLogPatchItem.create({
|
|
437
|
+
key: keySingular,
|
|
438
|
+
value: undefined,
|
|
439
|
+
oldValue: getAutoEncoderName(original) || keySingular,
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (value.getMoves().length > 0) {
|
|
445
|
+
items.push(
|
|
446
|
+
AuditLogPatchItem.create({
|
|
447
|
+
key: '_order',
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return items;
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function createSimpleArrayChangeHandler(key: string) {
|
|
456
|
+
return (oldValue: unknown, value: unknown) => {
|
|
457
|
+
if (!Array.isArray(oldValue)) {
|
|
458
|
+
// Not supported
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
|
|
462
|
+
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
if (!value.every(v => typeof v === 'string')) {
|
|
465
|
+
// Not supported
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
if (!oldValue.every(v => typeof v === 'string')) {
|
|
469
|
+
// Not supported
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Simple change
|
|
474
|
+
const valueStr = (value as string[]).join(', ');
|
|
475
|
+
const oldValueStr = (oldValue as string[]).join(', ');
|
|
476
|
+
|
|
477
|
+
if (valueStr === oldValueStr) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return [
|
|
482
|
+
AuditLogPatchItem.create({
|
|
483
|
+
key: keySingular,
|
|
484
|
+
oldValue: oldValue.length ? oldValueStr : undefined,
|
|
485
|
+
value: value.length ? valueStr : undefined,
|
|
486
|
+
}),
|
|
487
|
+
];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!isPatchableArray(value)) {
|
|
491
|
+
// Not supported
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const items: AuditLogPatchItem[] = [];
|
|
496
|
+
const createdIdSet = new Set<string>();
|
|
497
|
+
|
|
498
|
+
for (const { put } of value.getPuts()) {
|
|
499
|
+
if (typeof put !== 'string') {
|
|
500
|
+
// Not supported
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Little hack: detect PUT/DELETE behaviour:
|
|
505
|
+
const original = oldValue.find(v => v === put);
|
|
506
|
+
|
|
507
|
+
// Added a new parent
|
|
508
|
+
if (!original) {
|
|
509
|
+
items.push(
|
|
510
|
+
AuditLogPatchItem.create({
|
|
511
|
+
key: keySingular,
|
|
512
|
+
value: put,
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
createdIdSet.add(put);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const id of value.getDeletes()) {
|
|
520
|
+
if (typeof id !== 'string') {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (createdIdSet.has(id)) {
|
|
525
|
+
// DELETE + PUT happened
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const original = oldValue.find(v => v === id);
|
|
530
|
+
if (!original || typeof original !== 'string') {
|
|
531
|
+
// Not supported
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
items.push(
|
|
536
|
+
AuditLogPatchItem.create({
|
|
537
|
+
key: keySingular,
|
|
538
|
+
value: undefined,
|
|
539
|
+
oldValue: original,
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (value.getMoves().length > 0) {
|
|
545
|
+
items.push(
|
|
546
|
+
AuditLogPatchItem.create({
|
|
547
|
+
key: '_order',
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
return items;
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getExplainerForField(field: Field<any>) {
|
|
556
|
+
if (field.decoder === StringDecoder || field.decoder instanceof EnumDecoder) {
|
|
557
|
+
return createStringChangeHandler(field.property);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (field.decoder === DateDecoder) {
|
|
561
|
+
return createDateChangeHandler(field.property);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (field.decoder === BooleanDecoder) {
|
|
565
|
+
return createBooleanChangeHandler(field.property);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (field.decoder === IntegerDecoder) {
|
|
569
|
+
return createIntegerChangeHandler(field.property);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (field.decoder instanceof ArrayDecoder && field.decoder.decoder === StringDecoder) {
|
|
573
|
+
return createSimpleArrayChangeHandler(field.property);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (field.decoder instanceof ArrayDecoder) {
|
|
577
|
+
return createArrayChangeHandler(field.property);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (field.decoder === BooleanStatus) {
|
|
581
|
+
return (oldValue: unknown, value: unknown) => {
|
|
582
|
+
if (value === undefined) {
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const wasTrueOld = oldValue instanceof BooleanStatus ? oldValue.value : null;
|
|
587
|
+
const isTrue = value instanceof BooleanStatus ? value.value : null;
|
|
588
|
+
|
|
589
|
+
if (wasTrueOld === isTrue) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return [
|
|
594
|
+
AuditLogPatchItem.create({
|
|
595
|
+
key: field.property,
|
|
596
|
+
oldValue: wasTrueOld === true ? 'Aangevinkt' : (wasTrueOld === false ? 'Uitgevinkt' : undefined),
|
|
597
|
+
value: isTrue === true ? 'Aangevinkt' : (isTrue === false ? 'Uitgevinkt' : undefined),
|
|
598
|
+
}),
|
|
599
|
+
];
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if ((field.decoder as any).prototype instanceof AutoEncoder || field.decoder === AutoEncoder) {
|
|
604
|
+
return (oldValue: unknown, value: unknown) => {
|
|
605
|
+
if (!(value instanceof AutoEncoder) && value !== null) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (oldValue === value) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!oldValue && getAutoEncoderName(value as AutoEncoder)) {
|
|
614
|
+
// Simplify addition
|
|
615
|
+
return [
|
|
616
|
+
AuditLogPatchItem.create({
|
|
617
|
+
key: field.property,
|
|
618
|
+
value: getAutoEncoderName(value as AutoEncoder) || field.property,
|
|
619
|
+
}),
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (value === null) {
|
|
624
|
+
return [
|
|
625
|
+
AuditLogPatchItem.create({
|
|
626
|
+
key: field.property,
|
|
627
|
+
oldValue: getAutoEncoderName(oldValue as AutoEncoder) || field.property,
|
|
628
|
+
}),
|
|
629
|
+
];
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return explainPatch(oldValue as AutoEncoder | null, value).map((i) => {
|
|
633
|
+
i.key = field.property + '.' + i.key;
|
|
634
|
+
return i;
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Simple addition/delete/change detection
|
|
640
|
+
return (oldValue: unknown, value: unknown) => {
|
|
641
|
+
if (value === undefined) {
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (oldValue === value) {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return [
|
|
650
|
+
AuditLogPatchItem.create({
|
|
651
|
+
key: field.property,
|
|
652
|
+
}),
|
|
653
|
+
];
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function explainPatch<T extends AutoEncoder>(original: T | null, patch: AutoEncoderPatchType<T> | T): AuditLogPatchItem[] {
|
|
658
|
+
const items: AuditLogPatchItem[] = [];
|
|
659
|
+
|
|
660
|
+
for (const key in patch) {
|
|
661
|
+
const field = original ? original.static.fields.find(f => f.property === key) : patch.static.fields.find(f => f.property === key);
|
|
662
|
+
if (!field) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const oldValue = original?.[key] ?? null;
|
|
667
|
+
const value = patch[key];
|
|
668
|
+
|
|
669
|
+
if (patch.isPut() && key === 'id') {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const handler = getExplainerForField(field);
|
|
674
|
+
if (!handler) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
items.push(...handler(oldValue, value));
|
|
679
|
+
}
|
|
680
|
+
return items;
|
|
681
|
+
}
|