@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.
@@ -0,0 +1,170 @@
1
+ import { Organization, Platform } from '@stamhoofd/models';
2
+ import { QueueHandler } from '@stamhoofd/queues';
3
+ import { OrganizationTag, TagHelper as SharedTagHelper } from '@stamhoofd/structures';
4
+ import { ModelHelper } from './ModelHelper';
5
+
6
+ export class TagHelper extends SharedTagHelper {
7
+ static async updateOrganizations() {
8
+ const queueId = 'update-tags-on-organizations';
9
+ QueueHandler.cancel(queueId);
10
+
11
+ await QueueHandler.schedule(queueId, async () => {
12
+ let platform = await Platform.getShared();
13
+
14
+ const tagCounts = new Map<string, number>();
15
+ await this.loopOrganizations(async (organizations) => {
16
+ for (const organization of organizations) {
17
+ organization.meta.tags = this.getAllTagsFromHierarchy(organization.meta.tags, platform.config.tags);
18
+
19
+ for (const tag of organization.meta.tags) {
20
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
21
+ }
22
+ }
23
+
24
+ await Promise.all(organizations.map(organization => organization.save()));
25
+ });
26
+
27
+ // Reload platform to avoid race conditions
28
+ platform = await Platform.getShared();
29
+ for (const [tag, count] of tagCounts.entries()) {
30
+ const tagObject = platform.config.tags.find(t => t.id === tag);
31
+ if (tagObject) {
32
+ tagObject.organizationCount = count;
33
+ }
34
+ }
35
+ await platform.save();
36
+ });
37
+ }
38
+
39
+ private static async loopOrganizations(onBatchReceived: (batch: Organization[]) => Promise<void>) {
40
+ await ModelHelper.loop(Organization, 'id', onBatchReceived, { limit: 10 });
41
+ }
42
+
43
+ /**
44
+ * Removes child tag ids that do not exist and sorts the tags.
45
+ * @param platformTags
46
+ * @returns
47
+ */
48
+ static getCleanedUpTags(platformTags: OrganizationTag[]): OrganizationTag[] {
49
+ const existingTags = new Set(platformTags.map(t => t.id));
50
+
51
+ for (const tag of platformTags) {
52
+ tag.childTags = tag.childTags.filter(tag => existingTags.has(tag));
53
+ }
54
+
55
+ return this.getSortedTags(platformTags);
56
+ }
57
+
58
+ private static getSortedTags(tags: OrganizationTag[]): OrganizationTag[] {
59
+ // keep original order, but add child tags below parent tag
60
+ const map = new Map(tags.map(tag => [tag.id, tag]));
61
+ const rootTags = this.getRootTags(tags);
62
+ const sortedTags = this.sortTagsHelper(rootTags, map);
63
+
64
+ return Array.from(sortedTags);
65
+ }
66
+
67
+ private static sortTagsHelper(tags: OrganizationTag[], allTagsMap: Map<string, OrganizationTag>): Set<OrganizationTag> {
68
+ // set to prevent duplicates
69
+ const result = new Set<OrganizationTag>();
70
+
71
+ for (const tag of tags) {
72
+ result.add(tag);
73
+ if (tag.childTags) {
74
+ const childTags = tag.childTags.map(id => allTagsMap.get(id)).filter(x => x !== undefined);
75
+ this.sortTagsHelper(childTags, allTagsMap).forEach(tag => result.add(tag));
76
+ }
77
+ }
78
+
79
+ return result;
80
+ }
81
+
82
+ static validateTags(platformTags: OrganizationTag[]): boolean {
83
+ const tagMap = new Map(platformTags.map(tag => [tag.id, tag]));
84
+
85
+ for (const tag of platformTags) {
86
+ const tagId = tag.id;
87
+
88
+ if (tag.childTags.includes(tagId)) {
89
+ // a tag cannot contain itself
90
+ console.error(`Tag ${tag.name} contains itself.`);
91
+ return false;
92
+ }
93
+
94
+ let isChildTag = false;
95
+
96
+ for (const otherTag of platformTags) {
97
+ const otherTagId = otherTag.id;
98
+
99
+ if (tagId === otherTagId) {
100
+ continue;
101
+ }
102
+
103
+ const isChildOfOtherTag = otherTag.childTags.includes(tagId);
104
+
105
+ if (isChildOfOtherTag) {
106
+ if (isChildTag) {
107
+ // a tag can only be a child tag of 1 tag
108
+ console.error(`Tag ${tag.name} is a child tag of multiple tags.`);
109
+ return false;
110
+ }
111
+
112
+ isChildTag = true;
113
+
114
+ // infinite loop should not be possible
115
+ // infinite loop if tag contains other tag in hierarchy
116
+ if (this.containsDeep(tagId, otherTagId, { tagMap })) {
117
+ console.error(`Tag ${tag.name} contains an infinite loop with ${otherTag.name}.`);
118
+ return false;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ return true;
125
+ }
126
+
127
+ static getAllTagsFromHierarchy(tagIds: string[], platformTags: OrganizationTag[]) {
128
+ const result = new Set<string>();
129
+ const tagMap = new Map(platformTags.map(tag => [tag.id, tag]));
130
+
131
+ this.recursivelyGetAllTagsFromHierarchy(tagIds, tagMap, result);
132
+ const sorted = Array.from(result);
133
+
134
+ // Sort tags based on platform config order
135
+ sorted.sort((a, b) => {
136
+ const aIndex = platformTags.findIndex(t => t.id === a);
137
+ const bIndex = platformTags.findIndex(t => t.id === b);
138
+ return aIndex - bIndex;
139
+ });
140
+
141
+ return sorted;
142
+ }
143
+
144
+ private static recursivelyGetAllTagsFromHierarchy(tagIds: string[], tagMap: Map<string, OrganizationTag>, result: Set<string>): void {
145
+ for (const tagId of tagIds) {
146
+ const tag = tagMap.get(tagId);
147
+ if (tag) {
148
+ result.add(tagId);
149
+
150
+ const addedChildTags: string[] = [];
151
+
152
+ for (const [otherId, otherTag] of tagMap.entries()) {
153
+ if (otherId === tagId) {
154
+ tagMap.delete(tagId);
155
+ continue;
156
+ }
157
+ if (otherTag.childTags.some(childTagId => childTagId === tagId)) {
158
+ if (!result.has(otherId)) {
159
+ addedChildTags.push(otherId);
160
+ }
161
+ }
162
+ }
163
+
164
+ if (addedChildTags.length > 0) {
165
+ this.recursivelyGetAllTagsFromHierarchy(addedChildTags, tagMap, result);
166
+ }
167
+ }
168
+ }
169
+ }
170
+ }
@@ -0,0 +1,28 @@
1
+ import { Migration } from '@simonbackx/simple-database';
2
+ import { Order } from '@stamhoofd/models';
3
+ import { sleep } from '@stamhoofd/utility';
4
+ import { ModelHelper } from '../helpers/ModelHelper';
5
+
6
+ export default new Migration(async () => {
7
+ if (STAMHOOFD.environment === 'test') {
8
+ console.log('skipped in tests');
9
+ return;
10
+ }
11
+
12
+ console.log('Start saving orders.');
13
+
14
+ const limit = 100;
15
+ let count = limit;
16
+
17
+ await ModelHelper.loop(Order, 'id', async (batch: Order[]) => {
18
+ console.log('Saving orders...', `(${count})`);
19
+ // save all orders to update the new columns
20
+ await Promise.all(batch.map(order => order.save()));
21
+ count += limit;
22
+ },
23
+ { limit });
24
+
25
+ await sleep(1000);
26
+
27
+ console.log('Finished saving orders.');
28
+ });
@@ -8,26 +8,26 @@ import { registrationFilterCompilers } from './registrations';
8
8
  */
9
9
  export const memberFilterCompilers: SQLFilterDefinitions = {
10
10
  ...baseSQLFilterCompilers,
11
- id: createSQLColumnFilterCompiler('id'),
12
- memberNumber: createSQLColumnFilterCompiler('memberNumber'),
13
- firstName: createSQLColumnFilterCompiler('firstName'),
14
- lastName: createSQLColumnFilterCompiler('lastName'),
15
- name: createSQLExpressionFilterCompiler(
11
+ 'id': createSQLColumnFilterCompiler('id'),
12
+ 'memberNumber': createSQLColumnFilterCompiler('memberNumber'),
13
+ 'firstName': createSQLColumnFilterCompiler('firstName'),
14
+ 'lastName': createSQLColumnFilterCompiler('lastName'),
15
+ 'name': createSQLExpressionFilterCompiler(
16
16
  new SQLConcat(
17
17
  SQL.column('firstName'),
18
18
  new SQLScalar(' '),
19
19
  SQL.column('lastName'),
20
20
  ),
21
21
  ),
22
- age: createSQLExpressionFilterCompiler(
22
+ 'age': createSQLExpressionFilterCompiler(
23
23
  new SQLAge(SQL.column('birthDay')),
24
24
  { nullable: true },
25
25
  ),
26
- gender: createSQLExpressionFilterCompiler(
26
+ 'gender': createSQLExpressionFilterCompiler(
27
27
  SQL.jsonValue(SQL.column('details'), '$.value.gender'),
28
28
  { isJSONValue: true, type: SQLValueType.JSONString },
29
29
  ),
30
- birthDay: createSQLColumnFilterCompiler('birthDay', {
30
+ 'birthDay': createSQLColumnFilterCompiler('birthDay', {
31
31
  normalizeValue: (d) => {
32
32
  if (typeof d === 'number') {
33
33
  const date = new Date(d);
@@ -36,41 +36,76 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
36
36
  return d;
37
37
  },
38
38
  }),
39
- organizationName: createSQLExpressionFilterCompiler(
39
+ 'organizationName': createSQLExpressionFilterCompiler(
40
40
  SQL.column('organizations', 'name'),
41
41
  ),
42
42
 
43
- email: createSQLExpressionFilterCompiler(
43
+ 'details.requiresFinancialSupport': createSQLExpressionFilterCompiler(
44
+ SQL.jsonValue(SQL.column('details'), '$.value.requiresFinancialSupport.value'),
45
+ { isJSONValue: true, type: SQLValueType.JSONBoolean },
46
+ ),
47
+
48
+ 'email': createSQLExpressionFilterCompiler(
44
49
  SQL.jsonValue(SQL.column('details'), '$.value.email'),
45
50
  { isJSONValue: true, type: SQLValueType.JSONString },
46
51
  ),
47
52
 
48
- parentEmail: createSQLExpressionFilterCompiler(
53
+ 'parentEmail': createSQLExpressionFilterCompiler(
49
54
  SQL.jsonValue(SQL.column('details'), '$.value.parents[*].email'),
50
55
  { isJSONValue: true, isJSONObject: true, type: SQLValueType.JSONString },
51
56
  ),
52
57
 
53
- unverifiedEmail: createSQLExpressionFilterCompiler(
58
+ 'unverifiedEmail': createSQLExpressionFilterCompiler(
54
59
  SQL.jsonValue(SQL.column('details'), '$.value.unverifiedEmails'),
55
60
  { isJSONValue: true, isJSONObject: true, type: SQLValueType.JSONString },
56
61
  ),
57
62
 
58
- phone: createSQLExpressionFilterCompiler(
63
+ 'phone': createSQLExpressionFilterCompiler(
59
64
  SQL.jsonValue(SQL.column('details'), '$.value.phone'),
60
65
  { isJSONValue: true },
61
66
  ),
62
67
 
63
- parentPhone: createSQLExpressionFilterCompiler(
68
+ 'details.address': createSQLFilterNamespace({
69
+ city: createSQLExpressionFilterCompiler(
70
+ SQL.jsonValue(SQL.column('details'), '$.value.address.city'),
71
+ { isJSONValue: true, type: SQLValueType.JSONString },
72
+ ),
73
+ postalCode: createSQLExpressionFilterCompiler(
74
+ SQL.jsonValue(SQL.column('details'), '$.value.address.postalCode'),
75
+ { isJSONValue: true, type: SQLValueType.JSONString },
76
+ ),
77
+ street: createSQLExpressionFilterCompiler(
78
+ SQL.jsonValue(SQL.column('details'), '$.value.address.street'),
79
+ { isJSONValue: true, type: SQLValueType.JSONString },
80
+ ),
81
+ }),
82
+
83
+ 'details.parents[*].address': createSQLFilterNamespace({
84
+ city: createSQLExpressionFilterCompiler(
85
+ SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.city'),
86
+ { isJSONValue: true, isJSONObject: true },
87
+ ),
88
+ postalCode: createSQLExpressionFilterCompiler(
89
+ SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.postalCode'),
90
+ { isJSONValue: true, isJSONObject: true },
91
+ ),
92
+ street: createSQLExpressionFilterCompiler(
93
+ SQL.jsonValue(SQL.column('details'), '$.value.parents[*].address.street'),
94
+ { isJSONValue: true, isJSONObject: true },
95
+ ),
96
+ }),
97
+
98
+ 'parentPhone': createSQLExpressionFilterCompiler(
64
99
  SQL.jsonValue(SQL.column('details'), '$.value.parents[*].phone'),
65
100
  { isJSONValue: true, isJSONObject: true },
66
101
  ),
67
102
 
68
- unverifiedPhone: createSQLExpressionFilterCompiler(
103
+ 'unverifiedPhone': createSQLExpressionFilterCompiler(
69
104
  SQL.jsonValue(SQL.column('details'), '$.value.unverifiedPhones'),
70
105
  { isJSONValue: true, isJSONObject: true },
71
106
  ),
72
107
 
73
- registrations: createSQLRelationFilterCompiler(
108
+ 'registrations': createSQLRelationFilterCompiler(
74
109
  SQL.select()
75
110
  .from(
76
111
  SQL.table('registrations'),
@@ -109,7 +144,7 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
109
144
  },
110
145
  ),
111
146
 
112
- responsibilities: createSQLRelationFilterCompiler(
147
+ 'responsibilities': createSQLRelationFilterCompiler(
113
148
  SQL.select()
114
149
  .from(
115
150
  SQL.table('member_responsibility_records'),
@@ -151,7 +186,7 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
151
186
  },
152
187
  ),
153
188
 
154
- platformMemberships: createSQLRelationFilterCompiler(
189
+ 'platformMemberships': createSQLRelationFilterCompiler(
155
190
  SQL.select()
156
191
  .from(
157
192
  SQL.table('member_platform_memberships'),
@@ -178,7 +213,7 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
178
213
  },
179
214
  ),
180
215
 
181
- organizations: createSQLRelationFilterCompiler(
216
+ 'organizations': createSQLRelationFilterCompiler(
182
217
  SQL.select()
183
218
  .from(
184
219
  SQL.table('registrations'),
@@ -1,9 +1,25 @@
1
- import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, SQL, SQLFilterDefinitions, SQLValueType } from '@stamhoofd/sql';
1
+ import { baseSQLFilterCompilers, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, SQL, SQLConcat, SQLFilterDefinitions, SQLScalar, SQLValueType } from '@stamhoofd/sql';
2
2
 
3
3
  export const orderFilterCompilers: SQLFilterDefinitions = {
4
4
  ...baseSQLFilterCompilers,
5
+ // only backend (not useful to filter on these in the frontend)
5
6
  organizationId: createSQLColumnFilterCompiler('organizationId'),
7
+ updatedAt: createSQLColumnFilterCompiler('updatedAt'),
8
+
9
+ // frontend and backend
10
+ webshopId: createSQLColumnFilterCompiler('webshopId'),
6
11
  id: createSQLColumnFilterCompiler('id'),
12
+ timeSlotEndTime: createSQLExpressionFilterCompiler(
13
+ SQL.jsonValue(SQL.column('data'), '$.value.timeSlot.endTime'),
14
+ // todo: type?
15
+ { isJSONValue: true, type: SQLValueType.JSONString },
16
+ ),
17
+ timeSlotStartTime: createSQLExpressionFilterCompiler(
18
+ SQL.jsonValue(SQL.column('data'), '$.value.timeSlot.startTime'),
19
+ // todo: type?
20
+ { isJSONValue: true, type: SQLValueType.JSONString },
21
+ ),
22
+ createdAt: createSQLColumnFilterCompiler('createdAt'),
7
23
  number: createSQLColumnFilterCompiler('number'),
8
24
  status: createSQLColumnFilterCompiler('status'),
9
25
  paymentMethod: createSQLExpressionFilterCompiler(
@@ -19,15 +35,34 @@ export const orderFilterCompilers: SQLFilterDefinitions = {
19
35
  // todo: type?
20
36
  { isJSONValue: true, type: SQLValueType.JSONString },
21
37
  ),
22
- timeSlotTime: createSQLExpressionFilterCompiler(
23
- SQL.jsonValue(SQL.column('data'), '$.value.timeSlot.endTime'),
38
+ validAt: createSQLColumnFilterCompiler('validAt'),
39
+
40
+ // todo: TEST!
41
+ name: createSQLExpressionFilterCompiler(
42
+ new SQLConcat(
43
+ SQL.jsonValue(SQL.column('data'), '$.value.customer.firstName'),
44
+ new SQLScalar(' '),
45
+ SQL.jsonValue(SQL.column('data'), '$.value.customer.lastName'),
46
+ ),
47
+ // SQL.jsonValue(SQL.column('data'), '$.value.customer.name'),
24
48
  // todo: type?
25
49
  { isJSONValue: true, type: SQLValueType.JSONString },
26
50
  ),
27
- validAt: createSQLColumnFilterCompiler('validAt'),
28
- totalPrice: createSQLExpressionFilterCompiler(
29
- SQL.jsonValue(SQL.column('data'), '$.value.totalPrice'),
51
+ email: createSQLExpressionFilterCompiler(
52
+ SQL.jsonValue(SQL.column('data'), '$.value.customer.email'),
53
+ // todo: type?
54
+ { isJSONValue: true, type: SQLValueType.JSONString },
55
+ ),
56
+ phone: createSQLExpressionFilterCompiler(
57
+ SQL.jsonValue(SQL.column('data'), '$.value.customer.phone'),
30
58
  // todo: type?
31
- { isJSONValue: true },
59
+ { isJSONValue: true, type: SQLValueType.JSONString },
32
60
  ),
61
+ totalPrice: createSQLColumnFilterCompiler('totalPrice'),
62
+ amount: createSQLColumnFilterCompiler('amount'),
63
+ timeSlotTime: createSQLColumnFilterCompiler('timeSlotTime'),
64
+
65
+ // only frontend
66
+ // openBalance
67
+ // location
33
68
  };
@@ -33,6 +33,10 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
33
33
  SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.city'),
34
34
  { isJSONValue: true, type: SQLValueType.JSONString },
35
35
  ),
36
+ postalCode: createSQLExpressionFilterCompiler(
37
+ SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.postalCode'),
38
+ { isJSONValue: true, type: SQLValueType.JSONString },
39
+ ),
36
40
  country: createSQLExpressionFilterCompiler(
37
41
  SQL.jsonValue(
38
42
  SQL.column('organizations', 'address'),
@@ -15,6 +15,7 @@ export const paymentFilterCompilers: SQLFilterDefinitions = {
15
15
  paidAt: createSQLColumnFilterCompiler('paidAt', { nullable: true }),
16
16
  price: createSQLColumnFilterCompiler('price'),
17
17
  provider: createSQLColumnFilterCompiler('provider', { nullable: true }),
18
+ transferDescription: createSQLColumnFilterCompiler('transferDescription', { nullable: true }),
18
19
  customer: createSQLFilterNamespace({
19
20
  ...baseSQLFilterCompilers,
20
21
  email: createSQLExpressionFilterCompiler(
@@ -1,4 +1,4 @@
1
- import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQL, createSQLFilterNamespace, createSQLExpressionFilterCompiler } from '@stamhoofd/sql';
1
+ import { SQLFilterDefinitions, baseSQLFilterCompilers, createSQLColumnFilterCompiler, SQL, createSQLFilterNamespace, createSQLExpressionFilterCompiler, SQLValueType } from '@stamhoofd/sql';
2
2
 
3
3
  export const registrationFilterCompilers: SQLFilterDefinitions = {
4
4
  ...baseSQLFilterCompilers,
@@ -15,9 +15,11 @@ export const registrationFilterCompilers: SQLFilterDefinitions = {
15
15
  id: createSQLColumnFilterCompiler('groupId'),
16
16
  name: createSQLExpressionFilterCompiler(
17
17
  SQL.jsonValue(SQL.column('groups', 'settings'), '$.value.name'),
18
+ { isJSONValue: true, type: SQLValueType.JSONString },
18
19
  ),
19
20
  status: createSQLExpressionFilterCompiler(
20
21
  SQL.column('groups', 'status'),
22
+ { isJSONValue: true, type: SQLValueType.JSONString },
21
23
  ),
22
24
  defaultAgeGroupId: createSQLColumnFilterCompiler(SQL.column('groups', 'defaultAgeGroupId'), { nullable: true }),
23
25
  }),