@stamhoofd/models 2.74.0 → 2.75.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 (60) hide show
  1. package/dist/src/factories/BalanceItemFactory.d.ts +25 -0
  2. package/dist/src/factories/BalanceItemFactory.d.ts.map +1 -0
  3. package/dist/src/factories/BalanceItemFactory.js +52 -0
  4. package/dist/src/factories/BalanceItemFactory.js.map +1 -0
  5. package/dist/src/factories/GroupFactory.d.ts +0 -2
  6. package/dist/src/factories/GroupFactory.d.ts.map +1 -1
  7. package/dist/src/factories/GroupFactory.js +0 -4
  8. package/dist/src/factories/GroupFactory.js.map +1 -1
  9. package/dist/src/index.d.ts +1 -0
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1 -0
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/migrations/1739017508-event-notifications.sql +25 -0
  14. package/dist/src/migrations/1739017509-event-notifications-link.sql +10 -0
  15. package/dist/src/migrations/1739282590-event-notifications-submitted-at.sql +3 -0
  16. package/dist/src/models/DocumentTemplate.d.ts.map +1 -1
  17. package/dist/src/models/DocumentTemplate.js +3 -1
  18. package/dist/src/models/DocumentTemplate.js.map +1 -1
  19. package/dist/src/models/EventNotification.d.ts +27 -0
  20. package/dist/src/models/EventNotification.d.ts.map +1 -0
  21. package/dist/src/models/EventNotification.js +95 -0
  22. package/dist/src/models/EventNotification.js.map +1 -0
  23. package/dist/src/models/Image.d.ts +1 -1
  24. package/dist/src/models/Image.d.ts.map +1 -1
  25. package/dist/src/models/Image.js +14 -2
  26. package/dist/src/models/Image.js.map +1 -1
  27. package/dist/src/models/MemberPlatformMembership.d.ts +1 -1
  28. package/dist/src/models/MemberPlatformMembership.d.ts.map +1 -1
  29. package/dist/src/models/MemberPlatformMembership.js +9 -2
  30. package/dist/src/models/MemberPlatformMembership.js.map +1 -1
  31. package/dist/src/models/MemberResponsibilityRecord.d.ts +1 -0
  32. package/dist/src/models/MemberResponsibilityRecord.d.ts.map +1 -1
  33. package/dist/src/models/MemberResponsibilityRecord.js +3 -0
  34. package/dist/src/models/MemberResponsibilityRecord.js.map +1 -1
  35. package/dist/src/models/Organization.js +1 -1
  36. package/dist/src/models/Organization.js.map +1 -1
  37. package/dist/src/models/User.d.ts +4 -1
  38. package/dist/src/models/User.d.ts.map +1 -1
  39. package/dist/src/models/User.js +31 -11
  40. package/dist/src/models/User.js.map +1 -1
  41. package/dist/src/models/index.d.ts +1 -0
  42. package/dist/src/models/index.d.ts.map +1 -1
  43. package/dist/src/models/index.js +1 -0
  44. package/dist/src/models/index.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +4 -4
  47. package/src/factories/BalanceItemFactory.ts +57 -0
  48. package/src/factories/GroupFactory.ts +1 -6
  49. package/src/index.ts +1 -0
  50. package/src/migrations/1739017508-event-notifications.sql +25 -0
  51. package/src/migrations/1739017509-event-notifications-link.sql +10 -0
  52. package/src/migrations/1739282590-event-notifications-submitted-at.sql +3 -0
  53. package/src/models/DocumentTemplate.ts +3 -1
  54. package/src/models/EventNotification.ts +77 -0
  55. package/src/models/Image.ts +15 -2
  56. package/src/models/MemberPlatformMembership.ts +11 -2
  57. package/src/models/MemberResponsibilityRecord.ts +5 -1
  58. package/src/models/Organization.ts +1 -1
  59. package/src/models/User.ts +53 -14
  60. package/src/models/index.ts +1 -0
