@stamhoofd/backend 2.43.1 → 2.43.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.43.1",
3
+ "version": "2.43.3",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -36,14 +36,14 @@
36
36
  "@simonbackx/simple-encoding": "2.15.1",
37
37
  "@simonbackx/simple-endpoints": "1.14.0",
38
38
  "@simonbackx/simple-logging": "^1.0.1",
39
- "@stamhoofd/backend-i18n": "2.43.1",
40
- "@stamhoofd/backend-middleware": "2.43.1",
41
- "@stamhoofd/email": "2.43.1",
42
- "@stamhoofd/models": "2.43.1",
43
- "@stamhoofd/queues": "2.43.1",
44
- "@stamhoofd/sql": "2.43.1",
45
- "@stamhoofd/structures": "2.43.1",
46
- "@stamhoofd/utility": "2.43.1",
39
+ "@stamhoofd/backend-i18n": "2.43.3",
40
+ "@stamhoofd/backend-middleware": "2.43.3",
41
+ "@stamhoofd/email": "2.43.3",
42
+ "@stamhoofd/models": "2.43.3",
43
+ "@stamhoofd/queues": "2.43.3",
44
+ "@stamhoofd/sql": "2.43.3",
45
+ "@stamhoofd/structures": "2.43.3",
46
+ "@stamhoofd/utility": "2.43.3",
47
47
  "archiver": "^7.0.1",
48
48
  "aws-sdk": "^2.885.0",
49
49
  "axios": "1.6.8",
@@ -60,5 +60,5 @@
60
60
  "postmark": "^4.0.5",
61
61
  "stripe": "^16.6.0"
62
62
  },
63
- "gitHead": "49f9a42eb52688f4f4d417871da7448969623013"
63
+ "gitHead": "1258338deacc6d61a53da939dda894dca3415c84"
64
64
  }
@@ -53,6 +53,11 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
53
53
  event.organizationId = put.organizationId;
54
54
  event.meta = put.meta;
55
55
 
56
+ if (event.organizationId === null && event.meta.groups !== null) {
57
+ event.meta.groups = null;
58
+ console.error('Removed groups because organizationId is null for new event');
59
+ }
60
+
56
61
  if (event.meta.groups && event.meta.groups.length === 0) {
57
62
  throw new SimpleError({
58
63
  code: 'invalid_field',
@@ -131,6 +136,11 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
131
136
  event.organizationId = patch.organizationId;
132
137
  }
133
138
 
139
+ if (event.organizationId === null && event.meta.groups !== null) {
140
+ event.meta.groups = null;
141
+ console.error('Removed groups because organizationId is null for event', event.id);
142
+ }
143
+
134
144
  if (event.meta.groups && event.meta.groups.length === 0) {
135
145
  throw new SimpleError({
136
146
  code: 'invalid_field',
@@ -0,0 +1,57 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse, PermissionLevel } from '@stamhoofd/structures';
4
+
5
+ import { Webshop } from '@stamhoofd/models';
6
+ import { Context } from '../../../../helpers/Context';
7
+ import { GetWebshopOrdersEndpoint } from './GetWebshopOrdersEndpoint';
8
+
9
+ type Params = { id: string };
10
+ type Query = CountFilteredRequest;
11
+ type Body = undefined;
12
+ type ResponseBody = CountResponse;
13
+
14
+ export class GetWebshopOrdersCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
16
+
17
+ protected doesMatch(request: Request): [true, Params] | [false] {
18
+ if (request.method !== 'GET') {
19
+ return [false];
20
+ }
21
+
22
+ const params = Endpoint.parseParameters(request.url, '/webshop/@id/orders/count', { id: String });
23
+
24
+ if (params) {
25
+ return [true, params as Params];
26
+ }
27
+ return [false];
28
+ }
29
+
30
+ async handle(request: DecodedRequest<Params, Query, Body>) {
31
+ const organization = await Context.setOrganizationScope();
32
+ await Context.authenticate();
33
+
34
+ // Fast throw first (more in depth checking for patches later)
35
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
36
+ throw Context.auth.error();
37
+ }
38
+
39
+ const webshopId = request.params.id;
40
+
41
+ const webshop = await Webshop.getByID(webshopId);
42
+ if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
43
+ throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot de bestellingen van deze webshop');
44
+ }
45
+
46
+ const query = GetWebshopOrdersEndpoint.buildQuery(webshopId, request.query);
47
+
48
+ const count = await query
49
+ .count();
50
+
51
+ return new Response(
52
+ CountResponse.create({
53
+ count,
54
+ }),
55
+ );
56
+ }
57
+ }
@@ -1,17 +1,25 @@
1
1
  import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
- import { Order, Webshop } from '@stamhoofd/models';
4
- import { PaginatedResponse, PermissionLevel, PrivateOrder, WebshopOrdersQuery } from '@stamhoofd/structures';
3
+ import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, PrivateOrder, StamhoofdFilter } from '@stamhoofd/structures';
5
4
 
