@stamhoofd/models 2.78.4 → 2.79.1
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/dist/src/factories/GroupFactory.d.ts +4 -0
- package/dist/src/factories/GroupFactory.d.ts.map +1 -1
- package/dist/src/factories/GroupFactory.js +11 -1
- package/dist/src/factories/GroupFactory.js.map +1 -1
- package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts +14 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.d.ts.map +1 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.js +34 -0
- package/dist/src/factories/MemberResponsibilityRecordFactory.js.map +1 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts +13 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.d.ts.map +1 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.js +20 -0
- package/dist/src/factories/OrganizationRegistrationPeriodFactory.js.map +1 -0
- package/dist/src/factories/OrganizationTagFactory.js +1 -1
- package/dist/src/factories/OrganizationTagFactory.js.map +1 -1
- package/dist/src/factories/PlatformResponsibilityFactory.d.ts +11 -0
- package/dist/src/factories/PlatformResponsibilityFactory.d.ts.map +1 -0
- package/dist/src/factories/PlatformResponsibilityFactory.js +25 -0
- package/dist/src/factories/PlatformResponsibilityFactory.js.map +1 -0
- package/dist/src/factories/RegistrationPeriodFactory.d.ts +1 -0
- package/dist/src/factories/RegistrationPeriodFactory.d.ts.map +1 -1
- package/dist/src/factories/RegistrationPeriodFactory.js +4 -0
- package/dist/src/factories/RegistrationPeriodFactory.js.map +1 -1
- package/dist/src/factories/index.d.ts +3 -0
- package/dist/src/factories/index.d.ts.map +1 -1
- package/dist/src/factories/index.js +3 -0
- package/dist/src/factories/index.js.map +1 -1
- package/dist/src/helpers/EmailBuilder.d.ts +8 -10
- package/dist/src/helpers/EmailBuilder.d.ts.map +1 -1
- package/dist/src/helpers/EmailBuilder.js +44 -42
- package/dist/src/helpers/EmailBuilder.js.map +1 -1
- package/dist/src/models/Email.d.ts +9 -2
- package/dist/src/models/Email.d.ts.map +1 -1
- package/dist/src/models/Email.js +18 -16
- package/dist/src/models/Email.js.map +1 -1
- package/dist/src/models/Email.test.js +183 -2
- package/dist/src/models/Email.test.js.map +1 -1
- package/dist/src/models/Member.d.ts +3 -1
- package/dist/src/models/Member.d.ts.map +1 -1
- package/dist/src/models/Member.js.map +1 -1
- package/dist/src/models/Order.d.ts +0 -2
- package/dist/src/models/Order.d.ts.map +1 -1
- package/dist/src/models/Order.js +0 -48
- package/dist/src/models/Order.js.map +1 -1
- package/dist/src/models/Organization.d.ts +1 -16
- package/dist/src/models/Organization.d.ts.map +1 -1
- package/dist/src/models/Organization.js +5 -86
- package/dist/src/models/Organization.js.map +1 -1
- package/dist/src/models/Platform.d.ts +11 -3
- package/dist/src/models/Platform.d.ts.map +1 -1
- package/dist/src/models/Platform.js +76 -24
- package/dist/src/models/Platform.js.map +1 -1
- package/dist/src/models/Platform.test.d.ts +2 -0
- package/dist/src/models/Platform.test.d.ts.map +1 -0
- package/dist/src/models/Platform.test.js +90 -0
- package/dist/src/models/Platform.test.js.map +1 -0
- package/dist/src/models/index.d.ts +1 -1
- package/dist/src/models/index.d.ts.map +1 -1
- package/dist/src/models/index.js +2 -3
- package/dist/src/models/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/factories/GroupFactory.ts +16 -3
- package/src/factories/MemberResponsibilityRecordFactory.ts +35 -0
- package/src/factories/OrganizationRegistrationPeriodFactory.ts +22 -0
- package/src/factories/OrganizationTagFactory.ts +1 -1
- package/src/factories/PlatformResponsibilityFactory.ts +25 -0
- package/src/factories/RegistrationPeriodFactory.ts +4 -0
- package/src/factories/index.ts +3 -0
- package/src/helpers/EmailBuilder.ts +54 -50
- package/src/models/Email.test.ts +217 -5
- package/src/models/Email.ts +22 -20
- package/src/models/Member.ts +3 -1
- package/src/models/Order.ts +0 -55
- package/src/models/Organization.ts +6 -101
- package/src/models/Platform.test.ts +107 -0
- package/src/models/Platform.ts +86 -25
- package/src/models/index.ts +1 -1
package/src/models/Order.ts
CHANGED
|
@@ -253,29 +253,6 @@ export class Order extends QueryableModel {
|
|
|
253
253
|
if (webshop) {
|
|
254
254
|
await this.setRelation(Order.webshop, webshop).updateTickets();
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
// Send an email if the payment failed after 15 minutes being pending
|
|
258
|
-
// const difference = new Date().getTime() - this.createdAt.getTime()
|
|
259
|
-
// if (difference > 1000 * 60 * 30 && difference < 1000 * 60 * 60 * 24) {
|
|
260
|
-
// if (payment && payment.method !== PaymentMethod.Transfer && payment.method !== PaymentMethod.PointOfSale) {
|
|
261
|
-
// console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Sending email.')
|
|
262
|
-
// const webshop = await Webshop.getByID(this.webshopId)
|
|
263
|
-
// if (!webshop) {
|
|
264
|
-
// console.error("Missing organization or webshop for order "+this.id)
|
|
265
|
-
// return
|
|
266
|
-
// }
|
|
267
|
-
// const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true)
|
|
268
|
-
// await this.setRelation(Order.webshop, webshop.setRelation(Order.organization, organization)).sendEmailTemplate({
|
|
269
|
-
// type: EmailTemplateType.OrderOnlinePaymentFailed,
|
|
270
|
-
// from,
|
|
271
|
-
// replyTo
|
|
272
|
-
// })
|
|
273
|
-
// } else {
|
|
274
|
-
// console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Payment method not matching.')
|
|
275
|
-
// }
|
|
276
|
-
// } else {
|
|
277
|
-
// console.log('Marked order '+this.id+' as payment failed after ' + (difference / 1000 / 60).toFixed(1) + ' mins. Not sending email.')
|
|
278
|
-
// }
|
|
279
256
|
}
|
|
280
257
|
else {
|
|
281
258
|
this.markUpdated();
|
|
@@ -763,28 +740,18 @@ export class Order extends QueryableModel {
|
|
|
763
740
|
}
|
|
764
741
|
|
|
765
742
|
async sendPaidMail(this: Order & { webshop: Webshop & { organization: Organization } }) {
|
|
766
|
-
const organization = this.webshop.organization;
|
|
767
|
-
const { from, replyTo } = organization.getEmail(this.webshop.privateMeta.defaultEmailId, true);
|
|
768
|
-
|
|
769
743
|
// For a tickets webshop, where the order was marked as paid / non-paid, we should still send the tickets email
|
|
770
744
|
// - because the normal email is not editable
|
|
771
745
|
const hasTickets = this.webshop.meta.hasTickets;
|
|
772
746
|
|
|
773
747
|
await this.sendEmailTemplate({
|
|
774
748
|
type: hasTickets ? EmailTemplateType.TicketsReceivedTransfer : EmailTemplateType.OrderReceivedTransfer,
|
|
775
|
-
from,
|
|
776
|
-
replyTo,
|
|
777
749
|
});
|
|
778
750
|
}
|
|
779
751
|
|
|
780
752
|
async sendTickets(this: Order & { webshop: Webshop & { organization: Organization } }) {
|
|
781
|
-
const organization = this.webshop.organization;
|
|
782
|
-
const { from, replyTo } = organization.getEmail(this.webshop.privateMeta.defaultEmailId, true);
|
|
783
|
-
|
|
784
753
|
await this.sendEmailTemplate({
|
|
785
754
|
type: EmailTemplateType.TicketsReceivedTransfer,
|
|
786
|
-
from,
|
|
787
|
-
replyTo,
|
|
788
755
|
});
|
|
789
756
|
}
|
|
790
757
|
|
|
@@ -894,8 +861,6 @@ export class Order extends QueryableModel {
|
|
|
894
861
|
|
|
895
862
|
async sendEmailTemplate(this: Order & { webshop: Webshop & { organization: Organization } }, data: {
|
|
896
863
|
type: EmailTemplateType;
|
|
897
|
-
from: string;
|
|
898
|
-
replyTo?: string;
|
|
899
864
|
to?: Recipient;
|
|
900
865
|
}) {
|
|
901
866
|
// Never send an email for archived webshops
|
|
@@ -970,25 +935,16 @@ export class Order extends QueryableModel {
|
|
|
970
935
|
await this.save();
|
|
971
936
|
|
|
972
937
|
if (this.data.customer.email.length > 0) {
|
|
973
|
-
const webshop = this.webshop;
|
|
974
|
-
const organization = webshop.organization;
|
|
975
|
-
|
|
976
|
-
const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true);
|
|
977
|
-
|
|
978
938
|
if (tickets.length > 0) {
|
|
979
939
|
// Also send a copy
|
|
980
940
|
if (payment && payment.method === PaymentMethod.PointOfSale) {
|
|
981
941
|
await this.sendEmailTemplate({
|
|
982
942
|
type: EmailTemplateType.TicketsConfirmationPOS,
|
|
983
|
-
from,
|
|
984
|
-
replyTo,
|
|
985
943
|
});
|
|
986
944
|
}
|
|
987
945
|
else {
|
|
988
946
|
await this.sendEmailTemplate({
|
|
989
947
|
type: EmailTemplateType.TicketsConfirmation,
|
|
990
|
-
from,
|
|
991
|
-
replyTo,
|
|
992
948
|
});
|
|
993
949
|
}
|
|
994
950
|
}
|
|
@@ -998,31 +954,23 @@ export class Order extends QueryableModel {
|
|
|
998
954
|
// Also send a copy
|
|
999
955
|
await this.sendEmailTemplate({
|
|
1000
956
|
type: EmailTemplateType.OrderConfirmationTransfer,
|
|
1001
|
-
from,
|
|
1002
|
-
replyTo,
|
|
1003
957
|
});
|
|
1004
958
|
}
|
|
1005
959
|
else if (payment && payment.method === PaymentMethod.PointOfSale) {
|
|
1006
960
|
await this.sendEmailTemplate({
|
|
1007
961
|
type: EmailTemplateType.OrderConfirmationPOS,
|
|
1008
|
-
from,
|
|
1009
|
-
replyTo,
|
|
1010
962
|
});
|
|
1011
963
|
}
|
|
1012
964
|
else {
|
|
1013
965
|
// Also send a copy
|
|
1014
966
|
await this.sendEmailTemplate({
|
|
1015
967
|
type: EmailTemplateType.OrderConfirmationOnline,
|
|
1016
|
-
from,
|
|
1017
|
-
replyTo,
|
|
1018
968
|
});
|
|
1019
969
|
}
|
|
1020
970
|
}
|
|
1021
971
|
else {
|
|
1022
972
|
await this.sendEmailTemplate({
|
|
1023
973
|
type: EmailTemplateType.TicketsConfirmationTransfer,
|
|
1024
|
-
from,
|
|
1025
|
-
replyTo,
|
|
1026
974
|
});
|
|
1027
975
|
}
|
|
1028
976
|
}
|
|
@@ -1031,7 +979,6 @@ export class Order extends QueryableModel {
|
|
|
1031
979
|
if (this.webshop.privateMeta.notificationEmails) {
|
|
1032
980
|
const webshop = this.webshop;
|
|
1033
981
|
const organization = webshop.organization;
|
|
1034
|
-
const { from, replyTo } = organization.getEmail(webshop.privateMeta.defaultEmailId, true);
|
|
1035
982
|
const i18n = organization.i18n;
|
|
1036
983
|
|
|
1037
984
|
const webshopDashboardUrl = 'https://' + (STAMHOOFD.domains.dashboard ?? 'stamhoofd.app') + '/' + i18n.locale + '/webshops/' + Formatter.slug(webshop.meta.name) + '/orders';
|
|
@@ -1040,8 +987,6 @@ export class Order extends QueryableModel {
|
|
|
1040
987
|
for (const email of this.webshop.privateMeta.notificationEmails) {
|
|
1041
988
|
await this.sendEmailTemplate({
|
|
1042
989
|
type: EmailTemplateType.OrderNotification,
|
|
1043
|
-
from,
|
|
1044
|
-
replyTo,
|
|
1045
990
|
to: Recipient.create({
|
|
1046
991
|
email,
|
|
1047
992
|
replacements: [
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { column, Database } from '@simonbackx/simple-database';
|
|
2
|
-
import { DecodedRequest } from '@simonbackx/simple-endpoints';
|
|
3
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
3
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
5
4
|
import { Email, EmailInterfaceRecipient } from '@stamhoofd/email';
|
|
@@ -12,7 +11,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
12
11
|
|
|
13
12
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
14
13
|
import { validateDNSRecords } from '../helpers/DNSValidator';
|
|
15
|
-
import {
|
|
14
|
+
import { sendEmailTemplate } from '../helpers/EmailBuilder';
|
|
16
15
|
import { OrganizationServerMetaData } from '../structures/OrganizationServerMetaData';
|
|
17
16
|
import { OrganizationRegistrationPeriod, StripeAccount } from './';
|
|
18
17
|
|
|
@@ -420,7 +419,6 @@ export class Organization extends QueryableModel {
|
|
|
420
419
|
async sendEmailTemplate(data: {
|
|
421
420
|
type: EmailTemplateType;
|
|
422
421
|
personal?: boolean;
|
|
423
|
-
replyTo?: string;
|
|
424
422
|
bcc?: boolean;
|
|
425
423
|
}) {
|
|
426
424
|
const recipients = await this.getAdminRecipients();
|
|
@@ -443,15 +441,13 @@ export class Organization extends QueryableModel {
|
|
|
443
441
|
];
|
|
444
442
|
|
|
445
443
|
// Create e-mail builder
|
|
446
|
-
|
|
444
|
+
await sendEmailTemplate(null, {
|
|
447
445
|
replaceAll,
|
|
448
446
|
recipients,
|
|
449
447
|
template: {
|
|
450
448
|
type: data.type,
|
|
451
449
|
},
|
|
452
|
-
|
|
453
|
-
singleBcc: data.bcc ? 'simon@stamhoofd.be' : undefined,
|
|
454
|
-
replyTo: data.replyTo,
|
|
450
|
+
singleBcc: data.bcc ? { email: 'simon@stamhoofd.be' } : undefined,
|
|
455
451
|
type: 'transactional',
|
|
456
452
|
defaultReplacements: [
|
|
457
453
|
Replacement.create({
|
|
@@ -462,10 +458,6 @@ export class Organization extends QueryableModel {
|
|
|
462
458
|
unsubscribeType: 'marketing',
|
|
463
459
|
fromStamhoofd: true,
|
|
464
460
|
});
|
|
465
|
-
|
|
466
|
-
if (builder) {
|
|
467
|
-
Email.schedule(builder);
|
|
468
|
-
}
|
|
469
461
|
}
|
|
470
462
|
|
|
471
463
|
async deleteAWSMailIdenitity(mailDomain: string) {
|
|
@@ -855,99 +847,12 @@ export class Organization extends QueryableModel {
|
|
|
855
847
|
/**
|
|
856
848
|
* Return default e-mail address if no email addresses are set.
|
|
857
849
|
*/
|
|
858
|
-
getDefaultFrom(i18n: I18n,
|
|
850
|
+
getDefaultFrom(i18n: I18n, type: 'transactional' | 'broadcast' = 'broadcast'): EmailInterfaceRecipient {
|
|
859
851
|
const domain = type === 'transactional' ? i18n.localizedDomains.defaultTransactionalEmail() : i18n.localizedDomains.defaultBroadcastEmail();
|
|
860
852
|
|
|
861
|
-
if (!withName) {
|
|
862
|
-
return ('noreply-' + this.uri + '@' + domain);
|
|
863
|
-
}
|
|
864
|
-
return '"' + this.name.replaceAll('"', '\\"') + '" <' + ('noreply-' + this.uri + '@' + domain) + '>';
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* @deprecated Switch to EmailBuilder.sendEmailTemplate
|
|
869
|
-
*/
|
|
870
|
-
getEmail(id: string | null, strongDefault = false): { from: string; replyTo: string | undefined } {
|
|
871
|
-
if (id === null) {
|
|
872
|
-
return this.getDefaultEmail(strongDefault);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Send confirmation e-mail
|
|
876
|
-
let from = this.getDefaultFrom(this.i18n, false, strongDefault ? 'transactional' : 'broadcast');
|
|
877
|
-
const sender: OrganizationEmail | undefined = this.privateMeta.emails.find(e => e.id === id);
|
|
878
|
-
let replyTo: string | undefined = undefined;
|
|
879
|
-
|
|
880
|
-
if (sender) {
|
|
881
|
-
replyTo = sender.email;
|
|
882
|
-
|
|
883
|
-
// Can we send from this e-mail or reply-to?
|
|
884
|
-
if (replyTo && this.privateMeta.mailDomain && this.privateMeta.mailDomainActive && sender.email.endsWith('@' + this.privateMeta.mailDomain)) {
|
|
885
|
-
from = sender.email;
|
|
886
|
-
replyTo = undefined;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Include name in form field
|
|
890
|
-
if (sender.name) {
|
|
891
|
-
from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (replyTo) {
|
|
898
|
-
if (sender.name) {
|
|
899
|
-
replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
900
|
-
}
|
|
901
|
-
else {
|
|
902
|
-
replyTo = '"' + this.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
return { from, replyTo };
|
|
906
|
-
}
|
|
907
|
-
return this.getDefaultEmail(strongDefault);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* @deprecated Switch to EmailBuilder.sendEmailTemplate
|
|
912
|
-
*/
|
|
913
|
-
getDefaultEmail(strongDefault = false): { from: string; replyTo: string | undefined } {
|
|
914
|
-
// Send confirmation e-mail
|
|
915
|
-
let from = strongDefault ? this.getDefaultFrom(this.i18n, false) : this.uri + '@stamhoofd.email';
|
|
916
|
-
const sender: OrganizationEmail | undefined = this.privateMeta.emails.find(e => e.default) ?? this.privateMeta.emails[0];
|
|
917
|
-
let replyTo: string | undefined = undefined;
|
|
918
|
-
|
|
919
|
-
if (sender) {
|
|
920
|
-
replyTo = sender.email;
|
|
921
|
-
|
|
922
|
-
// Can we send from this e-mail or reply-to?
|
|
923
|
-
if (replyTo && this.privateMeta.mailDomain && this.privateMeta.mailDomainActive && sender.email.endsWith('@' + this.privateMeta.mailDomain)) {
|
|
924
|
-
from = sender.email;
|
|
925
|
-
replyTo = undefined;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// Include name in form field
|
|
929
|
-
if (sender.name) {
|
|
930
|
-
from = '"' + sender.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
931
|
-
}
|
|
932
|
-
else {
|
|
933
|
-
from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (replyTo) {
|
|
937
|
-
if (sender.name) {
|
|
938
|
-
replyTo = '"' + sender.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
replyTo = '"' + this.name.replaceAll('"', '\\"') + '" <' + replyTo + '>';
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
else {
|
|
946
|
-
from = '"' + this.name.replaceAll('"', '\\"') + '" <' + from + '>';
|
|
947
|
-
}
|
|
948
|
-
|
|
949
853
|
return {
|
|
950
|
-
|
|
854
|
+
name: this.name,
|
|
855
|
+
email: ('noreply-' + this.uri + '@' + domain),
|
|
951
856
|
};
|
|
952
857
|
}
|
|
953
858
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { PlatformConfig, PlatformMembershipType } from '@stamhoofd/structures';
|
|
2
|
+
import { Platform } from './Platform';
|
|
3
|
+
import { Database } from '@simonbackx/simple-database';
|
|
4
|
+
|
|
5
|
+
describe('Model.Platform', () => {
|
|
6
|
+
describe('Shared caches', () => {
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
const platform = await Platform.getByID('1');
|
|
9
|
+
platform!.config = PlatformConfig.create({});
|
|
10
|
+
await platform!.save();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('Editable model changes do not propagate', async () => {
|
|
14
|
+
const editable = await Platform.getForEditing();
|
|
15
|
+
editable.config.membershipTypes = [
|
|
16
|
+
PlatformMembershipType.create({ id: '1', name: 'Test' }),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const shared = (await Platform.getShared()) as any;
|
|
20
|
+
expect(shared.config.membershipTypes).toHaveLength(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('Shared model is immutable', async () => {
|
|
24
|
+
const editable = await Platform.getForEditing();
|
|
25
|
+
editable.config.membershipTypes = [
|
|
26
|
+
PlatformMembershipType.create({ id: '1', name: 'Test' }),
|
|
27
|
+
];
|
|
28
|
+
await editable.save();
|
|
29
|
+
|
|
30
|
+
const shared = (await Platform.getShared()) as any;
|
|
31
|
+
expect(() => {
|
|
32
|
+
shared.privateConfig.roles = [];
|
|
33
|
+
}).toThrow();
|
|
34
|
+
|
|
35
|
+
expect(() => {
|
|
36
|
+
shared.membershipOrganizationId = '2';
|
|
37
|
+
}).toThrow();
|
|
38
|
+
|
|
39
|
+
expect(shared.config.membershipTypes).toHaveLength(1);
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
shared.concat.membershipTypes[0].name = 'Test2';
|
|
43
|
+
}).toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Saving changes propagates to all shared states', async () => {
|
|
47
|
+
const structBefore = await Platform.getSharedStruct();
|
|
48
|
+
const privateStructBefore = await Platform.getSharedPrivateStruct();
|
|
49
|
+
const sharedModelBefore = await Platform.getShared();
|
|
50
|
+
|
|
51
|
+
const editable = await Platform.getForEditing();
|
|
52
|
+
editable.config.membershipTypes = [
|
|
53
|
+
PlatformMembershipType.create({ id: '1', name: 'Hey there' }),
|
|
54
|
+
];
|
|
55
|
+
await editable.save();
|
|
56
|
+
|
|
57
|
+
const structAfter = await Platform.getSharedStruct();
|
|
58
|
+
const privateStructAfter = await Platform.getSharedPrivateStruct();
|
|
59
|
+
const sharedModelAfter = await Platform.getShared();
|
|
60
|
+
|
|
61
|
+
expect(structAfter.config.membershipTypes[0].name).toEqual('Hey there');
|
|
62
|
+
expect(privateStructAfter.config.membershipTypes[0].name).toEqual('Hey there');
|
|
63
|
+
expect(sharedModelAfter.config.membershipTypes[0].name).toEqual('Hey there');
|
|
64
|
+
|
|
65
|
+
// Test before state not altered
|
|
66
|
+
expect(structBefore.config.membershipTypes).toHaveLength(0);
|
|
67
|
+
expect(privateStructBefore.config.membershipTypes).toHaveLength(0);
|
|
68
|
+
expect(sharedModelBefore.config.membershipTypes).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('Creating fresh platform', () => {
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
await Database.delete('DELETE FROM platform');
|
|
75
|
+
await Platform.clearCacheWithoutRefresh();
|
|
76
|
+
if (await Platform.getByID('1')) {
|
|
77
|
+
throw new Error('Platform 1 should not exist');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('when requesting getForEditing', async () => {
|
|
82
|
+
const editable = await Platform.getForEditing();
|
|
83
|
+
expect(editable.id).toBe('1');
|
|
84
|
+
|
|
85
|
+
expect(await Platform.getByID('1')).toEqual(editable);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('when requesting getShared', async () => {
|
|
89
|
+
const shared = await Platform.getShared();
|
|
90
|
+
expect(shared.id).toBe('1');
|
|
91
|
+
|
|
92
|
+
expect(await Platform.getByID('1')).toMatchObject(shared);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('when requesting getSharedPrivateStruct', async () => {
|
|
96
|
+
const editable = await Platform.getSharedPrivateStruct();
|
|
97
|
+
expect(editable).toBeDefined();
|
|
98
|
+
expect(await Platform.getByID('1')).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('when requesting getSharedStruct', async () => {
|
|
102
|
+
const editable = await Platform.getSharedStruct();
|
|
103
|
+
expect(editable).toBeDefined();
|
|
104
|
+
expect(await Platform.getByID('1')).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/models/Platform.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { column } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
3
|
import { QueryableModel } from '@stamhoofd/sql';
|
|
4
4
|
import { PlatformConfig, PlatformPrivateConfig, PlatformServerConfig, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
5
|
+
import { deepFreeze } from '@stamhoofd/utility';
|
|
5
6
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
7
|
import { RegistrationPeriod } from './RegistrationPeriod';
|
|
7
8
|
|
|
@@ -36,15 +37,20 @@ export class Platform extends QueryableModel {
|
|
|
36
37
|
@column({ type: 'json', decoder: PlatformServerConfig })
|
|
37
38
|
serverConfig: PlatformServerConfig = PlatformServerConfig.create({});
|
|
38
39
|
|
|
39
|
-
static
|
|
40
|
+
private static shared: Platform | null = null;
|
|
41
|
+
private static sharedPrivateStruct: PlatformStruct & { privateConfig: PlatformPrivateConfig } | null = null;
|
|
42
|
+
private static sharedStruct: PlatformStruct | null = null;
|
|
40
43
|
|
|
41
44
|
static async getSharedStruct(): Promise<PlatformStruct> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
clone.setShared();
|
|
45
|
+
if (!this.sharedStruct) {
|
|
46
|
+
await this.loadCaches();
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
if (!this.sharedStruct || !!this.sharedStruct.privateConfig) {
|
|
50
|
+
throw new Error('[Platform] Failed to load platform shared struct');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return this.sharedStruct;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
async setPreviousPeriodId() {
|
|
@@ -53,28 +59,24 @@ export class Platform extends QueryableModel {
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
static async getSharedPrivateStruct(): Promise<PlatformStruct & { privateConfig: PlatformPrivateConfig }> {
|
|
56
|
-
if (this.
|
|
57
|
-
|
|
62
|
+
if (!this.sharedPrivateStruct) {
|
|
63
|
+
await this.loadCaches();
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const struct = PlatformStruct.create({
|
|
64
|
-
...model,
|
|
65
|
-
period: period?.getStructure() ?? undefined,
|
|
66
|
-
});
|
|
67
|
-
this.sharedStruct = struct;
|
|
66
|
+
if (!this.sharedPrivateStruct || !this.sharedPrivateStruct.privateConfig) {
|
|
67
|
+
throw new Error('[Platform] Failed to load platform shared private struct');
|
|
68
|
+
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
});
|
|
70
|
+
return this.sharedPrivateStruct;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
static async
|
|
74
|
-
return QueueHandler.schedule('Platform.
|
|
73
|
+
static async getForEditing(): Promise<Platform> {
|
|
74
|
+
return QueueHandler.schedule('Platform.getModel', async () => {
|
|
75
75
|
// Build a new one
|
|
76
76
|
let model = await this.getByID('1');
|
|
77
77
|
if (!model) {
|
|
78
|
+
console.info('[Platform] Creating new platform');
|
|
79
|
+
|
|
78
80
|
// Create a new platform
|
|
79
81
|
model = new Platform();
|
|
80
82
|
model.id = '1';
|
|
@@ -86,16 +88,75 @@ export class Platform extends QueryableModel {
|
|
|
86
88
|
});
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
static
|
|
90
|
-
|
|
91
|
+
static async getShared(): Promise<Readonly<Platform> & { save: never }> {
|
|
92
|
+
return QueueHandler.schedule('Platform.getShared', async () => {
|
|
93
|
+
if (this.shared) {
|
|
94
|
+
return this.shared;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build a new one
|
|
98
|
+
const model = await this.getForEditing();
|
|
99
|
+
deepFreeze(model);
|
|
100
|
+
this.shared = model;
|
|
101
|
+
return model as any;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static async loadCaches(): Promise<void> {
|
|
106
|
+
await QueueHandler.schedule('Platform.loadCaches', async () => {
|
|
107
|
+
if (this.sharedPrivateStruct && this.sharedStruct) {
|
|
108
|
+
// Already loaded (possible if multiple calls to loadCaches were made)
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Build a new one
|
|
112
|
+
const model = await this.getForEditing();
|
|
113
|
+
await this.setCachesFromModel(model);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static async setCachesFromModel(model: Platform) {
|
|
118
|
+
// Set structure cache
|
|
119
|
+
const period = await RegistrationPeriod.getByID(model.periodId);
|
|
120
|
+
const struct = PlatformStruct.create({
|
|
121
|
+
...model,
|
|
122
|
+
period: period?.getStructure() ?? undefined,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// We clone to avoid the chance of updating the platform model
|
|
126
|
+
this.sharedPrivateStruct = struct.clone() as PlatformStruct & { privateConfig: PlatformPrivateConfig };
|
|
127
|
+
|
|
128
|
+
const clone = struct.clone();
|
|
129
|
+
clone.privateConfig = null;
|
|
130
|
+
clone.setShared();
|
|
131
|
+
this.sharedStruct = clone;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static async clearCache() {
|
|
135
|
+
await this.clearCacheWithoutRefresh();
|
|
136
|
+
await this.loadCaches();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
static async clearCacheWithoutRefresh() {
|
|
140
|
+
await QueueHandler.schedule('Platform.loadCaches', async () => {
|
|
141
|
+
this.sharedStruct = null;
|
|
142
|
+
this.sharedPrivateStruct = null;
|
|
143
|
+
});
|
|
144
|
+
await QueueHandler.schedule('Platform.getShared', async () => {
|
|
145
|
+
this.shared = null;
|
|
146
|
+
});
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
async save() {
|
|
150
|
+
let update = false;
|
|
151
|
+
if (this.existsInDatabase) {
|
|
152
|
+
update = true;
|
|
153
|
+
}
|
|
94
154
|
const s = await super.save();
|
|
95
|
-
Platform.clearCache();
|
|
96
155
|
|
|
97
|
-
|
|
98
|
-
|
|
156
|
+
if (update) {
|
|
157
|
+
// Force update cache immediately
|
|
158
|
+
await Platform.clearCache();
|
|
159
|
+
}
|
|
99
160
|
|
|
100
161
|
return s;
|
|
101
162
|
}
|
package/src/models/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { Organization } from './Organization';
|
|
|
2
2
|
export { User } from './User';
|
|
3
3
|
export { Payment } from './Payment';
|
|
4
4
|
export { Registration } from './Registration';
|
|
5
|
-
export
|
|
5
|
+
export * from './Member';
|
|
6
6
|
export { MergedMember } from './MergedMember';
|
|
7
7
|
|
|
8
8
|
export * from './EmailVerificationCode';
|