@stamhoofd/backend 2.57.1 → 2.59.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.
Files changed (48) hide show
  1. package/index.ts +6 -1
  2. package/package.json +13 -13
  3. package/src/audit-logs/EventLogger.ts +30 -0
  4. package/src/audit-logs/GroupLogger.ts +95 -0
  5. package/src/audit-logs/MemberLogger.ts +24 -0
  6. package/src/audit-logs/MemberPlatformMembershipLogger.ts +57 -0
  7. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
  8. package/src/audit-logs/ModelLogger.ts +218 -0
  9. package/src/audit-logs/OrderLogger.ts +57 -0
  10. package/src/audit-logs/OrganizationLogger.ts +26 -0
  11. package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
  12. package/src/audit-logs/PaymentLogger.ts +43 -0
  13. package/src/audit-logs/PlatformLogger.ts +13 -0
  14. package/src/audit-logs/RegistrationLogger.ts +53 -0
  15. package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
  16. package/src/audit-logs/StripeAccountLogger.ts +47 -0
  17. package/src/audit-logs/WebshopLogger.ts +35 -0
  18. package/src/crons.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
  20. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +4 -18
  21. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -3
  22. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -18
  23. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +5 -2
  25. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
  26. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +5 -15
  27. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +18 -28
  28. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +2 -1
  29. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -1
  30. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +6 -3
  31. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
  32. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
  33. package/src/helpers/AuthenticatedStructures.ts +16 -1
  34. package/src/helpers/Context.ts +8 -2
  35. package/src/helpers/MemberUserSyncer.ts +45 -40
  36. package/src/helpers/PeriodHelper.ts +31 -27
  37. package/src/helpers/TagHelper.ts +23 -20
  38. package/src/seeds/1722344162-update-membership.ts +2 -2
  39. package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
  40. package/src/services/AuditLogService.ts +89 -216
  41. package/src/services/BalanceItemPaymentService.ts +1 -1
  42. package/src/services/BalanceItemService.ts +14 -5
  43. package/src/services/MemberNumberService.ts +120 -0
  44. package/src/services/PaymentService.ts +199 -193
  45. package/src/services/PlatformMembershipService.ts +284 -0
  46. package/src/services/RegistrationService.ts +76 -27
  47. package/src/services/explainPatch.ts +528 -316
  48. package/src/helpers/MembershipHelper.ts +0 -54
@@ -1,6 +1,7 @@
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';
1
+ import { ArrayDecoder, AutoEncoder, BooleanDecoder, DateDecoder, EnumDecoder, Field, getOptionalId, IntegerDecoder, isPatchableArray, isPatchMap, MapDecoder, StringDecoder, SymbolDecoder } from '@simonbackx/simple-encoding';
2
+ import { AuditLogPatchItem, AuditLogPatchItemType, AuditLogReplacement, AuditLogReplacementType, BooleanStatus, Image, isEmptyFilter, isUuid, PropertyFilter, RichText, Version } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
+ import { get } from 'http';
4
5
 
5
6
  export type PatchExplainer = {
6
7
  key: string;
@@ -20,14 +21,34 @@ function createStringChangeHandler(key: string) {
20
21
 
21
22
  return [
22
23
  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,
24
+ key: getAutoEncoderKey(key),
25
+ oldValue: getAutoEncoderValue(oldValue, key) || getAutoEncoderName(oldValue) || undefined,
26
+ value: getAutoEncoderValue(value, key) || getAutoEncoderName(value) || undefined,
26
27
  }).autoType(),
27
28
  ];
28
29
  };
29
30
  }
30
31
 
32
+ function createEnumChangeHandler(key: string) {
33
+ return (oldValue: unknown, value: unknown) => {
34
+ if (oldValue === value) {
35
+ return [];
36
+ }
37
+
38
+ if (value === undefined) {
39
+ // Not altered
40
+ return [];
41
+ }
42
+
43
+ return [
44
+ AuditLogPatchItem.create({
45
+ key: getAutoEncoderKey(key),
46
+ oldValue: typeof oldValue === 'string' ? AuditLogReplacement.key(oldValue) : undefined,
47
+ value: typeof value === 'string' ? AuditLogReplacement.key(value) : undefined,
48
+ }).autoType(),
49
+ ];
50
+ };
51
+ }
31
52
  function createIntegerChangeHandler(key: string) {
32
53
  return (oldValue: unknown, value: unknown) => {
33
54
  if ((typeof oldValue !== 'number' && oldValue !== null) || (typeof value !== 'number' && value !== null)) {
@@ -37,12 +58,11 @@ function createIntegerChangeHandler(key: string) {
37
58
  return [];
38
59
  }
39
60
 
40
- const formatter: (typeof Formatter.price | typeof Formatter.integer) = key.toLowerCase().includes('price') ? Formatter.price.bind(Formatter) : Formatter.integer.bind(Formatter);
41
61
  return [
42
62
  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,
63
+ key: getAutoEncoderKey(key),
64
+ oldValue: oldValue !== null ? (getAutoEncoderValue(oldValue, key) || undefined) : undefined,
65
+ value: value !== null ? (getAutoEncoderValue(value, key) || undefined) : undefined,
46
66
  }).autoType(),
47
67
  ];
48
68
  };
