@stamhoofd/backend 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 (47) hide show
  1. package/index.ts +7 -2
  2. package/package.json +13 -13
  3. package/src/crons/update-cached-balances.ts +1 -2
  4. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -15
  6. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +2 -2
  7. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +43 -0
  8. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +181 -0
  9. package/src/endpoints/global/events/GetEventsEndpoint.ts +2 -2
  10. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +288 -0
  11. package/src/endpoints/global/events/PatchEventsEndpoint.ts +2 -2
  12. package/src/endpoints/global/files/UploadFile.ts +56 -4
  13. package/src/endpoints/global/files/UploadImage.ts +9 -3
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +2 -2
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +10 -1
  16. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -5
  17. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -0
  18. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +1 -1
  19. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +1756 -164
  20. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -2
  21. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +48 -2
  22. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +2 -2
  23. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  24. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +2 -2
  25. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +2 -2
  26. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +8 -0
  27. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +3 -3
  28. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +2 -2
  29. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +2 -2
  30. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +1 -2
  31. package/src/helpers/AdminPermissionChecker.ts +80 -2
  32. package/src/helpers/AuthenticatedStructures.ts +88 -2
  33. package/src/helpers/FlagMomentCleanup.ts +1 -8
  34. package/src/helpers/GlobalHelper.ts +15 -0
  35. package/src/helpers/MembershipCharger.ts +2 -1
  36. package/src/services/EventNotificationService.ts +201 -0
  37. package/src/services/FileSignService.ts +227 -0
  38. package/src/sql-filters/event-notifications.ts +39 -0
  39. package/src/sql-filters/organizations.ts +1 -1
  40. package/src/sql-sorters/event-notifications.ts +96 -0
  41. package/src/sql-sorters/events.ts +2 -2
  42. package/src/sql-sorters/organizations.ts +2 -2
  43. package/tests/e2e/private-files.test.ts +497 -0
  44. package/tests/e2e/register.test.ts +762 -0
  45. package/tests/helpers/TestServer.ts +3 -0
  46. package/tests/jest.setup.ts +15 -2
  47. package/tsconfig.json +1 -0