@@ -0,0 +1,25 @@
1
+ CREATE TABLE `event_notifications` (
2
+ `id` varchar(36) NOT NULL DEFAULT '',
3
+ `typeId` varchar(36) NOT NULL,
4
+ `periodId` varchar(36) NOT NULL,
5
+ `startDate` datetime NOT NULL,
6
+ `endDate` datetime NOT NULL,
7
+ `status` varchar(36) NOT NULL,
8
+ `feedbackText` text NULL,
9
+ `organizationId` varchar(36) DEFAULT NULL,
10
+ `recordAnswers` json NOT NULL DEFAULT ('{"value": {}, "version": 0}'),
11
+ `createdBy` varchar(36) DEFAULT NULL,
12
+ `submittedBy` varchar(36) DEFAULT NULL,
13
+ `createdAt` datetime NOT NULL,
14
+ `updatedAt` datetime NOT NULL,
15
+ PRIMARY KEY (`id`),
16
+ KEY `organizationId` (`organizationId`),
17
+ KEY `periodId` (`periodId`),
18
+ KEY `createdBy` (`createdBy`),
19
+ KEY `submittedBy` (`submittedBy`),
20
+ KEY `startDate` (`startDate`) USING BTREE,
21
+ CONSTRAINT `organizationId` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
22
+ CONSTRAINT `createdBy` FOREIGN KEY (`createdBy`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
23
+ CONSTRAINT `submittedBy` FOREIGN KEY (`submittedBy`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,
24
+ CONSTRAINT `periodId` FOREIGN KEY (`periodId`) REFERENCES `registration_periods` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
25
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
@@ -0,0 +1,10 @@
1
+ CREATE TABLE `_event_notifications_events` (
2
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
3
+ `event_notificationsId` varchar(36) NOT NULL,
4
+ `eventsId` varchar(36) NOT NULL,
5
+ PRIMARY KEY (`id`),
6
+ KEY `event_notificationsId` (`event_notificationsId`),
7
+ KEY `eventsId` (`eventsId`),
8
+ CONSTRAINT `event_notificationsId` FOREIGN KEY (`event_notificationsId`) REFERENCES `event_notifications` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
9
+ CONSTRAINT `eventsId` FOREIGN KEY (`eventsId`) REFERENCES `events` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
10
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
@@ -0,0 +1,3 @@
1
+ ALTER TABLE `event_notifications`
2
+ ADD COLUMN `submittedAt` datetime NULL AFTER `submittedBy`,
3
+ ADD INDEX `submittedAt` (`submittedAt` DESC) USING BTREE;
@@ -432,7 +432,9 @@ export class DocumentTemplate extends QueryableModel {
432
432
  for (const document of existingDocuments) {
433
433
  await this.updateDocumentWithAnswers(document, fieldAnswers);
434
434
  document.data.name = this.settings.name;
435
- document.data.description = description;
435
+ if (existingDocuments.length === 1) {
436
+ document.data.description = description;
437
+ }
436
438
  if (document.status === DocumentStatus.Draft || document.status === DocumentStatus.Published) {
437
439
  document.status = this.status;
438
440
  }
@@ -0,0 +1,77 @@
1
+ import { column, ManyToManyRelation } from '@simonbackx/simple-database';
2
+ import { MapDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
+ import { QueryableModel } from '@stamhoofd/sql';
4
+ import { EventNotificationStatus, RecordAnswer, RecordAnswerDecoder } from '@stamhoofd/structures';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { Event } from './Event';
7
+
8
+ export class EventNotification extends QueryableModel {
9
+ static table = 'event_notifications';
10
+
11
+ @column({ primary: true, type: 'string', beforeSave(value) {
12
+ return value ?? uuidv4();
13
+ } })
14
+ id!: string;
15
+
16
+ @column({ type: 'string' })
17
+ typeId: string;
18
+
19
+ @column({ type: 'string' })
20
+ periodId: string;
21
+
22
+ @column({ type: 'datetime' })
23
+ startDate: Date;
24
+
25
+ @column({ type: 'datetime' })
26
+ endDate: Date;
27
+
28
+ @column({ type: 'string' })
29
+ status = EventNotificationStatus.Draft;
30
+
31
+ /**
32
+ * Feedback on a review, e.g. when it is declined (explains which changes need to be made).
33
+ * It is only visible when the status is not 'accepted' or 'pending'.
34
+ */
35
+ @column({ type: 'string', nullable: true })
36
+ feedbackText: string | null = null;
37
+
38
+ @column({ type: 'string' })
39
+ organizationId: string;
40
+
41
+ @column({ type: 'json', decoder: new MapDecoder(StringDecoder, RecordAnswerDecoder) })
42
+ recordAnswers: Map<string, RecordAnswer> = new Map();
43
+
44
+ @column({ type: 'string', nullable: true })
45
+ createdBy: string | null = null;
46
+
47
+ @column({ type: 'string', nullable: true })
48
+ submittedBy: string | null = null;
49
+
50
+ @column({ type: 'datetime', nullable: true })
51
+ submittedAt: Date | null = null;
52
+
53
+ @column({
54
+ type: 'datetime', beforeSave(old?: any) {
55
+ if (old !== undefined) {
56
+ return old;
57
+ }
58
+ const date = new Date();
59
+ date.setMilliseconds(0);
60
+ return date;
61
+ },
62
+ })
63
+ createdAt: Date;
64
+
65
+ @column({
66
+ type: 'datetime', beforeSave() {
67
+ const date = new Date();
68
+ date.setMilliseconds(0);
69
+ return date;
70
+ },
71
+ skipUpdate: true,
72
+ })
73
+ updatedAt: Date;
74
+
75
+ // Note: all relations should point to their parents, not the other way around to avoid reference cycles
76
+ static events = new ManyToManyRelation(EventNotification, Event, 'events');
77
+ }
@@ -24,7 +24,7 @@ export class Image extends QueryableModel {
24
24
  @column({ type: 'datetime' })
25
25
  createdAt: Date = new Date();
26
26
 
27
- static async create(fileContent: string | Buffer, type: string | undefined, resolutions: ResolutionRequest[]): Promise<Image> {
27
+ static async create(fileContent: string | Buffer, type: string | undefined, resolutions: ResolutionRequest[], isPrivateFile: boolean = false): Promise<Image> {
28
28
  if (!STAMHOOFD.SPACES_BUCKET || !STAMHOOFD.SPACES_ENDPOINT || !STAMHOOFD.SPACES_KEY || !STAMHOOFD.SPACES_SECRET) {
29
29
  throw new SimpleError({
30
30
  code: 'not_available',
@@ -105,7 +105,7 @@ export class Image extends QueryableModel {
105
105
  Key: key,
106
106
  Body: f.data,
107
107
  ContentType: !supportsTransparency ? 'image/jpeg' : 'image/png',
108
- ACL: 'public-read',
108
+ ACL: isPrivateFile ? 'private' : 'public-read',
109
109
  };
110
110
 
111
111
  uploadPromises.push(s3.putObject(params).promise());
@@ -115,8 +115,20 @@ export class Image extends QueryableModel {
115
115
  server: 'https://' + STAMHOOFD.SPACES_BUCKET + '.' + STAMHOOFD.SPACES_ENDPOINT,
116
116
  path: key,
117
117
  size: f.info.size,
118
+ isPrivate: isPrivateFile,
118
119
  });
119
120
 
121
+ if (isPrivateFile) {
122
+ if (!await _file.sign()) {
123
+ throw new SimpleError({
124
+ code: 'failed_to_sign',
125
+ message: 'Failed to sign file',
126
+ human: $t('Er ging iet mis bij het uploaden van jouw bestand. Probeer het later opnieuw (foutcode: SIGN).'),
127
+ statusCode: 500,
128
+ });
129
+ }
130
+ }
131
+
120
132
  const _image = new Resolution({
121
133
  file: _file,
122
134
  width: f.info.width,
@@ -142,6 +154,7 @@ export class Image extends QueryableModel {
142
154
  server: 'https://' + STAMHOOFD.SPACES_BUCKET + '.' + STAMHOOFD.SPACES_ENDPOINT,
143
155
  path: key,
144
156
  size: fileContent.length,
157
+ // Don't set private here, as we don't allow to download this file
145
158
  });
146
159
 
147
160
  uploadPromises.push(s3.putObject(params).promise());
@@ -108,8 +108,12 @@ export class MemberPlatformMembership extends QueryableModel {
108
108
  @column({ type: 'boolean' })
109
109
  locked = false;
110
110
 
111
- canDelete() {
112
- if (this.balanceItemId) {
111
+ canDelete(hasPlatformFullAccess = false) {
112
+ if (this.locked) {
113
+ return false;
114
+ }
115
+
116
+ if (this.balanceItemId && !hasPlatformFullAccess) {
113
117
  return false;
114
118
  }
115
119
  return true;
@@ -150,6 +154,11 @@ export class MemberPlatformMembership extends QueryableModel {
150
154
  }
151
155
 
152
156
  async calculatePrice(member: Member, registration?: Registration) {
157
+ if (this.locked) {
158
+ // price of locked membership cannot be changed
159
+ return;
160
+ }
161
+
153
162
  const platform = await Platform.getSharedPrivateStruct();
154
163
  const membershipType = platform.config.membershipTypes.find(m => m.id === this.membershipTypeId);
155
164
 
@@ -1,5 +1,5 @@
1
1
  import { column } from '@simonbackx/simple-database';
2
- import { QueryableModel } from '@stamhoofd/sql';
2
+ import { QueryableModel, SQL } from '@stamhoofd/sql';
3
3
  import { Group as GroupStruct, MemberResponsibilityRecordBase, MemberResponsibilityRecord as MemberResponsibilityRecordStruct } from '@stamhoofd/structures';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
5
 
@@ -53,4 +53,8 @@ export class MemberResponsibilityRecord extends QueryableModel {
53
53
  group,
54
54
  });
55
55
  }
56
+
57
+ static get whereActive() {
58
+ return SQL.where('startDate', '<', new Date()).and(SQL.where('endDate', null).or('endDate', '>', new Date()));
59
+ }
56
60
  }
@@ -806,7 +806,7 @@ export class Organization extends QueryableModel {
806
806
  async getAdmins() {
807
807
  // Circular reference fix
808
808
  const User = (await import('./User')).User;
809
- return await User.getAdmins([this.id], { verified: true });
809
+ return await User.getAdmins(this.id, { verified: true });
810
810
  }
811
811
 
812
812
  /**
@@ -1,6 +1,6 @@
1
1
  import { column, Database, ManyToOneRelation } from '@simonbackx/simple-database';
2
2
  import { EmailInterfaceRecipient } from '@stamhoofd/email';
3
- import { QueryableModel } from '@stamhoofd/sql';
3
+ import { QueryableModel, SQL, SQLExpression, SQLJsonExtract } from '@stamhoofd/sql';
4
4
  import { LoginProviderType, NewUser, Permissions, Recipient, Replacement, UserMeta, UserPermissions, User as UserStruct } from '@stamhoofd/structures';
5
5
  import argon2 from 'argon2';
6
6
  import { v4 as uuidv4 } from 'uuid';
@@ -117,15 +117,52 @@ export class User extends QueryableModel {
117
117
  });
118
118
  }
119
119
 
120
- static async getAdmins(organizationIds: string[], options?: { verified?: boolean }) {
121
- if (organizationIds.length == 0) {
122
- return [];
120
+ static async getPlatformAdmins(options?: { verified?: boolean }) {
121
+ // Custom implementation
122
+ const q = User.select()
123
+ .where('organizationId', null)
124
+ .where('permissions', '!=', null)
125
+ .where(
126
+ SQL.jsonValue(SQL.column('permissions'), '$.value.globalPermissions'),
127
+ '!=',
128
+ null,
129
+ )
130
+ .where('id', '!=', '1');
131
+
132
+ if (options?.verified !== undefined) {
133
+ q.where('verified', options.verified);
123
134
  }
124
135
 
136
+ let global = await q.fetch();
137
+
138
+ // Hide api accounts
139
+ global = global.filter(a => !a.isApiUser);
140
+
141
+ return global;
142
+ }
143
+
144
+ static async getAdmins(organizationId: string, options?: { verified?: boolean }) {
125
145
  if (STAMHOOFD.userMode === 'platform') {
126
146
  // Custom implementation
127
- let global = (await User.where({ organizationId: null, permissions: { sign: '!=', value: null } }));
128
- global = global.filter(u => organizationIds.find(organizationId => u.permissions?.organizationPermissions.has(organizationId)));
147
+ const q = User.select()
148
+ .where('organizationId', null)
149
+ .where('permissions', '!=', null)
150
+ .where(
151
+ SQL.jsonValue(
152
+ SQL.jsonValue(SQL.column('permissions'), '$.value.organizationPermissions'),
153
+ '$."' + organizationId + '"',
154
+ true,
155
+ ),
156
+ '!=',
157
+ null,
158
+ )
159
+ ;
160
+
161
+ if (options?.verified !== undefined) {
162
+ q.where('verified', options.verified);
163
+ }
164
+
165
+ let global = await q.fetch();
129
166
 
130
167
  // Hide api accounts
131
168
  global = global.filter(a => !a.isApiUser);
@@ -133,19 +170,21 @@ export class User extends QueryableModel {
133
170
  return global;
134
171
  }
135
172
 
136
- const query: any = {
137
- permissions: { sign: '!=', value: null },
138
- organizationId: { sign: 'IN', value: organizationIds },
139
- };
173
+ const q = User.select()
174
+ .where('organizationId', organizationId)
175
+ .where('permissions', '!=', null)
176
+ ;
140
177
 
141
178
  if (options?.verified !== undefined) {
142
- query.verified = options.verified;
179
+ q.where('verified', options.verified);
143
180
  }
144
181
 
145
- return (
182
+ let global = await q.fetch();
183
+
184
+ // Hide api accounts
185
+ global = global.filter(a => !a.isApiUser);
146
186
 
147
- await User.where(query)
148
- ).filter(a => !a.isApiUser);
187
+ return global;
149
188
  }
150
189
 
151
190
  static async getApiUsers(organizationIds: string[]) {
@@ -51,6 +51,7 @@ export * from './MemberPlatformMembership';
51
51
  export * from './Email';
52
52
  export * from './EmailRecipient';
53
53
  export * from './Event';
54
+ export * from './EventNotification';
54
55
  export * from './CachedBalance';
55
56
  export * from './AuditLog';
56
57
  export * from './MemberUser';