5
+ import { Order, Webshop } from '@stamhoofd/models';
6
+ import { compileToSQLFilter, compileToSQLSorter, SQL, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
6
8
  import { Context } from '../../../../helpers/Context';
9
+ import { LimitedFilteredRequestHelper } from '../../../../helpers/LimitedFilteredRequestHelper';
10
+ import { orderFilterCompilers } from '../../../../sql-filters/orders';
11
+ import { orderSorters } from '../../../../sql-sorters/orders';
7
12
 
8
13
  type Params = { id: string };
9
- type Query = WebshopOrdersQuery;
14
+ type Query = LimitedFilteredRequest;
10
15
  type Body = undefined;
11
- type ResponseBody = PaginatedResponse<PrivateOrder[], Query>;
16
+ type ResponseBody = PaginatedResponse<PrivateOrder[], LimitedFilteredRequest>;
17
+
18
+ const filterCompilers: SQLFilterDefinitions = orderFilterCompilers;
19
+ const sorters: SQLSortDefinitions<Order> = orderSorters;
12
20
 
13
21
  export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
- queryDecoder = WebshopOrdersQuery as Decoder<WebshopOrdersQuery>;
22
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
15
23
 
16
24
  protected doesMatch(request: Request): [true, Params] | [false] {
17
25
  if (request.method !== 'GET') {
@@ -26,22 +34,124 @@ export class GetWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Resp
26
34
  return [false];
27
35
  }
28
36
 
29
- async handle(_: DecodedRequest<Params, Query, Body>): Promise<Response<ResponseBody>> {
30
- await Promise.resolve();
31
- throw new Error('Not implemented');
32
- /* const organization = await Context.setOrganizationScope();
33
- await Context.authenticate()
37
+ static buildQuery(webshopId: string, q: CountFilteredRequest | LimitedFilteredRequest) {
38
+ // todo: filter userId???
39
+ const organization = Context.organization!;
40
+
41
+ if (!webshopId) {
42
+ // todo
43
+ throw new Error();
44
+ }
45
+
46
+ const ordersTable: string = Order.table;
47
+
48
+ const query = SQL
49
+ .select(SQL.wildcard(ordersTable))
50
+ .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
+ .where(compileToSQLFilter({
54
+ $or: [
55
+ {
56
+ organizationId: organization.id,
57
+ },
58
+ {
59
+ organizationId: null,
60
+ },
61
+ ],
62
+ }, filterCompilers));
63
+
64
+ if (q.filter) {
65
+ query.where(compileToSQLFilter(q.filter, filterCompilers));
66
+ }
67
+
68
+ if (q.search) {
69
+ let searchFilter: StamhoofdFilter | null = null;
70
+
71
+ // todo: detect special search patterns and adjust search filter if needed
72
+ searchFilter = {
73
+ name: {
74
+ $contains: q.search,
75
+ },
76
+ };
77
+
78
+ if (searchFilter) {
79
+ query.where(compileToSQLFilter(searchFilter, filterCompilers));
80
+ }
81
+ }
82
+
83
+ if (q instanceof LimitedFilteredRequest) {
84
+ if (q.pageFilter) {
85
+ query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
86
+ }
87
+
88
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
89
+ query.orderBy(compileToSQLSorter(q.sort, sorters));
90
+ query.limit(q.limit);
91
+ }
92
+
93
+ return query;
94
+ }
95
+
96
+ static async buildData(webshopId: string, requestQuery: LimitedFilteredRequest) {
97
+ const query = this.buildQuery(webshopId, requestQuery);
98
+ const data = await query.fetch();
99
+
100
+ const orders: Order[] = Order.fromRows(data, Order.table);
101
+
102
+ let next: LimitedFilteredRequest | undefined;
103
+
104
+ if (orders.length >= requestQuery.limit) {
105
+ const lastObject = orders[orders.length - 1];
106
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
107
+
108
+ next = new LimitedFilteredRequest({
109
+ filter: requestQuery.filter,
110
+ pageFilter: nextFilter,
111
+ sort: requestQuery.sort,
112
+ limit: requestQuery.limit,
113
+ search: requestQuery.search,
114
+ });
115
+
116
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
117
+ console.error('Found infinite loading loop for', requestQuery);
118
+ next = undefined;
119
+ }
120
+ }
121
+
122
+ return new PaginatedResponse<PrivateOrder[], LimitedFilteredRequest>({
123
+ results: await AuthenticatedStructures.orders(orders),
124
+ next,
125
+ });
126
+ }
127
+
128
+ async handle(request: DecodedRequest<Params, Query, Body>): Promise<Response<ResponseBody>> {
129
+ const organization = await Context.setOrganizationScope();
130
+ await Context.authenticate();
34
131
 
35
132
  // Fast throw first (more in depth checking for patches later)
36
133
  if (!await Context.auth.hasSomeAccess(organization.id)) {
37
- throw Context.auth.error()
134
+ throw Context.auth.error();
38
135
  }
39
136
 
40
- const webshop = await Webshop.getByID(request.params.id)
137
+ LimitedFilteredRequestHelper.throwIfInvalidLimit({
138
+ request: request.query,
139
+ maxLimit: Context.auth.hasSomePlatformAccess() ? 1000 : 100,
140
+ });
141
+
142
+ const webshopId = request.params.id;
143
+
144
+ const webshop = await Webshop.getByID(webshopId);
41
145
  if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Read)) {
42
- throw Context.auth.notFoundOrNoAccess("Je hebt geen toegang tot de bestellingen van deze webshop")
146
+ throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot de bestellingen van deze webshop');
43
147
  }
44
148
 
149
+ return new Response(
150
+ await GetWebshopOrdersEndpoint.buildData(webshopId, request.query),
151
+ );
152
+
153
+ /*
154
+
45
155
  let orders: Order[] | undefined = undefined
46
156
  const limit = 50
47
157
 
@@ -1,6 +1,6 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { CachedOutstandingBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
3
- import { AccessRight, CachedOutstandingBalanceObject, CachedOutstandingBalanceObjectContact, CachedOutstandingBalance as CachedOutstandingBalanceStruct, CachedOutstandingBalanceType, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
2
+ import { CachedOutstandingBalance, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
3
+ import { AccessRight, CachedOutstandingBalanceObject, CachedOutstandingBalanceObjectContact, CachedOutstandingBalance as CachedOutstandingBalanceStruct, CachedOutstandingBalanceType, 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';
4
4
 
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { Context } from './Context';
@@ -423,6 +423,29 @@ export class AuthenticatedStructures {
423
423
  return result;
424
424
  }
425
425
 
426
+ static async orders(orders: Order[]): Promise<PrivateOrder[]> {
427
+ // Load groups
428
+ // const groupIds = orders.map(e => e.groupId).filter(id => id !== null);
429
+ // const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : [];
430
+ // const groupStructs = await this.groups(groups);
431
+
432
+ const result: PrivateOrder[] = [];
433
+
434
+ for (const order of orders) {
435
+ // const group = groupStructs.find(g => g.id == event.groupId) ?? null;
436
+
437
+ const struct = PrivateOrder.create({
438
+ ...order,
439
+ // todo!!!!!
440
+ balanceItems: [],
441
+ });
442
+
443
+ result.push(struct);
444
+ }
445
+
446
+ return result;
447
+ }
448
+
426
449
  static async cachedOutstandingBalances(balances: CachedOutstandingBalance[]): Promise<CachedOutstandingBalanceStruct[]> {
427
450
  if (balances.length === 0) {
428
451
  return [];
@@ -0,0 +1,24 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { LimitedFilteredRequest } from '@stamhoofd/structures';
3
+
4
+ export class LimitedFilteredRequestHelper {
5
+ static throwIfInvalidLimit({ request, maxLimit }: { request: LimitedFilteredRequest; maxLimit: number }) {
6
+ const requestLimit = request.limit;
7
+
8
+ if (requestLimit > maxLimit) {
9
+ throw new SimpleError({
10
+ code: 'invalid_field',
11
+ field: 'limit',
12
+ message: 'Limit can not be more than ' + maxLimit,
13
+ });
14
+ }
15
+
16
+ if (requestLimit < 1) {
17
+ throw new SimpleError({
18
+ code: 'invalid_field',
19
+ field: 'limit',
20
+ message: 'Limit can not be less than 1',
21
+ });
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,27 @@
1
+ import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQLFilterDefinitions } from '@stamhoofd/sql';
2
+
3
+ export const orderFilterCompilers: SQLFilterDefinitions = {
4
+ ...baseSQLFilterCompilers,
5
+ 'id': createSQLColumnFilterCompiler('id'),
6
+ 'name': createSQLColumnFilterCompiler('name'),
7
+ 'organizationId': createSQLColumnFilterCompiler('organizationId'),
8
+ '#': createSQLColumnFilterCompiler('number'),
9
+ // 'startDate': createSQLColumnFilterCompiler('startDate'),
10
+ // 'endDate': createSQLColumnFilterCompiler('endDate'),
11
+ // 'groupIds': createSQLExpressionFilterCompiler(
12
+ // SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
13
+ // { isJSONValue: true, isJSONObject: true },
14
+ // ),
15
+ // 'defaultAgeGroupIds': createSQLExpressionFilterCompiler(
16
+ // SQL.jsonValue(SQL.column('meta'), '$.value.defaultAgeGroupIds'),
17
+ // { isJSONValue: true, isJSONObject: true },
18
+ // ),
19
+ // 'organizationTagIds': createSQLExpressionFilterCompiler(
20
+ // SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
21
+ // { isJSONValue: true, isJSONObject: true },
22
+ // ),
23
+ // 'meta.visible': createSQLExpressionFilterCompiler(
24
+ // SQL.jsonValue(SQL.column('meta'), '$.value.visible'),
25
+ // { isJSONValue: true, type: SQLValueType.JSONBoolean },
26
+ // ),
27
+ };
@@ -0,0 +1,58 @@
1
+ import { Order } from '@stamhoofd/models';
2
+ import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
3
+ import { Formatter } from '@stamhoofd/utility';
4
+
5
+ export const orderSorters: SQLSortDefinitions<Order> = {
6
+ // WARNING! TEST NEW SORTERS THOROUGHLY!
7
+ // Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
8
+ // An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
9
+ // You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
10
+ // Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
11
+ // And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
12
+ // What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
13
+
14
+ '#': {
15
+ getValue(a) {
16
+ return a.number;
17
+ },
18
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
19
+ return new SQLOrderBy({
20
+ column: SQL.column('number'),
21
+ direction,
22
+ });
23
+ },
24
+ },
25
+ 'id': {
26
+ getValue(a) {
27
+ return a.id;
28
+ },
29
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
30
+ return new SQLOrderBy({
31
+ column: SQL.column('id'),
32
+ direction,
33
+ });
34
+ },
35
+ },
36
+ 'name': {
37
+ getValue(a) {
38
+ return a.id;
39
+ },
40
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
41
+ return new SQLOrderBy({
42
+ column: SQL.column('name'),
43
+ direction,
44
+ });
45
+ },
46
+ },
47
+ 'createdAt': {
48
+ getValue(a) {
49
+ return Formatter.dateTimeIso(a.createdAt);
50
+ },
51
+ toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
52
+ return new SQLOrderBy({
53
+ column: SQL.column('createdAt'),
54
+ direction,
55
+ });
56
+ },
57
+ },
58
+ };