@stamhoofd/backend 2.83.4 → 2.84.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 (100) hide show
  1. package/index.ts +19 -4
  2. package/package.json +18 -14
  3. package/src/crons/amazon-ses.ts +26 -5
  4. package/src/crons/balance-emails.ts +18 -17
  5. package/src/email-recipient-loaders/registrations.ts +87 -0
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
  7. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
  8. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
  10. package/src/endpoints/global/files/UploadFile.ts +11 -16
  11. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
  12. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
  13. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
  17. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
  18. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
  19. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
  20. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
  21. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
  25. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
  27. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
  28. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
  29. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
  30. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
  31. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
  32. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
  33. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
  34. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
  35. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
  36. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
  37. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
  38. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
  39. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
  40. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
  41. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
  42. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
  43. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
  44. package/src/excel-loaders/members.ts +233 -232
  45. package/src/excel-loaders/payments.ts +1 -1
  46. package/src/excel-loaders/receivable-balances.ts +1 -1
  47. package/src/excel-loaders/registrations.ts +153 -0
  48. package/src/helpers/AdminPermissionChecker.ts +65 -37
  49. package/src/helpers/AuthenticatedStructures.ts +43 -3
  50. package/src/helpers/Context.ts +29 -1
  51. package/src/helpers/GlobalHelper.ts +3 -1
  52. package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
  53. package/src/helpers/GroupedThrottledQueue.ts +108 -0
  54. package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
  55. package/src/helpers/MemberCharger.ts +0 -5
  56. package/src/helpers/MembershipCharger.ts +3 -9
  57. package/src/helpers/OrganizationCharger.ts +0 -5
  58. package/src/helpers/ThrottledQueue.test.ts +194 -0
  59. package/src/helpers/ThrottledQueue.ts +145 -0
  60. package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
  61. package/src/middleware/ContextMiddleware.ts +1 -1
  62. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
  63. package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
  64. package/src/services/BalanceItemPaymentService.ts +1 -33
  65. package/src/services/BalanceItemService.ts +167 -48
  66. package/src/services/FileSignService.ts +18 -13
  67. package/src/services/MemberRecordStore.ts +28 -19
  68. package/src/services/PaymentReallocationService.test.ts +25 -14
  69. package/src/services/PaymentReallocationService.ts +29 -10
  70. package/src/services/PaymentService.ts +4 -16
  71. package/src/services/PlatformMembershipService.ts +8 -4
  72. package/src/services/RegistrationService.ts +66 -2
  73. package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
  74. package/src/sql-filters/groups.ts +67 -0
  75. package/src/sql-filters/members.ts +33 -58
  76. package/src/sql-filters/organization-registration-periods.ts +8 -0
  77. package/src/sql-filters/registration-periods.ts +8 -0
  78. package/src/sql-filters/registrations.ts +11 -22
  79. package/src/sql-sorters/groups.ts +24 -0
  80. package/src/sql-sorters/organization-registration-periods.ts +24 -0
  81. package/src/sql-sorters/registration-periods.ts +47 -0
  82. package/src/sql-sorters/registrations.ts +77 -0
  83. package/tests/actions/patchOrganizationMember.ts +27 -0
  84. package/tests/actions/patchPaymentStatus.ts +45 -0
  85. package/tests/actions/patchUserMember.ts +27 -0
  86. package/tests/assertions/assertBalances.ts +49 -0
  87. package/tests/e2e/api-rate-limits.test.ts +5 -5
  88. package/tests/e2e/bundle-discounts.test.ts +4060 -0
  89. package/tests/e2e/charge-members.test.ts +27 -24
  90. package/tests/e2e/documents.test.ts +398 -0
  91. package/tests/e2e/register.test.ts +292 -312
  92. package/tests/helpers/PayconiqMocker.ts +55 -0
  93. package/tests/init/index.ts +5 -0
  94. package/tests/init/initAdmin.ts +14 -0
  95. package/tests/init/initBundleDiscount.ts +47 -0
  96. package/tests/init/initPayconiq.ts +9 -0
  97. package/tests/init/initPlatformAdmin.ts +13 -0
  98. package/tests/init/initStripe.ts +21 -0
  99. package/tests/jest.setup.ts +29 -0
  100. package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
