@stamhoofd/backend 2.45.0 → 2.48.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.
@@ -1,8 +1,8 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, PrivateOrder, StamhoofdFilter } from '@stamhoofd/structures';
3
+ import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PrivateOrder, StamhoofdFilter } from '@stamhoofd/structures';
4
4
 
5
- import { Order, Webshop } from '@stamhoofd/models';
5
+ import { Order } from '@stamhoofd/models';
6
6
  import { compileToSQLFilter, compileToSQLSorter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
7
7
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
8
  import { Context } from '../../../../helpers/Context';
@@ -10,7 +10,7 @@ import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFiltere
10
10
  import { orderFilterCompilers } from '../../../../sql-filters/orders';
11
11
  import { orderSorters } from '../../../../sql-sorters/orders';
12
12
 
13
- type Params = { id: string };
13
+ type Params = Record<string, never>;
14
14
  type Query = LimitedFilteredRequest;
15
15
  type Body = undefined;
16
16
  type ResponseBody = PaginatedResponse<PrivateOrder[], LimitedFilteredRequest>;
@@ -26,7 +26,7 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
26
26
  return [false];
27
27
  }
28
28
 
29
- const params = Endpoint.parseParameters(request.url, '/webshop/@id/orders', { id: String });
29
+ const params = Endpoint.parseParameters(request.url, '/webshop/orders', {});
30
30
 
31
31
  if (params) {
32
32
  return [true, params as Params];
@@ -34,37 +34,24 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
34
34
  return [false];
35
35
  }
36
36
 
37
- static buildQuery(webshopId: string, q: CountFilteredRequest | LimitedFilteredRequest) {
37
+ static buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
38
  // todo: filter userId???
39
39
  const organization = Context.organization!;
40
40
 
41
- if (!webshopId) {
42
- // todo
43
- throw new Error();
44
- }
45
-
46
41
  const ordersTable: string = Order.table;
47
42
 
48
43
  const query = SQL
49
44
  .select(SQL.wildcard(ordersTable))
50
45
  .from(SQL.table(ordersTable))
51
- // todo: extra check on webshopId to prevent all orders are returned if webshopId is null?
52
- .where('webshopId', webshopId)
53
46
  .where(compileToSQLFilter({
54
- $or: [
55
- {
56
- organizationId: organization.id,
57
- },
58
- {
59
- organizationId: null,
60
- },
61
- ],
47
+ organizationId: organization.id,
62
48
  }, filterCompilers));
63
49
 
64
50
  if (q.filter) {
65
51
  query.where(compileToSQLFilter(q.filter, filterCompilers));
66
52
  }
67
53
 
54
+ // todo: use same logic as frontend
68
55
  if (q.search) {
69
56
  let searchFilter: StamhoofdFilter | null = null;
70
57
 
@@ -93,8 +80,8 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
93
80
  return query;
94
81
  }
95
82
 
96
- static async buildData(webshopId: string, requestQuery: LimitedFilteredRequest) {
97
- const query = this.buildQuery(webshopId, requestQuery);
83
+ static async buildData(requestQuery: LimitedFilteredRequest) {
84
+ const query = this.buildQuery(requestQuery);
98
85
  const data = await query.fetch();
99
86
 
100
87
  const orders: Order[] = Order.fromRows(data, Order.table);
@@ -139,15 +126,8 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
139
126
  maxLimit: Context.auth.hasSomePlatformAccess() ? 1000 : 100,
140
127
  });
141
128
 
142
- const webshopId = request.params.id;
143
-
144
- const webshop = await Webshop.getByID(webshopId);
145
- if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
146
- throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot de bestellingen van deze webshop');
147
- }
148
-
149
129
  return new Response(
150
- await GetWebshopOrdersEndpoint.buildData(webshopId, request.query),
130
+ await GetWebshopOrdersEndpoint.buildData(request.query),
151
131
  );
152
132
 