@@ -0,0 +1,201 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { EventNotification, Member, MemberResponsibilityRecord, Organization, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
3
+ import { EmailTemplateType, PermissionLevel, Recipient, RecordCategory, Replacement } from '@stamhoofd/structures';
4
+ import { Formatter } from '@stamhoofd/utility';
5
+ import { AdminPermissionChecker } from '../helpers/AdminPermissionChecker';
6
+ import { Context } from '../helpers/Context';
7
+ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
8
+
9
+ export class EventNotificationService {
10
+ static async validateType(notification: EventNotification) {
11
+ const platform = await Platform.getSharedPrivateStruct();
12
+ const type = platform.config.eventNotificationTypes.find(t => t.id === notification.typeId);
13
+
14
+ if (!type) {
15
+ throw new SimpleError({
16
+ code: 'invalid_field',
17
+ message: 'Invalid type',
18
+ human: Context.i18n.$t('4d8be2b1-559a-4c16-a76f-67a8ba85de7f'),
19
+ field: 'typeId',
20
+ });
21
+ }
22
+
23
+ return type;
24
+ }
25
+
26
+ static async cleanAnswers(notification: EventNotification) {
27
+ const type = await this.validateType(notification);
28
+ const struct = await AuthenticatedStructures.eventNotification(notification);
29
+ const patchedStruct = RecordCategory.removeOldAnswers(type.recordCategories, struct);
30
+ notification.recordAnswers = patchedStruct.getRecordAnswers();
31
+ }
32
+
33
+ static async getSubmitterRecipients(notification: EventNotification): Promise<Recipient[]> {
34
+ // Send the email to all users with full permissions + the submitter + the creator
35
+ const type = await this.validateType(notification);
36
+ const responsibilityIds = type.contactResponsibilityIds;
37
+ const organizationId = notification.organizationId;
38
+ const platform = await Platform.getSharedPrivateStruct();
39
+
40
+ const recipients: Recipient[] = [];
41
+
42
+ if (responsibilityIds.length) {
43
+ // Query all users with the responsibility
44
+ const records = await MemberResponsibilityRecord.select()
45
+ .where('organizationId', organizationId)
46
+ .andWhere('responsibilityId', responsibilityIds)
47
+ .andWhere(MemberResponsibilityRecord.whereActive)
48
+ .fetch();
49
+ const memberIds = records.map(r => r.memberId);
50
+ const members = await Member.getByIDs(...memberIds);
51
+
52
+ for (const member of members) {
53
+ if (!member.details.email) {
54
+ continue;
55
+ }
56
+ recipients.push(Recipient.create({
57
+ firstName: member.details.firstName,
58
+ lastName: member.details.lastName,
59
+ email: member.details.email,
60
+ }));
61
+ }
62
+ }
63
+
64
+ if (notification.submittedBy) {
65
+ const user = await User.getByID(notification.submittedBy);
66
+ if (user && user.verified) {
67
+ const p = new AdminPermissionChecker(user, platform);
68
+
69
+ if (await p.canAccessEventNotification(notification, PermissionLevel.Write)) {
70
+ recipients.push(Recipient.create({
71
+ firstName: user.firstName,
72
+ lastName: user.lastName,
73
+ email: user.email,
74
+ }));
75
+ }
76
+ }
77
+ }
78
+
79
+ if (notification.createdBy) {
80
+ const user = await User.getByID(notification.createdBy);
81
+ if (user && user.verified) {
82
+ const p = new AdminPermissionChecker(user, platform);
83
+
84
+ if (await p.canAccessEventNotification(notification, PermissionLevel.Write)) {
85
+ recipients.push(Recipient.create({
86
+ firstName: user.firstName,
87
+ lastName: user.lastName,
88
+ email: user.email,
89
+ }));
90
+ }
91
+ }
92
+ }
93
+
94
+ // Remove duplicates
95
+ const emails = new Set<string>();
96
+ const filteredRecipients: Recipient[] = [];
97
+ for (const recipient of recipients) {
98
+ if (!emails.has(recipient.email)) {
99
+ emails.add(recipient.email);
100
+ filteredRecipients.push(recipient);
101
+ }
102
+ }
103
+
104
+ return filteredRecipients;
105
+ }
106
+
107
+ static async getReviewerRecipients(notification: EventNotification): Promise<Recipient[]> {
108
+ // Find all users that have permission to review this notification
109
+ const platformAdmins = await User.getPlatformAdmins();
110
+ const recipients: Recipient[] = [];
111
+ const platform = await Platform.getSharedPrivateStruct();
112
+
113
+ for (const user of platformAdmins) {
114
+ const p = new AdminPermissionChecker(user, platform);
115
+
116
+ if (await p.canReviewEventNotification(notification)) {
117
+ recipients.push(Recipient.create({
118
+ firstName: user.firstName,
119
+ lastName: user.lastName,
120
+ email: user.email,
121
+ }));
122
+ }
123
+ }
124
+
125
+ return recipients;
126
+ }
127
+
128
+ static async getEmailReplacements(notification: EventNotification, forReviewers = false) {
129
+ const organization = await Organization.getByID(notification.organizationId);
130
+ if (!organization) {
131
+ throw new Error('Organization not found');
132
+ }
133
+ const events = EventNotification.events.isLoaded(notification) ? notification.events : await EventNotification.events.load(notification);
134
+ const type = await this.validateType(notification);
135
+ let submitterName = 'Anoniem';
136
+
137
+ if (notification.submittedBy) {
138
+ const user = await User.getByID(notification.submittedBy);
139
+ if (user) {
140
+ submitterName = user.name ?? user.email;
141
+ }
142
+ }
143
+
144
+ return [
145
+ Replacement.create({
146
+ token: 'eventName',
147
+ value: events.map(e => e.name).join(', '),
148
+ }),
149
+ Replacement.create({
150
+ token: 'organizationName',
151
+ value: organization.name,
152
+ }),
153
+ Replacement.create({
154
+ token: 'reviewUrl',
155
+ value: forReviewers ? Context.i18n.localizedDomains.adminUrl + '/kampmeldingen/' + encodeURIComponent(notification.id) : (events.length === 0 ? organization.getBaseStructure().dashboardUrl : (organization.getBaseStructure().dashboardUrl + '/activiteiten/' + events[0].id + '/' + Formatter.slug(type.title))),
156
+ }),
157
+ Replacement.create({
158
+ token: 'dateRange',
159
+ value: Formatter.dateRange(notification.startDate, notification.endDate, undefined, false),
160
+ }),
161
+ Replacement.create({
162
+ token: 'submitterName',
163
+ value: submitterName,
164
+ }),
165
+ Replacement.create({
166
+ token: 'feedbackText',
167
+ html: notification.feedbackText ? `<p class="pre-wrap"><em>${Formatter.escapeHtml(notification.feedbackText)}</em></p>` : `<p class="pre-wrap"><em>${Formatter.escapeHtml($t('4c3149b3-e02a-4071-bf21-941711e0238d'))}</em></p>`,
168
+ }),
169
+ ];
170
+ }
171
+
172
+ static async sendSubmitterEmail(type: EmailTemplateType, notification: EventNotification) {
173
+ if (notification.endDate < new Date()) {
174
+ // Ignore
175
+ return;
176
+ }
177
+ await sendEmailTemplate(null, {
178
+ recipients: await this.getSubmitterRecipients(notification),
179
+ template: {
180
+ type,
181
+ },
182
+ defaultReplacements: await this.getEmailReplacements(notification),
183
+ type: 'transactional',
184
+ });
185
+ }
186
+
187
+ static async sendReviewerEmail(type: EmailTemplateType, notification: EventNotification) {
188
+ if (notification.endDate < new Date()) {
189
+ // Ignore
190
+ return;
191
+ }
192
+ await sendEmailTemplate(null, {
193
+ recipients: await this.getReviewerRecipients(notification),
194
+ template: {
195
+ type,
196
+ },
197
+ defaultReplacements: await this.getEmailReplacements(notification, true),
198
+ type: 'transactional',
199
+ });
200
+ }
201
+ };
@@ -0,0 +1,227 @@
1
+ import { DecodedRequest, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { SimpleError } from '@simonbackx/simple-errors';
3
+ import { File } from '@stamhoofd/structures';
4
+ import AWS from 'aws-sdk';
5
+ import * as jose from 'jose';
6
+ import chalk from 'chalk';
7
+
8
+ /**
9
+ * This service creates signed urls for valid files
10
+ */
11
+ export class FileSignService {
12
+ static s3 = new AWS.S3({
13
+ endpoint: STAMHOOFD.SPACES_ENDPOINT,
14
+ accessKeyId: STAMHOOFD.SPACES_KEY,
15
+ secretAccessKey: STAMHOOFD.SPACES_SECRET,
16
+ });
17
+
18
+ static async load() {
19
+ /**
20
+ * Note the algorithm is only used for signing. For verification the algorithm inside the public keys are used
21
+ */
22
+ const alg = STAMHOOFD.FILE_SIGNING_ALG || 'ES256';
23
+
24
+ if (!STAMHOOFD.FILE_SIGNING_PUBLIC_KEY || !STAMHOOFD.FILE_SIGNING_PRIVATE_KEY) {
25
+ if (STAMHOOFD.environment !== 'development') {
26
+ throw new Error('Missing environment variables for file signing. Please make sure FILE_SIGNING_PUBLIC_KEY, FILE_SIGNING_PRIVATE_KEY and FILE_SIGNING_ALG are set.');
27
+ }
28
+
29
+ console.warn(chalk.yellow('File signing is disabled because the environment variables are not set. Please make sure FILE_SIGNING_PUBLIC_KEY, FILE_SIGNING_PRIVATE_KEY and FILE_SIGNING_ALG are set.'));
30
+
31
+ const { publicKey, privateKey } = await jose.generateKeyPair(alg);
32
+
33
+ const exportedPublicKey = await jose.exportJWK(publicKey);
34
+ const exportedPrivateKey = await jose.exportJWK(privateKey);
35
+
36
+ console.log('Example keys you can use in your development environment:');
37
+ console.log('FILE_SIGNING_PUBLIC_KEY:', JSON.stringify(exportedPublicKey));
38
+ console.log('FILE_SIGNING_ALG:', JSON.stringify(exportedPrivateKey));
39
+
40
+ return;
41
+ }
42
+
43
+ const privateKey = await jose.importJWK(STAMHOOFD.FILE_SIGNING_PRIVATE_KEY!);
44
+
45
+ // Support for multiple public keys (in case we need to rotate keys)
46
+ const jwks = jose.createLocalJWKSet({
47
+ keys: [
48
+ STAMHOOFD.FILE_SIGNING_PUBLIC_KEY!,
49
+ ],
50
+ });
51
+
52
+ File.verifyFile = async (file) => {
53
+ if (!file.signature) {
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ const { payload, protectedHeader } = await jose.compactVerify(file.signature, jwks);
59
+
60
+ // Check if payload matches the file
61
+ const decoded = (new TextDecoder().decode(payload));
62
+ if (decoded !== file.signPayload) {
63
+ console.error('Invalid payload:', decoded);
64
+ return false;
65
+ }
66
+
67
+ return true;
68
+ }
69
+ catch (e) {
70
+ if (e instanceof jose.errors.JWSSignatureVerificationFailed) {
71
+ return false;
72
+ }
73
+ console.error('Failed to verify file:', e);
74
+ return false;
75
+ }
76
+ };
77
+
78
+ File.signFile = async (file) => {
79
+ if (!STAMHOOFD.FILE_SIGNING_PRIVATE_KEY) {
80
+ return;
81
+ }
82
+ const jws = await new jose.CompactSign(
83
+ new TextEncoder().encode(file.signPayload),
84
+ )
85
+ .setProtectedHeader({ alg })
86
+ .sign(privateKey);
87
+
88
+ file.signature = jws;
89
+ };
90
+ }
91
+
92
+ static async fillSignedUrl(file: File, duration = 60 * 60) {
93
+ if (!file.isPrivate) {
94
+ if (file.signedUrl) {
95
+ console.error('Warning: public file has a signed url');
96
+ // this will not be encoded because of the file encode implementation
97
+ }
98
+ return;
99
+ }
100
+
101
+ // Only created signed urls for files that were generated by our own server
102
+ if (!await file.verify()) {
103
+ console.error('Failed to verify file:', file.id);
104
+ return;
105
+ }
106
+
107
+ console.log('Signing file:', file.id);
108
+
109
+ if (file.signedUrl) {
110
+ console.error('Warning: file already signed');
111
+ }
112
+
113
+ try {
114
+ const url = await this.s3.getSignedUrlPromise('getObject', {
115
+ Bucket: STAMHOOFD.SPACES_BUCKET,
116
+ Key: file.path,
117
+ Expires: duration,
118
+ });
119
+
120
+ console.log('Signed url:', url);
121
+
122
+ return new File({
123
+ ...file,
124
+ signedUrl: url,
125
+ });
126
+ }
127
+ catch (e) {
128
+ console.error('Failed to sign file:', e);
129
+ return file;
130
+ }
131
+ }
132
+
133
+ static async fillSignedUrlsForStruct(data: any) {
134
+ if (data instanceof File) {
135
+ return await this.fillSignedUrl(data);
136
+ }
137
+
138
+ if (Array.isArray(data)) {
139
+ for (let i = 0; i < data.length; i++) {
140
+ const value = data[i];
141
+ const r = await this.fillSignedUrlsForStruct(value);
142
+ if (r !== undefined) {
143
+ data[i] = r;
144
+ }
145
+ }
146
+ return;
147
+ }
148
+
149
+ if (data instanceof Map) {
150
+ for (const [key, value] of data.entries()) {
151
+ const r = await this.fillSignedUrlsForStruct(value);
152
+
153
+ if (r !== undefined) {
154
+ data.set(key, r);
155
+ continue;
156
+ }
157
+ }
158
+ return;
159
+ }
160
+
161
+ // Loop all keys and search for File objects + replace them with the signed variant
162
+ if (typeof data === 'object' && data !== null) {
163
+ for (const key in data) {
164
+ const r = await this.fillSignedUrlsForStruct(data[key]);
165
+ if (r !== undefined) {
166
+ data[key] = r;
167
+ }
168
+ }
169
+ return;
170
+ }
171
+ }
172
+
173
+ static async verifyFilesInStruct(data: any) {
174
+ if (data instanceof File) {
175
+ if (!data.isPrivate) {
176
+ return;
177
+ }
178
+
179
+ // Clear all incoming signed urls
180
+ data.signedUrl = null;
181
+
182
+ if (!await data.verify()) {
183
+ // This is the first level of defence - it prevents the server from storing untrusted files
184
+ // but in the case this check is bypassed, we still need to check the signature when creating signed urls
185
+ throw new SimpleError({
186
+ code: 'invalid_signature',
187
+ message: 'Invalid signature for file',
188
+ human: $t('Je probeert een bestand up te loaden dat niet door ons is gegenereerd. Probeer het opnieuw.'),
189
+ });
190
+ }
191
+ return;
192
+ }
193
+
194
+ if (Array.isArray(data)) {
195
+ for (const value of data) {
196
+ await this.verifyFilesInStruct(value);
197
+ }
198
+ return;
199
+ }
200
+
201
+ if (data instanceof Map) {
202
+ for (const [key, value] of data.entries()) {
203
+ await this.verifyFilesInStruct(value);
204
+ }
205
+ return;
206
+ }
207
+
208
+ // Loop all keys and search for File objects + replace them with the signed variant
209
+ if (typeof data === 'object' && data !== null) {
210
+ for (const key in data) {
211
+ await this.verifyFilesInStruct(data[key]);
212
+ }
213
+ return;
214
+ }
215
+ }
216
+
217
+ static async handleResponse(request: Request, response: Response) {
218
+ const r = await this.fillSignedUrlsForStruct(response.body);
219
+ if (r !== undefined) {
220
+ response.body = r;
221
+ }
222
+ }
223
+
224
+ static async handleDecodedRequest(decodedRequest: DecodedRequest<unknown, unknown, unknown>, _endpoint) {
225
+ await this.verifyFilesInStruct(decodedRequest.body);
226
+ }
227
+ }
@@ -0,0 +1,39 @@
1
+ import { SQL, SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLJoinedRelationFilterCompiler, createSQLRelationFilterCompiler } from '@stamhoofd/sql';
2
+ import { eventFilterCompilers } from './events';
3
+ import { organizationFilterCompilers } from './organizations';
4
+
5
+ export const organizationJoin = SQL.join('organizations').where(SQL.column('organizations', 'id'), SQL.column('event_notifications', 'organizationId'));
6
+
7
+ export const eventNotificationsFilterCompilers: SQLFilterDefinitions = {
8
+ ...baseSQLFilterCompilers,
9
+ id: createSQLColumnFilterCompiler('id'),
10
+ typeId: createSQLColumnFilterCompiler('typeId'),
11
+ periodId: createSQLColumnFilterCompiler('periodId'),
12
+ organizationId: createSQLColumnFilterCompiler('organizationId'),
13
+ startDate: createSQLColumnFilterCompiler('startDate'),
14
+ endDate: createSQLColumnFilterCompiler('endDate'),
15
+ submittedAt: createSQLColumnFilterCompiler('submittedAt'),
16
+ createdAt: createSQLColumnFilterCompiler('createdAt'),
17
+ status: createSQLColumnFilterCompiler('status'),
18
+ organization: createSQLJoinedRelationFilterCompiler(
19
+ organizationJoin,
20
+ organizationFilterCompilers,
21
+ ),
22
+ events: createSQLRelationFilterCompiler(
23
+ SQL.select()
24
+ .from(
25
+ SQL.table('events'),
26
+ ).join(
27
+ SQL.join(
28
+ SQL.table('_event_notifications_events'),
29
+ ).where(
30
+ SQL.column('_event_notifications_events', 'eventsId'),
31
+ SQL.column('events', 'id'),
32
+ ),
33
+ ).where(
34
+ SQL.column('_event_notifications_events', 'event_notificationsId'),
35
+ SQL.column('event_notifications', 'id'),
36
+ ),
37
+ eventFilterCompilers,
38
+ ),
39
+ };
@@ -22,7 +22,7 @@ import { SetupStepType } from '@stamhoofd/structures';
22
22
  export const organizationFilterCompilers: SQLFilterDefinitions = {
23
23
  ...baseSQLFilterCompilers,
24
24
  id: createSQLExpressionFilterCompiler(SQL.column('organizations', 'id')),
25
- uriPadded: createSQLExpressionFilterCompiler(SQL.lpad(SQL.column('organizations', 'uri'), 100, '0')),
25
+ uriPadded: createSQLExpressionFilterCompiler(SQL.lpad(SQL.column('organizations', 'uri'), 10, '0')),
26
26
  uri: createSQLExpressionFilterCompiler(SQL.column('organizations', 'uri')),
27
27
  name: createSQLExpressionFilterCompiler(
28
28
  SQL.column('organizations', 'name'),
@@ -0,0 +1,96 @@
1
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
2
+ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+ import { organizationJoin } from '../sql-filters/event-notifications';
5
+
6
+ export const eventNotificationsSorters: SQLSortDefinitions<SQLResultNamespacedRow> = {
7
+ // WARNING! TEST NEW SORTERS THOROUGHLY!
8
+ // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
9
+ // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
10
+ // You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
11
+ // Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
12
+ // And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
13
+ // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
14
+
15
+ 'id': {
16
+ getValue(a) {
17
+ return a['event_notifications'].id;
18
+ },
19
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
20
+ return new SQLOrderBy({
21
+ column: SQL.column('id'),
22
+ direction,
23
+ });
24
+ },
25
+ },
26
+ 'status': {
27
+ getValue(a) {
28
+ return a['event_notifications'].status;
29
+ },
30
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
31
+ return new SQLOrderBy({
32
+ column: SQL.column('status'),
33
+ direction,
34
+ });
35
+ },
36
+ },
37
+ 'startDate': {
38
+ getValue(a) {
39
+ return Formatter.dateTimeIso(a['event_notifications'].startDate as Date, 'UTC');
40
+ },
41
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
42
+ return new SQLOrderBy({
43
+ column: SQL.column('startDate'),
44
+ direction,
45
+ });
46
+ },
47
+ },
48
+ 'endDate': {
49
+ getValue(a) {
50
+ return Formatter.dateTimeIso(a['event_notifications'].endDate as Date, 'UTC');
51
+ },
52
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
53
+ return new SQLOrderBy({
54
+ column: SQL.column('endDate'),
55
+ direction,
56
+ });
57
+ },
58
+ },
59
+ 'submittedAt': {
60
+ getValue(a) {
61
+ return a['event_notifications'].submittedAt !== null ? Formatter.dateTimeIso(a['event_notifications'].submittedAt as Date, 'UTC') : null;
62
+ },
63
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
64
+ return new SQLOrderBy({
65
+ column: SQL.column('submittedAt'),
66
+ direction,
67
+ });
68
+ },
69
+ },
70
+ 'organization.name': {
71
+ getValue(a) {
72
+ return a.organizations.name;
73
+ },
74
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
75
+ return new SQLOrderBy({
76
+ column: SQL.column('organizations', 'name'),
77
+ direction,
78
+ });
79
+ },
80
+ join: organizationJoin,
81
+ select: [SQL.column('organizations', 'name')],
82
+ },
83
+ 'organization.uriPadded': {
84
+ getValue(a) {
85
+ return (a.organizations.uri as string).padStart(10, '0');
86
+ },
87
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
88
+ return new SQLOrderBy({
89
+ column: SQL.lpad(SQL.column('organizations', 'uri'), 10, '0'),
90
+ direction,
91
+ });
92
+ },
93
+ join: organizationJoin,
94
+ select: [SQL.column('organizations', 'uri')],
95
+ },
96
+ };
@@ -35,7 +35,7 @@ export const eventSorters: SQLSortDefinitions<Event> = {
35
35
  },
36
36
  startDate: {
37
37
  getValue(a) {
38
- return Formatter.dateTimeIso(a.startDate);
38
+ return Formatter.dateTimeIso(a.startDate, 'UTC');
39
39
  },
40
40
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
41
41
  return new SQLOrderBy({
@@ -46,7 +46,7 @@ export const eventSorters: SQLSortDefinitions<Event> = {
46
46
  },
47
47
  endDate: {
48
48
  getValue(a) {
49
- return Formatter.dateTimeIso(a.endDate);
49
+ return Formatter.dateTimeIso(a.endDate, 'UTC');
50
50
  },
51
51
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
52
52
  return new SQLOrderBy({
@@ -45,11 +45,11 @@ export const organizationSorters: SQLSortDefinitions<Organization> = {
45
45
  },
46
46
  uriPadded: {
47
47
  getValue(a) {
48
- return a.uri.padStart(100, '0');
48
+ return a.uri.padStart(10, '0');
49
49
  },
50
50
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
51
51
  return new SQLOrderBy({
52
- column: SQL.lpad(SQL.column('uri'), 100, '0'),
52
+ column: SQL.lpad(SQL.column('uri'), 10, '0'),
53
53
  direction,
54
54
  });
55
55
  },