@@ -50,7 +70,11 @@ function createIntegerChangeHandler(key: string) {
50
70
 
51
71
  function createDateChangeHandler(key: string) {
52
72
  return (oldValue: unknown, value: unknown) => {
53
- if ((!(oldValue instanceof Date) && oldValue !== null) || (!(value instanceof Date)) && value !== null) {
73
+ if (!(oldValue instanceof Date) && oldValue !== null) {
74
+ return [];
75
+ }
76
+
77
+ if ((!(value instanceof Date)) && value !== null) {
54
78
  return [];
55
79
  }
56
80
 
@@ -60,7 +84,7 @@ function createDateChangeHandler(key: string) {
60
84
  let dno = oldValue ? Formatter.dateNumber(oldValue, true) : undefined;
61
85
  let dn = value ? Formatter.dateNumber(value, true) : undefined;
62
86
 
63
- if (dno && dn && (dno === dn || (Formatter.time(oldValue!) !== Formatter.time(value!)))) {
87
+ if (dno && dn && (dno === dn || (Formatter.time(oldValue!) !== Formatter.time(value!))) && key !== 'birthDay') {
64
88
  // Add time
65
89
  dno += ' ' + Formatter.time(oldValue!);
66
90
  dn += ' ' + Formatter.time(value!);
@@ -68,7 +92,7 @@ function createDateChangeHandler(key: string) {
68
92
 
69
93
  return [
70
94
  AuditLogPatchItem.create({
71
- key: AuditLogReplacement.key(key),
95
+ key: getAutoEncoderKey(key),
72
96
  oldValue: dno ? AuditLogReplacement.string(dno) : undefined,
73
97
  value: dn ? AuditLogReplacement.string(dn) : undefined,
74
98
  }).autoType(),
@@ -92,48 +116,98 @@ function createBooleanChangeHandler(key: string) {
92
116
 
93
117
  return [
94
118
  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),
119
+ key: getAutoEncoderKey(key),
120
+ oldValue: oldValue === true ? AuditLogReplacement.key('on') : (oldValue === false ? AuditLogReplacement.key('off') : undefined),
121
+ value: value === true ? AuditLogReplacement.key('on') : (value === false ? AuditLogReplacement.key('off') : undefined),
98
122
  }).autoType(),
99
123
  ];
100
124
  };
101
125
  }
102
126
 
103
- function getAutoEncoderName(autoEncoder: unknown) {
127
+ function getAutoEncoderKey(autoEncoder: string): AuditLogReplacement;
128
+ function getAutoEncoderKey(autoEncoder: unknown): AuditLogReplacement | null;
129
+ function getAutoEncoderKey(autoEncoder: unknown): AuditLogReplacement | null {
104
130
  if (typeof autoEncoder === 'string') {
105
- return autoEncoder;
131
+ if (isUuid(autoEncoder)) {
132
+ return AuditLogReplacement.uuid(autoEncoder);
133
+ }
134
+ return AuditLogReplacement.key(autoEncoder);
106
135
  }
136
+ return null;
137
+ }
107
138
 
108
- if (autoEncoder instanceof Parent) {
109
- return autoEncoder.name + ` (${ParentTypeHelper.getName(autoEncoder.type)})`;
139
+ function getAutoEncoderName(autoEncoder: unknown): AuditLogReplacement | null {
140
+ if (typeof autoEncoder === 'string') {
141
+ if (isUuid(autoEncoder)) {
142
+ return AuditLogReplacement.uuid(autoEncoder);
143
+ }
144
+ return AuditLogReplacement.string(autoEncoder);
110
145
  }
111
146
 
112
- if (autoEncoder instanceof Address) {
113
- return autoEncoder.shortString();
147
+ if (typeof autoEncoder === 'object' && autoEncoder !== null && 'getPatchName' in autoEncoder && typeof autoEncoder.getPatchName === 'function') {
148
+ const name = autoEncoder.getPatchName();
149
+ if (typeof name === 'string') {
150
+ return name ? AuditLogReplacement.string(name) : AuditLogReplacement.key('untitled');
151
+ }
114
152
  }
115
153
 
116
154
  if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
117
- return autoEncoder.name;
155
+ return autoEncoder.name ? AuditLogReplacement.string(autoEncoder.name) : AuditLogReplacement.key('untitled');
118
156
  }
119
157
  return null;
120
158
  }
159
+ function getAutoEncoderPutValue(autoEncoder: unknown, key?: string): AuditLogReplacement | null {
160
+ if (typeof autoEncoder === 'object' && autoEncoder !== null && 'getPutValue' in autoEncoder && typeof autoEncoder.getPutValue === 'function') {
161
+ const name = autoEncoder.getPutValue();
162
+ if (typeof name === 'string') {
163
+ return AuditLogReplacement.string(name);
164
+ }
165
+ if (name instanceof AuditLogReplacement) {
166
+ return name;
167
+ }
168
+ }
169
+ return getAutoEncoderValue(autoEncoder, key);
170
+ }
121
171
 
122
- function getAutoEncoderValue(autoEncoder: unknown): AuditLogReplacement | null {
172
+ function getAutoEncoderValue(autoEncoder: unknown, key?: string): AuditLogReplacement | null {
123
173
  if (typeof autoEncoder === 'string') {
174
+ if (isUuid(autoEncoder)) {
175
+ return AuditLogReplacement.uuid(autoEncoder);
176
+ }
177
+ if (key && key === 'status') {
178
+ // Will be an enum
179
+ return AuditLogReplacement.key(autoEncoder);
180
+ }
124
181
  return AuditLogReplacement.string(autoEncoder);
125
182
  }
126
183
 
127
- if (autoEncoder instanceof Parent) {
128
- return AuditLogReplacement.string(autoEncoder.name + ` (${ParentTypeHelper.getName(autoEncoder.type)})`);
184
+ if (typeof autoEncoder === 'symbol') {
185
+ const name = Symbol.keyFor(autoEncoder);
186
+ if (name) {
187
+ return AuditLogReplacement.key(name);
188
+ }
189
+ return AuditLogReplacement.key('unknown');
190
+ }
191
+
192
+ if (typeof autoEncoder === 'number') {
193
+ if (key && (key.toLowerCase().includes('price') || key.toLowerCase().includes('fee'))) {
194
+ return AuditLogReplacement.string(Formatter.price(autoEncoder));
195
+ }
196
+ return AuditLogReplacement.string(Formatter.integer(autoEncoder));
129
197
  }
130
198
 
131
- if (autoEncoder instanceof Address) {
132
- return AuditLogReplacement.string(autoEncoder.shortString());
199
+ if (autoEncoder instanceof Date) {
200
+ return AuditLogReplacement.string(Formatter.dateTime(autoEncoder, true, true));
133
201
  }
134
202
 
135
- if (typeof autoEncoder === 'object' && autoEncoder !== null && 'name' in autoEncoder && typeof autoEncoder.name === 'string') {
136
- return AuditLogReplacement.string(autoEncoder.name);
203
+ if (typeof autoEncoder === 'object' && autoEncoder !== null && 'getPatchValue' in autoEncoder && typeof autoEncoder.getPatchValue === 'function') {
204
+ const name = autoEncoder.getPatchValue();
205
+ if (typeof name === 'string') {
206
+ return AuditLogReplacement.string(name);
207
+ }
208
+ if (name instanceof AuditLogReplacement) {
209
+ return name;
210
+ }
137
211
  }
138
212
 
139
213
  if (autoEncoder instanceof Image) {
@@ -147,144 +221,270 @@ function getAutoEncoderValue(autoEncoder: unknown): AuditLogReplacement | null {
147
221
  if (autoEncoder instanceof RichText) {
148
222
  return AuditLogReplacement.string(autoEncoder.text);
149
223
  }
224
+
225
+ if (autoEncoder instanceof PropertyFilter) {
226
+ if (autoEncoder.isAlwaysEnabledAndRequired) {
227
+ return AuditLogReplacement.key('alwaysEnabledAndRequired');
228
+ }
229
+ if (autoEncoder.enabledWhen === null && autoEncoder.requiredWhen === null) {
230
+ return AuditLogReplacement.key('alwaysEnabledAndOptional');
231
+ }
232
+ if (autoEncoder.enabledWhen !== null && autoEncoder.requiredWhen === null) {
233
+ return AuditLogReplacement.key('sometimesEnabledAndOptional');
234
+ }
235
+ if (autoEncoder.enabledWhen === null && autoEncoder.requiredWhen !== null) {
236
+ return AuditLogReplacement.key('alwaysEnabledAndSometimesRequired');
237
+ }
238
+ if (autoEncoder.enabledWhen !== null && isEmptyFilter(autoEncoder.requiredWhen)) {
239
+ return AuditLogReplacement.key('sometimesEnabledAndRequired');
240
+ }
241
+ return AuditLogReplacement.key('sometimesEnabledAndSometimesRequired');
242
+ }
243
+
150
244
  return null;
151
245
  }
152
246
 
153
- function createArrayChangeHandler(key: string) {
154
- return (oldValue: unknown, value: unknown) => {
155
- if (!isPatchableArray(value)) {
156
- // Not supported
157
- return [];
247
+ function getKeySingular(key: string) {
248
+ return key.replace(/ies$/, 'y').replace(/s$/, '');
249
+ }
250
+
251
+ function findOriginalById(id: unknown, oldArray: unknown[]): unknown | null {
252
+ return id ? oldArray.find(v => getId(v) === id) : null;
253
+ }
254
+
255
+ function findOriginalIndexById(id: unknown, oldArray: unknown[]): number {
256
+ return id ? oldArray.findIndex(v => getId(v) === id) : -1;
257
+ }
258
+
259
+ function getId(object: unknown): string | number | null {
260
+ const id = getOptionalId(object);
261
+ if (typeof id !== 'string' && typeof id !== 'number') {
262
+ if (object instanceof AutoEncoder) {
263
+ const encoded = object.encode({ version: Version });
264
+ return JSON.stringify(encoded);
158
265
  }
159
- if (!Array.isArray(oldValue)) {
160
- // Not supported
161
- return [];
266
+ return JSON.stringify(object);
267
+ }
268
+ return id;
269
+ }
270
+
271
+ function findOriginal(put: unknown, oldArray: unknown[]): unknown | null {
272
+ return findOriginalById(getId(put), oldArray);
273
+ }
274
+
275
+ function findIndex(put: unknown, oldArray: unknown[]): number {
276
+ return findOriginalIndexById(getId(put), oldArray);
277
+ }
278
+
279
+ function processPut(key: string, put: unknown, original: unknown | null, createdIdSet?: Set<string>): AuditLogPatchItem[] {
280
+ const items: AuditLogPatchItem[] = [];
281
+ const keySingular = getKeySingular(key);
282
+ const v = getAutoEncoderPutValue(put);
283
+
284
+ // Added a new parent
285
+ if (!original) {
286
+ const n = getAutoEncoderName(put);
287
+ items.push(
288
+ AuditLogPatchItem.create({
289
+ key: AuditLogReplacement.key(keySingular).append(n),
290
+ value: (n?.toString() !== v?.toString()) ? (v || undefined) : undefined,
291
+ type: AuditLogPatchItemType.Added,
292
+ }),
293
+ );
294
+ }
295
+
296
+ // Little hack: detect PUT/DELETE behaviour:
297
+ if (createdIdSet) {
298
+ const id = getId(put);
299
+ if (id && typeof id === 'string') {
300
+ createdIdSet.add(id);
162
301
  }
302
+ }
163
303
 
164
- const items: AuditLogPatchItem[] = [];
165
- const createdIdSet = new Set<string>();
304
+ if (!original && (v || getAutoEncoderName(put))) {
305
+ // Simplify addition: don't show all added properties
306
+ return items;
307
+ }
166
308
 
167
- const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
309
+ items.push(
310
+ ...explainPatch(
311
+ original ?? null,
312
+ put,
313
+ ).map((i) => {
314
+ i.key = i.key.prepend(getAutoEncoderName(original) || getAutoEncoderName(put) || AuditLogReplacement.key('item'));
315
+ i.key = i.key.prepend(getAutoEncoderKey(key));
316
+ return i;
317
+ }),
318
+ );
319
+ return items;
320
+ }
168
321
 
169
- for (const { put } of value.getPuts()) {
170
- if (!(put instanceof AutoEncoder)) {
171
- // Not supported
172
- continue;
173
- }
322
+ function processPatch(key: string, patch: unknown, original: unknown | null): AuditLogPatchItem[] {
323
+ if (!original) {
324
+ // Not supported
325
+ return [];
326
+ }
174
327
 
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
- }
328
+ if (patch === original) {
329
+ return [];
330
+ }
181
331
 
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
- }
332
+ const items: AuditLogPatchItem[] = [];
333
+ const keySingular = getKeySingular(key);
334
+
335
+ const l = explainPatch(
336
+ original,
337
+ patch,
338
+ ).map((i) => {
339
+ i.key = i.key.prepend(getAutoEncoderName(original) || getAutoEncoderName(patch) || AuditLogReplacement.key('item'));
340
+ i.key = i.key.prepend(getAutoEncoderKey(key));
341
+ return i;
342
+ });
343
+ let ov = getAutoEncoderValue(original);
344
+ let v = getAutoEncoderValue(patch);
345
+
346
+ if (l.length === 0 && patch instanceof AutoEncoder && patch.isPatch()) {
347
+ items.push(
348
+ AuditLogPatchItem.create({
349
+ key: getAutoEncoderKey(keySingular).append(getAutoEncoderName(original) || getAutoEncoderName(patch) || AuditLogReplacement.key('item')),
350
+ oldValue: ov || undefined,
351
+ value: v || undefined,
352
+ type: AuditLogPatchItemType.Changed,
353
+ }),
354
+ );
355
+ return items;
356
+ }
192
357
 
193
- if ('id' in put && typeof put.id === 'string') {
194
- createdIdSet.add(put.id);
195
- }
358
+ if (ov && v) {
359
+ if (ov.toString() === v.toString()) {
360
+ ov = null;
361
+ v = null;
196
362
 
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
- );
363
+ // if (l.length === 0) {
364
+ // Probably no change
365
+ return [];
366
+ // }
210
367
  }
211
368
 
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
- }
369
+ // Simplify changes by providing one change instead of for all keys
370
+ items.push(
371
+ AuditLogPatchItem.create({
372
+ key: getAutoEncoderKey(keySingular).append(getAutoEncoderName(original) || getAutoEncoderName(v) || AuditLogReplacement.key('item')),
373
+ oldValue: ov || undefined,
374
+ value: v || undefined,
375
+ type: AuditLogPatchItemType.Changed,
376
+ }),
377
+ );
378
+ return items;
379
+ }
222
380
 
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
- });
381
+ items.push(
382
+ ...l,
383
+ );
234
384
 
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
- }
385
+ return items;
386
+ }
244
387
 
245
- items.push(
246
- ...l,
247
- );
388
+ function processDelete(key: string, deletedItem: unknown, createdIdSet?: Set<string>): AuditLogPatchItem[] {
389
+ if (createdIdSet) {
390
+ const id = getId(deletedItem);
391
+ if (id && typeof id === 'string' && createdIdSet.has(id)) {
392
+ // DELETE + PUT happened
393
+ return [];
248
394
  }
395
+ }
249
396
 
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
- }
397
+ const v = getAutoEncoderPutValue(deletedItem);
398
+ const n = getAutoEncoderName(deletedItem);
263
399
 
264
- if (createdIdSet.has(id)) {
265
- // DELETE + PUT happened
266
- continue;
267
- }
400
+ const keySingular = getKeySingular(key);
401
+ const k = AuditLogReplacement.key(keySingular).append(n);
268
402
 
269
- let k = AuditLogReplacement.key(keySingular);
403
+ return [
404
+ AuditLogPatchItem.create({
405
+ key: k,
406
+ type: AuditLogPatchItemType.Removed,
407
+ oldValue: (n?.toString() !== v?.toString()) ? (v || undefined) : undefined,
408
+ }),
409
+ ];
410
+ }
270
411
 
271
- const name = getAutoEncoderName(original);
272
- if (name) {
273
- k = k.prepend(AuditLogReplacement.string(name));
412
+ function createArrayChangeHandler(key: string) {
413
+ return (oldValue: unknown, value: unknown) => {
414
+ if (!Array.isArray(oldValue)) {
415
+ // Not supported
416
+ return [];
417
+ }
418
+ const items: AuditLogPatchItem[] = [];
419
+
420
+ if (!isPatchableArray(value)) {
421
+ if (Array.isArray(value)) {
422
+ // Search for puts
423
+ let orderChanged = false;
424
+ let added = 0;
425
+ for (const [index, newItem] of value.entries()) {
426
+ const originalIndex = findIndex(newItem, oldValue);
427
+
428
+ if (originalIndex === -1) {
429
+ // Has been added
430
+ items.push(...processPut(key, newItem, null));
431
+ added++;
432
+ }
433
+ else {
434
+ // Has been overwritten
435
+ const original = oldValue[originalIndex];
436
+ items.push(...processPatch(key, newItem, original));
437
+
438
+ if ((index - added) !== originalIndex) {
439
+ // Order has changed
440
+ orderChanged = true;
441
+ }
442
+ }
443
+ }
444
+
445
+ // Search for deletes
446
+ for (const original of oldValue) {
447
+ const newItem = findOriginal(original, value);
448
+ if (!newItem) {
449
+ // Has been deleted
450
+ items.push(...processDelete(key, original));
451
+
452
+ orderChanged = false; // ignore order changed as delete will have set it to true
453
+ }
454
+ }
455
+
456
+ if (orderChanged) {
457
+ // Check if order has changed
458
+ items.push(
459
+ AuditLogPatchItem.create({
460
+ key: getAutoEncoderKey(key),
461
+ type: AuditLogPatchItemType.Reordered,
462
+ }),
463
+ );
464
+ }
274
465
  }
466
+ // Not supported
467
+ return items;
468
+ }
275
469
 
276
- items.push(
277
- AuditLogPatchItem.create({
278
- key: k,
279
- type: AuditLogPatchItemType.Removed,
280
- }),
281
- );
470
+ const createdIdSet = new Set<string>();
471
+
472
+ for (const { put } of value.getPuts()) {
473
+ items.push(...processPut(key, put, findOriginal(put, oldValue), createdIdSet));
474
+ }
475
+
476
+ for (const patch of value.getPatches()) {
477
+ items.push(...processPatch(key, patch, findOriginal(patch, oldValue)));
478
+ }
479
+
480
+ for (const id of value.getDeletes()) {
481
+ items.push(...processDelete(key, findOriginalById(id, oldValue), createdIdSet));
282
482
  }
283
483
 
284
484
  if (value.getMoves().length > 0) {
285
485
  items.push(
286
486
  AuditLogPatchItem.create({
287
- key: AuditLogReplacement.key(key),
487
+ key: getAutoEncoderKey(key),
288
488
  type: AuditLogPatchItemType.Reordered,
289
489
  }),
290
490
  );
@@ -293,9 +493,9 @@ function createArrayChangeHandler(key: string) {
293
493
  };
294
494
  }
295
495
 
296
- function createMapChangeHandler(key: string) {
496
+ function createMapChangeHandler(key?: string) {
297
497
  return (oldValue: unknown, value: unknown) => {
298
- if (!isPatchMap(value)) {
498
+ if (!(value instanceof Map)) {
299
499
  // Not supported
300
500
  return [];
301
501
  }
@@ -305,22 +505,27 @@ function createMapChangeHandler(key: string) {
305
505
  }
306
506
 
307
507
  const items: AuditLogPatchItem[] = [];
308
- const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
508
+ const keySingular = key ? key.replace(/ies$/, 'y').replace(/s$/, '') : key;
509
+ const isPatch = isPatchMap(value);
309
510
 
310
511
  for (const [k, v] of value.entries()) {
311
512
  if (typeof k !== 'string') {
312
513
  // Not supported
313
514
  continue;
314
515
  }
315
- const original = oldValue.get(k);
516
+ let original = oldValue.get(k);
316
517
 
317
- if (v === null) {
518
+ if (v instanceof Map && !original) {
519
+ original = new Map();
520
+ }
521
+
522
+ if (v === null && isPatch) {
318
523
  // Delete
319
524
  if (original) {
320
525
  items.push(
321
526
  AuditLogPatchItem.create({
322
- key: AuditLogReplacement.key(keySingular),
323
- oldValue: getAutoEncoderValue(original as AutoEncoder) || AuditLogReplacement.key(k),
527
+ key: AuditLogReplacement.key(keySingular).append(getAutoEncoderKey(k)).append(getAutoEncoderName(original)),
528
+ oldValue: getAutoEncoderPutValue(original) || undefined,
324
529
  type: AuditLogPatchItemType.Removed,
325
530
  }),
326
531
  );
@@ -332,30 +537,55 @@ function createMapChangeHandler(key: string) {
332
537
  // added
333
538
  items.push(
334
539
  AuditLogPatchItem.create({
335
- key: AuditLogReplacement.key(keySingular),
336
- value: getAutoEncoderValue(v as AutoEncoder) || AuditLogReplacement.key(k),
540
+ key: AuditLogReplacement.key(keySingular).append(getAutoEncoderKey(k)).append(getAutoEncoderName(v)),
541
+ value: getAutoEncoderPutValue(v) || undefined,
337
542
  type: AuditLogPatchItemType.Added,
338
543
  }),
339
544
  );
340
545
  }
341
546
  else {
547
+ let ov = getAutoEncoderValue(original);
548
+ let nv = getAutoEncoderValue(v);
549
+
342
550
  const c = explainPatch(
343
551
  original,
344
- v as AutoEncoder,
552
+ v,
345
553
  ).map((i) => {
346
- const name = getAutoEncoderValue(original as AutoEncoder);
347
- if (name) {
348
- i.key = i.key.prepend(name);
349
- }
554
+ i.key = i.key.prepend(getAutoEncoderName(original) || getAutoEncoderName(v) || getAutoEncoderKey(k));
350
555
  i.key = i.key.prepend(AuditLogReplacement.key(keySingular));
351
556
  return i;
352
557
  });
353
558
 
354
- if (c.length === 0) {
559
+ if (ov && nv) {
560
+ if (ov.toString() === nv.toString()) {
561
+ ov = null;
562
+ nv = null;
563
+
564
+ // if (c.length === 0) {
565
+ // Probably no change
566
+ continue;
567
+ // }
568
+ }
569
+
570
+ // Simplify change
571
+ items.push(
572
+ AuditLogPatchItem.create({
573
+ key: AuditLogReplacement.key(keySingular).append(getAutoEncoderKey(k)).append(getAutoEncoderName(original) || getAutoEncoderName(v)),
574
+ oldValue: ov || undefined,
575
+ value: nv || undefined,
576
+ type: AuditLogPatchItemType.Changed,
577
+ }),
578
+ );
579
+ continue;
580
+ }
581
+
582
+ if (c.length === 0 && v instanceof AutoEncoder && v.isPatch()) {
355
583
  // Manual log
356
584
  items.push(
357
585
  AuditLogPatchItem.create({
358
- key: AuditLogReplacement.key(keySingular).append(getAutoEncoderValue(original as AutoEncoder) || AuditLogReplacement.key(k)),
586
+ key: AuditLogReplacement.key(keySingular).append(getAutoEncoderKey(k)).append(getAutoEncoderName(original) || getAutoEncoderName(v)),
587
+ oldValue: getAutoEncoderValue(original) || undefined,
588
+ value: getAutoEncoderValue(v) || undefined,
359
589
  type: AuditLogPatchItemType.Changed,
360
590
  }),
361
591
  );
@@ -368,121 +598,124 @@ function createMapChangeHandler(key: string) {
368
598
  }
369
599
  }
370
600
 
601
+ if (!isPatch) {
602
+ // Loop old values
603
+ for (const [k, v] of oldValue.entries()) {
604
+ if (typeof k !== 'string') {
605
+ // Not supported
606
+ continue;
607
+ }
608
+
609
+ if (value.has(k)) {
610
+ continue;
611
+ }
612
+
613
+ items.push(
614
+ AuditLogPatchItem.create({
615
+ key: AuditLogReplacement.key(keySingular).append(getAutoEncoderKey(k)).append(getAutoEncoderName(v)),
616
+ oldValue: getAutoEncoderPutValue(v) || undefined,
617
+ type: AuditLogPatchItemType.Removed,
618
+ }),
619
+ );
620
+ }
621
+ }
622
+
371
623
  return items;
372
624
  };
373
625
  }
374
626
 
375
- function createSimpleArrayChangeHandler(key: string) {
627
+ export function createUnknownChangeHandler(key: string) {
376
628
  return (oldValue: unknown, value: unknown) => {
377
- if (!Array.isArray(oldValue)) {
378
- // Not supported
629
+ if (oldValue === value) {
379
630
  return [];
380
631
  }
381
- const keySingular = key.replace(/ies$/, 'y').replace(/s$/, '');
382
632
 
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
- }
633
+ if ((Array.isArray(oldValue) || Array.isArray(value)) && (!oldValue || Array.isArray(oldValue)) && (Array.isArray(value) || !value)) {
634
+ return createArrayChangeHandler(key)(oldValue, value);
635
+ }
400
636
 
637
+ if (!oldValue && oldValue !== 0 && getAutoEncoderValue(value, key)) {
638
+ // Simplify addition
401
639
  return [
402
640
  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(),
641
+ key: getAutoEncoderKey(key),
642
+ value: getAutoEncoderPutValue(value, key) || undefined,
643
+ type: AuditLogPatchItemType.Added,
644
+ }),
407
645
  ];
408
646
  }
409
647
 
410
- if (!isPatchableArray(value)) {
411
- // Not supported
412
- return [];
648
+ if ((oldValue || oldValue === 0) && (value === null || value === undefined)) {
649
+ return [
650
+ AuditLogPatchItem.create({
651
+ key: getAutoEncoderKey(key),
652
+ oldValue: getAutoEncoderPutValue(oldValue, key) || undefined,
653
+ type: AuditLogPatchItemType.Removed,
654
+ }),
655
+ ];
413
656
  }
414
657
 
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
- }
658
+ const items = explainPatch(oldValue, value).map((i) => {
659
+ i.key = i.key.prepend(getAutoEncoderKey(key));
660
+ return i;
661
+ });
423
662
 
424
- // Little hack: detect PUT/DELETE behaviour:
425
- const original = oldValue.find(v => v === put);
663
+ let v = getAutoEncoderValue(value, key);
664
+ let ov = getAutoEncoderValue(oldValue, key);
426
665
 
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);
666
+ if (oldValue !== undefined && oldValue !== null && value !== undefined && value !== null && getAutoEncoderValue(value, key) && items.length === 0 && value instanceof AutoEncoder && value.isPatch()) {
667
+ return [
668
+ AuditLogPatchItem.create({
669
+ key: getAutoEncoderKey(key),
670
+ value: v || undefined,
671
+ oldValue: ov || undefined,
672
+ type: AuditLogPatchItemType.Changed,
673
+ }),
674
+ ];
438
675
  }
439
676
 
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
- }
677
+ if (v && ov) {
678
+ // Simplify change
679
+ if (v.toString() === ov.toString()) {
680
+ v = null;
681
+ ov = null;
449
682
 
450
- const original = oldValue.find(v => v === id);
451
- if (!original || typeof original !== 'string') {
452
- // Not supported
453
- continue;
683
+ // if (items.length === 0) {
684
+ // Probably no change
685
+ return [];
686
+ // }
454
687
  }
455
688
 
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(
689
+ return [
467
690
  AuditLogPatchItem.create({
468
- key: AuditLogReplacement.key(key),
469
- type: AuditLogPatchItemType.Reordered,
691
+ key: getAutoEncoderKey(key),
692
+ value: v || undefined,
693
+ oldValue: ov || undefined,
694
+ type: AuditLogPatchItemType.Changed,
470
695
  }),
471
- );
696
+ ];
472
697
  }
473
698
  return items;
474
699
  };
475
700
  }
476
701
 
477
702
  function getExplainerForField(field: Field<any>) {
478
- if (field.decoder === StringDecoder || field.decoder instanceof EnumDecoder) {
703
+ if (field.decoder === StringDecoder) {
479
704
  return createStringChangeHandler(field.property);
480
705
  }
481
706
 
707
+ if (field.decoder instanceof EnumDecoder) {
708
+ return createEnumChangeHandler(field.property);
709
+ }
710
+
482
711
  if (field.decoder instanceof SymbolDecoder) {
483
- if (field.decoder.decoder === StringDecoder || field.decoder.decoder instanceof EnumDecoder) {
712
+ if (field.decoder.decoder === StringDecoder) {
484
713
  return createStringChangeHandler(field.property);
485
714
  }
715
+
716
+ if (field.decoder.decoder instanceof EnumDecoder) {
717
+ return createEnumChangeHandler(field.property);
718
+ }
486
719
  }
487
720
 
488
721
  if (field.decoder === DateDecoder) {
@@ -497,12 +730,40 @@ function getExplainerForField(field: Field<any>) {
497
730
  return createIntegerChangeHandler(field.property);
498
731
  }
499
732
 
500
- if (field.decoder instanceof ArrayDecoder && field.decoder.decoder === StringDecoder) {
501
- return createSimpleArrayChangeHandler(field.property);
502
- }
503
-
504
733
  if (field.decoder instanceof ArrayDecoder) {
505
- return createArrayChangeHandler(field.property);
734
+ const handler = createArrayChangeHandler(field.property);
735
+
736
+ if (field.decoder.decoder instanceof EnumDecoder) {
737
+ // Map values to keys
738
+ return (oldValue: unknown, value: unknown) => {
739
+ const items = handler(oldValue, value);
740
+
741
+ for (const item of items) {
742
+ if (item.oldValue && !item.oldValue.type) {
743
+ item.oldValue.type = AuditLogReplacementType.Key;
744
+ }
745
+ if (item.value && !item.value.type) {
746
+ item.value.type = AuditLogReplacementType.Key;
747
+ }
748
+
749
+ // If item.key is an array that ends with a 'value', also change it
750
+ if (item.key.type === AuditLogReplacementType.Array) {
751
+ const lastKeyItem = item.key.values[item.key.values.length - 1];
752
+ if (!lastKeyItem.type) {
753
+ lastKeyItem.type = AuditLogReplacementType.Key;
754
+ }
755
+ }
756
+ else {
757
+ if (!item.key.type) {
758
+ item.key.type = AuditLogReplacementType.Key;
759
+ }
760
+ }
761
+ }
762
+ return items;
763
+ };
764
+ }
765
+
766
+ return handler;
506
767
  }
507
768
 
508
769
  if (field.decoder instanceof MapDecoder) {
@@ -524,106 +785,57 @@ function getExplainerForField(field: Field<any>) {
524
785
 
525
786
  return [
526
787
  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
- }),
788
+ key: getAutoEncoderKey(field.property),
789
+ oldValue: wasTrueOld === true ? AuditLogReplacement.key('checked') : (wasTrueOld === false ? AuditLogReplacement.key('unchecked') : undefined),
790
+ value: isTrue === true ? AuditLogReplacement.key('checked') : (isTrue === false ? AuditLogReplacement.key('unchecked') : undefined),
791
+ }).autoType(),
531
792
  ];
532
793
  };
533
794
  }
534
795
 
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
- };
796
+ return createUnknownChangeHandler(field.property);
602
797
  }
603
798
 
604
- export function explainPatch<T extends AutoEncoder>(original: T | null, patch: AutoEncoderPatchType<T> | T): AuditLogPatchItem[] {
799
+ export function explainPatch(original: unknown | null, patch: unknown): AuditLogPatchItem[] {
605
800
  if (isPatchableArray(patch)) {
606
801
  const b = createArrayChangeHandler('items');
607
802
  return b(original, patch);
608
803
  }
609
- if (!(patch instanceof AutoEncoder)) {
804
+
805
+ if (original instanceof Map) {
806
+ const b = createMapChangeHandler();
807
+ return b(original, patch);
808
+ }
809
+
810
+ if (typeof patch !== 'object' || patch === null) {
811
+ if (patch === null) {
812
+ // todo
813
+ }
610
814
  return [];
611
815
  }
612
- if (original && !(original instanceof AutoEncoder)) {
816
+ if (original && typeof original !== 'object') {
613
817
  return [];
614
818
  }
615
819
 
616
820
  const items: AuditLogPatchItem[] = [];
617
821
 
618
822
  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
-
823
+ const field = original instanceof AutoEncoder
824
+ ? original.static.latestFields.find(f => f.property === key)
825
+ : (
826
+ patch instanceof AutoEncoder
827
+ ? patch.static.latestFields.find(f => f.property === key)
828
+ : null
829
+ );
624
830
  const oldValue = original?.[key] ?? null;
625
831
  const value = patch[key];
626
832
 
833
+ if (!(patch instanceof AutoEncoder) || !field) {
834
+ // try manual without type information
835
+ items.push(...createUnknownChangeHandler(key)(oldValue, value));
836
+ continue;
837
+ }
838
+
627
839
  if (patch.isPut() && key === 'id') {
628
840
  continue;
629
841
  }