@@ -0,0 +1,234 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { GroupFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { BundleDiscountGroupPriceSettings, GroupPrice, GroupType, LimitedFilteredRequest, StamhoofdFilter } from '@stamhoofd/structures';
4
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
+ import { testServer } from '../../../../tests/helpers/TestServer';
6
+ import { initAdmin, initBundleDiscount, initPlatformAdmin } from '../../../../tests/init';
7
+ import { GetGroupsEndpoint } from './GetGroupsEndpoint';
8
+ import { Database } from '@simonbackx/simple-database';
9
+
10
+ const baseUrl = `/groups`;
11
+
12
+ describe('Endpoint.GetGroupsEndpoint', () => {
13
+ const endpoint = new GetGroupsEndpoint();
14
+
15
+ let registrationPeriod1: RegistrationPeriod;
16
+ let registrationPeriod2: RegistrationPeriod;
17
+
18
+ const get = async ({ organization, token, filter }: { organization?: Organization; token: Token; filter?: StamhoofdFilter }) => {
19
+ const request = Request.get({
20
+ path: baseUrl,
21
+ host: organization?.getApiHost() ?? '',
22
+ query: new LimitedFilteredRequest({
23
+ limit: 100,
24
+ filter,
25
+ }),
26
+ headers: {
27
+ authorization: 'Bearer ' + token.accessToken,
28
+ },
29
+ });
30
+ return (await testServer.test(endpoint, request)).body.results;
31
+ };
32
+
33
+ beforeAll(async () => {
34
+ TestUtils.setPermanentEnvironment('userMode', 'platform');
35
+
36
+ registrationPeriod2 = await new RegistrationPeriodFactory({
37
+ startDate: new Date(2022, 0, 1),
38
+ endDate: new Date(2022, 11, 31),
39
+ }).create();
40
+
41
+ registrationPeriod1 = await new RegistrationPeriodFactory({
42
+ startDate: new Date(2023, 0, 1),
43
+ endDate: new Date(2023, 11, 31),
44
+ previousPeriodId: registrationPeriod2.id,
45
+ }).create();
46
+ });
47
+
48
+ afterEach(async () => {
49
+ await Database.delete('DELETE FROM `groups`');
50
+ });
51
+
52
+ const initOrganization = async (registrationPeriod: RegistrationPeriod) => {
53
+ const organization = await new OrganizationFactory({ period: registrationPeriod })
54
+ .create();
55
+
56
+ const organizationRegistrationPeriod = await new OrganizationRegistrationPeriodFactory({
57
+ organization,
58
+ period: registrationPeriod1,
59
+ }).create();
60
+
61
+ return { organization, organizationRegistrationPeriod };
62
+ };
63
+
64
+ async function initTest() {
65
+ const { organization, organizationRegistrationPeriod } = await initOrganization(registrationPeriod1);
66
+ const { organization: otherOrganization } = await initOrganization(registrationPeriod1);
67
+
68
+ const user = await new UserFactory({ organization }).create();
69
+ const token = await Token.createToken(user);
70
+
71
+ const waitingList = await new GroupFactory({
72
+ organization,
73
+ type: GroupType.WaitingList,
74
+ }).create();
75
+
76
+ const event = await new GroupFactory({
77
+ organization,
78
+ type: GroupType.EventRegistration,
79
+ }).create();
80
+
81
+ const group = await new GroupFactory({
82
+ organization,
83
+ type: GroupType.Membership,
84
+ }).create();
85
+
86
+ // Create a group that should not be returned
87
+ const otherGroup = await new GroupFactory({
88
+ organization: otherOrganization,
89
+ type: GroupType.Membership,
90
+ }).create();
91
+
92
+ return { organizationRegistrationPeriod, organization, token, waitingList, event, group, otherGroup };
93
+ }
94
+
95
+ it('Can return all groups for an organization', async () => {
96
+ const { organization, token, waitingList, event, group } = await initTest();
97
+ const result = await get({ organization, token });
98
+ expect(result).toHaveLength(3);
99
+ expect(result).toIncludeSameMembers([
100
+ expect.objectContaining({
101
+ id: waitingList.id,
102
+ }),
103
+ expect.objectContaining({
104
+ id: event.id,
105
+ }),
106
+ expect.objectContaining({
107
+ id: group.id,
108
+ }),
109
+ ]);
110
+ });
111
+
112
+ it('Can return all groups to a platform admin', async () => {
113
+ const { waitingList, event, group, otherGroup } = await initTest();
114
+ const { adminToken } = await initPlatformAdmin();
115
+
116
+ const result = await get({ token: adminToken });
117
+ expect(result).toHaveLength(4);
118
+ expect(result).toIncludeSameMembers([
119
+ expect.objectContaining({
120
+ id: waitingList.id,
121
+ }),
122
+ expect.objectContaining({
123
+ id: event.id,
124
+ }),
125
+ expect.objectContaining({
126
+ id: group.id,
127
+ }),
128
+ expect.objectContaining({
129
+ id: otherGroup.id,
130
+ }),
131
+ ]);
132
+ });
133
+
134
+ it('Cannot return all groups to a normal admin', async () => {
135
+ const { organization } = await initTest();
136
+ const { adminToken } = await initAdmin({ organization });
137
+ await expect(get({ token: adminToken })).rejects.toThrow(STExpect.simpleError({ code: 'permission_denied' }));
138
+ });
139
+
140
+ it('Can return a group by ID for an organization', async () => {
141
+ const { organization, token, group } = await initTest();
142
+ const result = await get({
143
+ organization,
144
+ token,
145
+ filter: {
146
+ id: group.id,
147
+ },
148
+ });
149
+ expect(result).toIncludeSameMembers([
150
+ expect.objectContaining({
151
+ id: group.id,
152
+ }),
153
+ ]);
154
+ });
155
+
156
+ it('Can return a group by bundle discount id', async () => {
157
+ const { organizationRegistrationPeriod, organization, token, group, event } = await initTest();
158
+ const bundleDiscount = await initBundleDiscount({ organizationRegistrationPeriod, discount: {} });
159
+
160
+ // Add a price to event with a bundle discount
161
+ const eventGroupPriceAdded = GroupPrice.create({
162
+ bundleDiscounts: new Map([
163
+ [bundleDiscount.id, BundleDiscountGroupPriceSettings.create({ name: bundleDiscount.name })],
164
+ ]),
165
+ });
166
+ event.settings.prices.push(eventGroupPriceAdded);
167
+ await event.save();
168
+
169
+ // Set the main group price to the group
170
+ group.settings.prices[0].bundleDiscounts.set(bundleDiscount.id, BundleDiscountGroupPriceSettings.create({
171
+ name: bundleDiscount.name,
172
+ }));
173
+ await group.save();
174
+
175
+ const result = await get({
176
+ organization,
177
+ token,
178
+ filter: {
179
+ bundleDiscounts: {
180
+ [bundleDiscount.id]: {
181
+ $neq: null,
182
+ },
183
+ },
184
+ },
185
+ });
186
+ expect(result).toIncludeSameMembers([
187
+ expect.objectContaining({
188
+ id: event.id,
189
+ }),
190
+ expect.objectContaining({
191
+ id: group.id,
192
+ }),
193
+ ]);
194
+ });
195
+
196
+ it('Safely escapes ids', async () => {
197
+ const { organizationRegistrationPeriod, organization, token, group, event } = await initTest();
198
+ const bundleDiscount = await initBundleDiscount({ organizationRegistrationPeriod, discount: {} });
199
+ const id = `This Id. contains "[10] and .**' inside of it... \\"\\'"'`;
200
+ const differentId = `This Id. contains "[10] and .**' inside of it... "\\'"'`;
201
+
202
+ // Add a price to event with a bundle discount
203
+ const eventGroupPriceAdded = GroupPrice.create({
204
+ bundleDiscounts: new Map([
205
+ [id, BundleDiscountGroupPriceSettings.create({ name: bundleDiscount.name })],
206
+ ]),
207
+ });
208
+ event.settings.prices.push(eventGroupPriceAdded);
209
+ await event.save();
210
+
211
+ // Set the main group price to the group
212
+ group.settings.prices[0].bundleDiscounts.set(differentId, BundleDiscountGroupPriceSettings.create({
213
+ name: bundleDiscount.name,
214
+ }));
215
+ await group.save();
216
+
217
+ const result = await get({
218
+ organization,
219
+ token,
220
+ filter: {
221
+ bundleDiscounts: {
222
+ [id]: {
223
+ $neq: null,
224
+ },
225
+ },
226
+ },
227
+ });
228
+ expect(result).toIncludeSameMembers([
229
+ expect.objectContaining({
230
+ id: event.id,
231
+ }),
232
+ ]);
233
+ });
234
+ });
@@ -1,34 +1,25 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { Group, Organization } from '@stamhoofd/models';
3
- import { GroupsWithOrganizations } from '@stamhoofd/structures';
2
+ import { assertSort, CountFilteredRequest, getSortFilter, Group as GroupStruct, LimitedFilteredRequest, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
4
3
 
5
- import { AutoEncoder, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
6
- import { Formatter } from '@stamhoofd/utility';
7
- import { StringArrayDecoder } from '../../../decoders/StringArrayDecoder';
4
+ import { Decoder } from '@simonbackx/simple-encoding';
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { Group } from '@stamhoofd/models';
7
+ import { applySQLSorter, compileToModernSQLFilter, SQLModernFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
8
8
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
9
9
  import { Context } from '../../../helpers/Context';
10
- import { SimpleError } from '@simonbackx/simple-errors';
11
- type Params = Record<string, never>;
12
-
13
- class Query extends AutoEncoder {
14
- @field({ decoder: new StringArrayDecoder(StringDecoder) })
15
- ids: string[];
16
-
17
- /**
18
- * List of organizations the requester already knows and doesn't need to be included in the response
19
- */
20
- @field({ decoder: new StringArrayDecoder(StringDecoder), optional: true })
21
- excludeOrganizationIds: string[] = [];
22
- }
10
+ import { groupFilterCompilers } from '../../../sql-filters/groups';
11
+ import { groupSorters } from '../../../sql-sorters/groups';
23
12
 
13
+ type Params = Record<string, never>;
14
+ type Query = LimitedFilteredRequest;
24
15
  type Body = undefined;
25
- type ResponseBody = GroupsWithOrganizations;
16
+ type ResponseBody = PaginatedResponse<GroupStruct[], LimitedFilteredRequest>;
17
+
18
+ const filterCompilers: SQLModernFilterDefinitions = groupFilterCompilers;
19
+ const sorters: SQLSortDefinitions<Group> = groupSorters;
26
20
 
27
- /**
28
- * Get the members of the user
29
- */
30
21
  export class GetGroupsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
31
- queryDecoder = Query as Decoder<Query>;
22
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
32
23
 
33
24
  protected doesMatch(request: Request): [true, Params] | [false] {
34
25
  if (request.method !== 'GET') {
@@ -40,40 +31,123 @@ export class GetGroupsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
40
31
  if (params) {
41
32
  return [true, params as Params];
42
33
  }
43
-
44
34
  return [false];
45
35
  }
46
36
 
37
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
+ const organization = Context.organization;
39
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
40
+
41
+ if (organization) {
42
+ scopeFilter = {
43
+ organizationId: organization.id,
44
+ };
45
+ }
46
+
47
+ const query = Group.select();
48
+
49
+ if (scopeFilter) {
50
+ query.where(await compileToModernSQLFilter(scopeFilter, filterCompilers));
51
+ }
52
+
53
+ if (q.filter) {
54
+ query.where(await compileToModernSQLFilter(q.filter, filterCompilers));
55
+ }
56
+
57
+ if (q.search) {
58
+ let searchFilter: StamhoofdFilter | null = null;
59
+
60
+ searchFilter = {
61
+ id: q.search,
62
+ };
63
+
64
+ if (searchFilter) {
65
+ query.where(await compileToModernSQLFilter(searchFilter, filterCompilers));
66
+ }
67
+ }
68
+
69
+ if (q instanceof LimitedFilteredRequest) {
70
+ if (q.pageFilter) {
71
+ query.where(await compileToModernSQLFilter(q.pageFilter, filterCompilers));
72
+ }
73
+
74
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
75
+ applySQLSorter(query, q.sort, sorters);
76
+ query.limit(q.limit);
77
+ }
78
+
79
+ return query;
80
+ }
81
+
82
+ static async buildData(requestQuery: LimitedFilteredRequest) {
83
+ const query = await GetGroupsEndpoint.buildQuery(requestQuery);
84
+ const groups = await query.fetch();
85
+
86
+ let next: LimitedFilteredRequest | undefined;
87
+
88
+ if (groups.length >= requestQuery.limit) {
89
+ const lastObject = groups[groups.length - 1];
90
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
91
+
92
+ next = new LimitedFilteredRequest({
93
+ filter: requestQuery.filter,
94
+ pageFilter: nextFilter,
95
+ sort: requestQuery.sort,
96
+ limit: requestQuery.limit,
97
+ search: requestQuery.search,
98
+ });
99
+
100
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
101
+ console.error('Found infinite loading loop for', requestQuery);
102
+ next = undefined;
103
+ }
104
+ }
105
+
106
+ return new PaginatedResponse<GroupStruct[], LimitedFilteredRequest>({
107
+ results: await AuthenticatedStructures.groups(groups),
108
+ next,
109
+ });
110
+ }
111
+
47
112
  async handle(request: DecodedRequest<Params, Query, Body>) {
48
- await Context.setOptionalOrganizationScope();
113
+ if (request.request.getVersion() < 373) {
114
+ throw new SimpleError({
115
+ code: 'client_update_required',
116
+ statusCode: 400,
117
+ message: 'Er is een noodzakelijke update beschikbaar. Herlaad de pagina en wis indien nodig de cache van jouw browser.',
118
+ human: $t(`adb0e7c8-aed7-43f5-bfcc-a350f03aaabe`),
119
+ });
120
+ }
121
+
122
+ const organization = await Context.setOptionalOrganizationScope();
49
123
  await Context.optionalAuthenticate();
50
124
 
51
- if (request.query.ids.length === 0) {
52
- return new Response(
53
- GroupsWithOrganizations.create({
54
- groups: [],
55
- organizations: [],
56
- }),
57
- );
125
+ if (!organization) {
126
+ if (!Context.auth.hasSomePlatformAccess()) {
127
+ throw Context.auth.error();
128
+ }
58
129
  }
59
130
 
60
- if (request.query.ids.length > 100) {
131
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
132
+
133
+ if (request.query.limit > maxLimit) {
61
134
  throw new SimpleError({
62
- code: 'too_many_ids',
63
- message: "You can't request more than 100 groups at once",
135
+ code: 'invalid_field',
136
+ field: 'limit',
137
+ message: 'Limit can not be more than ' + maxLimit,
64
138
  });
65
139
  }
66
140
 
67
- const groups = await Group.getByIDs(...request.query.ids);
68
- const organizationIds = Formatter.uniqueArray(groups.map(g => g.organizationId).filter(id => !request.query.excludeOrganizationIds.includes(id)));
69
-
70
- const organizations = organizationIds.length > 0 ? (await Organization.getByIDs(...organizationIds)) : [];
141
+ if (request.query.limit < 1) {
142
+ throw new SimpleError({
143
+ code: 'invalid_field',
144
+ field: 'limit',
145
+ message: 'Limit can not be less than 1',
146
+ });
147
+ }
71
148
 
72
149
  return new Response(
73
- GroupsWithOrganizations.create({
74
- groups: await AuthenticatedStructures.groups(groups),
75
- organizations: await AuthenticatedStructures.organizations(organizations),
76
- }),
150
+ await GetGroupsEndpoint.buildData(request.query),
77
151
  );
78
152
  }
79
153
  }