@stamhoofd/models 2.115.0 → 2.116.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 (51) hide show
  1. package/dist/src/factories/MemberFactory.d.ts +3 -3
  2. package/dist/src/factories/MemberFactory.d.ts.map +1 -1
  3. package/dist/src/factories/MemberFactory.js.map +1 -1
  4. package/dist/src/migrations/1770917406-create-cached-balances-global-index.sql +2 -0
  5. package/dist/src/migrations/1770917407-create-member-name-ordering-index.sql +2 -0
  6. package/dist/src/migrations/1770917408-create-member-name-ordering-index-desc.sql +2 -0
  7. package/dist/src/migrations/1770917409-create-member-age-ordering-index.sql +2 -0
  8. package/dist/src/migrations/1770917410-create-member-age-ordering-index-desc.sql +2 -0
  9. package/dist/src/migrations/1770917411-drop-duplicate-member-indexes.sql +3 -0
  10. package/dist/src/models/Document.d.ts +3 -3
  11. package/dist/src/models/Document.d.ts.map +1 -1
  12. package/dist/src/models/Document.js +11 -7
  13. package/dist/src/models/Document.js.map +1 -1
  14. package/dist/src/models/DocumentTemplate.d.ts +4 -0
  15. package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
  16. package/dist/src/models/DocumentTemplate.js +33 -12
  17. package/dist/src/models/DocumentTemplate.js.map +1 -1
  18. package/dist/src/models/Group.d.ts +1 -5
  19. package/dist/src/models/Group.d.ts.map +1 -1
  20. package/dist/src/models/Group.js +1 -55
  21. package/dist/src/models/Group.js.map +1 -1
  22. package/dist/src/models/Member.d.ts +51 -10
  23. package/dist/src/models/Member.d.ts.map +1 -1
  24. package/dist/src/models/Member.js +209 -126
  25. package/dist/src/models/Member.js.map +1 -1
  26. package/dist/src/models/Organization.d.ts.map +1 -1
  27. package/dist/src/models/Organization.js +1 -0
  28. package/dist/src/models/Organization.js.map +1 -1
  29. package/dist/src/models/Platform.test.js +3 -3
  30. package/dist/src/models/Platform.test.js.map +1 -1
  31. package/dist/src/models/Registration.js +1 -1
  32. package/dist/src/models/Registration.js.map +1 -1
  33. package/dist/src/models/User.d.ts.map +1 -1
  34. package/dist/src/models/User.js +1 -0
  35. package/dist/src/models/User.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/factories/MemberFactory.ts +3 -3
  38. package/src/migrations/1770917406-create-cached-balances-global-index.sql +2 -0
  39. package/src/migrations/1770917407-create-member-name-ordering-index.sql +2 -0
  40. package/src/migrations/1770917408-create-member-name-ordering-index-desc.sql +2 -0
  41. package/src/migrations/1770917409-create-member-age-ordering-index.sql +2 -0
  42. package/src/migrations/1770917410-create-member-age-ordering-index-desc.sql +2 -0
  43. package/src/migrations/1770917411-drop-duplicate-member-indexes.sql +3 -0
  44. package/src/models/Document.ts +13 -10
  45. package/src/models/DocumentTemplate.ts +37 -13
  46. package/src/models/Group.ts +4 -71
  47. package/src/models/Member.ts +262 -156
  48. package/src/models/Organization.ts +2 -1
  49. package/src/models/Platform.test.ts +3 -3
  50. package/src/models/Registration.ts +1 -1
  51. package/src/models/User.ts +2 -1
