core-services-sdk 1.3.61 → 1.3.62
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 +1 -1
- package/src/postgresql/apply-filter.js +275 -0
- package/src/postgresql/index.js +1 -0
- package/tests/instant-messages/message-unified-mapper-telegram.unit.test.js +5 -6
- package/tests/instant-messages/message-unified-mapper-whatsapp.unit.test.js +2 -2
- package/tests/postgresql/apply-filter.integration.test.js +549 -0
- package/tests/resources/create-im-mapp-diff.js +15 -0
- package/.claude/settings.local.json +0 -22
package/package.json
CHANGED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { toSnakeCase } from '../core/case-mapper.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} OperatorFunction
|
|
5
|
+
* @property {import('knex').Knex.QueryBuilder} query - Knex query builder instance
|
|
6
|
+
* @property {string} key - Column name (qualified with table name)
|
|
7
|
+
* @property {*} value - Value to compare against
|
|
8
|
+
* @returns {import('knex').Knex.QueryBuilder} Modified query builder
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map of filter operators to their corresponding Knex query builder methods.
|
|
13
|
+
* Each operator function applies a WHERE condition to the query.
|
|
14
|
+
*
|
|
15
|
+
* @type {Object<string, OperatorFunction>}
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
const OPERATORS = {
|
|
19
|
+
/**
|
|
20
|
+
* Array membership operator. Checks if column value is in the provided array.
|
|
21
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
22
|
+
* @param {string} key - Column name
|
|
23
|
+
* @param {Array<*>} value - Array of values to match
|
|
24
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE IN clause
|
|
25
|
+
*/
|
|
26
|
+
in: (q, key, value) => q.whereIn(key, value),
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Not in array operator. Checks if column value is NOT in the provided array.
|
|
30
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
31
|
+
* @param {string} key - Column name
|
|
32
|
+
* @param {Array<*>} value - Array of values to exclude
|
|
33
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT IN clause
|
|
34
|
+
*/
|
|
35
|
+
nin: (q, key, value) => q.whereNotIn(key, value),
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Equality operator. Checks if column value equals the provided value.
|
|
39
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
40
|
+
* @param {string} key - Column name
|
|
41
|
+
* @param {*} value - Value to match
|
|
42
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE = clause
|
|
43
|
+
*/
|
|
44
|
+
eq: (q, key, value) => q.where(key, '=', value),
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Not equal operator. Checks if column value does not equal the provided value.
|
|
48
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
49
|
+
* @param {string} key - Column name
|
|
50
|
+
* @param {*} value - Value to exclude
|
|
51
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
|
|
52
|
+
*/
|
|
53
|
+
ne: (q, key, value) => q.where(key, '!=', value),
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Not equal operator (alias for `ne`). Checks if column value does not equal the provided value.
|
|
57
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
58
|
+
* @param {string} key - Column name
|
|
59
|
+
* @param {*} value - Value to exclude
|
|
60
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE != clause
|
|
61
|
+
*/
|
|
62
|
+
neq: (q, key, value) => q.where(key, '!=', value),
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Greater than operator. Checks if column value is greater than the provided value.
|
|
66
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
67
|
+
* @param {string} key - Column name
|
|
68
|
+
* @param {number|string|Date} value - Value to compare against
|
|
69
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE > clause
|
|
70
|
+
*/
|
|
71
|
+
gt: (q, key, value) => q.where(key, '>', value),
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Greater than or equal operator. Checks if column value is greater than or equal to the provided value.
|
|
75
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
76
|
+
* @param {string} key - Column name
|
|
77
|
+
* @param {number|string|Date} value - Value to compare against
|
|
78
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE >= clause
|
|
79
|
+
*/
|
|
80
|
+
gte: (q, key, value) => q.where(key, '>=', value),
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Less than operator. Checks if column value is less than the provided value.
|
|
84
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
85
|
+
* @param {string} key - Column name
|
|
86
|
+
* @param {number|string|Date} value - Value to compare against
|
|
87
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE < clause
|
|
88
|
+
*/
|
|
89
|
+
lt: (q, key, value) => q.where(key, '<', value),
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Less than or equal operator. Checks if column value is less than or equal to the provided value.
|
|
93
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
94
|
+
* @param {string} key - Column name
|
|
95
|
+
* @param {number|string|Date} value - Value to compare against
|
|
96
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE <= clause
|
|
97
|
+
*/
|
|
98
|
+
lte: (q, key, value) => q.where(key, '<=', value),
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Case-sensitive pattern matching operator. Uses SQL LIKE for pattern matching.
|
|
102
|
+
* Supports wildcards: `%` (any sequence) and `_` (single character).
|
|
103
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
104
|
+
* @param {string} key - Column name
|
|
105
|
+
* @param {string} value - Pattern to match (e.g., '%invoice%')
|
|
106
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE LIKE clause
|
|
107
|
+
*/
|
|
108
|
+
like: (q, key, value) => q.where(key, 'like', value),
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Case-insensitive pattern matching operator. Uses PostgreSQL ILIKE for pattern matching.
|
|
112
|
+
* Supports wildcards: `%` (any sequence) and `_` (single character).
|
|
113
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
114
|
+
* @param {string} key - Column name
|
|
115
|
+
* @param {string} value - Pattern to match (e.g., '%invoice%')
|
|
116
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE ILIKE clause
|
|
117
|
+
*/
|
|
118
|
+
ilike: (q, key, value) => q.where(key, 'ilike', value),
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Null check operator. Checks if column value is NULL or NOT NULL based on the boolean value.
|
|
122
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
123
|
+
* @param {string} key - Column name
|
|
124
|
+
* @param {boolean} value - If true, checks for NULL; if false, checks for NOT NULL
|
|
125
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE NULL or WHERE NOT NULL clause
|
|
126
|
+
*/
|
|
127
|
+
isNull: (q, key, value) => (value ? q.whereNull(key) : q.whereNotNull(key)),
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Not null check operator. Checks if column value is NOT NULL or NULL based on the boolean value.
|
|
131
|
+
* @param {import('knex').Knex.QueryBuilder} q - Query builder
|
|
132
|
+
* @param {string} key - Column name
|
|
133
|
+
* @param {boolean} value - If true, checks for NOT NULL; if false, checks for NULL
|
|
134
|
+
* @returns {import('knex').Knex.QueryBuilder} Query with WHERE NOT NULL or WHERE NULL clause
|
|
135
|
+
*/
|
|
136
|
+
isNotNull: (q, key, value) =>
|
|
137
|
+
value ? q.whereNotNull(key) : q.whereNull(key),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Applies filter operators from an object to a Knex query.
|
|
142
|
+
* Processes each operator in the value object and applies the corresponding WHERE clause.
|
|
143
|
+
* Unknown operators are silently ignored.
|
|
144
|
+
*
|
|
145
|
+
* @param {import('knex').Knex.QueryBuilder} q - Knex query builder instance
|
|
146
|
+
* @param {string} qualifiedKey - Fully qualified column name (e.g., "table.column")
|
|
147
|
+
* @param {Object<string, *>} value - Object containing operator-value pairs
|
|
148
|
+
* @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied filters
|
|
149
|
+
* @private
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* // Apply multiple operators on the same field
|
|
153
|
+
* applyFilterObject(query, 'assets.price', { gte: 100, lte: 200 })
|
|
154
|
+
* // Results in: WHERE assets.price >= 100 AND assets.price <= 200
|
|
155
|
+
*/
|
|
156
|
+
const applyFilterObject = (q, qualifiedKey, value) => {
|
|
157
|
+
return Object.entries(value).reduce(
|
|
158
|
+
(q, [operator, val]) =>
|
|
159
|
+
OPERATORS[operator] ? OPERATORS[operator](q, qualifiedKey, val) : q,
|
|
160
|
+
q,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Builds a Knex query with MongoDB-style filter operators.
|
|
166
|
+
* Pure utility function that can be used across repositories.
|
|
167
|
+
*
|
|
168
|
+
* This function converts camelCase filter keys to snake_case and applies
|
|
169
|
+
* various filter operators to build SQL WHERE clauses. All column names
|
|
170
|
+
* are automatically qualified with the provided table name.
|
|
171
|
+
*
|
|
172
|
+
* **Supported Operators:**
|
|
173
|
+
* - `eq` - Equality (default for simple values)
|
|
174
|
+
* - `ne` / `neq` - Not equal
|
|
175
|
+
* - `in` - Array membership (or pass array directly)
|
|
176
|
+
* - `nin` - Not in array
|
|
177
|
+
* - `gt` - Greater than
|
|
178
|
+
* - `gte` - Greater than or equal
|
|
179
|
+
* - `lt` - Less than
|
|
180
|
+
* - `lte` - Less than or equal
|
|
181
|
+
* - `like` - Case-sensitive pattern matching (SQL LIKE)
|
|
182
|
+
* - `ilike` - Case-insensitive pattern matching (PostgreSQL ILIKE)
|
|
183
|
+
* - `isNull` - Check if field is NULL
|
|
184
|
+
* - `isNotNull` - Check if field is NOT NULL
|
|
185
|
+
*
|
|
186
|
+
* **Key Features:**
|
|
187
|
+
* - Automatic camelCase to snake_case conversion for filter keys
|
|
188
|
+
* - Qualified column names (table.column format)
|
|
189
|
+
* - Multiple operators can be applied to the same field
|
|
190
|
+
* - Unknown operators are silently ignored
|
|
191
|
+
* - Arrays are automatically converted to IN clauses
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} params - Function parameters
|
|
194
|
+
* @param {import('knex').Knex.QueryBuilder} params.query - Knex query builder instance to apply filters to
|
|
195
|
+
* @param {Object<string, *>} params.filter - Filter object with camelCase keys (will be converted to snake_case)
|
|
196
|
+
* Filter values can be:
|
|
197
|
+
* - Simple values (string, number, boolean) → treated as equality
|
|
198
|
+
* - Arrays → treated as IN operator
|
|
199
|
+
* - Objects with operator keys → apply specific operators
|
|
200
|
+
* @param {string} params.tableName - Table name used to qualify column names (e.g., "assets" → "assets.column_name")
|
|
201
|
+
* @returns {import('knex').Knex.QueryBuilder} Modified query builder with applied WHERE clauses
|
|
202
|
+
*
|
|
203
|
+
* @throws {TypeError} If query is not a valid Knex QueryBuilder instance
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* // Simple equality - converts to WHERE assets.status = 'active'
|
|
207
|
+
* const query = db('assets').select('*')
|
|
208
|
+
* applyFilter({ query, filter: { status: 'active' }, tableName: 'assets' })
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* // Not equal - converts to WHERE assets.status != 'deleted'
|
|
212
|
+
* applyFilter({ query, filter: { status: { ne: 'deleted' } }, tableName: 'assets' })
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* // Array/IN operator - converts to WHERE assets.status IN ('active', 'pending')
|
|
216
|
+
* applyFilter({ query, filter: { status: ['active', 'pending'] }, tableName: 'assets' })
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* // Explicit IN operator
|
|
220
|
+
* applyFilter({ query, filter: { status: { in: ['active', 'pending'] } }, tableName: 'assets' })
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* // Not in array - converts to WHERE assets.status NOT IN ('deleted', 'archived')
|
|
224
|
+
* applyFilter({ query, filter: { status: { nin: ['deleted', 'archived'] } }, tableName: 'assets' })
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* // Range operators - converts to WHERE assets.price >= 100 AND assets.price <= 200
|
|
228
|
+
* applyFilter({ query, filter: { price: { gte: 100, lte: 200 } }, tableName: 'assets' })
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* // Pattern matching (case-sensitive) - converts to WHERE assets.name LIKE '%invoice%'
|
|
232
|
+
* applyFilter({ query, filter: { name: { like: '%invoice%' } }, tableName: 'assets' })
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // Pattern matching (case-insensitive) - converts to WHERE assets.name ILIKE '%invoice%'
|
|
236
|
+
* applyFilter({ query, filter: { name: { ilike: '%invoice%' } }, tableName: 'assets' })
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* // Null checks - converts to WHERE assets.deleted_at IS NULL
|
|
240
|
+
* applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* // Multiple filters - converts to WHERE assets.status = 'active' AND assets.type = 'invoice' AND assets.price >= 100
|
|
244
|
+
* applyFilter({
|
|
245
|
+
* query,
|
|
246
|
+
* filter: {
|
|
247
|
+
* status: 'active',
|
|
248
|
+
* type: 'invoice',
|
|
249
|
+
* price: { gte: 100 }
|
|
250
|
+
* },
|
|
251
|
+
* tableName: 'assets'
|
|
252
|
+
* })
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* // camelCase conversion - deletedAt converts to deleted_at
|
|
256
|
+
* applyFilter({ query, filter: { deletedAt: { isNull: true } }, tableName: 'assets' })
|
|
257
|
+
* // SQL: WHERE assets.deleted_at IS NULL
|
|
258
|
+
*/
|
|
259
|
+
export function applyFilter({ query, filter, tableName }) {
|
|
260
|
+
const convertedFilter = toSnakeCase(filter)
|
|
261
|
+
|
|
262
|
+
return Object.entries(convertedFilter).reduce((q, [key, value]) => {
|
|
263
|
+
const qualifiedKey = `${tableName}.${key}`
|
|
264
|
+
|
|
265
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
266
|
+
return applyFilterObject(q, qualifiedKey, value)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
return OPERATORS.in(q, qualifiedKey, value)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return OPERATORS.eq(q, qualifiedKey, value)
|
|
274
|
+
}, query)
|
|
275
|
+
}
|
package/src/postgresql/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { fileURLToPath } from 'url'
|
|
1
2
|
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { dirname, join, extname } from 'path'
|
|
2
4
|
import { readFileSync, readdirSync } from 'fs'
|
|
3
|
-
import { join, extname } from 'path'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
import { dirname } from 'path'
|
|
6
|
-
import { mapMessageTelegram } from '../../src/instant-messages/message-unified-mapper.js'
|
|
7
5
|
|
|
6
|
+
import { writeFilesReceiveMapped } from '../resources/create-im-mapp-diff.js'
|
|
8
7
|
import { getTelegramMessageType } from '../../src/instant-messages/message-type.js'
|
|
8
|
+
import { mapMessageTelegram } from '../../src/instant-messages/message-unified-mapper.js'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
MESSAGE_MEDIA_TYPE,
|
|
@@ -24,7 +24,6 @@ describe('Telegram unified message mapper – all mock samples', () => {
|
|
|
24
24
|
if (files.length === 0) {
|
|
25
25
|
throw new Error('No Telegram mock message files found in directory.')
|
|
26
26
|
}
|
|
27
|
-
|
|
28
27
|
for (const file of files) {
|
|
29
28
|
const filePath = join(MOCK_DIR_TELEGRAM, file)
|
|
30
29
|
const raw = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
@@ -36,7 +35,7 @@ describe('Telegram unified message mapper – all mock samples', () => {
|
|
|
36
35
|
const unifiedMessage = mapMessageTelegram({
|
|
37
36
|
imMessage: raw,
|
|
38
37
|
})
|
|
39
|
-
|
|
38
|
+
writeFilesReceiveMapped({ raw, prefix: 'telegram', unifiedMessage })
|
|
40
39
|
expect(unifiedMessage).toBeTypeOf('object')
|
|
41
40
|
expect(unifiedType).toBeTypeOf('string')
|
|
42
41
|
expect(unifiedMessage.type).toBe(
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getMessageType,
|
|
8
8
|
mapMessageWhatsApp,
|
|
9
9
|
} from '../../src/instant-messages/message-unified-mapper.js'
|
|
10
|
-
|
|
10
|
+
import { writeFilesReceiveMapped } from '../resources/create-im-mapp-diff.js'
|
|
11
11
|
import {
|
|
12
12
|
MESSAGE_MEDIA_TYPE,
|
|
13
13
|
MESSAGE_MEDIA_TYPE_MAPPER,
|
|
@@ -38,7 +38,7 @@ describe('WhatsApp unified message mapper – all mock samples', () => {
|
|
|
38
38
|
const unifiedMessage = mapMessageWhatsApp({
|
|
39
39
|
imMessage: raw,
|
|
40
40
|
})
|
|
41
|
-
|
|
41
|
+
writeFilesReceiveMapped({ raw, prefix: 'whatsapp', unifiedMessage })
|
|
42
42
|
expect(unifiedMessage).toBeTypeOf('object')
|
|
43
43
|
expect(unifiedType).toBeTypeOf('string')
|
|
44
44
|
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, beforeAll, afterAll, beforeEach, expect } from 'vitest'
|
|
3
|
+
import knex from 'knex'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
stopPostgres,
|
|
7
|
+
startPostgres,
|
|
8
|
+
buildPostgresUri,
|
|
9
|
+
} from '../../src/postgresql/start-stop-postgres-docker.js'
|
|
10
|
+
|
|
11
|
+
import { applyFilter } from '../../src/postgresql/apply-filter.js'
|
|
12
|
+
|
|
13
|
+
const PG_OPTIONS = {
|
|
14
|
+
port: 5444,
|
|
15
|
+
containerName: 'postgres-apply-filter-test',
|
|
16
|
+
user: 'testuser',
|
|
17
|
+
pass: 'testpass',
|
|
18
|
+
db: 'testdb',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DATABASE_URI = buildPostgresUri(PG_OPTIONS)
|
|
22
|
+
|
|
23
|
+
let db
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
startPostgres(PG_OPTIONS)
|
|
27
|
+
|
|
28
|
+
db = knex({
|
|
29
|
+
client: 'pg',
|
|
30
|
+
connection: DATABASE_URI,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
await db.schema.createTable('assets', (table) => {
|
|
34
|
+
table.uuid('id').primary()
|
|
35
|
+
table.string('name').notNullable()
|
|
36
|
+
table.string('status')
|
|
37
|
+
table.string('type')
|
|
38
|
+
table.decimal('price', 10, 2)
|
|
39
|
+
table.timestamp('created_at').notNullable()
|
|
40
|
+
table.timestamp('deleted_at')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
await db.schema.createTable('invoices', (table) => {
|
|
44
|
+
table.uuid('id').primary()
|
|
45
|
+
table.string('invoice_number').notNullable()
|
|
46
|
+
table.decimal('amount', 10, 2).notNullable()
|
|
47
|
+
table.string('status')
|
|
48
|
+
table.uuid('customer_id')
|
|
49
|
+
table.timestamp('created_at').notNullable()
|
|
50
|
+
table.timestamp('paid_at')
|
|
51
|
+
table.timestamp('deleted_at')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
if (db) {
|
|
57
|
+
await db.destroy()
|
|
58
|
+
}
|
|
59
|
+
stopPostgres(PG_OPTIONS.containerName)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
await db('assets').truncate()
|
|
64
|
+
await db('invoices').truncate()
|
|
65
|
+
|
|
66
|
+
await db('assets').insert([
|
|
67
|
+
{
|
|
68
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
69
|
+
name: 'Asset One',
|
|
70
|
+
status: 'active',
|
|
71
|
+
type: 'invoice',
|
|
72
|
+
price: 100.0,
|
|
73
|
+
created_at: new Date('2024-01-01'),
|
|
74
|
+
deleted_at: null,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: '00000000-0000-0000-0000-000000000002',
|
|
78
|
+
name: 'Asset Two',
|
|
79
|
+
status: 'active',
|
|
80
|
+
type: 'receipt',
|
|
81
|
+
price: 200.0,
|
|
82
|
+
created_at: new Date('2024-01-02'),
|
|
83
|
+
deleted_at: null,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: '00000000-0000-0000-0000-000000000003',
|
|
87
|
+
name: 'Asset Three',
|
|
88
|
+
status: 'pending',
|
|
89
|
+
type: 'invoice',
|
|
90
|
+
price: 150.0,
|
|
91
|
+
created_at: new Date('2024-01-03'),
|
|
92
|
+
deleted_at: null,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: '00000000-0000-0000-0000-000000000004',
|
|
96
|
+
name: 'Deleted Asset',
|
|
97
|
+
status: 'deleted',
|
|
98
|
+
type: 'receipt',
|
|
99
|
+
price: 50.0,
|
|
100
|
+
created_at: new Date('2024-01-04'),
|
|
101
|
+
deleted_at: new Date('2024-01-05'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: '00000000-0000-0000-0000-000000000005',
|
|
105
|
+
name: 'Expensive Asset',
|
|
106
|
+
status: 'active',
|
|
107
|
+
type: 'invoice',
|
|
108
|
+
price: 500.0,
|
|
109
|
+
created_at: new Date('2024-01-05'),
|
|
110
|
+
deleted_at: null,
|
|
111
|
+
},
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
await db('invoices').insert([
|
|
115
|
+
{
|
|
116
|
+
id: '00000000-0000-0000-0000-000000000101',
|
|
117
|
+
invoice_number: 'INV-001',
|
|
118
|
+
amount: 1000.0,
|
|
119
|
+
status: 'paid',
|
|
120
|
+
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
121
|
+
created_at: new Date('2024-01-01'),
|
|
122
|
+
paid_at: new Date('2024-01-02'),
|
|
123
|
+
deleted_at: null,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: '00000000-0000-0000-0000-000000000102',
|
|
127
|
+
invoice_number: 'INV-002',
|
|
128
|
+
amount: 2500.0,
|
|
129
|
+
status: 'pending',
|
|
130
|
+
customer_id: '00000000-0000-0000-0000-000000000202',
|
|
131
|
+
created_at: new Date('2024-01-02'),
|
|
132
|
+
paid_at: null,
|
|
133
|
+
deleted_at: null,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: '00000000-0000-0000-0000-000000000103',
|
|
137
|
+
invoice_number: 'INV-003',
|
|
138
|
+
amount: 500.0,
|
|
139
|
+
status: 'paid',
|
|
140
|
+
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
141
|
+
created_at: new Date('2024-01-03'),
|
|
142
|
+
paid_at: new Date('2024-01-04'),
|
|
143
|
+
deleted_at: null,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: '00000000-0000-0000-0000-000000000104',
|
|
147
|
+
invoice_number: 'INV-004',
|
|
148
|
+
amount: 3000.0,
|
|
149
|
+
status: 'overdue',
|
|
150
|
+
customer_id: '00000000-0000-0000-0000-000000000203',
|
|
151
|
+
created_at: new Date('2024-01-04'),
|
|
152
|
+
paid_at: null,
|
|
153
|
+
deleted_at: null,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: '00000000-0000-0000-0000-000000000105',
|
|
157
|
+
invoice_number: 'INV-005',
|
|
158
|
+
amount: 750.0,
|
|
159
|
+
status: 'cancelled',
|
|
160
|
+
customer_id: '00000000-0000-0000-0000-000000000201',
|
|
161
|
+
created_at: new Date('2024-01-05'),
|
|
162
|
+
paid_at: null,
|
|
163
|
+
deleted_at: new Date('2024-01-06'),
|
|
164
|
+
},
|
|
165
|
+
])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('applyFilter integration', () => {
|
|
169
|
+
it('applies simple equality filter (eq)', async () => {
|
|
170
|
+
const query = db('assets').select('*')
|
|
171
|
+
const filteredQuery = applyFilter({
|
|
172
|
+
query,
|
|
173
|
+
filter: { status: 'active' },
|
|
174
|
+
tableName: 'assets',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const results = await filteredQuery
|
|
178
|
+
|
|
179
|
+
expect(results).toHaveLength(3)
|
|
180
|
+
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('converts camelCase keys to snake_case', async () => {
|
|
184
|
+
const query = db('assets').select('*')
|
|
185
|
+
const filteredQuery = applyFilter({
|
|
186
|
+
query,
|
|
187
|
+
filter: { deletedAt: { isNull: true }, name: 'Asset One' },
|
|
188
|
+
tableName: 'assets',
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const results = await filteredQuery
|
|
192
|
+
|
|
193
|
+
expect(results).toHaveLength(1)
|
|
194
|
+
expect(results[0].name).toBe('Asset One')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('applies not equal filter (ne)', async () => {
|
|
198
|
+
const query = db('assets').select('*')
|
|
199
|
+
const filteredQuery = applyFilter({
|
|
200
|
+
query,
|
|
201
|
+
filter: { status: { ne: 'deleted' } },
|
|
202
|
+
tableName: 'assets',
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const results = await filteredQuery
|
|
206
|
+
|
|
207
|
+
expect(results).toHaveLength(4)
|
|
208
|
+
expect(results.every((r) => r.status !== 'deleted')).toBe(true)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('applies not equal filter (neq alias)', async () => {
|
|
212
|
+
const query = db('assets').select('*')
|
|
213
|
+
const filteredQuery = applyFilter({
|
|
214
|
+
query,
|
|
215
|
+
filter: { status: { neq: 'deleted' } },
|
|
216
|
+
tableName: 'assets',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const results = await filteredQuery
|
|
220
|
+
|
|
221
|
+
expect(results).toHaveLength(4)
|
|
222
|
+
expect(results.every((r) => r.status !== 'deleted')).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('applies IN filter with array directly', async () => {
|
|
226
|
+
const query = db('assets').select('*')
|
|
227
|
+
const filteredQuery = applyFilter({
|
|
228
|
+
query,
|
|
229
|
+
filter: { status: ['active', 'pending'] },
|
|
230
|
+
tableName: 'assets',
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const results = await filteredQuery
|
|
234
|
+
|
|
235
|
+
expect(results).toHaveLength(4)
|
|
236
|
+
expect(
|
|
237
|
+
results.every((r) => r.status === 'active' || r.status === 'pending'),
|
|
238
|
+
).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('applies IN filter with operator', async () => {
|
|
242
|
+
const query = db('assets').select('*')
|
|
243
|
+
const filteredQuery = applyFilter({
|
|
244
|
+
query,
|
|
245
|
+
filter: { status: { in: ['active', 'pending'] } },
|
|
246
|
+
tableName: 'assets',
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const results = await filteredQuery
|
|
250
|
+
|
|
251
|
+
expect(results).toHaveLength(4)
|
|
252
|
+
expect(
|
|
253
|
+
results.every((r) => r.status === 'active' || r.status === 'pending'),
|
|
254
|
+
).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('applies NOT IN filter (nin)', async () => {
|
|
258
|
+
const query = db('assets').select('*')
|
|
259
|
+
const filteredQuery = applyFilter({
|
|
260
|
+
query,
|
|
261
|
+
filter: { status: { nin: ['deleted', 'archived'] } },
|
|
262
|
+
tableName: 'assets',
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const results = await filteredQuery
|
|
266
|
+
|
|
267
|
+
expect(results).toHaveLength(4)
|
|
268
|
+
expect(
|
|
269
|
+
results.every((r) => r.status !== 'deleted' && r.status !== 'archived'),
|
|
270
|
+
).toBe(true)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('applies greater than filter (gt)', async () => {
|
|
274
|
+
const query = db('assets').select('*')
|
|
275
|
+
const filteredQuery = applyFilter({
|
|
276
|
+
query,
|
|
277
|
+
filter: { price: { gt: 150 } },
|
|
278
|
+
tableName: 'assets',
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const results = await filteredQuery
|
|
282
|
+
|
|
283
|
+
expect(results).toHaveLength(2)
|
|
284
|
+
expect(results.every((r) => parseFloat(r.price) > 150)).toBe(true)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('applies greater than or equal filter (gte)', async () => {
|
|
288
|
+
const query = db('assets').select('*')
|
|
289
|
+
const filteredQuery = applyFilter({
|
|
290
|
+
query,
|
|
291
|
+
filter: { price: { gte: 150 } },
|
|
292
|
+
tableName: 'assets',
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const results = await filteredQuery
|
|
296
|
+
|
|
297
|
+
expect(results).toHaveLength(3)
|
|
298
|
+
expect(results.every((r) => parseFloat(r.price) >= 150)).toBe(true)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('applies less than filter (lt)', async () => {
|
|
302
|
+
const query = db('assets').select('*')
|
|
303
|
+
const filteredQuery = applyFilter({
|
|
304
|
+
query,
|
|
305
|
+
filter: { price: { lt: 150 } },
|
|
306
|
+
tableName: 'assets',
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const results = await filteredQuery
|
|
310
|
+
|
|
311
|
+
expect(results).toHaveLength(2)
|
|
312
|
+
expect(results.every((r) => parseFloat(r.price) < 150)).toBe(true)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('applies less than or equal filter (lte)', async () => {
|
|
316
|
+
const query = db('assets').select('*')
|
|
317
|
+
const filteredQuery = applyFilter({
|
|
318
|
+
query,
|
|
319
|
+
filter: { price: { lte: 150 } },
|
|
320
|
+
tableName: 'assets',
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const results = await filteredQuery
|
|
324
|
+
|
|
325
|
+
expect(results).toHaveLength(3)
|
|
326
|
+
expect(results.every((r) => parseFloat(r.price) <= 150)).toBe(true)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('applies range filter with gte and lte', async () => {
|
|
330
|
+
const query = db('assets').select('*')
|
|
331
|
+
const filteredQuery = applyFilter({
|
|
332
|
+
query,
|
|
333
|
+
filter: { price: { gte: 100, lte: 200 } },
|
|
334
|
+
tableName: 'assets',
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const results = await filteredQuery
|
|
338
|
+
|
|
339
|
+
expect(results).toHaveLength(3)
|
|
340
|
+
expect(
|
|
341
|
+
results.every(
|
|
342
|
+
(r) => parseFloat(r.price) >= 100 && parseFloat(r.price) <= 200,
|
|
343
|
+
),
|
|
344
|
+
).toBe(true)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('applies case-sensitive LIKE filter', async () => {
|
|
348
|
+
const query = db('assets').select('*')
|
|
349
|
+
const filteredQuery = applyFilter({
|
|
350
|
+
query,
|
|
351
|
+
filter: { name: { like: '%Asset%' } },
|
|
352
|
+
tableName: 'assets',
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const results = await filteredQuery
|
|
356
|
+
|
|
357
|
+
expect(results.length).toBeGreaterThan(0)
|
|
358
|
+
expect(results.every((r) => r.name.includes('Asset'))).toBe(true)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('applies case-insensitive ILIKE filter', async () => {
|
|
362
|
+
const query = db('assets').select('*')
|
|
363
|
+
const filteredQuery = applyFilter({
|
|
364
|
+
query,
|
|
365
|
+
filter: { name: { ilike: '%asset%' } },
|
|
366
|
+
tableName: 'assets',
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const results = await filteredQuery
|
|
370
|
+
|
|
371
|
+
expect(results.length).toBeGreaterThan(0)
|
|
372
|
+
expect(
|
|
373
|
+
results.every((r) =>
|
|
374
|
+
r.name.toLowerCase().includes('asset'.toLowerCase()),
|
|
375
|
+
),
|
|
376
|
+
).toBe(true)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('applies isNull filter when value is true', async () => {
|
|
380
|
+
const query = db('assets').select('*')
|
|
381
|
+
const filteredQuery = applyFilter({
|
|
382
|
+
query,
|
|
383
|
+
filter: { deletedAt: { isNull: true } },
|
|
384
|
+
tableName: 'assets',
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const results = await filteredQuery
|
|
388
|
+
|
|
389
|
+
expect(results).toHaveLength(4)
|
|
390
|
+
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('applies isNotNull filter when isNull value is false', async () => {
|
|
394
|
+
const query = db('assets').select('*')
|
|
395
|
+
const filteredQuery = applyFilter({
|
|
396
|
+
query,
|
|
397
|
+
filter: { deletedAt: { isNull: false } },
|
|
398
|
+
tableName: 'assets',
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
const results = await filteredQuery
|
|
402
|
+
|
|
403
|
+
expect(results).toHaveLength(1)
|
|
404
|
+
expect(results[0].deleted_at).not.toBe(null)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
it('applies isNotNull filter when value is true', async () => {
|
|
408
|
+
const query = db('assets').select('*')
|
|
409
|
+
const filteredQuery = applyFilter({
|
|
410
|
+
query,
|
|
411
|
+
filter: { deletedAt: { isNotNull: true } },
|
|
412
|
+
tableName: 'assets',
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const results = await filteredQuery
|
|
416
|
+
|
|
417
|
+
expect(results).toHaveLength(1)
|
|
418
|
+
expect(results[0].deleted_at).not.toBe(null)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('applies isNull filter when isNotNull value is false', async () => {
|
|
422
|
+
const query = db('assets').select('*')
|
|
423
|
+
const filteredQuery = applyFilter({
|
|
424
|
+
query,
|
|
425
|
+
filter: { deletedAt: { isNotNull: false } },
|
|
426
|
+
tableName: 'assets',
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const results = await filteredQuery
|
|
430
|
+
|
|
431
|
+
expect(results).toHaveLength(4)
|
|
432
|
+
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('applies multiple filters together', async () => {
|
|
436
|
+
const query = db('assets').select('*')
|
|
437
|
+
const filteredQuery = applyFilter({
|
|
438
|
+
query,
|
|
439
|
+
filter: {
|
|
440
|
+
status: 'active',
|
|
441
|
+
type: 'invoice',
|
|
442
|
+
price: { gte: 100 },
|
|
443
|
+
},
|
|
444
|
+
tableName: 'assets',
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
const results = await filteredQuery
|
|
448
|
+
|
|
449
|
+
expect(results).toHaveLength(2)
|
|
450
|
+
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
451
|
+
expect(results.every((r) => r.type === 'invoice')).toBe(true)
|
|
452
|
+
expect(results.every((r) => parseFloat(r.price) >= 100)).toBe(true)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('returns empty results when filter matches nothing', async () => {
|
|
456
|
+
const query = db('assets').select('*')
|
|
457
|
+
const filteredQuery = applyFilter({
|
|
458
|
+
query,
|
|
459
|
+
filter: { status: 'non-existing' },
|
|
460
|
+
tableName: 'assets',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const results = await filteredQuery
|
|
464
|
+
|
|
465
|
+
expect(results).toHaveLength(0)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('uses qualified column names with tableName', async () => {
|
|
469
|
+
const query = db('assets').select('assets.*')
|
|
470
|
+
const filteredQuery = applyFilter({
|
|
471
|
+
query,
|
|
472
|
+
filter: { status: 'active' },
|
|
473
|
+
tableName: 'assets',
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
const results = await filteredQuery
|
|
477
|
+
|
|
478
|
+
expect(results).toHaveLength(3)
|
|
479
|
+
expect(results.every((r) => r.status === 'active')).toBe(true)
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('ignores unknown operators', async () => {
|
|
483
|
+
const query = db('assets').select('*')
|
|
484
|
+
const filteredQuery = applyFilter({
|
|
485
|
+
query,
|
|
486
|
+
filter: { status: { unknownOperator: 'value' } },
|
|
487
|
+
tableName: 'assets',
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const results = await filteredQuery
|
|
491
|
+
|
|
492
|
+
// Should return all records since unknown operator is ignored
|
|
493
|
+
expect(results).toHaveLength(5)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('works with invoices table', async () => {
|
|
497
|
+
const query = db('invoices').select('*')
|
|
498
|
+
const filteredQuery = applyFilter({
|
|
499
|
+
query,
|
|
500
|
+
filter: { status: 'paid', deletedAt: { isNull: true } },
|
|
501
|
+
tableName: 'invoices',
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
const results = await filteredQuery
|
|
505
|
+
|
|
506
|
+
expect(results).toHaveLength(2)
|
|
507
|
+
expect(results.every((r) => r.status === 'paid')).toBe(true)
|
|
508
|
+
expect(results.every((r) => r.deleted_at === null)).toBe(true)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('works with invoices table using camelCase conversion', async () => {
|
|
512
|
+
const query = db('invoices').select('*')
|
|
513
|
+
const filteredQuery = applyFilter({
|
|
514
|
+
query,
|
|
515
|
+
filter: {
|
|
516
|
+
customerId: '00000000-0000-0000-0000-000000000201',
|
|
517
|
+
amount: { gte: 500 },
|
|
518
|
+
},
|
|
519
|
+
tableName: 'invoices',
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
const results = await filteredQuery
|
|
523
|
+
|
|
524
|
+
expect(results).toHaveLength(3)
|
|
525
|
+
expect(
|
|
526
|
+
results.every(
|
|
527
|
+
(r) =>
|
|
528
|
+
r.customer_id === '00000000-0000-0000-0000-000000000201' &&
|
|
529
|
+
parseFloat(r.amount) >= 500,
|
|
530
|
+
),
|
|
531
|
+
).toBe(true)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
it('works with invoices table using IN filter', async () => {
|
|
535
|
+
const query = db('invoices').select('*')
|
|
536
|
+
const filteredQuery = applyFilter({
|
|
537
|
+
query,
|
|
538
|
+
filter: { status: ['paid', 'pending'] },
|
|
539
|
+
tableName: 'invoices',
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const results = await filteredQuery
|
|
543
|
+
|
|
544
|
+
expect(results).toHaveLength(3)
|
|
545
|
+
expect(
|
|
546
|
+
results.every((r) => r.status === 'paid' || r.status === 'pending'),
|
|
547
|
+
).toBe(true)
|
|
548
|
+
})
|
|
549
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs'
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
export const writeFilesReceiveMapped = ({ prefix, raw, unifiedMessage }) => {
|
|
4
|
+
writeFileSync(
|
|
5
|
+
`./.exclude/${prefix}.${unifiedMessage.type}.json`,
|
|
6
|
+
JSON.stringify(
|
|
7
|
+
{
|
|
8
|
+
received: raw,
|
|
9
|
+
mapped: unifiedMessage,
|
|
10
|
+
},
|
|
11
|
+
null,
|
|
12
|
+
2,
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm test:*)",
|
|
5
|
-
"Bash(docker rm:*)",
|
|
6
|
-
"Bash(docker run:*)",
|
|
7
|
-
"Bash(mongosh:*)",
|
|
8
|
-
"Bash(for:*)",
|
|
9
|
-
"Bash(do if mongosh --port 27099 --eval \"db.runCommand({ ping: 1 })\")",
|
|
10
|
-
"Bash(then echo \"Connected after $i attempts\")",
|
|
11
|
-
"Bash(break)",
|
|
12
|
-
"Bash(fi)",
|
|
13
|
-
"Bash(echo:*)",
|
|
14
|
-
"Bash(done)",
|
|
15
|
-
"Bash(docker logs:*)",
|
|
16
|
-
"Bash(docker system:*)",
|
|
17
|
-
"Bash(docker volume prune:*)",
|
|
18
|
-
"Bash(docker image prune:*)",
|
|
19
|
-
"Bash(docker builder prune:*)"
|
|
20
|
-
]
|
|
21
|
-
}
|
|
22
|
-
}
|