@axinom/mosaic-graphql-common 0.28.0-rc.8 → 0.28.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.
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.d.ts +49 -1
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.d.ts.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.js +121 -26
- package/dist/plugins/bulk-edit/bulk-edit-async-plugin-factory.js.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.d.ts +8 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.d.ts.map +1 -1
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.js +45 -39
- package/dist/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.js.map +1 -1
- package/package.json +8 -8
- package/src/plugins/bulk-edit/bulk-edit-async-plugin-factory.spec.ts +1047 -0
- package/src/plugins/bulk-edit/bulk-edit-async-plugin-factory.ts +209 -34
- package/src/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.spec.ts +360 -0
- package/src/plugins/bulk-edit/bulk-edit-item-change-handler-helpers.ts +67 -47
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { PerformItemChangeCommand } from '@axinom/mosaic-messages';
|
|
2
|
+
import { ClientBase, QueryResult } from 'pg';
|
|
3
|
+
import { handlePerformItemChangeCommand } from './bulk-edit-item-change-handler-helpers';
|
|
4
|
+
|
|
5
|
+
describe('bulk-edit-item-change-handler-helpers', () => {
|
|
6
|
+
let mockClient: ClientBase;
|
|
7
|
+
let mockQuery: jest.MockedFunction<
|
|
8
|
+
(queryText: string, values?: any[]) => Promise<QueryResult>
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
// Helper to create a proper QueryResult object
|
|
12
|
+
const createQueryResult = (rows: any[] = []): QueryResult => ({
|
|
13
|
+
rows,
|
|
14
|
+
command: 'SELECT',
|
|
15
|
+
rowCount: rows.length,
|
|
16
|
+
oid: 0,
|
|
17
|
+
fields: [],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockQuery = jest.fn().mockResolvedValue(createQueryResult([]));
|
|
22
|
+
mockClient = {
|
|
23
|
+
query: mockQuery,
|
|
24
|
+
} as unknown as ClientBase;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
jest.restoreAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('handlePerformItemChangeCommand', () => {
|
|
32
|
+
describe('SET_FIELD_VALUES action', () => {
|
|
33
|
+
it('should update main entity fields', async () => {
|
|
34
|
+
// Arrange
|
|
35
|
+
const payload: PerformItemChangeCommand = {
|
|
36
|
+
table_name: 'images',
|
|
37
|
+
action: 'SET_FIELD_VALUES',
|
|
38
|
+
stringified_condition: JSON.stringify({ id: 'test-id-123' }),
|
|
39
|
+
stringified_payload: JSON.stringify({
|
|
40
|
+
title: 'Updated Title',
|
|
41
|
+
description: 'Updated Description',
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Act
|
|
46
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(mockQuery).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
51
|
+
'UPDATE "images" SET "title" = $1, "description" = $2 WHERE "id" = $3 RETURNING *',
|
|
52
|
+
['Updated Title', 'Updated Description', 'test-id-123'],
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle single field update', async () => {
|
|
57
|
+
// Arrange
|
|
58
|
+
const payload: PerformItemChangeCommand = {
|
|
59
|
+
table_name: 'users',
|
|
60
|
+
action: 'SET_FIELD_VALUES',
|
|
61
|
+
stringified_condition: JSON.stringify({ id: 'user-123' }),
|
|
62
|
+
stringified_payload: JSON.stringify({ email: 'new@example.com' }),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Act
|
|
66
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
67
|
+
|
|
68
|
+
// Assert
|
|
69
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
70
|
+
'UPDATE "users" SET "email" = $1 WHERE "id" = $2 RETURNING *',
|
|
71
|
+
['new@example.com', 'user-123'],
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('CLEAR_RELATED_ENTITIES action', () => {
|
|
77
|
+
it('should delete all related entities matching condition', async () => {
|
|
78
|
+
// Arrange
|
|
79
|
+
const payload: PerformItemChangeCommand = {
|
|
80
|
+
table_name: 'images_tags',
|
|
81
|
+
action: 'CLEAR_RELATED_ENTITIES',
|
|
82
|
+
stringified_condition: JSON.stringify({ image_id: 'image-123' }),
|
|
83
|
+
stringified_payload: JSON.stringify({}),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Act
|
|
87
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
88
|
+
|
|
89
|
+
// Assert
|
|
90
|
+
expect(mockQuery).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
92
|
+
'DELETE FROM "images_tags" WHERE "image_id" = $1 RETURNING *',
|
|
93
|
+
['image-123'],
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle multiple condition fields', async () => {
|
|
98
|
+
// Arrange
|
|
99
|
+
const payload: PerformItemChangeCommand = {
|
|
100
|
+
table_name: 'junction_table',
|
|
101
|
+
action: 'CLEAR_RELATED_ENTITIES',
|
|
102
|
+
stringified_condition: JSON.stringify({
|
|
103
|
+
parent_id: 'parent-123',
|
|
104
|
+
tenant_id: 'tenant-456',
|
|
105
|
+
}),
|
|
106
|
+
stringified_payload: JSON.stringify({}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Act
|
|
110
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
114
|
+
'DELETE FROM "junction_table" WHERE "parent_id" = $1 AND "tenant_id" = $2 RETURNING *',
|
|
115
|
+
['parent-123', 'tenant-456'],
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('REMOVE_RELATED_ENTITY action', () => {
|
|
121
|
+
it('should delete specific related entity', async () => {
|
|
122
|
+
// Arrange
|
|
123
|
+
const payload: PerformItemChangeCommand = {
|
|
124
|
+
table_name: 'images_tags',
|
|
125
|
+
action: 'REMOVE_RELATED_ENTITY',
|
|
126
|
+
stringified_condition: '',
|
|
127
|
+
stringified_payload: JSON.stringify({
|
|
128
|
+
image_id: 'image-123',
|
|
129
|
+
name: 'tag-to-remove',
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Act
|
|
134
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
135
|
+
|
|
136
|
+
// Assert
|
|
137
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
138
|
+
'DELETE FROM "images_tags" WHERE "image_id" = $1 AND "name" = $2 RETURNING *',
|
|
139
|
+
['image-123', 'tag-to-remove'],
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle composite key deletion', async () => {
|
|
144
|
+
// Arrange
|
|
145
|
+
const payload: PerformItemChangeCommand = {
|
|
146
|
+
table_name: 'user_role_assignments',
|
|
147
|
+
action: 'REMOVE_RELATED_ENTITY',
|
|
148
|
+
stringified_condition: '',
|
|
149
|
+
stringified_payload: JSON.stringify({
|
|
150
|
+
user_id: 'user-123',
|
|
151
|
+
user_role_id: 'role-456',
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Act
|
|
156
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
157
|
+
|
|
158
|
+
// Assert
|
|
159
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
160
|
+
'DELETE FROM "user_role_assignments" WHERE "user_id" = $1 AND "user_role_id" = $2 RETURNING *',
|
|
161
|
+
['user-123', 'role-456'],
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('ADD_RELATED_ENTITY action', () => {
|
|
167
|
+
it('should upsert new related entity', async () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const payload: PerformItemChangeCommand = {
|
|
170
|
+
table_name: 'images_tags',
|
|
171
|
+
action: 'ADD_RELATED_ENTITY',
|
|
172
|
+
stringified_condition: '',
|
|
173
|
+
stringified_payload: JSON.stringify({
|
|
174
|
+
image_id: 'image-123',
|
|
175
|
+
name: 'new-tag',
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Act
|
|
180
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
181
|
+
|
|
182
|
+
// Assert
|
|
183
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
184
|
+
'INSERT INTO "images_tags" ("image_id", "name") VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING *',
|
|
185
|
+
['image-123', 'new-tag'],
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should handle multiple fields upsert', async () => {
|
|
190
|
+
// Arrange
|
|
191
|
+
const payload: PerformItemChangeCommand = {
|
|
192
|
+
table_name: 'user_role_assignments',
|
|
193
|
+
action: 'ADD_RELATED_ENTITY',
|
|
194
|
+
stringified_condition: '',
|
|
195
|
+
stringified_payload: JSON.stringify({
|
|
196
|
+
user_id: 'user-123',
|
|
197
|
+
user_role_id: 'role-456',
|
|
198
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
199
|
+
}),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Act
|
|
203
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
204
|
+
|
|
205
|
+
// Assert
|
|
206
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
207
|
+
'INSERT INTO "user_role_assignments" ("user_id", "user_role_id", "created_at") VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING *',
|
|
208
|
+
['user-123', 'role-456', '2024-01-01T00:00:00Z'],
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('Error handling', () => {
|
|
214
|
+
it('should throw foreign key constraint errors', async () => {
|
|
215
|
+
// Arrange
|
|
216
|
+
const payload: PerformItemChangeCommand = {
|
|
217
|
+
table_name: 'images_tags',
|
|
218
|
+
action: 'ADD_RELATED_ENTITY',
|
|
219
|
+
stringified_condition: '',
|
|
220
|
+
stringified_payload: JSON.stringify({
|
|
221
|
+
image_id: 'image-123',
|
|
222
|
+
name: 'tag',
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const foreignKeyError = new Error(
|
|
227
|
+
'insert or update on table "images_tags" violates foreign key constraint',
|
|
228
|
+
);
|
|
229
|
+
mockQuery.mockRejectedValueOnce(foreignKeyError);
|
|
230
|
+
|
|
231
|
+
// Act & Assert
|
|
232
|
+
await expect(
|
|
233
|
+
handlePerformItemChangeCommand(payload, mockClient),
|
|
234
|
+
).rejects.toThrow(
|
|
235
|
+
'insert or update on table "images_tags" violates foreign key constraint',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should throw database connection errors', async () => {
|
|
240
|
+
// Arrange
|
|
241
|
+
const payload: PerformItemChangeCommand = {
|
|
242
|
+
table_name: 'images',
|
|
243
|
+
action: 'SET_FIELD_VALUES',
|
|
244
|
+
stringified_condition: JSON.stringify({ id: 'test-id' }),
|
|
245
|
+
stringified_payload: JSON.stringify({ title: 'Test' }),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const connectionError = new Error('ECONNREFUSED');
|
|
249
|
+
mockQuery.mockRejectedValueOnce(connectionError);
|
|
250
|
+
|
|
251
|
+
// Act & Assert
|
|
252
|
+
await expect(
|
|
253
|
+
handlePerformItemChangeCommand(payload, mockClient),
|
|
254
|
+
).rejects.toThrow('ECONNREFUSED');
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('Edge cases', () => {
|
|
259
|
+
it('should handle empty payload for CLEAR_RELATED_ENTITIES', async () => {
|
|
260
|
+
// Arrange
|
|
261
|
+
const payload: PerformItemChangeCommand = {
|
|
262
|
+
table_name: 'images_tags',
|
|
263
|
+
action: 'CLEAR_RELATED_ENTITIES',
|
|
264
|
+
stringified_condition: JSON.stringify({ image_id: 'empty-image' }),
|
|
265
|
+
stringified_payload: JSON.stringify({}),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Act
|
|
269
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
270
|
+
|
|
271
|
+
// Assert
|
|
272
|
+
expect(mockQuery).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should handle special characters in table names', async () => {
|
|
276
|
+
// Arrange
|
|
277
|
+
const payload: PerformItemChangeCommand = {
|
|
278
|
+
table_name: 'app_public.images_tags',
|
|
279
|
+
action: 'CLEAR_RELATED_ENTITIES',
|
|
280
|
+
stringified_condition: JSON.stringify({ image_id: 'test' }),
|
|
281
|
+
stringified_payload: JSON.stringify({}),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Act
|
|
285
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
286
|
+
|
|
287
|
+
// Assert
|
|
288
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
289
|
+
'DELETE FROM "app_public.images_tags" WHERE "image_id" = $1 RETURNING *',
|
|
290
|
+
['test'],
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should handle NULL values in payload', async () => {
|
|
295
|
+
// Arrange
|
|
296
|
+
const payload: PerformItemChangeCommand = {
|
|
297
|
+
table_name: 'images',
|
|
298
|
+
action: 'SET_FIELD_VALUES',
|
|
299
|
+
stringified_condition: JSON.stringify({ id: 'test-id' }),
|
|
300
|
+
stringified_payload: JSON.stringify({
|
|
301
|
+
description: null,
|
|
302
|
+
tags: null,
|
|
303
|
+
}),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Act
|
|
307
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
308
|
+
|
|
309
|
+
// Assert
|
|
310
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
311
|
+
'UPDATE "images" SET "description" = $1, "tags" = $2 WHERE "id" = $3 RETURNING *',
|
|
312
|
+
[null, null, 'test-id'],
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('SQL injection protection', () => {
|
|
319
|
+
it('should properly escape column names with quotes', async () => {
|
|
320
|
+
// Arrange
|
|
321
|
+
const payload: PerformItemChangeCommand = {
|
|
322
|
+
table_name: 'images',
|
|
323
|
+
action: 'SET_FIELD_VALUES',
|
|
324
|
+
stringified_condition: JSON.stringify({ id: 'test-id' }),
|
|
325
|
+
stringified_payload: JSON.stringify({
|
|
326
|
+
'column; DROP TABLE images;': 'malicious',
|
|
327
|
+
}),
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Act
|
|
331
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
332
|
+
|
|
333
|
+
// Assert - Column names should be quoted, preventing SQL injection
|
|
334
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
335
|
+
'UPDATE "images" SET "column; DROP TABLE images;" = $1 WHERE "id" = $2 RETURNING *',
|
|
336
|
+
['malicious', 'test-id'],
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should use parameterized queries for values', async () => {
|
|
341
|
+
// Arrange
|
|
342
|
+
const maliciousValue = "'; DROP TABLE images; --";
|
|
343
|
+
const payload: PerformItemChangeCommand = {
|
|
344
|
+
table_name: 'images',
|
|
345
|
+
action: 'SET_FIELD_VALUES',
|
|
346
|
+
stringified_condition: JSON.stringify({ id: 'test-id' }),
|
|
347
|
+
stringified_payload: JSON.stringify({ title: maliciousValue }),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Act
|
|
351
|
+
await handlePerformItemChangeCommand(payload, mockClient);
|
|
352
|
+
|
|
353
|
+
// Assert - Value should be parameterized, not interpolated
|
|
354
|
+
expect(mockQuery).toHaveBeenCalledWith(
|
|
355
|
+
'UPDATE "images" SET "title" = $1 WHERE "id" = $2 RETURNING *',
|
|
356
|
+
[maliciousValue, 'test-id'],
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -7,16 +7,18 @@ import {
|
|
|
7
7
|
import { ClientBase, QueryResult } from 'pg';
|
|
8
8
|
import { DatabaseClient } from 'pg-transactional-outbox';
|
|
9
9
|
|
|
10
|
-
async function
|
|
10
|
+
async function upsert(
|
|
11
11
|
client: ClientBase,
|
|
12
12
|
tableName: string,
|
|
13
13
|
jsonObject: Record<string, unknown>,
|
|
14
14
|
): Promise<QueryResult> {
|
|
15
|
-
const columns = Object.keys(jsonObject)
|
|
15
|
+
const columns = Object.keys(jsonObject)
|
|
16
|
+
.map((col) => `"${col}"`)
|
|
17
|
+
.join(', ');
|
|
16
18
|
const values = Object.values(jsonObject);
|
|
17
19
|
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
|
|
18
20
|
|
|
19
|
-
const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
|
21
|
+
const query = `INSERT INTO "${tableName}" (${columns}) VALUES (${placeholders}) ON CONFLICT DO NOTHING RETURNING *`;
|
|
20
22
|
|
|
21
23
|
return client.query(query, values);
|
|
22
24
|
}
|
|
@@ -28,86 +30,104 @@ async function update(
|
|
|
28
30
|
condition: Record<string, unknown>,
|
|
29
31
|
): Promise<QueryResult> {
|
|
30
32
|
const setClause = Object.keys(jsonObject)
|
|
31
|
-
.map((key, index) =>
|
|
33
|
+
.map((key, index) => `"${key}" = $${index + 1}`)
|
|
32
34
|
.join(', ');
|
|
33
35
|
const values = Object.values(jsonObject);
|
|
34
36
|
|
|
35
|
-
const conditionClause =
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
37
|
+
const { clause: conditionClause, values: conditionValues } = buildWhereClause(
|
|
38
|
+
condition,
|
|
39
|
+
values.length,
|
|
40
|
+
);
|
|
39
41
|
|
|
40
|
-
const query = `UPDATE ${tableName} SET ${setClause} WHERE ${conditionClause} RETURNING *`;
|
|
42
|
+
const query = `UPDATE "${tableName}" SET ${setClause} WHERE ${conditionClause} RETURNING *`;
|
|
41
43
|
|
|
42
44
|
return client.query(query, [...values, ...conditionValues]);
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Helper to build WHERE clause from a condition object
|
|
49
|
+
*/
|
|
50
|
+
function buildWhereClause(
|
|
51
|
+
condition: Record<string, unknown>,
|
|
52
|
+
parameterOffset = 0,
|
|
53
|
+
): { clause: string; values: unknown[] } {
|
|
54
|
+
const values = Object.values(condition);
|
|
55
|
+
const clause = Object.keys(condition)
|
|
56
|
+
.map((key, index) => `"${key}" = $${index + 1 + parameterOffset}`)
|
|
57
|
+
.join(' AND ');
|
|
58
|
+
|
|
59
|
+
return { clause, values };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deletes records from a table matching the given condition
|
|
64
|
+
*/
|
|
65
|
+
async function deleteWhere(
|
|
46
66
|
client: ClientBase,
|
|
47
67
|
tableName: string,
|
|
48
|
-
|
|
68
|
+
condition: Record<string, unknown>,
|
|
49
69
|
): Promise<QueryResult> {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
.join(' AND ');
|
|
53
|
-
const conditionValues = Object.values(jsonObject);
|
|
70
|
+
const { clause, values } = buildWhereClause(condition);
|
|
71
|
+
const query = `DELETE FROM "${tableName}" WHERE ${clause} RETURNING *`;
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return client.query(query, conditionValues);
|
|
73
|
+
return client.query(query, values);
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
export async function handlePerformItemChangeCommand(
|
|
61
77
|
payload: PerformItemChangeCommand,
|
|
62
78
|
client: ClientBase,
|
|
63
79
|
): Promise<void> {
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
switch (payload.action) {
|
|
81
|
+
case 'SET_FIELD_VALUES':
|
|
66
82
|
await update(
|
|
67
83
|
client,
|
|
68
84
|
payload.table_name,
|
|
69
85
|
JSON.parse(payload.stringified_payload),
|
|
70
86
|
{ id: JSON.parse(payload.stringified_condition).id },
|
|
71
87
|
);
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'CLEAR_RELATED_ENTITIES':
|
|
91
|
+
await deleteWhere(
|
|
92
|
+
client,
|
|
93
|
+
payload.table_name,
|
|
94
|
+
JSON.parse(payload.stringified_condition),
|
|
95
|
+
);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'REMOVE_RELATED_ENTITY':
|
|
99
|
+
await deleteWhere(
|
|
74
100
|
client,
|
|
75
101
|
payload.table_name,
|
|
76
102
|
JSON.parse(payload.stringified_payload),
|
|
77
103
|
);
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'ADD_RELATED_ENTITY':
|
|
107
|
+
await upsert(
|
|
80
108
|
client,
|
|
81
109
|
payload.table_name,
|
|
82
110
|
JSON.parse(payload.stringified_payload),
|
|
83
111
|
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// TODO: Publish success event or implement a JOB tracking/monitoring mechanism
|
|
87
|
-
} catch (error) {
|
|
88
|
-
// TODO: If error is a known/expected one like `duplicate key value violates unique constraint` we ignore it, else we throw it and bubble up.
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
-
const errorAsAny = error as any;
|
|
91
|
-
if (
|
|
92
|
-
errorAsAny.message &&
|
|
93
|
-
typeof errorAsAny.message === 'string' &&
|
|
94
|
-
errorAsAny.message.startsWith(
|
|
95
|
-
'duplicate key value violates unique constraint',
|
|
96
|
-
)
|
|
97
|
-
) {
|
|
98
|
-
// Skipping duplicate key error
|
|
99
|
-
} else {
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
112
|
+
break;
|
|
102
113
|
}
|
|
114
|
+
|
|
115
|
+
// TODO: Publish success event or implement a JOB tracking/monitoring mechanism
|
|
103
116
|
}
|
|
104
117
|
|
|
118
|
+
/**
|
|
119
|
+
* !NOTE!
|
|
120
|
+
*
|
|
121
|
+
* This is a temporary stub implementation that needs to be properly implemented later.
|
|
122
|
+
* The idea is to have a way to track failed item change commands and notify the users
|
|
123
|
+
* or systems about the failures.
|
|
124
|
+
*/
|
|
105
125
|
export async function handlePerformItemChangeCommandError(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
_config: BasicConfig,
|
|
127
|
+
_error: Error,
|
|
128
|
+
_message: TypedTransactionalMessage<PerformItemChangeCommand>,
|
|
129
|
+
_storeOutboxMessage: StoreOutboxMessage,
|
|
130
|
+
_client: DatabaseClient,
|
|
111
131
|
): Promise<void> {
|
|
112
132
|
// TODO: Publish failed event or implement a JOB tracking/monitoring mechanism
|
|
113
133
|
//
|