153
133
  /*
@@ -0,0 +1,47 @@
1
+ import { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
+ import { ExcelExportType, LimitedFilteredRequest, Organization as OrganizationStruct } from '@stamhoofd/structures';
3
+ import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint';
4
+ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
+
6
+ // Assign to a typed variable to assure we have correct type checking in place
7
+ const sheet: XlsxTransformerSheet<OrganizationStruct, OrganizationStruct> = {
8
+ id: 'organizations',
9
+ name: 'Leden',
10
+ columns: [
11
+ {
12
+ id: 'id',
13
+ name: 'ID',
14
+ width: 20,
15
+ getValue: (object: OrganizationStruct) => ({
16
+ value: object.id,
17
+ }),
18
+ },
19
+ {
20
+ id: 'uri',
21
+ name: 'Groepsnummer',
22
+ width: 20,
23
+ getValue: (object: OrganizationStruct) => ({
24
+ value: object.uri,
25
+ }),
26
+ },
27
+ {
28
+ id: 'name',
29
+ name: 'Naam',
30
+ width: 50,
31
+ getValue: (object: OrganizationStruct) => ({
32
+ value: object.name,
33
+ }),
34
+ },
35
+ ],
36
+ };
37
+
38
+ ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
39
+ fetch: async (query: LimitedFilteredRequest) => {
40
+ const result = await GetOrganizationsEndpoint.buildData(query);
41
+
42
+ return result;
43
+ },
44
+ sheets: [
45
+ sheet,
46
+ ],
47
+ });
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { CachedBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
3
- import { AccessRight, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateOrder, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
3
+ import { AccessRight, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { Context } from './Context';
@@ -130,6 +130,10 @@ export class AuthenticatedStructures {
130
130
  return (await this.organizations([organization]))[0];
131
131
  }
132
132
 
133
+ static webshopPreview(webshop: Webshop) {
134
+ return WebshopPreview.create(webshop);
135
+ }
136
+
133
137
  static async organizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
134
138
  if (organizations.length === 0) {
135
139
  return [];
@@ -210,7 +214,7 @@ export class AuthenticatedStructures {
210
214
 
211
215
  const organizationId = w.organizationId;
212
216
  const array = webshopPreviews.get(organizationId);
213
- const preview = WebshopPreview.create(w);
217
+ const preview = this.webshopPreview(w);
214
218
 
215
219
  if (array) {
216
220
  array.push(preview);
@@ -0,0 +1,28 @@
1
+ import { Model } from '@simonbackx/simple-database';
2
+
3
+ // todo: move for reuse?
4
+ type KeysMatching<T, V> = { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T];
5
+
6
+ export class ModelHelper {
7
+ static async loop<M extends typeof Model>(m: M, idKey: KeysMatching<InstanceType<M>, string> & string, onBatchReceived: (batch: InstanceType<M>[]) => Promise<void>, options: { limit?: number } = {}) {
8
+ let lastId = '';
9
+ const limit = options.limit ?? 10;
10
+
11
+ while (true) {
12
+ const models = await m.where(
13
+ { [idKey]: { sign: '>', value: lastId } },
14
+ { limit, sort: [idKey] });
15
+
16
+ if (models.length === 0) {
17
+ break;
18
+ }
19
+
20
+ await onBatchReceived(models);
21
+
22
+ lastId
23
+ = models[
24
+ models.length - 1
25
+ ][idKey] as string;
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,373 @@
1
+ import { OrganizationTag } from '@stamhoofd/structures';
2
+ import { TagHelper } from './TagHelper';
3
+
4
+ // todo: move tests for methods of shared package to shared package
5
+ describe('TagHelper', () => {
6
+ describe('containsDeep', () => {
7
+ it('should return true if the tag contains the tag to search recursively or false otherwise', () => {
8
+ // arrange
9
+ const tags = new Map<string, OrganizationTag>([
10
+ [
11
+ 'id1',
12
+ OrganizationTag.create({
13
+ id: 'id1',
14
+ name: 'tag1',
15
+ childTags: [],
16
+ }),
17
+ ],
18
+ [
19
+ 'id2',
20
+ OrganizationTag.create({
21
+ id: 'id2',
22
+ name: 'tag2',
23
+ childTags: [],
24
+ }),
25
+ ],
26
+ [
27
+ 'id3',
28
+ OrganizationTag.create({
29
+ id: 'id3',
30
+ name: 'tag3',
31
+ childTags: ['id2', 'id4'],
32
+ }),
33
+ ],
34
+ [
35
+ 'id4',
36
+ OrganizationTag.create({
37
+ id: 'id4',
38
+ name: 'tag4',
39
+ childTags: ['id5'],
40
+ }),
41
+ ],
42
+ [
43
+ 'id5',
44
+ OrganizationTag.create({
45
+ id: 'id5',
46
+ name: 'tag5',
47
+ childTags: [],
48
+ }),
49
+ ],
50
+ ]);
51
+
52
+ // act
53
+ const doesTag3ContainTag4 = TagHelper.containsDeep('id3', 'id4', tags);
54
+ const doesTag3ContainTag5 = TagHelper.containsDeep('id3', 'id5', tags);
55
+ const doesTag3ContainTag1 = TagHelper.containsDeep('id3', 'id1', tags);
56
+
57
+ // assert
58
+ expect(doesTag3ContainTag4).toBe(true);
59
+ expect(doesTag3ContainTag5).toBe(true);
60
+ expect(doesTag3ContainTag1).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe('getAllTagsFromHierarchy', () => {
65
+ it('should return array with the tag ids that are known by the platform and add tag ids if the organization has a tag that is a child tag of that tag', () => {
66
+ // arrange
67
+ const originalTagIds: string[] = ['id5', 'id3', 'unknownTagId'];
68
+ const platformTags: OrganizationTag[] = [
69
+ OrganizationTag.create({
70
+ id: 'id0',
71
+ name: 'tag0',
72
+ childTags: ['id7', 'id9'],
73
+ }),
74
+ OrganizationTag.create({
75
+ id: 'id1',
76
+ name: 'tag1',
77
+ childTags: [],
78
+ }),
79
+ OrganizationTag.create({
80
+ id: 'id2',
81
+ name: 'tag2',
82
+ childTags: [],
83
+ }),
84
+ OrganizationTag.create({
85
+ id: 'id3',
86
+ name: 'tag3',
87
+ childTags: [],
88
+ }),
89
+ OrganizationTag.create({
90
+ id: 'id4',
91
+ name: 'tag4',
92
+ childTags: ['id3'],
93
+ }),
94
+ OrganizationTag.create({
95
+ id: 'id5',
96
+ name: 'tag5',
97
+ childTags: [],
98
+ }),
99
+ OrganizationTag.create({
100
+ id: 'id6',
101
+ name: 'tag6',
102
+ childTags: ['id5'],
103
+ }),
104
+ OrganizationTag.create({
105
+ id: 'id7',
106
+ name: 'tag7',
107
+ childTags: ['id6', 'id8', 'id9'],
108
+ }),
109
+ OrganizationTag.create({
110
+ id: 'id8',
111
+ name: 'tag8',
112
+ childTags: [],
113
+ }),
114
+ OrganizationTag.create({
115
+ id: 'id9',
116
+ name: 'tag9',
117
+ childTags: [],
118
+ }),
119
+ ];
120
+
121
+ // act
122
+ const result = TagHelper.getAllTagsFromHierarchy(originalTagIds, platformTags);
123
+
124
+ // assert
125
+ expect(result).toHaveLength(6);
126
+ expect(result).toInclude('id5');
127
+ expect(result).toInclude('id3');
128
+ expect(result).toInclude('id4');
129
+ expect(result).toInclude('id6');
130
+ expect(result).toInclude('id7');
131
+ expect(result).toInclude('id0');
132
+ expect(result).not.toInclude('unknownTagId');
133
+ });
134
+ });
135
+
136
+ describe('getCleanedUpTags', () => {
137
+ it('should remove child tag ids that do not exist', () => {
138
+ // arrange
139
+ const tag5 = OrganizationTag.create({
140
+ id: 'id5',
141
+ name: 'tag5',
142
+ childTags: ['doesNotExist2'],
143
+ });
144
+
145
+ const tag7 = OrganizationTag.create({
146
+ id: 'id7',
147
+ name: 'tag7',
148
+ childTags: ['id6', 'id8', 'id9', 'doesNotExist1'],
149
+ });
150
+
151
+ const platformTags: OrganizationTag[] = [
152
+ OrganizationTag.create({
153
+ id: 'id0',
154
+ name: 'tag0',
155
+ childTags: ['id7', 'id9'],
156
+ }),
157
+ OrganizationTag.create({
158
+ id: 'id1',
159
+ name: 'tag1',
160
+ childTags: [],
161
+ }),
162
+ OrganizationTag.create({
163
+ id: 'id2',
164
+ name: 'tag2',
165
+ childTags: [],
166
+ }),
167
+ OrganizationTag.create({
168
+ id: 'id3',
169
+ name: 'tag3',
170
+ childTags: [],
171
+ }),
172
+ OrganizationTag.create({
173
+ id: 'id4',
174
+ name: 'tag4',
175
+ childTags: ['id3'],
176
+ }),
177
+ tag5,
178
+ OrganizationTag.create({
179
+ id: 'id6',
180
+ name: 'tag6',
181
+ childTags: ['id5'],
182
+ }),
183
+ tag7,
184
+ OrganizationTag.create({
185
+ id: 'id8',
186
+ name: 'tag8',
187
+ childTags: [],
188
+ }),
189
+ OrganizationTag.create({
190
+ id: 'id9',
191
+ name: 'tag9',
192
+ childTags: [],
193
+ }),
194
+ ];
195
+
196
+ // act
197
+ TagHelper.getCleanedUpTags(platformTags);
198
+
199
+ // assert
200
+ expect(tag5.childTags).toHaveLength(0);
201
+ expect(tag7.childTags).toHaveLength(3);
202
+ expect(tag7.childTags).not.toInclude('doesNotExist1');
203
+ });
204
+
205
+ it('should return array of tags in the correct order', () => {
206
+ // arrange
207
+ const platformTags: OrganizationTag[] = [
208
+ OrganizationTag.create({
209
+ id: 'id2b1',
210
+ name: 'tag2b1',
211
+ childTags: [],
212
+ }),
213
+ OrganizationTag.create({
214
+ id: 'id1',
215
+ name: 'tag1',
216
+ childTags: [],
217
+ }),
218
+ OrganizationTag.create({
219
+ id: 'id2',
220
+ name: 'tag2',
221
+ childTags: ['id2a', 'id2b'],
222
+ }),
223
+ OrganizationTag.create({
224
+ id: 'id3',
225
+ name: 'tag3',
226
+ childTags: [],
227
+ }),
228
+ OrganizationTag.create({
229
+ id: 'id2a',
230
+ name: 'tag2a',
231
+ childTags: ['id2a1', 'id2a2'],
232
+ }),
233
+ OrganizationTag.create({
234
+ id: 'id2b',
235
+ name: 'tag2b',
236
+ childTags: ['id2b1'],
237
+ }),
238
+ OrganizationTag.create({
239
+ id: 'id2a1',
240
+ name: 'tag2a1',
241
+ childTags: [],
242
+ }),
243
+ OrganizationTag.create({
244
+ id: 'id2a2',
245
+ name: 'tag2a2',
246
+ childTags: [],
247
+ }),
248
+ ];
249
+
250
+ // act
251
+ const result = TagHelper.getCleanedUpTags(platformTags);
252
+
253
+ // assert
254
+ expect(result).toHaveLength(8);
255
+ expect(result[0].id).toBe('id1');
256
+ expect(result[1].id).toBe('id2');
257
+ expect(result[2].id).toBe('id2a');
258
+ expect(result[3].id).toBe('id2a1');
259
+ expect(result[4].id).toBe('id2a2');
260
+ expect(result[5].id).toBe('id2b');
261
+ expect(result[6].id).toBe('id2b1');
262
+ expect(result[7].id).toBe('id3');
263
+ });
264
+ });
265
+
266
+ describe('validateTags', () => {
267
+ it('should return false if a tag is a child tag of itself', () => {
268
+ // arrange
269
+ const invalidPlatformTags: OrganizationTag[] = [
270
+ OrganizationTag.create({
271
+ id: 'id1',
272
+ name: 'tag1',
273
+ childTags: ['id1'],
274
+ }),
275
+ ];
276
+
277
+ const validPlatformTags: OrganizationTag[] = [
278
+ OrganizationTag.create({
279
+ id: 'id1',
280
+ name: 'tag1',
281
+ childTags: ['id2'],
282
+ }),
283
+ OrganizationTag.create({
284
+ id: 'id2',
285
+ name: 'tag2',
286
+ childTags: [],
287
+ }),
288
+ ];
289
+
290
+ // act
291
+ const result1 = TagHelper.validateTags(invalidPlatformTags);
292
+ const result2 = TagHelper.validateTags(validPlatformTags);
293
+
294
+ // assert
295
+ expect(result1).toBeFalse();
296
+ expect(result2).toBeTrue();
297
+ });
298
+
299
+ it('should return false if a tag is a child tag of multiple tags', () => {
300
+ // arrange
301
+ const invalidPlatformTags: OrganizationTag[] = [
302
+ OrganizationTag.create({
303
+ id: 'id1',
304
+ name: 'tag1',
305
+ childTags: [],
306
+ }),
307
+ OrganizationTag.create({
308
+ id: 'id2',
309
+ name: 'tag2',
310
+ childTags: ['id1'],
311
+ }),
312
+ OrganizationTag.create({
313
+ id: 'id3',
314
+ name: 'tag3',
315
+ childTags: ['id1'],
316
+ }),
317
+ ];
318
+
319
+ const validPlatformTags: OrganizationTag[] = [
320
+ OrganizationTag.create({
321
+ id: 'id1',
322
+ name: 'tag1',
323
+ childTags: [],
324
+ }),
325
+ OrganizationTag.create({
326
+ id: 'id2',
327
+ name: 'tag2',
328
+ childTags: ['id3'],
329
+ }),
330
+ OrganizationTag.create({
331
+ id: 'id3',
332
+ name: 'tag3',
333
+ childTags: ['id1'],
334
+ }),
335
+ ];
336
+
337
+ // act
338
+ const result1 = TagHelper.validateTags(invalidPlatformTags);
339
+ const result2 = TagHelper.validateTags(validPlatformTags);
340
+
341
+ // assert
342
+ expect(result1).toBeFalse();
343
+ expect(result2).toBeTrue();
344
+ });
345
+
346
+ it('should return false if the child tags contain an infinite loop', () => {
347
+ // arrange
348
+ const platformTagsWithInfiniteLoop: OrganizationTag[] = [
349
+ OrganizationTag.create({
350
+ id: 'id1',
351
+ name: 'tag1',
352
+ childTags: ['id2'],
353
+ }),
354
+ OrganizationTag.create({
355
+ id: 'id2',
356
+ name: 'tag2',
357
+ childTags: ['id3'],
358
+ }),
359
+ OrganizationTag.create({
360
+ id: 'id3',
361
+ name: 'tag3',
362
+ childTags: ['id1'],
363
+ }),
364
+ ];
365
+
366
+ // act
367
+ const result = TagHelper.validateTags(platformTagsWithInfiniteLoop);
368
+
369
+ // assert
370
+ expect(result).toBeFalse();
371
+ });
372
+ });
373
+ });