@stamhoofd/backend 2.108.0 → 2.109.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 (38) hide show
  1. package/index.ts +2 -2
  2. package/package.json +20 -20
  3. package/src/crons/disable-auto-update-documents.test.ts +164 -0
  4. package/src/crons/disable-auto-update-documents.ts +82 -0
  5. package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +8 -7
  7. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
  8. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
  9. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
  10. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
  11. package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
  12. package/src/excel-loaders/members.ts +61 -7
  13. package/src/excel-loaders/registrations.ts +123 -2
  14. package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
  15. package/src/helpers/SQLTranslatedString.ts +14 -0
  16. package/src/helpers/TagHelper.test.ts +9 -9
  17. package/src/helpers/outstandingBalanceJoin.ts +49 -0
  18. package/src/seeds/1765896674-document-update-year.test.ts +179 -0
  19. package/src/seeds/1765896674-document-update-year.ts +75 -0
  20. package/src/seeds/1766150402-document-published-at.test.ts +46 -0
  21. package/src/seeds/1766150402-document-published-at.ts +20 -0
  22. package/src/services/PaymentService.ts +14 -32
  23. package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
  24. package/src/sql-filters/document-templates.ts +45 -0
  25. package/src/sql-filters/documents.ts +1 -1
  26. package/src/sql-filters/events.ts +6 -6
  27. package/src/sql-filters/groups.ts +7 -6
  28. package/src/sql-filters/members.ts +31 -26
  29. package/src/sql-filters/orders.ts +16 -16
  30. package/src/sql-filters/organizations.ts +11 -11
  31. package/src/sql-filters/payments.ts +10 -10
  32. package/src/sql-filters/registrations.ts +14 -6
  33. package/src/sql-sorters/document-templates.ts +79 -0
  34. package/src/sql-sorters/documents.ts +1 -1
  35. package/src/sql-sorters/members.ts +22 -0
  36. package/src/sql-sorters/orders.ts +5 -5
  37. package/src/sql-sorters/organizations.ts +3 -3
  38. package/src/sql-sorters/registrations.ts +186 -15
