@stamhoofd/backend 2.122.4 → 2.122.6
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/package.json +17 -17
- package/src/helpers/AdminPermissionChecker.ts +40 -6
- package/src/helpers/AuthenticatedStructures.ts +6 -3
- package/src/seeds/{1780665428-group-update-occupancy.ts → 1782218615-group-update-occupancy.ts} +8 -3
- package/src/seeds/1782222337-waiting-lists.ts +85 -0
- package/src/seeds/1782230891-old-registrations-fix.ts +343 -0
- package/src/seeds/1782232168-restore-default-data-acess.ts +67 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.122.
|
|
3
|
+
"version": "2.122.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
58
58
|
"@simonbackx/simple-errors": "1.5.0",
|
|
59
59
|
"@simonbackx/simple-logging": "1.0.1",
|
|
60
|
-
"@stamhoofd/backend-env": "2.122.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.122.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.122.
|
|
63
|
-
"@stamhoofd/crons": "2.122.
|
|
64
|
-
"@stamhoofd/email": "2.122.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.122.
|
|
66
|
-
"@stamhoofd/logging": "2.122.
|
|
67
|
-
"@stamhoofd/models": "2.122.
|
|
68
|
-
"@stamhoofd/object-differ": "2.122.
|
|
69
|
-
"@stamhoofd/queues": "2.122.
|
|
70
|
-
"@stamhoofd/sql": "2.122.
|
|
71
|
-
"@stamhoofd/structures": "2.122.
|
|
72
|
-
"@stamhoofd/types": "2.122.
|
|
73
|
-
"@stamhoofd/utility": "2.122.
|
|
60
|
+
"@stamhoofd/backend-env": "2.122.6",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.122.6",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.122.6",
|
|
63
|
+
"@stamhoofd/crons": "2.122.6",
|
|
64
|
+
"@stamhoofd/email": "2.122.6",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.122.6",
|
|
66
|
+
"@stamhoofd/logging": "2.122.6",
|
|
67
|
+
"@stamhoofd/models": "2.122.6",
|
|
68
|
+
"@stamhoofd/object-differ": "2.122.6",
|
|
69
|
+
"@stamhoofd/queues": "2.122.6",
|
|
70
|
+
"@stamhoofd/sql": "2.122.6",
|
|
71
|
+
"@stamhoofd/structures": "2.122.6",
|
|
72
|
+
"@stamhoofd/types": "2.122.6",
|
|
73
|
+
"@stamhoofd/utility": "2.122.6",
|
|
74
74
|
"archiver": "7.0.1",
|
|
75
75
|
"axios": "1.16.0",
|
|
76
76
|
"base-x": "3.0.11",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"stripe": "16.12.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stamhoofd/test-utils": "2.122.
|
|
94
|
+
"@stamhoofd/test-utils": "2.122.6",
|
|
95
95
|
"@types/cookie": "0.6.0",
|
|
96
96
|
"@types/luxon": "3.7.1",
|
|
97
97
|
"@types/mailparser": "3.4.6",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "7a9fd6be1074753301ef8e3dad8f79164522567f"
|
|
111
111
|
}
|
|
@@ -201,6 +201,14 @@ export class AdminPermissionChecker {
|
|
|
201
201
|
return p;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
getUnloadedOrganizationPermissions(organizationOrId: string | Organization) {
|
|
205
|
+
if (!this.user.permissions) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return this.user.permissions.organizationPermissions.get(typeof organizationOrId === 'string' ? organizationOrId : organizationOrId.id) ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
async canAccessPrivateOrganizationData(organization: Organization) {
|
|
205
213
|
if (!this.checkScope(organization.id)) {
|
|
206
214
|
return false;
|
|
@@ -212,6 +220,24 @@ export class AdminPermissionChecker {
|
|
|
212
220
|
return true;
|
|
213
221
|
}
|
|
214
222
|
|
|
223
|
+
/**
|
|
224
|
+
* This is required to match the frontend, which expects privateMeta
|
|
225
|
+
* as soon as the user has unloaded permissions (it cannot load the permissions yet without the private meta).
|
|
226
|
+
*
|
|
227
|
+
* If the user expected private meta, but should not be allowed to have private meta (canAccessPrivateOrganizationData) (= meaning permissions resolve to nothing)
|
|
228
|
+
* then we return a stubbed private meta object
|
|
229
|
+
*/
|
|
230
|
+
async doesFrontendExpectPrivateMeta(organization: Organization) {
|
|
231
|
+
if (!this.checkScope(organization.id)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!await this.hasSomeUnloadedAccess(organization)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
215
241
|
checkScope(organizationId: string | null) {
|
|
216
242
|
if (organizationId) {
|
|
217
243
|
// If request is scoped to a different organization
|
|
@@ -1096,6 +1122,14 @@ export class AdminPermissionChecker {
|
|
|
1096
1122
|
return !!organizationPermissions && !organizationPermissions.isEmpty;
|
|
1097
1123
|
}
|
|
1098
1124
|
|
|
1125
|
+
/**
|
|
1126
|
+
* Use this as a circuit breaker to avoid queries for non-admin users
|
|
1127
|
+
*/
|
|
1128
|
+
async hasSomeUnloadedAccess(organizationOrId: string | Organization): Promise<boolean> {
|
|
1129
|
+
const unloadedPermissions = this.getUnloadedOrganizationPermissions(organizationOrId);
|
|
1130
|
+
return !!unloadedPermissions && !unloadedPermissions.isEmpty;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1099
1133
|
async canManageAdmins(organizationId: string) {
|
|
1100
1134
|
return !this.user.isApiUser && (await this.hasFullAccess(organizationId));
|
|
1101
1135
|
}
|
|
@@ -1363,12 +1397,12 @@ export class AdminPermissionChecker {
|
|
|
1363
1397
|
}
|
|
1364
1398
|
|
|
1365
1399
|
private async loopRecordAnswerSettingsAccess<T extends RecordAnswer | AutoEncoderPatchType<RecordAnswer>>({ member, level, recordAnswers, callback }:
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1400
|
+
{
|
|
1401
|
+
member: MemberWithUsersRegistrationsAndGroups;
|
|
1402
|
+
level: PermissionLevel;
|
|
1403
|
+
recordAnswers: PatchMap<string, T | null | undefined> | Map<string, T>;
|
|
1404
|
+
callback: (result: { canAccess: false; record: RecordSettings | null } | { canAccess: true; record: RecordSettings }, entry: [keys: string, value: T | null | undefined]) => void;
|
|
1405
|
+
}): Promise<void> {
|
|
1372
1406
|
if (STAMHOOFD.userMode !== 'platform') {
|
|
1373
1407
|
let organization = this.organization;
|
|
1374
1408
|
|
|
@@ -2,7 +2,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
2
2
|
import type { AuditLog, Document, EventNotification, MemberWithUsersRegistrationsAndGroups, Order, Ticket } from '@stamhoofd/models';
|
|
3
3
|
import { BalanceItem, CachedBalance, Event, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationInvitation, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import type { PaymentGeneral } from '@stamhoofd/structures';
|
|
5
|
-
import { BaseOrganization, getAppHost } from '@stamhoofd/structures';
|
|
5
|
+
import { BaseOrganization, getAppHost, OrganizationPrivateMetaData } from '@stamhoofd/structures';
|
|
6
6
|
import { Payment as PaymentStruct, AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, BalanceItem as BalanceItemStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvitationGroupData, InvitationMemberData, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberRegistrationInvitation, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationInvitation as RegistrationInvitationStruct, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
7
7
|
import { Sorter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
@@ -265,7 +265,7 @@ export class AuthenticatedStructures {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// Get registration period and whether private data can be accessed for each organization
|
|
268
|
-
const organizationData: Map<string, { organizationRegistrationPeriod: OrganizationRegistrationPeriodStruct; canAccessPrivateData: boolean }> = new Map();
|
|
268
|
+
const organizationData: Map<string, { organizationRegistrationPeriod: OrganizationRegistrationPeriodStruct; canAccessPrivateData: boolean; doesFrontendExpectPrivateMeta: boolean }> = new Map();
|
|
269
269
|
const allPeriodIds = periodIdOrganizationsMap.keys();
|
|
270
270
|
const allPeriods = await RegistrationPeriod.getByIDs(...allPeriodIds);
|
|
271
271
|
const periodMap = new Map<string, RegistrationPeriod>(allPeriods.map(p => [p.id, p]));
|
|
@@ -306,10 +306,12 @@ export class AuthenticatedStructures {
|
|
|
306
306
|
|
|
307
307
|
// check if private data can be accessed
|
|
308
308
|
const canAccessPrivateData = await Context.optionalAuth?.canAccessPrivateOrganizationData(organization) ?? false;
|
|
309
|
+
const doesFrontendExpectPrivateMeta = canAccessPrivateData || (await Context.optionalAuth?.doesFrontendExpectPrivateMeta(organization) ?? false);
|
|
310
|
+
|
|
309
311
|
if (canAccessPrivateData) {
|
|
310
312
|
organizationIdsToGetWebshopsFor.push(organization.id);
|
|
311
313
|
}
|
|
312
|
-
organizationData.set(organizationId, { organizationRegistrationPeriod, canAccessPrivateData });
|
|
314
|
+
organizationData.set(organizationId, { organizationRegistrationPeriod, canAccessPrivateData, doesFrontendExpectPrivateMeta });
|
|
313
315
|
} else {
|
|
314
316
|
// Should never happen
|
|
315
317
|
throw new Error('Organization registration period model not found for organization id ' + organizationId + ' and period id ' + periodId);
|
|
@@ -381,6 +383,7 @@ export class AuthenticatedStructures {
|
|
|
381
383
|
result = OrganizationStruct.create({
|
|
382
384
|
...baseStruct,
|
|
383
385
|
period,
|
|
386
|
+
privateMeta: data.doesFrontendExpectPrivateMeta ? OrganizationPrivateMetaData.create({}) : undefined,
|
|
384
387
|
});
|
|
385
388
|
}
|
|
386
389
|
|
package/src/seeds/{1780665428-group-update-occupancy.ts → 1782218615-group-update-occupancy.ts}
RENAMED
|
@@ -3,13 +3,18 @@ import { Group } from '@stamhoofd/models';
|
|
|
3
3
|
import { SeedTools } from '../helpers/SeedTools.js';
|
|
4
4
|
|
|
5
5
|
export default new Migration(async () => {
|
|
6
|
-
if (STAMHOOFD.environment
|
|
6
|
+
if (STAMHOOFD.environment === 'test') {
|
|
7
7
|
console.log('skipped in tests');
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
if (STAMHOOFD.userMode
|
|
12
|
-
console.log('skipped seed group-update-occupancy because usermode
|
|
11
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
12
|
+
console.log('skipped seed group-update-occupancy because usermode is platform');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
17
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
13
18
|
return;
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Group, Organization, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
+
import { GroupSettings, GroupType, TranslatedString, WaitingListType } from '@stamhoofd/structures';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dryRun = false;
|
|
18
|
+
await start(dryRun);
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
throw new Error('Migration did not finish because of dryRun');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function start(dryRun: boolean) {
|
|
26
|
+
await SeedTools.loop({
|
|
27
|
+
query: Organization.select(),
|
|
28
|
+
batchSize: 50,
|
|
29
|
+
useTransactionPerBatch: true,
|
|
30
|
+
action: async (organization: Organization) => {
|
|
31
|
+
const groups: Group[] = await Group.select()
|
|
32
|
+
.where('organizationId', organization.id)
|
|
33
|
+
// only for current period
|
|
34
|
+
.where('periodId', organization.periodId)
|
|
35
|
+
.where('type', GroupType.Membership)
|
|
36
|
+
.fetch();
|
|
37
|
+
|
|
38
|
+
if (groups.length === 0) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// const period = await OrganizationRegistrationPeriod.select().where('organizationId', organization.id).where('periodId', organization.periodId).first(true);
|
|
43
|
+
const period = await RegistrationPeriod.getByID(organization.periodId, true);
|
|
44
|
+
|
|
45
|
+
// handle group permissions
|
|
46
|
+
for (const group of groups) {
|
|
47
|
+
if (group.settings.waitingListType === WaitingListType.None) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (group.waitingListId) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const newWaitingList = await createWaitingList(group, period, dryRun);
|
|
56
|
+
group.waitingListId = newWaitingList.id;
|
|
57
|
+
|
|
58
|
+
if (!dryRun) {
|
|
59
|
+
await group.save();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cycleIfMigrated = -99;
|
|
68
|
+
|
|
69
|
+
async function createWaitingList(originalGroup: Group, period: RegistrationPeriod, dryRun: boolean) {
|
|
70
|
+
const newWaitingList = new Group();
|
|
71
|
+
newWaitingList.cycle = cycleIfMigrated;
|
|
72
|
+
newWaitingList.type = GroupType.WaitingList;
|
|
73
|
+
newWaitingList.organizationId = originalGroup.organizationId;
|
|
74
|
+
newWaitingList.periodId = period.id;
|
|
75
|
+
newWaitingList.settings = GroupSettings.create({
|
|
76
|
+
name: TranslatedString.create($t(`%yh`) + ' ' + originalGroup.settings.name.toString()),
|
|
77
|
+
period: period.getBaseStructure(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!dryRun) {
|
|
81
|
+
await newWaitingList.save();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return newWaitingList;
|
|
85
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Group, Organization, OrganizationRegistrationPeriod, Registration, RegistrationInvitation, RegistrationPeriod, V1GroupMigrationData, V1WaitingListMigrationData } from '@stamhoofd/models';
|
|
3
|
+
import { GroupPrivateSettings, GroupSettings, GroupStatus, GroupType, TranslatedString } from '@stamhoofd/structures';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const dryRun = false;
|
|
18
|
+
await start(dryRun);
|
|
19
|
+
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
throw new Error('Migration did not finish because of dryRun');
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
async function start(dryRun: boolean) {
|
|
26
|
+
const relevantOrganizations = new Set(['f2543d6b-523e-4b03-848b-838200b1ff41', '11a478dd-e8cf-4160-80dd-8b48d20a6d39', '6d1c32cf-2ef0-4738-bacb-d553488f97b9', '3e6f5cb9-e130-4dda-92c9-26682fa09509', '57815733-5750-492f-beb1-1efd6229803a', '7677e32d-763a-4033-bc74-30d13f27556d', '7ac75a0f-f365-4ddc-836c-66a3267f24f0', '9dced404-a92d-44c4-b301-ecd29b6ac4e0', '97928f8a-b8d1-44cc-918f-09d0ae896c3b', 'f0edb665-1ce0-442a-88e7-5a62ea78c5fc', '842ada22-82f3-4cef-a1da-ae5da20b208d', '33744473-9ad9-4d3e-a9fc-31cd1b6a2753', 'ed6e74f1-51a0-4e04-8195-e722b015eeb5', '10827cc5-5f29-46cf-b26e-5fd11a9342cc', '5f4d4a26-43c3-4f80-86ad-64d07d5a46a7', '74e2bcbf-0ff9-484b-988a-e11d147122c0', 'c69512bc-ea0c-427a-ab90-08c3dcf1c856', '16742d64-0b31-4ce7-9c1e-6194882045b9', '95d59803-c50e-4bc1-adc9-90ade32b7579', 'eb6ccffa-41c5-45df-b4e4-1eb5749bc0fe', '1a534005-2784-400c-b8fe-bde09c901259', '27e06924-49a6-468c-a16e-b5735ace1894', '7a86f3db-08d8-4752-a77d-eb85f2167942', 'f562c735-7bf4-4e2e-8c0f-6584e8e96a1d', 'e2afe517-cd35-4d9e-a561-125ad184a2ff', 'fd934e40-734c-442d-b338-c3ae288dd55d']);
|
|
27
|
+
|
|
28
|
+
const relevantGroups = new Set([
|
|
29
|
+
'020a313c-0e97-4068-84ba-783d91e768a4', '02420f62-1a07-4e2a-ae92-13c35320fb27', '0b6d8422-857f-46e4-a632-a4af322c9193', '12ba896a-e064-4697-a5df-69664c9bd8cb', '134e33f9-3560-4eca-977b-aaef01af5ed4', '1a9bb59a-c85a-4338-94c6-f82061f1ee0e', '1bf5519d-4645-4c51-8ae5-7a202bb74e9e', '1c86c401-c32b-4c6f-a2b5-fb15649efa74', '1ddc6fde-f512-43ac-8e15-2496601e900e', '20cab6ae-fb30-4de8-948e-60caf0979ff7', '22194f9d-fba2-4497-b7b0-de8ba6e884ff', '2384f112-d50d-4fcf-b6f2-45db73a8944c', '26693e8a-b73d-4f28-a41c-d281ba91fb3e', '287ff7b8-3319-40fb-ad25-26b139db3272', '2e473bf7-36f9-4276-8770-032db776d154', '3282e30c-a3db-4f00-b827-646fcc532c43', '3490cfd8-4c0b-487f-8f0d-c96c62950160', '3d68762b-8133-42e0-a642-7649864b09b3', '4a7d205b-e996-4841-b21c-1336642e670d', '52b0285c-4935-4577-bc7b-30d0b8804fc9', '579263b6-ceb1-4be0-a31e-9a4a904f0cae', '5870423f-6faf-492e-accb-4bd110365328', '60e24adb-5bb3-48de-9008-5b80c905a01a', '6535d9b4-f2ad-4f7e-b7e2-9e50ddf5eff6', '6a508234-08aa-4b0b-9647-7aeb9d80fa5a', '722f3bdf-d9df-4a78-a4c1-783c9af3d21b', '754bb111-caad-49b3-8dde-5bd0f3ad9104', '76aef892-19b5-4deb-9276-0c6b19b652fd', '78fa525b-e346-4b78-9e25-f9b1b5134b77', '7989ea9d-354d-4eff-9350-8eb8a6c4732b', '7aba3792-0138-4302-8550-2ccb456a37ad', '7c480cd5-8db3-4c03-8f0b-a88556df0234', '8113c2a2-e788-4f11-815f-4b019d7ca966', '82958c24-bc12-45f2-9d0d-3c8b902df052', '88c28670-cf97-42e2-ae9e-d836d349c2e6', '88d87c1a-749f-453c-a60b-e54a7001a9f0', '8983c1d5-e8aa-4628-a251-17220336fad5', '89ced76d-8c98-4f83-bffa-8b467289972d', '8abed111-cda2-4bb8-8d98-21291a85fb4f', '8bb9b1c6-2d40-4a69-bbd5-63ce152b1a91', '8f98e452-e174-4946-96ec-888cc3498f56', '8fdffd17-84fa-41f7-8b74-744578d4b9d9', '92ae627b-8aa4-4197-885e-603d4a1be8aa', '9852662c-7e9f-4c26-a6d3-6335b49b9b3b', 'a06ddcb5-dc9c-4bec-a0f8-40fd4ab20016', 'a25bbeae-86b9-4162-8570-d121845f26e9', 'a690c59b-72eb-40bb-b181-d9dc904f9ec1', 'a9b0a795-3b7e-40bb-b8ec-65280d149dba', 'ab9ddbb9-21bc-4a8e-be7f-036554b70db4', 'bea51786-4d47-4bcb-bf79-fc636f047cbb', 'c806a74d-f0ae-4473-86bd-facb92085eb1', 'c9064135-322e-4848-9de0-61011d067697', 'cb7e738f-bcb5-4bc5-9962-2608ea9c4094', 'cbc21e01-bb7e-4f1d-94c8-d4aac3b7247d', 'cf766627-3e32-45a1-8d62-865b6cc1d4ed', 'd0cf99ce-24e9-4912-9dc8-660b4ff57dcc', 'd2a536af-f23d-4c3e-8275-dc2e5116ab60', 'd715451c-6292-4d58-83f8-ffce115fca21', 'd76b57b8-10c4-43e4-94d1-bb66d1053bb4', 'dcffb511-131b-401c-a4b5-13810596bc42', 'df010c46-fc79-401c-a4a4-1fddb478acff', 'dfaf11b8-f835-4952-99e1-2b0279931b05', 'e0b8e823-4213-4919-b1ed-70395ad15df2', 'e9470495-097c-47b5-8e63-cd4f7370fa2d', 'e9fe89f8-780b-417f-a4ee-b48e0b0bb8ad', 'eda36826-ee68-414d-a773-cb41e1a1e2d6', 'edaa90fa-869b-4f3c-8b90-4ea41fe9a364', 'f163d78a-9539-43a3-aeb7-443559599342', 'f24e2ca9-3036-4839-9ff4-91bd4ecb5c91', 'f665642c-93e7-4886-bd6e-f94252d54e77', 'f7e5436c-855d-4bf9-b2de-94e9d0fd3052', 'f822b26a-715a-4154-b2a2-b11e38373fad', 'fe2e4ec5-cee4-4648-ae87-2f8dee7f42c8', 'ff3a7e68-feb7-4aca-983d-a8f8315851e7',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
let total = 0;
|
|
33
|
+
|
|
34
|
+
await SeedTools.loop({
|
|
35
|
+
query: Organization.select(),
|
|
36
|
+
batchSize: 50,
|
|
37
|
+
useTransactionPerBatch: true,
|
|
38
|
+
action: async (organization: Organization) => {
|
|
39
|
+
if (!relevantOrganizations.has(organization.id)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const groups: Group[] = await Group.select()
|
|
44
|
+
.where('organizationId', organization.id)
|
|
45
|
+
.where('periodId', organization.periodId)
|
|
46
|
+
.fetch();
|
|
47
|
+
|
|
48
|
+
for (const group of groups) {
|
|
49
|
+
if (!relevantGroups.has(group.id)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await fixRegistrations(organization, group, dryRun);
|
|
54
|
+
total += 1;
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
console.log('Fixed registrations for' + total + ' groups');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function getCurrentCycle(group: Group) {
|
|
64
|
+
const migrationData = await V1GroupMigrationData.select().where('oldGroupId', group.id).andWhere('newGroupId', group.id).first(true);
|
|
65
|
+
return migrationData.oldCycle;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fixRegistrations(organization: Organization, group: Group, dryRun: boolean) {
|
|
69
|
+
const registrations = await Registration.select()
|
|
70
|
+
.where('groupId', group.id)
|
|
71
|
+
.fetch();
|
|
72
|
+
|
|
73
|
+
const currentCycle = await getCurrentCycle(group);
|
|
74
|
+
|
|
75
|
+
const cycles = new Set<number>();
|
|
76
|
+
|
|
77
|
+
for (const registration of registrations) {
|
|
78
|
+
cycles.add(registration.cycle);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (cycles.size < 2) {
|
|
82
|
+
console.error('No multiple cycles found for group (not expected):', group.id);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sortedCycles = Array.from(cycles).filter(c => c !== currentCycle).sort((a, b) => b - a);
|
|
87
|
+
|
|
88
|
+
const cyclesToMigrate = sortedCycles;
|
|
89
|
+
|
|
90
|
+
const archivePeriod = await RegistrationPeriod.select().where('organizationId', group.organizationId).where('customName', 'Gearchiveerde periodes').first(true);
|
|
91
|
+
const archiveOrganizationPeriod = await OrganizationRegistrationPeriod.select().where('organizationId', organization.id).where('periodId', archivePeriod.id).first(true);
|
|
92
|
+
|
|
93
|
+
const startIndex = await getStartIndex(group);
|
|
94
|
+
let currentIndex: number = startIndex;
|
|
95
|
+
|
|
96
|
+
const allNewGroups: Group[] = [];
|
|
97
|
+
|
|
98
|
+
for (const cycle of cyclesToMigrate) {
|
|
99
|
+
const newGroup = createPreviousGroup({ originalGroup: group, period: archivePeriod, index: currentIndex });
|
|
100
|
+
|
|
101
|
+
currentIndex = currentIndex + 1;
|
|
102
|
+
|
|
103
|
+
const registrationCount = await Registration.select()
|
|
104
|
+
.where('groupId', group.id)
|
|
105
|
+
.andWhere('cycle', cycle)
|
|
106
|
+
.count();
|
|
107
|
+
|
|
108
|
+
// only create group if there are registrations
|
|
109
|
+
if (registrationCount > 0) {
|
|
110
|
+
if (!dryRun) {
|
|
111
|
+
const migrationData = new V1GroupMigrationData();
|
|
112
|
+
migrationData.oldGroupId = group.id;
|
|
113
|
+
migrationData.oldCycle = cycle;
|
|
114
|
+
|
|
115
|
+
await newGroup.save();
|
|
116
|
+
migrationData.newGroupId = newGroup.id;
|
|
117
|
+
await migrationData.save();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await migrateRegistrations({ organization, period: archivePeriod, originalGroup: group, newGroup, cycle }, dryRun);
|
|
121
|
+
allNewGroups.push(newGroup);
|
|
122
|
+
} else {
|
|
123
|
+
console.error('No registrations found for group (not expected):', group.id);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// not needed to migrate requirePreviousGroupIds (was never set for these groups, checked in database)
|
|
127
|
+
// only 2 groups where preventPreviousGroupIds was set -> fix them manually ('6a508234-08aa-4b0b-9647-7aeb9d80fa5a', 'dfaf11b8-f835-4952-99e1-2b0279931b05')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (allNewGroups.length === 0) {
|
|
131
|
+
throw new Error('No new groups for group (unexpected): ' + group.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const parentCategory = await findCategory(group, archiveOrganizationPeriod);
|
|
135
|
+
|
|
136
|
+
for (const newGroup of allNewGroups) {
|
|
137
|
+
if (newGroup.id === undefined && dryRun) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (parentCategory) {
|
|
142
|
+
if (!parentCategory.groupIds.includes(newGroup.id)) {
|
|
143
|
+
parentCategory.groupIds.push(newGroup.id);
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error('group already in category:' + newGroup.id);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await cleanupGroup(newGroup, dryRun);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!dryRun) {
|
|
153
|
+
await archiveOrganizationPeriod.save();
|
|
154
|
+
await group.updateOccupancy();
|
|
155
|
+
await group.save();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function cleanupGroup(group: Group, dryRun: boolean) {
|
|
160
|
+
group.settings.cycleSettings = new Map();
|
|
161
|
+
group.settings.preventPreviousGroupIds = [];
|
|
162
|
+
group.settings.requirePreviousGroupIds = [];
|
|
163
|
+
group.cycle = cycleIfMigrated;
|
|
164
|
+
if (group.status === GroupStatus.Archived) {
|
|
165
|
+
group.status = GroupStatus.Closed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!dryRun) {
|
|
169
|
+
await group.updateOccupancy();
|
|
170
|
+
await group.save();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function findCategory(group: Group, archiveOrganizationPeriod: OrganizationRegistrationPeriod) {
|
|
175
|
+
const previousGroups = await V1GroupMigrationData.select().where('oldGroupId', group.id).andWhereNot('newGroupId', group.id).fetch();
|
|
176
|
+
|
|
177
|
+
for (const previousGroup of previousGroups) {
|
|
178
|
+
const previousGroupId = previousGroup.newGroupId;
|
|
179
|
+
if (previousGroup.oldGroupId !== group.id || previousGroup.newGroupId === group.id) {
|
|
180
|
+
throw new Error('group id mismatch');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const allCategories = archiveOrganizationPeriod.settings.categories;
|
|
184
|
+
const rootCategory = archiveOrganizationPeriod.settings.rootCategory;
|
|
185
|
+
if (!rootCategory) {
|
|
186
|
+
throw new Error('root category not found for archiveOrganizationPeriod: ' + archiveOrganizationPeriod.id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const parentCategory = allCategories.find(c => c.groupIds.includes(previousGroupId));
|
|
190
|
+
if (parentCategory) {
|
|
191
|
+
return parentCategory;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function getStartIndex(group: Group) {
|
|
199
|
+
return await V1GroupMigrationData.select().where('oldGroupId', group.id).andWhereNot('newGroupId', group.id).count();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const cycleIfMigrated = -99;
|
|
203
|
+
|
|
204
|
+
async function migrateRegistrations({ organization, period, originalGroup, newGroup, cycle }: { organization: Organization; period: RegistrationPeriod; originalGroup: Group; newGroup: Group; cycle: number }, dryRun: boolean) {
|
|
205
|
+
// what for waiting lists of archive groups (previous cycles)?
|
|
206
|
+
let waitingList: Group | null = null;
|
|
207
|
+
|
|
208
|
+
const getOrCreateWaitingList = async () => {
|
|
209
|
+
if (newGroup.waitingListId) {
|
|
210
|
+
if (waitingList !== null) {
|
|
211
|
+
return waitingList;
|
|
212
|
+
}
|
|
213
|
+
const fetchedWaitingList = await Group.getByID(newGroup.waitingListId);
|
|
214
|
+
|
|
215
|
+
if (!fetchedWaitingList) {
|
|
216
|
+
throw new Error(`Waiting list not found: (waitingListId: ${newGroup.waitingListId}, groupId: ${newGroup.id})`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newWaitingList = new Group();
|
|
221
|
+
newWaitingList.cycle = cycleIfMigrated;
|
|
222
|
+
newWaitingList.type = GroupType.WaitingList;
|
|
223
|
+
newWaitingList.organizationId = organization.id;
|
|
224
|
+
newWaitingList.periodId = period.id;
|
|
225
|
+
newWaitingList.settings = GroupSettings.create({
|
|
226
|
+
name: TranslatedString.create($t(`%yh`) + ' ' + newGroup.settings.name.toString()),
|
|
227
|
+
period: period.getBaseStructure(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const migrationData = new V1WaitingListMigrationData();
|
|
231
|
+
migrationData.oldGroupId = originalGroup.id;
|
|
232
|
+
migrationData.oldCycle = cycle;
|
|
233
|
+
|
|
234
|
+
if (!dryRun) {
|
|
235
|
+
await newWaitingList.save();
|
|
236
|
+
migrationData.newGroupId = newWaitingList.id;
|
|
237
|
+
await migrationData.save();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
waitingList = newWaitingList;
|
|
241
|
+
return newWaitingList;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const registrations = await Registration.select()
|
|
245
|
+
.where('groupId', originalGroup.id)
|
|
246
|
+
.andWhere('cycle', cycle)
|
|
247
|
+
.fetch();
|
|
248
|
+
|
|
249
|
+
for (const registration of registrations) {
|
|
250
|
+
let invitation: RegistrationInvitation | null = null;
|
|
251
|
+
|
|
252
|
+
if (registration.waitingList) {
|
|
253
|
+
const waitingList = await getOrCreateWaitingList();
|
|
254
|
+
if (newGroup.waitingListId !== waitingList.id) {
|
|
255
|
+
newGroup.waitingListId = waitingList.id;
|
|
256
|
+
|
|
257
|
+
if (!dryRun) {
|
|
258
|
+
await newGroup.save();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
registration.groupId = waitingList.id;
|
|
263
|
+
|
|
264
|
+
// in V1 registeredAt is not set on waiting list registrations
|
|
265
|
+
registration.registeredAt = registration.createdAt;
|
|
266
|
+
|
|
267
|
+
if (registration.canRegister) {
|
|
268
|
+
// we should create an invitation
|
|
269
|
+
invitation = new RegistrationInvitation();
|
|
270
|
+
invitation.groupId = newGroup.id;
|
|
271
|
+
invitation.memberId = registration.memberId;
|
|
272
|
+
invitation.organizationId = organization.id;
|
|
273
|
+
invitation.createdAt = registration.createdAt;
|
|
274
|
+
|
|
275
|
+
// deprecated -> set to false
|
|
276
|
+
registration.canRegister = false;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
registration.groupId = newGroup.id;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
registration.periodId = period.id;
|
|
283
|
+
registration.cycle = cycle;
|
|
284
|
+
|
|
285
|
+
if (!dryRun) {
|
|
286
|
+
await registration.save();
|
|
287
|
+
if (invitation) {
|
|
288
|
+
try {
|
|
289
|
+
await invitation.save();
|
|
290
|
+
} catch (e) {
|
|
291
|
+
// do not throw if duplicate
|
|
292
|
+
if (e.code !== 'ER_DUP_ENTRY') {
|
|
293
|
+
throw e;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createPreviousGroup({ originalGroup, period, index }: { originalGroup: Group; period: RegistrationPeriod; index: number }) {
|
|
302
|
+
const newGroup = new Group();
|
|
303
|
+
newGroup.organizationId = originalGroup.organizationId;
|
|
304
|
+
newGroup.periodId = period.id;
|
|
305
|
+
newGroup.status = originalGroup.status;
|
|
306
|
+
newGroup.createdAt = originalGroup.createdAt;
|
|
307
|
+
newGroup.deletedAt = originalGroup.deletedAt;
|
|
308
|
+
|
|
309
|
+
const originalPrivateSettings: GroupPrivateSettings = originalGroup.privateSettings;
|
|
310
|
+
|
|
311
|
+
// todo: should group ids in permissions get updated?
|
|
312
|
+
newGroup.privateSettings = GroupPrivateSettings.create({
|
|
313
|
+
...originalPrivateSettings,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
newGroup.cycle = cycleIfMigrated;
|
|
317
|
+
|
|
318
|
+
const originalSettings: GroupSettings = originalGroup.settings;
|
|
319
|
+
|
|
320
|
+
const periodStartDate: Date = period.startDate;
|
|
321
|
+
const periodEndDate: Date = period.endDate;
|
|
322
|
+
|
|
323
|
+
// todo: how to choose start and end dates?
|
|
324
|
+
const startDate = new Date(periodStartDate);
|
|
325
|
+
const endDate = new Date(periodEndDate);
|
|
326
|
+
|
|
327
|
+
const isPlural = index > 0;
|
|
328
|
+
const extraName = isPlural ? `${index + 1} periodes geleden` : `${index + 1} periode geleden`;
|
|
329
|
+
|
|
330
|
+
newGroup.settings = GroupSettings
|
|
331
|
+
.create({
|
|
332
|
+
...originalSettings,
|
|
333
|
+
cycleSettings: new Map(),
|
|
334
|
+
name: new TranslatedString(`${originalSettings.name.toString()} (${extraName})`),
|
|
335
|
+
startDate,
|
|
336
|
+
endDate,
|
|
337
|
+
hasCustomDates: true,
|
|
338
|
+
period: period.getBaseStructure(),
|
|
339
|
+
});
|
|
340
|
+
newGroup.type = originalGroup.type;
|
|
341
|
+
|
|
342
|
+
return newGroup;
|
|
343
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Organization } from '@stamhoofd/models';
|
|
3
|
+
import { AccessRight, PermissionLevel, PermissionRoleDetailed, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName.toLowerCase() !== 'stamhoofd') {
|
|
13
|
+
console.log('skipped for platform (only runs for Stamhoofd): ' + STAMHOOFD.platformName);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (STAMHOOFD.userMode !== 'organization') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (STAMHOOFD.environment !== 'production') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
await start();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function start() {
|
|
29
|
+
await SeedTools.loop({
|
|
30
|
+
query: Organization.select(),
|
|
31
|
+
batchSize: 50,
|
|
32
|
+
useTransactionPerBatch: true,
|
|
33
|
+
action: async (organization: Organization) => {
|
|
34
|
+
const base = PermissionRoleDetailed.create({
|
|
35
|
+
level: PermissionLevel.None,
|
|
36
|
+
accessRights: [],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (organization.meta.packages.useMembers) {
|
|
40
|
+
base.accessRights.push(
|
|
41
|
+
AccessRight.MemberWriteFinancialData,
|
|
42
|
+
AccessRight.MemberManageNRN,
|
|
43
|
+
);
|
|
44
|
+
base.resources.set(PermissionsResourceType.RecordCategories, new Map([
|
|
45
|
+
['', ResourcePermissions.create({
|
|
46
|
+
level: PermissionLevel.Full,
|
|
47
|
+
})],
|
|
48
|
+
]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Senders
|
|
52
|
+
base.resources.set(PermissionsResourceType.Senders, new Map([
|
|
53
|
+
['', ResourcePermissions.create({
|
|
54
|
+
level: PermissionLevel.None,
|
|
55
|
+
accessRights: [
|
|
56
|
+
AccessRight.SendMessages,
|
|
57
|
+
],
|
|
58
|
+
})],
|
|
59
|
+
]));
|
|
60
|
+
|
|
61
|
+
for (const role of organization.privateMeta.roles) {
|
|
62
|
+
role.add(base);
|
|
63
|
+
}
|
|
64
|
+
await organization.save();
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|