@@ -1,12 +1,11 @@
1
- import { column, Database, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
1
+ import { column, Database, ManyToOneRelation } from '@simonbackx/simple-database';
2
2
  import { GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType, StockReservation } from '@stamhoofd/structures';
3
3
  import { v4 as uuidv4 } from 'uuid';
4
4
 
5
5
  import { ArrayDecoder } from '@simonbackx/simple-encoding';
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { QueryableModel } from '@stamhoofd/sql';
8
- import { Formatter } from '@stamhoofd/utility';
9
- import { Member, MemberWithRegistrations, OrganizationRegistrationPeriod, Payment, Registration, User } from './index.js';
7
+ import { ModelCache, QueryableModel } from '@stamhoofd/sql';
8
+ import { Member, OrganizationRegistrationPeriod, Payment, Registration, User } from './index.js';
10
9
 
11
10
  if (Member === undefined) {
12
11
  throw new Error('Import Member is undefined');
@@ -23,6 +22,7 @@ if (Registration === undefined) {
23
22
 
24
23
  export class Group extends QueryableModel {
25
24
  static table = 'groups';
25
+ // static cache = new ModelCache<Group>();
26
26
 
27
27
  @column({
28
28
  primary: true, type: 'string', beforeSave(value) {
@@ -135,73 +135,6 @@ export class Group extends QueryableModel {
135
135
  return [...map.values()];
136
136
  }
137
137
 
138
- /**
139
- * Fetch all members with their corresponding (valid) registrations, users
140
- */
141
- async getMembersWithRegistration(waitingList = false, cycleOffset = 0): Promise<MemberWithRegistrations[]> {
142
- let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()}, ${User.getDefaultSelect()} from \`${Member.table}\`\n`;
143
-
144
- query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`waitingList\` = 1)\n`;
145
-
146
- if (waitingList) {
147
- query += `JOIN \`${Registration.table}\` as reg_filter ON reg_filter.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND reg_filter.\`waitingList\` = 1\n`;
148
- }
149
- else {
150
- query += `JOIN \`${Registration.table}\` as reg_filter ON reg_filter.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND reg_filter.\`waitingList\` = 0 AND reg_filter.\`registeredAt\` is not null\n`;
151
- }
152
-
153
- query += Member.users.joinQuery(Member.table, User.table) + '\n';
154
-
155
- // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
156
- query += `where reg_filter.\`groupId\` = ? AND reg_filter.\`cycle\` = ?`;
157
-
158
- const [results] = await Database.select(query, [this.id, this.cycle - cycleOffset]);
159
- const members: MemberWithRegistrations[] = [];
160
-
161
- const groupIds = results.map(r => r[Registration.table]?.groupId).filter(id => id) as string[];
162
- const groups = await Group.getByIDs(...Formatter.uniqueArray(groupIds));
163
-
164
- for (const row of results) {
165
- const foundMember = Member.fromRow(row[Member.table]);
166
- if (!foundMember) {
167
- throw new Error('Expected member in every row');
168
- }
169
- const _f = foundMember.setManyRelation(Member.registrations as unknown as OneToManyRelation<'registrations', Member, Registration & { group: Group }>, []).setManyRelation(Member.users, []);
170
-
171
- // Seach if we already got this member?
172
- const existingMember = members.find(m => m.id == _f.id);
173
-
174
- const member: MemberWithRegistrations = (existingMember ?? _f);
175
- if (!existingMember) {
176
- members.push(member);
177
- }
178
-
179
- // Check if we have a registration with a payment
180
- const registration = Registration.fromRow(row[Registration.table]);
181
- if (registration) {
182
- // Check if we already have this registration
183
- if (!member.registrations.find(r => r.id == registration.id)) {
184
- const group = groups.find(g => g.id == registration.groupId);
185
- if (!group) {
186
- throw new Error('Expected group');
187
- }
188
- member.registrations.push(registration.setRelation(Registration.group, group));
189
- }
190
- }
191
-
192
- // Check if we have a user
193
- const user = User.fromRow(row[User.table]);
194
- if (user) {
195
- // Check if we already have this registration
196
- if (!member.users.find(r => r.id == user.id)) {
197
- member.users.push(user);
198
- }
199
- }
200
- }
201
-
202
- return members;
203
- }
204
-
205
138
  /**
206
139
  * @deprecated
207
140
  */
@@ -1,22 +1,32 @@
1
1
  import { column, Database, ManyToManyRelation, ManyToOneRelation, OneToManyRelation } from '@simonbackx/simple-database';
2
- import { QueryableModel, SQL } from '@stamhoofd/sql';
2
+ import { ModelCache, QueryableModel, SQL } from '@stamhoofd/sql';
3
3
  import { MemberDetails, NationalRegisterNumberOptOut, RegistrationWithTinyMember, TinyMember } from '@stamhoofd/structures';
4
4
  import { Formatter } from '@stamhoofd/utility';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
 
7
- import { Group, MemberResponsibilityRecord, Payment, Registration, User } from './index.js';
7
+ import { Group, MemberResponsibilityRecord, MemberUser, Payment, Registration, User } from './index.js';
8
8
  export type MemberWithUsers = Member & {
9
9
  users: User[];
10
10
  };
11
- export type MemberWithRegistrations = MemberWithUsers & {
12
- registrations: (Registration & { group: Group })[];
11
+ export type MemberWithRegistrations<R extends Registration = Registration> = Member & {
12
+ registrations: (R)[];
13
13
  };
14
+ export type RegistrationWithGroup = Registration & { group: Group };
15
+ export type MemberWithRegistrationsAndGroups = MemberWithRegistrations<RegistrationWithGroup>;
16
+ export type MemberWithUsersAndRegistrations<R extends Registration = Registration> = MemberWithUsers & MemberWithRegistrations<R>;
17
+
18
+ /**
19
+ * @deprecated
20
+ * For performance reasons, avoid loading the groups of registrations when not required. Use MemberWithRegistrations instead.
21
+ */
22
+ export type MemberWithUsersRegistrationsAndGroups = MemberWithUsers & MemberWithUsersAndRegistrations<RegistrationWithGroup>;
14
23
 
15
24
  // Defined here to prevent cycles
16
25
  export type RegistrationWithMember = Registration & { member: Member };
17
26
 
18
27
  export class Member extends QueryableModel {
19
28
  static table = 'members';
29
+ // static cache = new ModelCache<Member>();
20
30
 
21
31
  // #region Columns
22
32
  @column({
@@ -102,10 +112,51 @@ export class Member extends QueryableModel {
102
112
  /**
103
113
  * Fetch all members with their corresponding (valid) registration
104
114
  */
105
- static async getWithRegistrations(id: string): Promise<MemberWithRegistrations | null> {
115
+ static async getWithRegistrations(id: string): Promise<MemberWithUsersRegistrationsAndGroups | null> {
106
116
  return (await this.getBlobByIds(id))[0] ?? null;
107
117
  }
108
118
 
119
+ /**
120
+ * Fetch all members with their corresponding (valid) registration
121
+ */
122
+ static async getByIdWithRegistrationsAndGroups(id: string): Promise<MemberWithRegistrationsAndGroups | null> {
123
+ const member = await this.getByID(id);
124
+ if (!member) {
125
+ return null;
126
+ }
127
+ await this.loadRegistrations([member], true);
128
+ return member as MemberWithRegistrationsAndGroups;
129
+ }
130
+
131
+ /**
132
+ * Fetch all members with their corresponding (valid) registration
133
+ */
134
+ static async getByIdWithUsers(id: string): Promise<MemberWithUsers | null> {
135
+ const member = await this.getByID(id);
136
+ if (!member) {
137
+ return null;
138
+ }
139
+ await this.loadUsers([member]);
140
+ return member as MemberWithUsers;
141
+ }
142
+
143
+ /**
144
+ * Fetch all members with their corresponding (valid) registration
145
+ */
146
+ static async getByIdWithUsersAndRegistrations(id: string): Promise<MemberWithUsersAndRegistrations | null> {
147
+ const member = await this.getByID(id);
148
+ if (!member) {
149
+ return null;
150
+ }
151
+ await this.loadRegistrationsAndUsers([member]);
152
+ return member as MemberWithUsersAndRegistrations;
153
+ }
154
+
155
+ static async getByIdsWithUsers(...ids: string[]): Promise<MemberWithUsers[]> {
156
+ const members = await Member.getByIDs(...ids);
157
+ return await Member.loadUsers(members);
158
+ }
159
+
109
160
  /**
110
161
  * Fetch all registrations with members with their corresponding (valid) registrations
111
162
  */
@@ -113,195 +164,243 @@ export class Member extends QueryableModel {
113
164
  if (ids.length === 0) {
114
165
  return [];
115
166
  }
116
- let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
117
167
 
118
- query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`;
119
-
120
- // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
121
- query += `where \`${Registration.table}\`.\`${Registration.primary.name}\` IN (?)`;
122
-
123
- const [results] = await Database.select(query, [ids]);
124
- const registrations: RegistrationWithMember[] = [];
125
-
126
- // In the future we might add a 'reverse' method on manytoone relation, instead of defining the new relation. But then we need to store 2 model types in the many to one relation.
127
- const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
128
- registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
168
+ const registrations = await Registration.getByIDs(...ids);
169
+ await this.loadMembersForRegistrations(registrations);
170
+ return registrations as RegistrationWithMember[];
171
+ }
129
172
 
130
- for (const row of results) {
131
- const registration = Registration.fromRow(row[Registration.table]);
132
- if (!registration) {
133
- throw new Error('Expected registration in every row');
134
- }
173
+ /**
174
+ * Fetch all registrations with members with their corresponding (valid) registrations
175
+ */
176
+ static async loadMembersForRegistrations(registrations: Registration[]): Promise<RegistrationWithMember[]> {
177
+ if (registrations.length === 0) {
178
+ return [];
179
+ }
135
180
 
136
- const foundMember = Member.fromRow(row[Member.table]);
137
- if (!foundMember) {
138
- throw new Error('Expected member in every row');
181
+ const memberIds = Formatter.uniqueArray(registrations.map(r => r.memberId));
182
+ if (memberIds.length) {
183
+ // In the future we might add a 'reverse' method on manytoone relation, instead of defining the new relation. But then we need to store 2 model types in the many to one relation.
184
+ const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
185
+ registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
186
+
187
+ const members = await Member.getByIDs(...memberIds);
188
+ for (const registration of registrations) {
189
+ const member = members.find(m => m.id === registration.memberId);
190
+ if (member) {
191
+ registration.setRelation(registrationMemberRelation, member);
192
+ }
193
+ else {
194
+ throw new Error('Unexpected missing member for registration ' + registration.id);
195
+ }
139
196
  }
140
-
141
- const _f = registration.setRelation(registrationMemberRelation, foundMember);
142
- registrations.push(_f);
143
197
  }
144
198
 
145
- return registrations;
199
+ return registrations as RegistrationWithMember[];
146
200
  }
147
201
 
148
202
  /**
149
203
  * Fetch all registrations with members with their corresponding (valid) registrations
150
204
  */
151
205
  static async getRegistrationWithMembersForGroup(groupId: string): Promise<RegistrationWithMember[]> {
152
- let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
206
+ const registrations = await Registration.select()
207
+ .where('groupId', groupId)
208
+ .where('registeredAt', '!=', null)
209
+ .where('deactivatedAt', null)
210
+ .fetch();
153
211
 
154
- query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND \`${Registration.table}\`.\`registeredAt\` is not null AND \`${Registration.table}\`.\`deactivatedAt\` is null\n`;
212
+ return this.loadMembersForRegistrations(registrations);
213
+ }
155
214
 
156
- // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
157
- query += `where \`${Registration.table}\`.\`groupId\` = ?`;
215
+ static async loadRegistrations(members: Member[], withGroups?: false): Promise<MemberWithRegistrations[]>;
216
+ static async loadRegistrations(members: Member[], withGroups: true): Promise<MemberWithRegistrationsAndGroups[]>;
217
+ static async loadRegistrations(members: Member[], withGroups: boolean | undefined = true): Promise<MemberWithRegistrations[] | MemberWithRegistrationsAndGroups[]> {
218
+ if (members.length === 0) {
219
+ return members as MemberWithRegistrations[];
220
+ }
158
221
 
159
- const [results] = await Database.select(query, [groupId]);
160
- const registrations: RegistrationWithMember[] = [];
222
+ // Load relations
223
+ // Load registrations of these members
224
+ const loadAllFor: string[] = [];
225
+ const alreadyLoadedRegistrations: (Registration | undefined)[] = [];
161
226
 
162
- // In the future we might add a 'reverse' method on manytoone relation, instead of defining the new relation. But then we need to store 2 model types in the many to one relation.
163
- const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
164
- registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
227
+ for (const member of members) {
228
+ if ('registrations' in member && Array.isArray(member.registrations)) {
229
+ if (member.registrations.length === 0) {
230
+ // Nothing to do
231
+ continue;
232
+ }
165
233
 
166
- for (const row of results) {
167
- const registration = Registration.fromRow(row[Registration.table]);
168
- if (!registration) {
169
- throw new Error('Expected registration in every row');
234
+ alreadyLoadedRegistrations.push(...member.registrations as Registration[]);
235
+ continue;
170
236
  }
237
+ loadAllFor.push(member.id);
238
+ }
171
239
 
172
- const foundMember = Member.fromRow(row[Member.table]);
173
- if (!foundMember) {
174
- throw new Error('Expected member in every row');
240
+ const ids = Formatter.uniqueArray(loadAllFor);
241
+ if (ids.length) {
242
+ const registrations = await Registration.select()
243
+ .where('memberId', ids)
244
+ .where(
245
+ SQL.where('registeredAt', '!=', null),
246
+ )
247
+ .fetch() as (Registration | undefined)[];
248
+ alreadyLoadedRegistrations.push(...registrations);
249
+ }
250
+ else {
251
+ if (!withGroups) {
252
+ return members as MemberWithRegistrations[];
175
253
  }
176
-
177
- const _f = registration.setRelation(registrationMemberRelation, foundMember);
178
- registrations.push(_f);
179
254
  }
180
255
 
181
- return registrations;
182
- }
256
+ if (withGroups) {
257
+ const groupIds = Formatter.uniqueArray(alreadyLoadedRegistrations.map(r => r && 'group' in r ? undefined : r?.groupId));
258
+ if (groupIds.length) {
259
+ const groups = await Group.getByIDs(...groupIds);
183
260
 
184
- /**
185
- * Fetch all registrations with members with their corresponding (valid) registrations and payment
186
- */
187
- static async getRegistrationWithMembersForPayment(paymentId: string): Promise<RegistrationWithMember[]> {
188
- const { BalanceItem, BalanceItemPayment } = await import('./index.js');
189
-
190
- let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()} from \`${Member.table}\`\n`;
261
+ for (const [index, registration] of alreadyLoadedRegistrations.entries()) {
262
+ if ('group' in registration!) {
263
+ continue;
264
+ }
265
+ const group = groups.find(g => g.id === registration!.groupId);
266
+ if (group && !group.deletedAt) {
267
+ registration!.setRelation(Registration.group, group);
268
+ }
269
+ else {
270
+ // Remove registration from list
271
+ alreadyLoadedRegistrations[index] = undefined;
272
+ }
273
+ }
274
+ }
275
+ else {
276
+ if (ids.length === 0) {
277
+ // Nothing loaded
278
+ return members as MemberWithRegistrations[];
279
+ }
280
+ }
281
+ }
191
282
 
192
- query += `JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\`\n`;
283
+ for (const member of members) {
284
+ // Add registrations
285
+ const memberRegistrations = alreadyLoadedRegistrations.filter(r => !!r && r.memberId === member.id) as Registration[];
286
+ member.setManyRelation(Member.registrations, memberRegistrations);
287
+ }
193
288
 
194
- query += `LEFT JOIN \`${BalanceItem.table}\` ON \`${BalanceItem.table}\`.\`registrationId\` = \`${Registration.table}\`.\`${Registration.primary.name}\`\n`;
195
- query += `LEFT JOIN \`${BalanceItemPayment.table}\` ON \`${BalanceItemPayment.table}\`.\`${BalanceItemPayment.balanceItem.foreignKey}\` = \`${BalanceItem.table}\`.\`${BalanceItem.primary.name}\`\n`;
196
- query += `JOIN \`${Payment.table}\` ON \`${Payment.table}\`.\`${Payment.primary.name}\` = \`${BalanceItemPayment.table}\`.\`${BalanceItemPayment.payment.foreignKey}\`\n`;
289
+ return members as MemberWithRegistrations[];
290
+ }
197
291
 
198
- // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
199
- query += `WHERE \`${Payment.table}\`.\`${Payment.primary.name}\` = ?\n`;
200
- query += `GROUP BY \`${Registration.table}\`.\`${Registration.primary.name}\`, \`${Member.table}\`.\`${Member.primary.name}\``;
292
+ static async loadUsers(members: Member[]): Promise<MemberWithUsers[]> {
293
+ // Load relations
294
+ // Load registrations of these members
295
+ const ids = Formatter.uniqueArray(members.map(m => ('users' in m) ? undefined : m.id));
296
+ if (ids.length === 0) {
297
+ return members as MemberWithUsers[];
298
+ }
201
299
 
202
- const [results] = await Database.select(query, [paymentId]);
203
- const registrations: RegistrationWithMember[] = [];
300
+ const users = await User.select(
301
+ SQL.wildcard(User.table),
302
+ SQL.column(MemberUser.table, 'membersId'),
303
+ )
304
+ .join(
305
+ SQL.join(MemberUser.table)
306
+ .where(
307
+ SQL.parentColumn('id'),
308
+ SQL.column('usersId'),
309
+ ),
310
+ )
311
+ .where(SQL.column(MemberUser.table, 'membersId'), ids)
312
+ .fetch();
313
+
314
+ for (const member of members) {
315
+ if ('users' in member) {
316
+ // Was already loaded
317
+ continue;
318
+ }
319
+ // Add registrations
320
+ // Add users
321
+ const memberUsers = users.filter((u) => {
322
+ const memberId = u.rawSelectedRow?.[MemberUser.table]?.['membersId'];
323
+ if (memberId) {
324
+ return memberId === member.id;
325
+ }
326
+ return false;
327
+ });
328
+ member.setManyRelation(Member.users, memberUsers);
329
+ }
204
330
 
205
- // In the future we might add a 'reverse' method on manytoone relation, instead of defining the new relation. But then we need to store 2 model types in the many to one relation.
206
- const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
207
- registrationMemberRelation.foreignKey = Member.registrations.foreignKey;
331
+ return members as MemberWithUsers[];
332
+ }
208
333
 
209
- for (const row of results) {
210
- const registration = Registration.fromRow(row[Registration.table]);
211
- if (!registration) {
212
- throw new Error('Expected registration in every row');
334
+ static unloadUsers(members: Member[]) {
335
+ for (const member of members) {
336
+ if ('users' in member) {
337
+ delete member.users;
213
338
  }
339
+ }
340
+ }
214
341
 
215
- const foundMember = Member.fromRow(row[Member.table]);
216
- if (!foundMember) {
217
- throw new Error('Expected member in every row');
342
+ static unloadRegistrations(members: Member[]) {
343
+ for (const member of members) {
344
+ if ('registrations' in member) {
345
+ delete member.registrations;
218
346
  }
219
-
220
- const _f = registration.setRelation(registrationMemberRelation, foundMember);
221
- registrations.push(_f);
222
347
  }
348
+ }
223
349
 
224
- return registrations;
350
+ /**
351
+ * Fetch all members with their corresponding (valid) registrations, users
352
+ */
353
+ static async loadRegistrationsAndUsers(members: Member[], withGroups?: false): Promise<MemberWithUsersAndRegistrations[]>;
354
+ static async loadRegistrationsAndUsers(members: Member[], withGroups: true): Promise<MemberWithUsersRegistrationsAndGroups[]>;
355
+ static async loadRegistrationsAndUsers(members: Member[], withGroups: boolean | undefined = true): Promise<MemberWithUsersAndRegistrations[] | MemberWithUsersRegistrationsAndGroups[]> {
356
+ await this.loadRegistrations(members, withGroups as true);
357
+ await this.loadUsers(members);
358
+ return members as MemberWithUsersAndRegistrations[];
225
359
  }
226
360
 
227
361
  /**
228
362
  * Fetch all members with their corresponding (valid) registrations, users
229
363
  */
230
- static async getBlobByIds(...ids: string[]): Promise<MemberWithRegistrations[]> {
364
+ static async getBlobByIds(...ids: string[]): Promise<MemberWithUsersRegistrationsAndGroups[]> {
231
365
  if (ids.length == 0) {
232
366
  return [];
233
367
  }
234
- let query = `SELECT ${Member.getDefaultSelect()}, ${Registration.getDefaultSelect()}, ${User.getDefaultSelect()} from \`${Member.table}\`\n`;
235
- query += `LEFT JOIN \`${Registration.table}\` ON \`${Registration.table}\`.\`${Member.registrations.foreignKey}\` = \`${Member.table}\`.\`${Member.primary.name}\` AND (\`${Registration.table}\`.\`registeredAt\` is not null OR \`${Registration.table}\`.\`canRegister\` = 1)\n`;
236
- query += Member.users.joinQuery(Member.table, User.table) + '\n';
237
-
238
- // We do an extra join because we also need to get the other registrations of each member (only one regitration has to match the query)
239
- query += `where \`${Member.table}\`.\`${Member.primary.name}\` IN (?)`;
240
-
241
- const [results] = await Database.select(query, [ids]);
242
- const members: MemberWithRegistrations[] = [];
243
-
244
- // Load groups
245
- const groupIds = results.map(r => r[Registration.table]?.groupId).filter(id => id) as string[];
246
- const groups = await Group.getByIDs(...Formatter.uniqueArray(groupIds));
247
-
248
- for (const row of results) {
249
- const foundMember = Member.fromRow(row[Member.table]);
250
- if (!foundMember) {
251
- throw new Error('Expected member in every row');
252
- }
253
- const _f = foundMember
254
- .setManyRelation(Member.registrations as unknown as OneToManyRelation<'registrations', Member, Registration & { group: Group }>, [])
255
- .setManyRelation(Member.users, []);
256
368
 
257
- // Seach if we already got this member?
258
- const existingMember = members.find(m => m.id == _f.id);
369
+ const baseMembers = await this.getByIDs(...ids);
259
370
 
260
- const member: MemberWithRegistrations = (existingMember ?? _f);
261
- if (!existingMember) {
262
- members.push(member);
263
- }
264
-
265
- // Check if we have a registration with a payment
266
- const registration = Registration.fromRow(row[Registration.table]);
267
- if (registration) {
268
- // Check if we already have this registration
269
- if (!member.registrations.find(r => r.id == registration.id)) {
270
- const g = groups.find(g => g.id == registration.groupId);
271
- if (!g) {
272
- throw new Error('Group not found');
273
- }
274
- if (g.deletedAt === null) {
275
- member.registrations.push(registration.setRelation(Registration.group, g));
276
- }
277
- }
278
- }
279
-
280
- // Check if we have a user
281
- const user = User.fromRow(row[User.table]);
282
- if (user) {
283
- // Check if we already have this registration
284
- if (!member.users.find(r => r.id == user.id)) {
285
- member.users.push(user);
286
- }
287
- }
371
+ if (baseMembers.length === 0) {
372
+ return [];
288
373
  }
289
374
 
290
- return members;
375
+ await this.loadRegistrationsAndUsers(baseMembers, true);
376
+ return baseMembers as MemberWithUsersRegistrationsAndGroups[];
291
377
  }
292
378
 
293
379
  /**
294
380
  * Fetch all members with their corresponding (valid) registrations and payment
295
381
  */
296
- static async getFamilyWithRegistrations(id: string): Promise<MemberWithRegistrations[]> {
297
- let query = `SELECT l2.membersId as id from _members_users l1\n`;
298
- query += `JOIN _members_users l2 on l2.usersId = l1.usersId \n`;
299
- query += `where l1.membersId = ? group by l2.membersId`;
382
+ static async getFamily(id: string): Promise<Member[]> {
383
+ const results = await SQL.select(
384
+ SQL.column('l2', 'membersId'),
385
+ )
386
+ .from('_members_users', 'l1')
387
+ .join(
388
+ SQL.join('_members_users', 'l2')
389
+ .where(
390
+ SQL.column('l2', 'usersId'),
391
+ SQL.column('l1', 'usersId'),
392
+ ),
393
+ )
394
+ .where(
395
+ SQL.column('l1', 'membersId'),
396
+ id,
397
+ )
398
+ .groupBy(SQL.column('l2', 'membersId'))
399
+ .fetch();
300
400
 
301
- const [results] = await Database.select(query, [id]);
302
401
  const ids: string[] = [];
303
402
  for (const row of results) {
304
- ids.push(row['l2']['id'] as string);
403
+ ids.push(row['l2']['membersId'] as string);
305
404
  }
306
405
 
307
406
  if (!ids.includes(id)) {
@@ -309,7 +408,16 @@ export class Member extends QueryableModel {
309
408
  ids.push(id);
310
409
  }
311
410
 
312
- return await this.getBlobByIds(...ids);
411
+ return await this.getByIDs(...ids);
412
+ }
413
+
414
+ /**
415
+ * @deprecated Please avoid implicit relation loading and only load the rel
416
+ * Fetch all members with their corresponding (valid) registrations and payment
417
+ */
418
+ static async getFamilyWithRegistrations(id: string): Promise<MemberWithUsersRegistrationsAndGroups[]> {
419
+ const members = await this.getFamily(id);
420
+ return await this.loadRegistrationsAndUsers(members, true);
313
421
  }
314
422
 
315
423
  /**
@@ -317,28 +425,26 @@ export class Member extends QueryableModel {
317
425
  */
318
426
  static async getMemberIdsForUser(user: User): Promise<string[]> {
319
427
  const query = SQL
320
- .select('id')
321
- .from(Member.table)
322
- .join(
323
- SQL
324
- .leftJoin('_members_users')
325
- .where(
326
- SQL.column('_members_users', 'membersId'),
327
- SQL.column(Member.table, 'id'),
328
- ),
329
- ).where(
330
- SQL.column('_members_users', 'usersId'),
331
- user.id,
332
- );
428
+ .select('membersId')
429
+ .from('_members_users')
430
+ .where('usersId', user.id);
333
431
 
334
432
  const data = await query.fetch();
335
- return Formatter.uniqueArray(data.map(r => r.members.id as string));
433
+ return Formatter.uniqueArray(data.map(r => r._members_users.membersId as string));
434
+ }
435
+
436
+ /**
437
+ * Fetch all members with their corresponding (valid) registrations or waiting lists and payments
438
+ */
439
+ static async getMembersForUser(user: User): Promise<Member[]> {
440
+ return this.getByIDs(...(await this.getMemberIdsForUser(user)));
336
441
  }
337
442
 
338
443
  /**
444
+ * @deprecated Use getMembersForUser and load relations as needed
339
445
  * Fetch all members with their corresponding (valid) registrations or waiting lists and payments
340
446
  */
341
- static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithRegistrations[]> {
447
+ static async getMembersWithRegistrationForUser(user: User): Promise<MemberWithUsersRegistrationsAndGroups[]> {
342
448
  return this.getBlobByIds(...(await this.getMemberIdsForUser(user)));
343
449
  }
344
450
 
@@ -2,7 +2,7 @@ import { column, Database } from '@simonbackx/simple-database';
2
2
  import { SimpleError } from '@simonbackx/simple-errors';
3
3
  import { I18n } from '@stamhoofd/backend-i18n';
4
4
  import { EmailInterfaceRecipient } from '@stamhoofd/email';
5
- import { QueryableModel } from '@stamhoofd/sql';
5
+ import { ModelCache, QueryableModel } from '@stamhoofd/sql';
6
6
  import { Address, appToUri, Country, DNSRecordStatus, EmailTemplateType, Language, OrganizationEmail, OrganizationMetaData, OrganizationPrivateMetaData, Organization as OrganizationStruct, PaymentMethod, PaymentProvider, PrivatePaymentConfiguration, Recipient, Replacement, STPackageType, TransferSettings } from '@stamhoofd/structures';
7
7
 
8
8
  import { CreateEmailIdentityCommand, DeleteEmailIdentityCommand, GetEmailIdentityCommand, GetEmailIdentityCommandOutput, PutEmailIdentityFeedbackAttributesCommand, PutEmailIdentityMailFromAttributesCommand, SESv2Client } from '@aws-sdk/client-sesv2';
@@ -18,6 +18,7 @@ import { OrganizationRegistrationPeriod, StripeAccount } from './index.js';
18
18
 
19
19
  export class Organization extends QueryableModel {
20
20
  static table = 'organizations';
21
+ // static cache = new ModelCache<Organization>();
21
22
 
22
23
  @column({
23
24
  primary: true, type: 'string', beforeSave(value) {