package/index.ts CHANGED
@@ -5,10 +5,10 @@ backendEnv.load({ service: 'api' }).catch((error) => {
5
5
  process.exit(1);
6
6
  }).then(async () => {
7
7
  if (STAMHOOFD.environment === 'development') {
8
- const { run } = await import('./src/migrate');
8
+ const { run } = await import('./src/migrate.js');
9
9
  await run();
10
10
  }
11
- const { boot } = await import('./src/boot');
11
+ const { boot } = await import('./src/boot.js');
12
12
 
13
13
  boot({ killProcess: true }).catch((error) => {
14
14
  console.error('unhandledRejection', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.108.0",
3
+ "version": "2.109.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -32,7 +32,7 @@
32
32
  "devDependencies": {
33
33
  "@types/cookie": "^0.6.0",
34
34
  "@types/luxon": "3.4.2",
35
- "@types/mailparser": "3.4.4",
35
+ "@types/mailparser": "^3",
36
36
  "@types/mysql": "^2.15.20",
37
37
  "@types/node": "^22",
38
38
  "nock": "^13.5.1",
@@ -40,33 +40,33 @@
40
40
  "sinon": "^18.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@aws-sdk/client-s3": "3.823.0",
44
- "@aws-sdk/client-ses": "3.823.0",
45
- "@aws-sdk/client-sesv2": "3.823.0",
46
- "@aws-sdk/client-sqs": "3.823.0",
47
- "@aws-sdk/s3-request-presigner": "3.823.0",
43
+ "@aws-sdk/client-s3": "^3.839.0",
44
+ "@aws-sdk/client-ses": "^3.839.0",
45
+ "@aws-sdk/client-sesv2": "^3.839.0",
46
+ "@aws-sdk/client-sqs": "^3.839.0",
47
+ "@aws-sdk/s3-request-presigner": "^3.839.0",
48
48
  "@bwip-js/node": "^4.5.1",
49
- "@mollie/api-client": "3.7.0",
49
+ "@mollie/api-client": "4.3.3",
50
50
  "@simonbackx/simple-database": "1.34.0",
51
51
  "@simonbackx/simple-encoding": "2.22.0",
52
52
  "@simonbackx/simple-endpoints": "1.20.1",
53
53
  "@simonbackx/simple-logging": "^1.0.1",
54
- "@stamhoofd/backend-i18n": "2.108.0",
55
- "@stamhoofd/backend-middleware": "2.108.0",
56
- "@stamhoofd/email": "2.108.0",
57
- "@stamhoofd/models": "2.108.0",
58
- "@stamhoofd/queues": "2.108.0",
59
- "@stamhoofd/sql": "2.108.0",
60
- "@stamhoofd/structures": "2.108.0",
61
- "@stamhoofd/utility": "2.108.0",
54
+ "@stamhoofd/backend-i18n": "2.109.0",
55
+ "@stamhoofd/backend-middleware": "2.109.0",
56
+ "@stamhoofd/email": "2.109.0",
57
+ "@stamhoofd/models": "2.109.0",
58
+ "@stamhoofd/queues": "2.109.0",
59
+ "@stamhoofd/sql": "2.109.0",
60
+ "@stamhoofd/structures": "2.109.0",
61
+ "@stamhoofd/utility": "2.109.0",
62
62
  "archiver": "^7.0.1",
63
- "axios": "^1.8.2",
63
+ "axios": "^1.13.2",
64
64
  "cookie": "^0.7.0",
65
65
  "formidable": "3.5.4",
66
66
  "handlebars": "^4.7.7",
67
- "jsonwebtoken": "9.0.0",
67
+ "jsonwebtoken": "9.0.3",
68
68
  "luxon": "3.4.4",
69
- "mailparser": "3.7.0",
69
+ "mailparser": "^3.9.1",
70
70
  "mockdate": "^3.0.2",
71
71
  "mysql2": "^3.14.1",
72
72
  "node-rsa": "1.1.1",
@@ -76,5 +76,5 @@
76
76
  "publishConfig": {
77
77
  "access": "public"
78
78
  },
79
- "gitHead": "c8dd24207c94a4c077448545cdfcb94369a7ef47"
79
+ "gitHead": "9a987df4a751f24aabd56e1480dfb9239fb8e019"
80
80
  }
@@ -0,0 +1,164 @@
1
+ import { DocumentTemplate, DocumentTemplateFactory } from '@stamhoofd/models';
2
+ import { DocumentStatus } from '@stamhoofd/structures';
3
+ import { disableAutoUpdateForFiscalDocuments, disableAutoUpdateForOtherDocuments } from './disable-auto-update-documents.js';
4
+
5
+ describe('cron.disable-auto-update-documents', () => {
6
+ describe('disableAutoUpdateForFiscalDocuments', () => {
7
+ it('should only run on the 1st of March', async () => {
8
+ // arrange
9
+ const firstOfMarch2025 = new Date(2025, 2, 1);
10
+
11
+ const cases: { now: Date; expected: boolean }[] = [
12
+ { now: new Date(firstOfMarch2025.getTime() - 1), expected: false },
13
+ { now: new Date(firstOfMarch2025), expected: true },
14
+ { now: new Date(2025, 2, 2), expected: false },
15
+ { now: new Date(new Date(2026, 2, 1)), expected: true },
16
+ ];
17
+
18
+ for (const { now, expected } of cases) {
19
+ // act
20
+ const result = await disableAutoUpdateForFiscalDocuments(now);
21
+ // assert
22
+ expect(result).toBe(expected);
23
+ }
24
+ });
25
+
26
+ it('should disable auto-update for fiscal documents of the previous year', async () => {
27
+ // // arrange
28
+ const firstOfMarch2025 = new Date(2025, 2, 1);
29
+
30
+ const document1 = await createDocument({
31
+ status: DocumentStatus.Published,
32
+ publishedAt: new Date(2025, 0, 1),
33
+ createdAt: new Date(2024, 11, 15),
34
+ year: 2024,
35
+ type: 'fiscal',
36
+ updatesEnabled: true,
37
+ });
38
+
39
+ // old year
40
+ const document2 = await createDocument({
41
+ status: DocumentStatus.Published,
42
+ publishedAt: new Date(2025, 0, 1),
43
+ createdAt: new Date(2024, 11, 15),
44
+ year: 2023,
45
+ type: 'fiscal',
46
+ updatesEnabled: true,
47
+ });
48
+
49
+ // current year
50
+ const document3 = await createDocument({
51
+ status: DocumentStatus.Published,
52
+ publishedAt: new Date(2025, 0, 1),
53
+ createdAt: new Date(2024, 11, 15),
54
+ year: 2025,
55
+ type: 'fiscal',
56
+ updatesEnabled: true,
57
+ });
58
+
59
+ // type participation
60
+ const document4 = await createDocument({
61
+ status: DocumentStatus.Published,
62
+ publishedAt: new Date(2025, 0, 1),
63
+ createdAt: new Date(2024, 11, 15),
64
+ year: 2024,
65
+ type: 'participation',
66
+ updatesEnabled: true,
67
+ });
68
+
69
+ // act
70
+ const didRun = await disableAutoUpdateForFiscalDocuments(firstOfMarch2025);
71
+
72
+ // assert
73
+ const updatedDocument1 = (await DocumentTemplate.getByID(document1.id))!;
74
+ const updatedDocument2 = (await DocumentTemplate.getByID(document2.id))!;
75
+ const updatedDocument3 = (await DocumentTemplate.getByID(document3.id))!;
76
+ const updatedDocument4 = (await DocumentTemplate.getByID(document4.id))!;
77
+
78
+ expect(didRun).toBe(true);
79
+
80
+ // should update because previous year, type is fiscal and updates enabled
81
+ expect(updatedDocument1.updatesEnabled).toBe(false);
82
+ // should not update because not previous year
83
+ expect(updatedDocument2.updatesEnabled).toBe(true);
84
+ // should not update because not previous year
85
+ expect(updatedDocument3.updatesEnabled).toBe(true);
86
+ // should not update because type is not fiscal
87
+ expect(updatedDocument4.updatesEnabled).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('disableAutoUpdateForOtherDocuments', () => {
92
+ it('should disable auto-update for documents, other than fiscal documents, that have been published 90 days ago', async () => {
93
+ // // arrange
94
+ const today = new Date(2025, 5, 15);
95
+ const ninetyDaysAgo = new Date(2025, 2, 17);
96
+
97
+ const cases: { document: DocumentTemplate; expected: boolean }[] = [
98
+ // should update because 90 days ago and type is not fiscal
99
+ {
100
+ document: await createDocument({
101
+ status: DocumentStatus.Published,
102
+ publishedAt: new Date(ninetyDaysAgo),
103
+ createdAt: new Date(2024, 11, 15),
104
+ year: 2024,
105
+ type: 'participation',
106
+ updatesEnabled: true,
107
+ }),
108
+ expected: false,
109
+ },
110
+ // day after
111
+ {
112
+ document: await createDocument({
113
+ status: DocumentStatus.Published,
114
+ publishedAt: new Date(2025, 2, 18),
115
+ createdAt: new Date(2024, 11, 15),
116
+ year: 2024,
117
+ type: 'participation',
118
+ updatesEnabled: true,
119
+ }),
120
+ expected: true,
121
+ },
122
+ // day before
123
+ {
124
+ document: await createDocument({
125
+ status: DocumentStatus.Published,
126
+ publishedAt: new Date(ninetyDaysAgo.getTime() - 1),
127
+ createdAt: new Date(2024, 11, 15),
128
+ year: 2024,
129
+ type: 'participation',
130
+ updatesEnabled: true,
131
+ }),
132
+ expected: true,
133
+ },
134
+ // type fiscal
135
+ {
136
+ document: await createDocument({
137
+ status: DocumentStatus.Published,
138
+ publishedAt: new Date(ninetyDaysAgo),
139
+ createdAt: new Date(2024, 11, 15),
140
+ year: 2024,
141
+ type: 'fiscal',
142
+ updatesEnabled: true,
143
+ }),
144
+ expected: true,
145
+ },
146
+ ];
147
+
148
+ // act
149
+ await disableAutoUpdateForOtherDocuments(today);
150
+
151
+ // assert
152
+ for (const { document, expected } of cases) {
153
+ const updatedDocument = (await DocumentTemplate.getByID(document.id))!;
154
+ expect(updatedDocument.updatesEnabled).toBe(expected);
155
+ }
156
+ });
157
+ });
158
+ });
159
+
160
+ async function createDocument({ year, status, createdAt, updatesEnabled, type, publishedAt }: { year: number; status: DocumentStatus; createdAt: Date; updatesEnabled: boolean; type: 'fiscal' | 'participation'; publishedAt: Date | null }) {
161
+ const document = await new DocumentTemplateFactory({ year, groups: [], status, updatesEnabled, publishedAt, createdAt, type }).create();
162
+
163
+ return document;
164
+ };
@@ -0,0 +1,82 @@
1
+ import { registerCron } from '@stamhoofd/crons';
2
+ import { DocumentTemplate } from '@stamhoofd/models';
3
+ import { SQL } from '@stamhoofd/sql';
4
+
5
+ let lastRunDate: number | null = null;
6
+
7
+ registerCron('disableAutoUpdateDocuments', disableAutoUpdateDocuments);
8
+
9
+ function shouldRun() {
10
+ const now = new Date();
11
+
12
+ if (now.getDate() === lastRunDate) {
13
+ return false;
14
+ }
15
+
16
+ const hour = now.getHours();
17
+
18
+ // between 5 and 6 AM
19
+ if (hour !== 5 && STAMHOOFD.environment !== 'development') {
20
+ return false;
21
+ }
22
+
23
+ return true;
24
+ }
25
+
26
+ async function disableAutoUpdateDocuments() {
27
+ if (!shouldRun()) {
28
+ return;
29
+ }
30
+
31
+ const now = new Date();
32
+ lastRunDate = now.getDate();
33
+
34
+ await disableAutoUpdateForFiscalDocuments(now);
35
+ await disableAutoUpdateForOtherDocuments(now);
36
+ }
37
+
38
+ /**
39
+ * Disable auto-update for fiscal documents on the 1st of March.
40
+ * @returns if query was run
41
+ */
42
+ export async function disableAutoUpdateForFiscalDocuments(now: Date): Promise<boolean> {
43
+ // only run on 1st of march
44
+ const isFirstOfMarch = now.getMonth() === 2 && now.getDate() === 1;
45
+ if (!isFirstOfMarch) {
46
+ return false;
47
+ }
48
+
49
+ // set updates enabled to false
50
+ await SQL.update(DocumentTemplate.table).set('updatesEnabled', false)
51
+ // where previous year
52
+ .where('year', now.getFullYear() - 1)
53
+ // where updates enabled
54
+ .andWhere('updatesEnabled', true)
55
+ // where type is fiscal
56
+ .andWhere(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal')
57
+ .update();
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Disable auto-update for documents, other then fiscal, that have been published 90 days ago.
64
+ */
65
+ export async function disableAutoUpdateForOtherDocuments(now: Date) {
66
+ const min = new Date(now);
67
+ min.setDate(min.getDate() - 90);
68
+
69
+ const max = new Date(now);
70
+ max.setDate(max.getDate() - 89);
71
+
72
+ // set updates enabled to false
73
+ await SQL.update(DocumentTemplate.table).set('updatesEnabled', false)
74
+ // where updates enabled
75
+ .where('updatesEnabled', true)
76
+ // where published 90 days ago
77
+ .andWhere('publishedAt', '>=', min)
78
+ .andWhere('publishedAt', '<', max)
79
+ // where type is not fiscal
80
+ .whereNot(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal')
81
+ .update();
82
+ }
@@ -8,11 +8,11 @@ import { DataValidator } from '@stamhoofd/utility';
8
8
 
9
9
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
10
10
  import parsePhoneNumber from 'libphonenumber-js/max';
11
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
- import { Context } from '../../../helpers/Context';
13
- import { memberFilterCompilers } from '../../../sql-filters/members';
14
- import { memberSorters } from '../../../sql-sorters/members';
15
- import { validateGroupFilter } from './helpers/validateGroupFilter';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
+ import { Context } from '../../../helpers/Context.js';
13
+ import { memberFilterCompilers } from '../../../sql-filters/members.js';
14
+ import { memberSorters } from '../../../sql-sorters/members.js';
15
+ import { validateGroupFilter } from './helpers/validateGroupFilter.js';
16
16
 
17
17
  type Params = Record<string, never>;
18
18
  type Query = LimitedFilteredRequest;
@@ -3,16 +3,15 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Group, Member, Platform, Registration } from '@stamhoofd/models';
5
5
  import { SQL, SQLExpression, SQLSelect, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
6
- import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
6
+ import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, RegistrationWithMemberBlob, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
7
7
 
8
8
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
9
  import { RegistrationsBlob } from '@stamhoofd/structures/dist/src/members/RegistrationsBlob';
10
- import { RegistrationWithMemberBlob } from '@stamhoofd/structures/dist/src/members/RegistrationWithMemberBlob';
11
10
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
11
  import { Context } from '../../../helpers/Context.js';
13
12
  import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper.js';
14
13
  import { groupJoin, registrationFilterCompilers } from '../../../sql-filters/registrations.js';
15
- import { registrationSorters } from '../../../sql-sorters/registrations.js';
14
+ import { RegistrationSortData, registrationSorters } from '../../../sql-sorters/registrations.js';
16
15
  import { GetMembersEndpoint } from '../members/GetMembersEndpoint.js';
17
16
  import { validateGroupFilter } from '../members/helpers/validateGroupFilter.js';
18
17
 
@@ -21,7 +20,7 @@ type Query = LimitedFilteredRequest;
21
20
  type Body = undefined;
22
21
  type ResponseBody = PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>;
23
22
 
24
- const sorters: SQLSortDefinitions<RegistrationWithMemberBlob> = registrationSorters;
23
+ const sorters: SQLSortDefinitions<RegistrationSortData> = registrationSorters;
25
24
  const filterCompilers = registrationFilterCompilers;
26
25
 
27
26
  export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
@@ -160,8 +159,6 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
160
159
  .setMaxExecutionTime(15 * 1000)
161
160
  .where('registeredAt', '!=', null);
162
161
 
163
- query;
164
-
165
162
  if (scopeFilter) {
166
163
  query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
167
164
  }
@@ -236,9 +233,13 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
236
233
 
237
234
  const registrationsBlob = await AuthenticatedStructures.registrationsBlob(data, members);
238
235
 
239
- const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
236
+ const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoopWithTransform<RegistrationWithMemberBlob, RegistrationSortData>({
240
237
  request: requestQuery,
241
238
  results: registrationsBlob.registrations,
239
+ transformer: registration => new RegistrationSortData({
240
+ registration,
241
+ organizations: registrationsBlob.organizations,
242
+ }),
242
243
  sorters,
243
244
  });
244
245
 
@@ -8,14 +8,14 @@ import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWi
8
8
  import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, ReceivableBalanceType, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
- import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
13
- import { Context } from '../../../helpers/Context';
14
- import { StripeHelper } from '../../../helpers/StripeHelper';
15
- import { BalanceItemService } from '../../../services/BalanceItemService';
16
- import { RegistrationService } from '../../../services/RegistrationService';
17
- import { PaymentService } from '../../../services/PaymentService';
18
- import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
+ import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
13
+ import { Context } from '../../../helpers/Context.js';
14
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
15
+ import { StripeHelper } from '../../../helpers/StripeHelper.js';
16
+ import { BalanceItemService } from '../../../services/BalanceItemService.js';
17
+ import { PaymentService } from '../../../services/PaymentService.js';
18
+ import { RegistrationService } from '../../../services/RegistrationService.js';
19
19
  type Params = Record<string, never>;
20
20
  type Query = undefined;
21
21
  type Body = IDRegisterCheckout;
@@ -381,6 +381,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
381
381
  registration.options = item.options;
382
382
  registration.recordAnswers = item.recordAnswers;
383
383
  registration.startDate = startDate;
384
+ registration.endDate = item.calculatedEndDate;
384
385
 
385
386
  // Clear if we are reusing an existing registration
386
387
  registration.trialUntil = null;
@@ -0,0 +1,48 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../../helpers/Context.js';
6
+ import { GetDocumentTemplatesEndpoint } from './GetDocumentTemplatesEndpoint.js';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetDocumentTemplatesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method !== 'GET') {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, '/organization/document-templates/count', {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOrganizationScope();
31
+ await Context.authenticate();
32
+
33
+ if (!await Context.auth.canManageDocuments(organization.id)) {
34
+ throw Context.auth.error();
35
+ }
36
+
37
+ const query = await GetDocumentTemplatesEndpoint.buildQuery(request.query);
38
+
39
+ const count = await query
40
+ .count();
41
+
42
+ return new Response(
43
+ CountResponse.create({
44
+ count,
45
+ }),
46
+ );
47
+ }
48
+ }
@@ -1,16 +1,25 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { SimpleError } from '@simonbackx/simple-errors';
3
- import { DocumentTemplate, Token } from '@stamhoofd/models';
4
- import { DocumentTemplatePrivate } from '@stamhoofd/structures';
2
+ import { DocumentTemplate } from '@stamhoofd/models';
3
+ import { assertSort, CountFilteredRequest, DocumentTemplatePrivate, LimitedFilteredRequest, PaginatedResponse, SearchFilterFactory, StamhoofdFilter } from '@stamhoofd/structures';
5
4
 
6
- import { Context } from '../../../../helpers/Context';
5
+ import { Decoder } from '@simonbackx/simple-encoding';
6
+ import { applySQLSorter, compileToSQLFilter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
7
+ import { Context } from '../../../../helpers/Context.js';
8
+ import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper.js';
9
+ import { documentTemplateFilterCompilers } from '../../../../sql-filters/document-templates.js';
10
+ import { documentTemplateSorters } from '../../../../sql-sorters/document-templates.js';
7
11
 
8
12
  type Params = Record<string, never>;
9
- type Query = undefined;
13
+ type Query = LimitedFilteredRequest;
10
14
  type Body = undefined;
11
- type ResponseBody = DocumentTemplatePrivate[];
15
+ type ResponseBody = PaginatedResponse<DocumentTemplatePrivate[], LimitedFilteredRequest>;
16
+
17
+ const filterCompilers: SQLFilterDefinitions = documentTemplateFilterCompilers;
18
+ const sorters: SQLSortDefinitions<DocumentTemplate> = documentTemplateSorters;
12
19
 
13
20
  export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
21
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
22
+
14
23
  protected doesMatch(request: Request): [true, Params] | [false] {
15
24
  if (request.method !== 'GET') {
16
25
  return [false];
@@ -24,7 +33,60 @@ export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body,
24
33
  return [false];
25
34
  }
26
35
 
27
- async handle(_: DecodedRequest<Params, Query, Body>) {
36
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
37
+ const organization = Context.organization!;
38
+
39
+ const templatesTable: string = DocumentTemplate.table;
40
+
41
+ const query = SQL
42
+ .select(SQL.wildcard(templatesTable))
43
+ .from(SQL.table(templatesTable))
44
+ .where('organizationId', organization.id);
45
+
46
+ if (q.filter) {
47
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
48
+ }
49
+
50
+ if (q.search) {
51
+ const searchFilter: StamhoofdFilter | null = getDocumentTemplateSearchFilter(q.search);
52
+
53
+ if (searchFilter) {
54
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
55
+ }
56
+ }
57
+
58
+ if (q instanceof LimitedFilteredRequest) {
59
+ if (q.pageFilter) {
60
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
61
+ }
62
+
63
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
64
+ applySQLSorter(query, q.sort, sorters);
65
+ query.limit(q.limit);
66
+ }
67
+
68
+ return query;
69
+ }
70
+
71
+ static async buildData(requestQuery: LimitedFilteredRequest) {
72
+ const query = await this.buildQuery(requestQuery);
73
+ const data = await query.fetch();
74
+
75
+ const templates: DocumentTemplate[] = DocumentTemplate.fromRows(data, DocumentTemplate.table);
76
+
77
+ const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
78
+ request: requestQuery,
79
+ results: templates,
80
+ sorters,
81
+ });
82
+
83
+ return new PaginatedResponse<DocumentTemplatePrivate[], LimitedFilteredRequest>({
84
+ results: templates.map(t => t.getPrivateStructure()),
85
+ next,
86
+ });
87
+ }
88
+
89
+ async handle(request: DecodedRequest<Params, Query, Body>) {
28
90
  const organization = await Context.setOrganizationScope();
29
91
  await Context.authenticate();
30
92
 
@@ -32,19 +94,33 @@ export class GetDocumentTemplatesEndpoint extends Endpoint<Params, Query, Body,
32
94
  throw Context.auth.error();
33
95
  }
34
96
 
35
- const templates = await DocumentTemplate.where(
36
- {
37
- organizationId: organization.id,
38
- },
39
- {
40
- sort: [{
41
- column: 'createdAt',
42
- direction: 'ASC',
43
- }],
44
- },
45
- );
97
+ LimitedFilteredRequestHelper.throwIfInvalidLimit({
98
+ request: request.query,
99
+ maxLimit: Context.auth.hasSomePlatformAccess() ? 1000 : 100,
100
+ });
101
+
46
102
  return new Response(
47
- templates.map(t => t.getPrivateStructure()),
103
+ await GetDocumentTemplatesEndpoint.buildData(request.query),
48
104
  );
49
105
  }
50
106
  }
107
+
108
+ function getDocumentTemplateSearchFilter(search: string | null): StamhoofdFilter | null {
109
+ if (search === null || search === undefined) {
110
+ return null;
111
+ }
112
+
113
+ const numberFilter = SearchFilterFactory.getIntegerFilter(search);
114
+
115
+ if (numberFilter) {
116
+ return {
117
+ year: numberFilter,
118
+ };
119
+ }
120
+
121
+ return {
122
+ name: {
123
+ $contains: search,
124
+ },
125
+ };
126
+ }