@stamhoofd/backend 2.93.0 → 2.94.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.
- package/package.json +10 -10
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +18 -0
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +12 -9
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +4 -15
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -0
- package/src/helpers/AdminPermissionChecker.ts +4 -0
- package/src/helpers/AuthenticatedStructures.ts +1 -1
- package/src/seeds/1756293699-fill-previous-next-period-id.ts +34 -0
- package/src/seeds/1756303697-update-email-counts.ts +76 -0
- package/src/sql-filters/emails.ts +5 -0
- package/src/sql-filters/events.ts +10 -0
- package/src/seeds/1734596144-fill-previous-period-id.ts +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.94.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.94.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.94.0",
|
|
50
|
+
"@stamhoofd/email": "2.94.0",
|
|
51
|
+
"@stamhoofd/models": "2.94.0",
|
|
52
|
+
"@stamhoofd/queues": "2.94.0",
|
|
53
|
+
"@stamhoofd/sql": "2.94.0",
|
|
54
|
+
"@stamhoofd/structures": "2.94.0",
|
|
55
|
+
"@stamhoofd/utility": "2.94.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "7a1a0be03249d69ad0664ee85d446c7b596768f0"
|
|
74
74
|
}
|
|
@@ -130,6 +130,24 @@ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
130
130
|
await model.queueForSending();
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Delete open drafts with the same content, from the same user
|
|
134
|
+
const duplicates = await Email.select()
|
|
135
|
+
.where('userId', user.id)
|
|
136
|
+
.where('organizationId', model.organizationId)
|
|
137
|
+
.where('status', EmailStatus.Draft)
|
|
138
|
+
.where('subject', model.subject)
|
|
139
|
+
.where('html', model.html)
|
|
140
|
+
.where('text', model.text)
|
|
141
|
+
.where('deletedAt', null)
|
|
142
|
+
.whereNot('id', model.id)
|
|
143
|
+
.limit(100)
|
|
144
|
+
.fetch();
|
|
145
|
+
|
|
146
|
+
for (const duplicate of duplicates) {
|
|
147
|
+
duplicate.deletedAt = new Date();
|
|
148
|
+
await duplicate.save();
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
return new Response(await model.getPreviewStructure());
|
|
134
152
|
}
|
|
135
153
|
}
|
|
@@ -104,7 +104,8 @@ export class GetAdminEmailsEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const query = Email.select()
|
|
107
|
+
const query = Email.select()
|
|
108
|
+
.where('deletedAt', null);
|
|
108
109
|
|
|
109
110
|
if (scopeFilter) {
|
|
110
111
|
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
@@ -50,15 +50,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
50
50
|
throw Context.auth.error();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (model.status !== EmailStatus.Draft) {
|
|
54
|
-
throw new SimpleError({
|
|
55
|
-
code: 'not_draft',
|
|
56
|
-
human: 'Email is not a draft',
|
|
57
|
-
message: $t(`298b5a46-2899-4aa1-89df-9b634c20806b`),
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
53
|
let rebuild = false;
|
|
63
54
|
|
|
64
55
|
if (request.body.subject !== undefined) {
|
|
@@ -78,6 +69,11 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
78
69
|
model.senderId = sender.id;
|
|
79
70
|
model.fromAddress = sender.email;
|
|
80
71
|
model.fromName = sender.name;
|
|
72
|
+
|
|
73
|
+
// Check if we still have write access to the email
|
|
74
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
|
|
75
|
+
throw Context.auth.error();
|
|
76
|
+
}
|
|
81
77
|
}
|
|
82
78
|
else {
|
|
83
79
|
throw new SimpleError({
|
|
@@ -138,6 +134,13 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
138
134
|
model.validateAttachments();
|
|
139
135
|
}
|
|
140
136
|
|
|
137
|
+
if (request.body.deletedAt !== undefined) {
|
|
138
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Full)) {
|
|
139
|
+
throw Context.auth.error();
|
|
140
|
+
}
|
|
141
|
+
model.deletedAt = request.body.deletedAt;
|
|
142
|
+
}
|
|
143
|
+
|
|
141
144
|
await model.save();
|
|
142
145
|
|
|
143
146
|
if (rebuild) {
|
|
@@ -92,9 +92,8 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
await period.setPreviousPeriodId();
|
|
96
|
-
|
|
97
95
|
await period.save();
|
|
96
|
+
await period.updatePreviousNextPeriods();
|
|
98
97
|
periods.push(period);
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -138,7 +137,7 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
138
137
|
model.settings = patchObject(model.settings, patch.settings);
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
await model.
|
|
140
|
+
await model.updatePreviousNextPeriods();
|
|
142
141
|
await model.save();
|
|
143
142
|
|
|
144
143
|
// Schedule patch of all groups in this period
|
|
@@ -156,23 +155,13 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
|
|
|
156
155
|
});
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
// Get before deleting the model
|
|
160
|
-
const updateWhere = await RegistrationPeriod.where({ previousPeriodId: model.id });
|
|
161
|
-
|
|
162
158
|
// Now delete the model
|
|
163
159
|
await model.delete();
|
|
164
|
-
|
|
165
|
-
// Update all previous period ids
|
|
166
|
-
await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
167
|
-
for (const period of updateWhere) {
|
|
168
|
-
await period.setPreviousPeriodId();
|
|
169
|
-
await period.save();
|
|
170
|
-
}
|
|
171
|
-
});
|
|
160
|
+
await RegistrationPeriod.updatePreviousNextPeriods(model.organizationId);
|
|
172
161
|
}
|
|
173
162
|
|
|
174
163
|
// Clear platform cache
|
|
175
|
-
Platform.clearCache();
|
|
164
|
+
await Platform.clearCache();
|
|
176
165
|
|
|
177
166
|
return new Response(
|
|
178
167
|
periods.map(p => p.getStructure()),
|
|
@@ -531,6 +531,8 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
531
531
|
model.type = struct.type;
|
|
532
532
|
model.settings.period = period.getBaseStructure();
|
|
533
533
|
model.settings.endDate = period.endDate;
|
|
534
|
+
model.settings.registeredMembers = 0;
|
|
535
|
+
model.settings.reservedMembers = 0;
|
|
534
536
|
|
|
535
537
|
// Note: start date is curomizable, as long as it stays between period start and end
|
|
536
538
|
if (model.settings.startDate < period.startDate || model.settings.startDate > period.endDate) {
|
|
@@ -883,6 +883,10 @@ export class AdminPermissionChecker {
|
|
|
883
883
|
return false;
|
|
884
884
|
}
|
|
885
885
|
|
|
886
|
+
if (email.deletedAt) {
|
|
887
|
+
return false;
|
|
888
|
+
}
|
|
889
|
+
|
|
886
890
|
if (email.userId === this.user.id) {
|
|
887
891
|
// User can always read their own emails
|
|
888
892
|
// Note; for sending we'll always need to use 'canSendEmailsFrom' externally
|
|
@@ -604,7 +604,7 @@ export class AuthenticatedStructures {
|
|
|
604
604
|
|
|
605
605
|
const registration = memberBlob.registrations.find(r => r.id === id);
|
|
606
606
|
if (!registration) {
|
|
607
|
-
throw new Error('Registration not found');
|
|
607
|
+
throw new Error('Registration not found: ' + id);
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
return RegistrationWithMemberBlob.create({
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
+
import { Organization, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment == 'test') {
|
|
7
|
+
console.log('skipped in tests');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
process.stdout.write('\n');
|
|
12
|
+
|
|
13
|
+
await logger.setContext({ tags: ['seed'] }, async () => {
|
|
14
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
15
|
+
await RegistrationPeriod.updatePreviousNextPeriods(null);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for await (const organization of Organization.select().all()) {
|
|
19
|
+
await RegistrationPeriod.updatePreviousNextPeriods(organization.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
console.log('Updated periods');
|
|
24
|
+
|
|
25
|
+
// Now update platform
|
|
26
|
+
const platform = await Platform.getForEditing();
|
|
27
|
+
await platform.setPreviousPeriodId();
|
|
28
|
+
await platform.save();
|
|
29
|
+
|
|
30
|
+
console.log('Updated platform');
|
|
31
|
+
|
|
32
|
+
// Do something here
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Email, EmailRecipient } from '@stamhoofd/models';
|
|
3
|
+
import { SQL, SQLAlias, SQLCount, SQLSelectAs } from '@stamhoofd/sql';
|
|
4
|
+
import { EmailStatus } from '@stamhoofd/structures';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log('Start setting counts of emails.');
|
|
13
|
+
|
|
14
|
+
const batchSize = 100;
|
|
15
|
+
let count = 0;
|
|
16
|
+
|
|
17
|
+
for await (const email of Email.select()
|
|
18
|
+
.where('status', EmailStatus.Sent)
|
|
19
|
+
.where('succeededCount', 0)
|
|
20
|
+
.where('failedCount', 0)
|
|
21
|
+
.where('emailRecipientsCount', '!=', 0)
|
|
22
|
+
.where('emailRecipientsCount', '!=', null)
|
|
23
|
+
.where('createdAt', '<', new Date('2025-08-28 00:00:00')).limit(batchSize).all()) {
|
|
24
|
+
const query = SQL.select(
|
|
25
|
+
new SQLSelectAs(
|
|
26
|
+
new SQLCount(
|
|
27
|
+
SQL.column('failError'),
|
|
28
|
+
),
|
|
29
|
+
new SQLAlias('data__failedCount'),
|
|
30
|
+
),
|
|
31
|
+
// If the current amount_due is negative, we can ignore that negative part if there is a future due item
|
|
32
|
+
new SQLSelectAs(
|
|
33
|
+
new SQLCount(
|
|
34
|
+
SQL.column('sentAt'),
|
|
35
|
+
),
|
|
36
|
+
new SQLAlias('data__succeededCount'),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
.from(EmailRecipient.table)
|
|
40
|
+
.where('emailId', email.id);
|
|
41
|
+
|
|
42
|
+
const result = await query.fetch();
|
|
43
|
+
if (result.length !== 1) {
|
|
44
|
+
console.error('Unexpected result', result);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const row = result[0]['data'];
|
|
48
|
+
if (!row) {
|
|
49
|
+
console.error('Unexpected result row', result);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let failedCount = row['failedCount'];
|
|
54
|
+
const succeededCount = row['succeededCount'];
|
|
55
|
+
|
|
56
|
+
if (typeof failedCount !== 'number' || typeof succeededCount !== 'number') {
|
|
57
|
+
console.error('Unexpected result values', row);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (email.emailRecipientsCount !== null && failedCount + succeededCount !== email.emailRecipientsCount) {
|
|
62
|
+
console.warn(`Email ${email.id} has ${email.emailRecipientsCount} recipients, but ${failedCount} failed and ${succeededCount} succeeded. Correcting failedCount to `, email.emailRecipientsCount - succeededCount);
|
|
63
|
+
failedCount = email.emailRecipientsCount - succeededCount;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Send an update query
|
|
67
|
+
await Email.update()
|
|
68
|
+
.where('id', email.id)
|
|
69
|
+
.set('succeededCount', succeededCount)
|
|
70
|
+
.set('failedCount', failedCount)
|
|
71
|
+
.update();
|
|
72
|
+
count += 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('Finished saving ' + count + ' emails.');
|
|
76
|
+
});
|
|
@@ -52,6 +52,16 @@ export const eventFilterCompilers: SQLFilterDefinitions = {
|
|
|
52
52
|
type: SQLValueType.JSONArray,
|
|
53
53
|
nullable: true,
|
|
54
54
|
}),
|
|
55
|
+
'minAge': createColumnFilter({
|
|
56
|
+
expression: SQL.jsonValue(SQL.column('meta'), '$.value.minAge'),
|
|
57
|
+
type: SQLValueType.Number,
|
|
58
|
+
nullable: true,
|
|
59
|
+
}),
|
|
60
|
+
'maxAge': createColumnFilter({
|
|
61
|
+
expression: SQL.jsonValue(SQL.column('meta'), '$.value.maxAge'),
|
|
62
|
+
type: SQLValueType.Number,
|
|
63
|
+
nullable: true,
|
|
64
|
+
}),
|
|
55
65
|
'meta.visible': createColumnFilter({
|
|
56
66
|
expression: SQL.jsonValue(SQL.column('meta'), '$.value.visible'),
|
|
57
67
|
type: SQLValueType.JSONBoolean,
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { logger } from '@simonbackx/simple-logging';
|
|
3
|
-
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
|
-
|
|
5
|
-
export default new Migration(async () => {
|
|
6
|
-
if (STAMHOOFD.environment == 'test') {
|
|
7
|
-
console.log('skipped in tests');
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
process.stdout.write('\n');
|
|
12
|
-
let c = 0;
|
|
13
|
-
let id: string = '';
|
|
14
|
-
|
|
15
|
-
await logger.setContext({ tags: ['seed'] }, async () => {
|
|
16
|
-
while (true) {
|
|
17
|
-
const items = await RegistrationPeriod.where({
|
|
18
|
-
id: {
|
|
19
|
-
value: id,
|
|
20
|
-
sign: '>',
|
|
21
|
-
},
|
|
22
|
-
}, { limit: 1000, sort: ['id'] });
|
|
23
|
-
|
|
24
|
-
if (items.length === 0) {
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
process.stdout.write('.');
|
|
29
|
-
|
|
30
|
-
for (const item of items) {
|
|
31
|
-
await item.setPreviousPeriodId();
|
|
32
|
-
if (await item.save()) {
|
|
33
|
-
c += 1;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (items.length < 1000) {
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
id = items[items.length - 1].id;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
console.log('Updated ' + c + ' registration periods');
|
|
45
|
-
|
|
46
|
-
// Now update platform
|
|
47
|
-
const platform = await Platform.getForEditing();
|
|
48
|
-
await platform.setPreviousPeriodId();
|
|
49
|
-
await platform.save();
|
|
50
|
-
|
|
51
|
-
console.log('Updated platform');
|
|
52
|
-
|
|
53
|
-
// Do something here
|
|
54
|
-
return Promise.resolve();
|
|
55
|
-
});
|