@expo/entity-database-adapter-knex 0.55.0 → 0.58.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/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +278 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
- package/build/src/BaseSQLQueryBuilder.js +87 -0
- package/build/src/BaseSQLQueryBuilder.js.map +1 -0
- package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
- package/build/src/EnforcingKnexEntityLoader.js +166 -0
- package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
- package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
- package/build/src/KnexEntityLoaderFactory.js +39 -0
- package/build/src/KnexEntityLoaderFactory.js.map +1 -0
- package/build/src/PaginationStrategy.d.ts +30 -0
- package/build/src/PaginationStrategy.js +35 -0
- package/build/src/PaginationStrategy.js.map +1 -0
- package/build/src/PostgresEntity.d.ts +25 -0
- package/build/src/PostgresEntity.js +39 -0
- package/build/src/PostgresEntity.js.map +1 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
- package/build/src/PostgresEntityDatabaseAdapter.js +33 -11
- package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
- package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
- package/build/src/ReadonlyPostgresEntity.js +39 -0
- package/build/src/ReadonlyPostgresEntity.js.map +1 -0
- package/build/src/SQLOperator.d.ts +267 -0
- package/build/src/SQLOperator.js +474 -0
- package/build/src/SQLOperator.js.map +1 -0
- package/build/src/index.d.ts +15 -0
- package/build/src/index.js +15 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
- package/build/src/internal/EntityKnexDataManager.js +453 -0
- package/build/src/internal/EntityKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexDataManager.d.ts +3 -0
- package/build/src/internal/getKnexDataManager.js +19 -0
- package/build/src/internal/getKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
- package/build/src/internal/utilityTypes.d.ts +5 -0
- package/build/src/internal/utilityTypes.js +5 -0
- package/build/src/internal/utilityTypes.js.map +1 -0
- package/build/src/internal/weakMaps.d.ts +9 -0
- package/build/src/internal/weakMaps.js +20 -0
- package/build/src/internal/weakMaps.js.map +1 -0
- package/build/src/knexLoader.d.ts +18 -0
- package/build/src/knexLoader.js +31 -0
- package/build/src/knexLoader.js.map +1 -0
- package/package.json +6 -5
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +537 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
- package/src/BaseSQLQueryBuilder.ts +114 -0
- package/src/EnforcingKnexEntityLoader.ts +271 -0
- package/src/KnexEntityLoaderFactory.ts +130 -0
- package/src/PaginationStrategy.ts +32 -0
- package/src/PostgresEntity.ts +118 -0
- package/src/PostgresEntityDatabaseAdapter.ts +81 -24
- package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
- package/src/ReadonlyPostgresEntity.ts +115 -0
- package/src/SQLOperator.ts +630 -0
- package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
- package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
- package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
- package/src/__tests__/EntityFields-test.ts +1 -1
- package/src/__tests__/PostgresEntity-test.ts +172 -0
- package/src/__tests__/ReadonlyEntity-test.ts +32 -0
- package/src/__tests__/SQLOperator-test.ts +871 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
- package/src/__tests__/fixtures/TestEntity.ts +131 -0
- package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
- package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
- package/src/index.ts +15 -0
- package/src/internal/EntityKnexDataManager.ts +832 -0
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
- package/src/internal/__tests__/weakMaps-test.ts +25 -0
- package/src/internal/getKnexDataManager.ts +43 -0
- package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
- package/src/internal/utilityTypes.ts +11 -0
- package/src/internal/weakMaps.ts +19 -0
- package/src/knexLoader.ts +110 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import { getDatabaseFieldForEntityField } from '@expo/entity';
|
|
2
|
+
import { describe, expect, it } from '@jest/globals';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
entityField,
|
|
6
|
+
identifier,
|
|
7
|
+
unsafeRaw,
|
|
8
|
+
sql,
|
|
9
|
+
SQLEntityField,
|
|
10
|
+
SQLFragment,
|
|
11
|
+
SQLFragmentHelpers,
|
|
12
|
+
SQLIdentifier,
|
|
13
|
+
} from '../SQLOperator';
|
|
14
|
+
import { TestFields, testEntityConfiguration } from './fixtures/TestEntity';
|
|
15
|
+
|
|
16
|
+
const getColumnForField = (fieldName: string): string =>
|
|
17
|
+
getDatabaseFieldForEntityField(testEntityConfiguration, fieldName as keyof TestFields);
|
|
18
|
+
|
|
19
|
+
describe('SQLOperator', () => {
|
|
20
|
+
describe('sql template literal', () => {
|
|
21
|
+
it('handles basic parameterized queries', () => {
|
|
22
|
+
const age = 18;
|
|
23
|
+
const status = 'active';
|
|
24
|
+
const fragment = sql`age >= ${age} AND status = ${status}`;
|
|
25
|
+
|
|
26
|
+
expect(fragment.sql).toBe('age >= ? AND status = ?');
|
|
27
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([18, 'active']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('handles nested SQL fragments', () => {
|
|
31
|
+
const condition1 = sql`age >= ${18}`;
|
|
32
|
+
const condition2 = sql`status = ${'active'}`;
|
|
33
|
+
const combined = sql`${condition1} AND ${condition2}`;
|
|
34
|
+
|
|
35
|
+
expect(combined.sql).toBe('age >= ? AND status = ?');
|
|
36
|
+
expect(combined.getKnexBindings(getColumnForField)).toEqual([18, 'active']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('handles SQL identifiers', () => {
|
|
40
|
+
const columnName = 'user_name';
|
|
41
|
+
const fragment = sql`${identifier(columnName)} = ${'John'}`;
|
|
42
|
+
|
|
43
|
+
expect(fragment.sql).toBe('?? = ?');
|
|
44
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['user_name', 'John']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('handles arrays for IN clauses', () => {
|
|
48
|
+
const values = ['active', 'pending', 'approved'];
|
|
49
|
+
const fragment = sql`status IN ${values}`;
|
|
50
|
+
|
|
51
|
+
expect(fragment.sql).toBe('status IN (?, ?, ?)');
|
|
52
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
53
|
+
'active',
|
|
54
|
+
'pending',
|
|
55
|
+
'approved',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles null values', () => {
|
|
60
|
+
const fragment = sql`field = ${null}`;
|
|
61
|
+
|
|
62
|
+
expect(fragment.sql).toBe('field = ?');
|
|
63
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([null]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles empty strings', () => {
|
|
67
|
+
const fragment = sql`field = ${''}`;
|
|
68
|
+
|
|
69
|
+
expect(fragment.sql).toBe('field = ?');
|
|
70
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles numbers including zero', () => {
|
|
74
|
+
const fragment = sql`count = ${0} OR count = ${42}`;
|
|
75
|
+
|
|
76
|
+
expect(fragment.sql).toBe('count = ? OR count = ?');
|
|
77
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([0, 42]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles boolean values', () => {
|
|
81
|
+
const fragment = sql`active = ${true} AND deleted = ${false}`;
|
|
82
|
+
|
|
83
|
+
expect(fragment.sql).toBe('active = ? AND deleted = ?');
|
|
84
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([true, false]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('handles raw SQL', () => {
|
|
88
|
+
const columnName = 'created_at';
|
|
89
|
+
const fragment = sql`ORDER BY ${unsafeRaw(columnName)} DESC`;
|
|
90
|
+
|
|
91
|
+
expect(fragment.sql).toBe('ORDER BY created_at DESC');
|
|
92
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles complex raw SQL expressions', () => {
|
|
96
|
+
const fragment = sql`WHERE ${unsafeRaw('EXTRACT(year FROM created_at)')} = ${2024}`;
|
|
97
|
+
|
|
98
|
+
expect(fragment.sql).toBe('WHERE EXTRACT(year FROM created_at) = ?');
|
|
99
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([2024]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('combines raw SQL with regular parameters', () => {
|
|
103
|
+
const sortColumn = 'name';
|
|
104
|
+
const fragment = sql`SELECT * FROM users WHERE age > ${18} ORDER BY ${unsafeRaw(sortColumn)} ${unsafeRaw('DESC')}`;
|
|
105
|
+
|
|
106
|
+
expect(fragment.sql).toBe('SELECT * FROM users WHERE age > ? ORDER BY name DESC');
|
|
107
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([18]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe(SQLFragment, () => {
|
|
112
|
+
describe(SQLFragment.prototype.append, () => {
|
|
113
|
+
it('appends fragments correctly', () => {
|
|
114
|
+
const fragment1 = new SQLFragment('age >= ?', [{ type: 'value', value: 18 }]);
|
|
115
|
+
const fragment2 = new SQLFragment('status = ?', [{ type: 'value', value: 'active' }]);
|
|
116
|
+
const combined = fragment1.append(fragment2);
|
|
117
|
+
|
|
118
|
+
expect(combined.sql).toBe('age >= ? status = ?');
|
|
119
|
+
expect(combined.getKnexBindings(getColumnForField)).toEqual([18, 'active']);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe(SQLFragment.joinWithCommaSeparator, () => {
|
|
124
|
+
it('handles empty array in join', () => {
|
|
125
|
+
const joined = SQLFragment.joinWithCommaSeparator();
|
|
126
|
+
|
|
127
|
+
expect(joined.sql).toBe('');
|
|
128
|
+
expect(joined.getKnexBindings(getColumnForField)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('joins SQL fragments with comma', () => {
|
|
132
|
+
const columns = [sql`name`, sql`age`, sql`email`];
|
|
133
|
+
const joined = SQLFragment.joinWithCommaSeparator(...columns);
|
|
134
|
+
|
|
135
|
+
expect(joined.sql).toBe('name, age, email');
|
|
136
|
+
expect(joined.getKnexBindings(getColumnForField)).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles single fragment', () => {
|
|
140
|
+
const single = [sql`name = ${'Alice'}`];
|
|
141
|
+
const joined = SQLFragment.joinWithCommaSeparator(...single);
|
|
142
|
+
|
|
143
|
+
expect(joined.sql).toBe('name = ?');
|
|
144
|
+
expect(joined.getKnexBindings(getColumnForField)).toEqual(['Alice']);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe(SQLFragment.concat, () => {
|
|
149
|
+
it('concatenates fragments with space separator', () => {
|
|
150
|
+
const select = new SQLFragment('SELECT * FROM users', []);
|
|
151
|
+
const where = new SQLFragment('WHERE age > ?', [{ type: 'value', value: 18 }]);
|
|
152
|
+
const orderBy = new SQLFragment('ORDER BY name', []);
|
|
153
|
+
|
|
154
|
+
const concatenated = SQLFragment.concat(select, where, orderBy);
|
|
155
|
+
|
|
156
|
+
expect(concatenated.sql).toBe('SELECT * FROM users WHERE age > ? ORDER BY name');
|
|
157
|
+
expect(concatenated.getKnexBindings(getColumnForField)).toEqual([18]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('handles single fragment in concat', () => {
|
|
161
|
+
const fragment = new SQLFragment('SELECT * FROM users', []);
|
|
162
|
+
const concatenated = SQLFragment.concat(fragment);
|
|
163
|
+
|
|
164
|
+
expect(concatenated.sql).toBe('SELECT * FROM users');
|
|
165
|
+
expect(concatenated.getKnexBindings(getColumnForField)).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('handles empty concat', () => {
|
|
169
|
+
const concatenated = SQLFragment.concat();
|
|
170
|
+
|
|
171
|
+
expect(concatenated.sql).toBe('');
|
|
172
|
+
expect(concatenated.getKnexBindings(getColumnForField)).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('supports dynamic query building with concat', () => {
|
|
176
|
+
// Build a query dynamically
|
|
177
|
+
const fragments: SQLFragment<Record<string, any>>[] = [sql`SELECT * FROM products`];
|
|
178
|
+
|
|
179
|
+
// Conditionally add WHERE clause
|
|
180
|
+
const filters: SQLFragment<Record<string, any>>[] = [];
|
|
181
|
+
filters.push(sql`price > ${100}`);
|
|
182
|
+
filters.push(sql`category = ${'electronics'}`);
|
|
183
|
+
|
|
184
|
+
if (filters.length > 0) {
|
|
185
|
+
fragments.push(sql`WHERE ${SQLFragmentHelpers.and(...filters)}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add ORDER BY
|
|
189
|
+
fragments.push(sql`ORDER BY created_at DESC`);
|
|
190
|
+
|
|
191
|
+
// Add LIMIT
|
|
192
|
+
fragments.push(sql`LIMIT ${10}`);
|
|
193
|
+
|
|
194
|
+
const query = SQLFragment.concat(...fragments);
|
|
195
|
+
|
|
196
|
+
expect(query.sql).toBe(
|
|
197
|
+
'SELECT * FROM products WHERE (price > ?) AND (category = ?) ORDER BY created_at DESC LIMIT ?',
|
|
198
|
+
);
|
|
199
|
+
expect(query.getKnexBindings(getColumnForField)).toEqual([100, 'electronics', 10]);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('getDebugString', () => {
|
|
204
|
+
it('generates debug text with values inline', () => {
|
|
205
|
+
const fragment = new SQLFragment(
|
|
206
|
+
'SELECT * FROM users WHERE name = ? AND age > ? AND active = ? AND created_at > ?',
|
|
207
|
+
[
|
|
208
|
+
{ type: 'value', value: 'Alice' },
|
|
209
|
+
{ type: 'value', value: 18 },
|
|
210
|
+
{ type: 'value', value: true },
|
|
211
|
+
{ type: 'value', value: new Date('2024-01-01') },
|
|
212
|
+
],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const text = fragment.getDebugString();
|
|
216
|
+
expect(text).toContain("'Alice'");
|
|
217
|
+
expect(text).toContain('18');
|
|
218
|
+
expect(text).toContain('TRUE');
|
|
219
|
+
expect(text).toContain('2024-01-01');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles null and special characters in text', () => {
|
|
223
|
+
const fragment = new SQLFragment('name = ? AND email = ? AND data = ?', [
|
|
224
|
+
{ type: 'value', value: null },
|
|
225
|
+
{ type: 'value', value: "O'Reilly" },
|
|
226
|
+
{ type: 'value', value: { key: 'value' } },
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const text = fragment.getDebugString();
|
|
230
|
+
expect(text).toContain('NULL');
|
|
231
|
+
expect(text).toContain("O''Reilly"); // SQL escaped single quote
|
|
232
|
+
expect(text).toContain(`'{"key":"value"}'::jsonb`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('handles all SupportedSQLValue types in getDebugString', () => {
|
|
236
|
+
const fragment = new SQLFragment('INSERT INTO test VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
237
|
+
{ type: 'value', value: 'string' },
|
|
238
|
+
{ type: 'value', value: 123 },
|
|
239
|
+
{ type: 'value', value: true },
|
|
240
|
+
{ type: 'value', value: null },
|
|
241
|
+
{ type: 'value', value: undefined },
|
|
242
|
+
{ type: 'value', value: new Date('2024-01-01T00:00:00.000Z') },
|
|
243
|
+
{ type: 'value', value: Buffer.from('hello') },
|
|
244
|
+
{ type: 'value', value: BigInt(999) },
|
|
245
|
+
{ type: 'value', value: [1, 2, 3] },
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
const text = fragment.getDebugString();
|
|
249
|
+
expect(text).toBe(
|
|
250
|
+
"INSERT INTO test VALUES ('string', 123, TRUE, NULL, NULL, '2024-01-01T00:00:00.000Z', '\\x68656c6c6f', 999, ARRAY[1, 2, 3])",
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('handles nested arrays in getDebugString', () => {
|
|
255
|
+
const fragment = new SQLFragment('SELECT * FROM test WHERE tags = ?', [
|
|
256
|
+
{ type: 'value', value: ['tag1', 'tag2', null] },
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
const text = fragment.getDebugString();
|
|
260
|
+
expect(text).toBe("SELECT * FROM test WHERE tags = ARRAY['tag1', 'tag2', NULL]");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('handles mismatched placeholders and values gracefully', () => {
|
|
264
|
+
const fragment = new SQLFragment('SELECT * FROM test WHERE field1 = ? AND field2 = ?', [
|
|
265
|
+
{ type: 'value', value: 'value1' },
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
const text = fragment.getDebugString();
|
|
269
|
+
expect(text).toBe("SELECT * FROM test WHERE field1 = 'value1' AND field2 = ?");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('handles non-SupportedSQLValue types gracefully', () => {
|
|
273
|
+
const fragment = new SQLFragment('SELECT * FROM test WHERE field = ? AND field2 = ?', [
|
|
274
|
+
{ type: 'value', value: new Error('wat') },
|
|
275
|
+
{ type: 'value', value: Object.create(null) },
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const text = fragment.getDebugString();
|
|
279
|
+
expect(text).toBe(
|
|
280
|
+
`SELECT * FROM test WHERE field = UnsupportedSQLValue[Error: wat] AND field2 = '{}'::jsonb`,
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('handles identifiers in getDebugString', () => {
|
|
285
|
+
const fragment = new SQLFragment('SELECT ?? FROM ?? WHERE ?? = ?', [
|
|
286
|
+
{ type: 'identifier', name: 'user_name' },
|
|
287
|
+
{ type: 'identifier', name: 'users' },
|
|
288
|
+
{ type: 'identifier', name: 'status' },
|
|
289
|
+
{ type: 'value', value: 'active' },
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const text = fragment.getDebugString();
|
|
293
|
+
expect(text).toBe('SELECT "user_name" FROM "users" WHERE "status" = \'active\'');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('handles entity fields in getDebugString', () => {
|
|
297
|
+
const fragment = new SQLFragment('SELECT ?? FROM ?? WHERE ?? = ?', [
|
|
298
|
+
{ type: 'entityField', fieldName: 'string_field' },
|
|
299
|
+
{ type: 'identifier', name: 'test' },
|
|
300
|
+
{ type: 'entityField', fieldName: 'number_field' },
|
|
301
|
+
{ type: 'value', value: 42 },
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
const text = fragment.getDebugString();
|
|
305
|
+
expect(text).toBe('SELECT "string_field" FROM "test" WHERE "number_field" = 42');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('handles undefined bindings gracefully', () => {
|
|
309
|
+
const fragment = new SQLFragment('SELECT * FROM users WHERE id = ? AND name = ?', [
|
|
310
|
+
{ type: 'value', value: 1 },
|
|
311
|
+
undefined as any, // Simulate an edge case with undefined binding
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
const text = fragment.getDebugString();
|
|
315
|
+
expect(text).toBe('SELECT * FROM users WHERE id = 1 AND name = ?');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('handles null bindings gracefully', () => {
|
|
319
|
+
const fragment = new SQLFragment('SELECT * FROM ?? WHERE ?? = ?', [
|
|
320
|
+
{ type: 'identifier', name: 'users' },
|
|
321
|
+
null as any, // Simulate an edge case with null binding
|
|
322
|
+
{ type: 'value', value: 'test' },
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const text = fragment.getDebugString();
|
|
326
|
+
// When a binding is null, it leaves the placeholder unchanged and doesn't advance the index
|
|
327
|
+
expect(text).toBe('SELECT * FROM "users" WHERE ?? = ?');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('handles mismatch between identifier placeholder and value binding', () => {
|
|
331
|
+
const fragment = new SQLFragment('SELECT * FROM ?? WHERE id = ?', [
|
|
332
|
+
{ type: 'value', value: 'users' }, // Wrong type - should be identifier
|
|
333
|
+
{ type: 'value', value: 1 },
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
const text = fragment.getDebugString();
|
|
337
|
+
// Mismatched binding type leaves the placeholder unchanged
|
|
338
|
+
expect(text).toBe('SELECT * FROM ?? WHERE id = 1');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('handles mismatch between value placeholder and identifier binding', () => {
|
|
342
|
+
const fragment = new SQLFragment('SELECT * FROM users WHERE ? = ?', [
|
|
343
|
+
{ type: 'identifier', name: 'status' }, // Wrong type - should be value
|
|
344
|
+
{ type: 'value', value: 'active' },
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
const text = fragment.getDebugString();
|
|
348
|
+
// Mismatched binding type leaves the placeholder unchanged
|
|
349
|
+
expect(text).toBe("SELECT * FROM users WHERE ? = 'active'");
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe(SQLIdentifier, () => {
|
|
355
|
+
it('stores raw identifier names', () => {
|
|
356
|
+
const id = identifier('user_name');
|
|
357
|
+
expect(id.name).toBe('user_name');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('stores identifier with quotes unchanged', () => {
|
|
361
|
+
const id = identifier('table"name');
|
|
362
|
+
expect(id.name).toBe('table"name');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('stores identifier with multiple quotes unchanged', () => {
|
|
366
|
+
const id = identifier('my"special"column');
|
|
367
|
+
expect(id.name).toBe('my"special"column');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('stores potential SQL injection attempts unchanged', () => {
|
|
371
|
+
const id = identifier('col"; DROP TABLE users; --');
|
|
372
|
+
expect(id.name).toBe('col"; DROP TABLE users; --');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('handles empty string identifier', () => {
|
|
376
|
+
const id = identifier('');
|
|
377
|
+
expect(id.name).toBe('');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('handles identifier with only quotes', () => {
|
|
381
|
+
const id = identifier('"""');
|
|
382
|
+
expect(id.name).toBe('"""');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('uses ?? placeholder in SQL fragments', () => {
|
|
386
|
+
const columnName = 'user"data';
|
|
387
|
+
const fragment = sql`SELECT ${identifier(columnName)} FROM users`;
|
|
388
|
+
expect(fragment.sql).toBe('SELECT ?? FROM users');
|
|
389
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['user"data']);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('delegates escaping to Knex for SQL injection prevention', () => {
|
|
393
|
+
const maliciousName = 'id"; DELETE FROM users WHERE "1"="1';
|
|
394
|
+
const fragment = sql`SELECT * FROM ${identifier(maliciousName)}`;
|
|
395
|
+
// The identifier is passed as a binding to Knex which will escape it
|
|
396
|
+
expect(fragment.sql).toBe('SELECT * FROM ??');
|
|
397
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
398
|
+
'id"; DELETE FROM users WHERE "1"="1',
|
|
399
|
+
]);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe(SQLEntityField, () => {
|
|
404
|
+
it('stores the entity field name', () => {
|
|
405
|
+
const field = entityField<TestFields>('stringField');
|
|
406
|
+
expect(field.fieldName).toBe('stringField');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('uses ?? placeholder in SQL fragments', () => {
|
|
410
|
+
const fragment = sql`SELECT ${entityField<TestFields>('stringField')} FROM users`;
|
|
411
|
+
|
|
412
|
+
expect(fragment.sql).toBe('SELECT ?? FROM users');
|
|
413
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field']);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('translates entity field name to database column name via getKnexBindings', () => {
|
|
417
|
+
const fragment = sql`WHERE ${entityField<TestFields>('intField')} = ${42}`;
|
|
418
|
+
|
|
419
|
+
expect(fragment.sql).toBe('WHERE ?? = ?');
|
|
420
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 42]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('translates the id field correctly', () => {
|
|
424
|
+
const fragment = sql`WHERE ${entityField<TestFields>('customIdField')} = ${'some-id'}`;
|
|
425
|
+
|
|
426
|
+
expect(fragment.sql).toBe('WHERE ?? = ?');
|
|
427
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['custom_id', 'some-id']);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('works alongside identifiers and values', () => {
|
|
431
|
+
const fragment = sql`SELECT ${identifier('table_name')}.${entityField<TestFields>('stringField')} WHERE ${entityField<TestFields>('intField')} > ${10}`;
|
|
432
|
+
|
|
433
|
+
expect(fragment.sql).toBe('SELECT ??.?? WHERE ?? > ?');
|
|
434
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
435
|
+
'table_name',
|
|
436
|
+
'string_field',
|
|
437
|
+
'number_field',
|
|
438
|
+
10,
|
|
439
|
+
]);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('works with multiple entity fields', () => {
|
|
443
|
+
const fragment = sql`SELECT ${entityField<TestFields>('stringField')}, ${entityField<TestFields>('intField')}, ${entityField<TestFields>('dateField')} FROM test`;
|
|
444
|
+
|
|
445
|
+
expect(fragment.sql).toBe('SELECT ??, ??, ?? FROM test');
|
|
446
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
447
|
+
'string_field',
|
|
448
|
+
'number_field',
|
|
449
|
+
'date_field',
|
|
450
|
+
]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('works in nested SQL fragments', () => {
|
|
454
|
+
const inner = sql`${entityField<TestFields>('stringField')} = ${'hello'}`;
|
|
455
|
+
const outer = sql`SELECT * FROM test WHERE ${inner}`;
|
|
456
|
+
|
|
457
|
+
expect(outer.sql).toBe('SELECT * FROM test WHERE ?? = ?');
|
|
458
|
+
expect(outer.getKnexBindings(getColumnForField)).toEqual(['string_field', 'hello']);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('SQLFragmentHelpers', () => {
|
|
463
|
+
describe(SQLFragmentHelpers.inArray, () => {
|
|
464
|
+
it('generates IN clause with values', () => {
|
|
465
|
+
const fragment = SQLFragmentHelpers.inArray('stringField', ['active', 'pending']);
|
|
466
|
+
|
|
467
|
+
expect(fragment.sql).toBe('?? IN (?, ?)');
|
|
468
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
469
|
+
'string_field',
|
|
470
|
+
'active',
|
|
471
|
+
'pending',
|
|
472
|
+
]);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('handles empty array', () => {
|
|
476
|
+
const fragment = SQLFragmentHelpers.inArray('stringField', []);
|
|
477
|
+
|
|
478
|
+
expect(fragment.sql).toBe('1 = 0'); // Always false
|
|
479
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([]);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
describe(SQLFragmentHelpers.notInArray, () => {
|
|
484
|
+
it('generates NOT IN clause with values', () => {
|
|
485
|
+
const fragment = SQLFragmentHelpers.notInArray('stringField', ['deleted', 'archived']);
|
|
486
|
+
|
|
487
|
+
expect(fragment.sql).toBe('?? NOT IN (?, ?)');
|
|
488
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
489
|
+
'string_field',
|
|
490
|
+
'deleted',
|
|
491
|
+
'archived',
|
|
492
|
+
]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('handles empty array', () => {
|
|
496
|
+
const fragment = SQLFragmentHelpers.notInArray('stringField', []);
|
|
497
|
+
|
|
498
|
+
expect(fragment.sql).toBe('1 = 1'); // Always true
|
|
499
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([]);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
describe(SQLFragmentHelpers.between, () => {
|
|
504
|
+
it('generates BETWEEN clause with numbers', () => {
|
|
505
|
+
const fragment = SQLFragmentHelpers.between('intField', 18, 65);
|
|
506
|
+
|
|
507
|
+
expect(fragment.sql).toBe('?? BETWEEN ? AND ?');
|
|
508
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 18, 65]);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('generates BETWEEN clause with dates', () => {
|
|
512
|
+
const date1 = new Date('2024-01-01');
|
|
513
|
+
const date2 = new Date('2024-12-31');
|
|
514
|
+
const fragment = SQLFragmentHelpers.between('dateField', date1, date2);
|
|
515
|
+
|
|
516
|
+
expect(fragment.sql).toBe('?? BETWEEN ? AND ?');
|
|
517
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['date_field', date1, date2]);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('generates BETWEEN clause with strings', () => {
|
|
521
|
+
const fragment = SQLFragmentHelpers.between('stringField', 'A', 'Z');
|
|
522
|
+
|
|
523
|
+
expect(fragment.sql).toBe('?? BETWEEN ? AND ?');
|
|
524
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', 'A', 'Z']);
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe(SQLFragmentHelpers.notBetween, () => {
|
|
529
|
+
it('generates NOT BETWEEN clause with numbers', () => {
|
|
530
|
+
const fragment = SQLFragmentHelpers.notBetween('intField', 18, 65);
|
|
531
|
+
|
|
532
|
+
expect(fragment.sql).toBe('?? NOT BETWEEN ? AND ?');
|
|
533
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 18, 65]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('generates NOT BETWEEN clause with dates', () => {
|
|
537
|
+
const date1 = new Date('2024-01-01');
|
|
538
|
+
const date2 = new Date('2024-12-31');
|
|
539
|
+
const fragment = SQLFragmentHelpers.notBetween('dateField', date1, date2);
|
|
540
|
+
|
|
541
|
+
expect(fragment.sql).toBe('?? NOT BETWEEN ? AND ?');
|
|
542
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['date_field', date1, date2]);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
describe(SQLFragmentHelpers.like, () => {
|
|
547
|
+
it('generates LIKE clause', () => {
|
|
548
|
+
const fragment = SQLFragmentHelpers.like('stringField', '%John%');
|
|
549
|
+
|
|
550
|
+
expect(fragment.sql).toBe('?? LIKE ?');
|
|
551
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', '%John%']);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe(SQLFragmentHelpers.notLike, () => {
|
|
556
|
+
it('generates NOT LIKE clause', () => {
|
|
557
|
+
const fragment = SQLFragmentHelpers.notLike('stringField', '%test%');
|
|
558
|
+
|
|
559
|
+
expect(fragment.sql).toBe('?? NOT LIKE ?');
|
|
560
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', '%test%']);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
describe(SQLFragmentHelpers.ilike, () => {
|
|
565
|
+
it('generates ILIKE clause for case-insensitive matching', () => {
|
|
566
|
+
const fragment = SQLFragmentHelpers.ilike('testIndexedField', '%@example.com');
|
|
567
|
+
|
|
568
|
+
expect(fragment.sql).toBe('?? ILIKE ?');
|
|
569
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
570
|
+
'test_index',
|
|
571
|
+
'%@example.com',
|
|
572
|
+
]);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe(SQLFragmentHelpers.notIlike, () => {
|
|
577
|
+
it('generates NOT ILIKE clause for case-insensitive non-matching', () => {
|
|
578
|
+
const fragment = SQLFragmentHelpers.notIlike('testIndexedField', '%@spam.com');
|
|
579
|
+
|
|
580
|
+
expect(fragment.sql).toBe('?? NOT ILIKE ?');
|
|
581
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['test_index', '%@spam.com']);
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
describe(SQLFragmentHelpers.isNull, () => {
|
|
586
|
+
it('generates IS NULL', () => {
|
|
587
|
+
const fragment = SQLFragmentHelpers.isNull('nullableField');
|
|
588
|
+
|
|
589
|
+
expect(fragment.sql).toBe('?? IS NULL');
|
|
590
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
describe(SQLFragmentHelpers.isNotNull, () => {
|
|
595
|
+
it('generates IS NOT NULL', () => {
|
|
596
|
+
const fragment = SQLFragmentHelpers.isNotNull('testIndexedField');
|
|
597
|
+
|
|
598
|
+
expect(fragment.sql).toBe('?? IS NOT NULL');
|
|
599
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['test_index']);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe(SQLFragmentHelpers.eq, () => {
|
|
604
|
+
it('generates equality check', () => {
|
|
605
|
+
const fragment = SQLFragmentHelpers.eq('stringField', 'active');
|
|
606
|
+
|
|
607
|
+
expect(fragment.sql).toBe('?? = ?');
|
|
608
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', 'active']);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('handles null in equality check', () => {
|
|
612
|
+
const fragment = SQLFragmentHelpers.eq('nullableField', null);
|
|
613
|
+
|
|
614
|
+
expect(fragment.sql).toBe('?? IS NULL');
|
|
615
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('handles undefined in equality check', () => {
|
|
619
|
+
const fragment = SQLFragmentHelpers.eq('nullableField', undefined);
|
|
620
|
+
|
|
621
|
+
expect(fragment.sql).toBe('?? IS NULL');
|
|
622
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe(SQLFragmentHelpers.neq, () => {
|
|
627
|
+
it('generates inequality check', () => {
|
|
628
|
+
const fragment = SQLFragmentHelpers.neq('stringField', 'deleted');
|
|
629
|
+
|
|
630
|
+
expect(fragment.sql).toBe('?? != ?');
|
|
631
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', 'deleted']);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('handles null in inequality check', () => {
|
|
635
|
+
const fragment = SQLFragmentHelpers.neq('nullableField', null);
|
|
636
|
+
|
|
637
|
+
expect(fragment.sql).toBe('?? IS NOT NULL');
|
|
638
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it('handles undefined in inequality check', () => {
|
|
642
|
+
const fragment = SQLFragmentHelpers.neq('nullableField', undefined);
|
|
643
|
+
|
|
644
|
+
expect(fragment.sql).toBe('?? IS NOT NULL');
|
|
645
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['nullable_field']);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
describe(SQLFragmentHelpers.gt, () => {
|
|
650
|
+
it('generates greater than', () => {
|
|
651
|
+
const fragment = SQLFragmentHelpers.gt('intField', 18);
|
|
652
|
+
|
|
653
|
+
expect(fragment.sql).toBe('?? > ?');
|
|
654
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 18]);
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
describe(SQLFragmentHelpers.gte, () => {
|
|
659
|
+
it('generates greater than or equal', () => {
|
|
660
|
+
const fragment = SQLFragmentHelpers.gte('intField', 18);
|
|
661
|
+
|
|
662
|
+
expect(fragment.sql).toBe('?? >= ?');
|
|
663
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 18]);
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe(SQLFragmentHelpers.lt, () => {
|
|
668
|
+
it('generates less than', () => {
|
|
669
|
+
const fragment = SQLFragmentHelpers.lt('intField', 65);
|
|
670
|
+
|
|
671
|
+
expect(fragment.sql).toBe('?? < ?');
|
|
672
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 65]);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe(SQLFragmentHelpers.lte, () => {
|
|
677
|
+
it('generates less than or equal', () => {
|
|
678
|
+
const fragment = SQLFragmentHelpers.lte('intField', 65);
|
|
679
|
+
|
|
680
|
+
expect(fragment.sql).toBe('?? <= ?');
|
|
681
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['number_field', 65]);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe(SQLFragmentHelpers.jsonContains, () => {
|
|
686
|
+
it('generates JSON contains', () => {
|
|
687
|
+
const fragment = SQLFragmentHelpers.jsonContains('stringField', { premium: true });
|
|
688
|
+
|
|
689
|
+
expect(fragment.sql).toBe('?? @> ?::jsonb');
|
|
690
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
691
|
+
'string_field',
|
|
692
|
+
'{"premium":true}',
|
|
693
|
+
]);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('generates JSON contains for null and undefined values', () => {
|
|
697
|
+
const fragmentNull = SQLFragmentHelpers.jsonContains('stringField', null);
|
|
698
|
+
const fragmentUndefined = SQLFragmentHelpers.jsonContains('stringField', undefined);
|
|
699
|
+
|
|
700
|
+
expect(fragmentNull.sql).toBe('?? @> ?::jsonb');
|
|
701
|
+
expect(fragmentNull.getKnexBindings(getColumnForField)).toEqual(['string_field', 'null']);
|
|
702
|
+
|
|
703
|
+
expect(fragmentUndefined.sql).toBe('?? @> ?::jsonb');
|
|
704
|
+
expect(fragmentUndefined.getKnexBindings(getColumnForField)).toEqual([
|
|
705
|
+
'string_field',
|
|
706
|
+
undefined,
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('throws when value is not JSON-serializable', () => {
|
|
711
|
+
expect(() => SQLFragmentHelpers.jsonContains('stringField', (() => {}) as any)).toThrow(
|
|
712
|
+
'jsonContains: value is not JSON-serializable',
|
|
713
|
+
);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
describe(SQLFragmentHelpers.jsonContainedBy, () => {
|
|
718
|
+
it('generates JSON contained by', () => {
|
|
719
|
+
const fragment = SQLFragmentHelpers.jsonContainedBy('stringField', {
|
|
720
|
+
theme: 'dark',
|
|
721
|
+
lang: 'en',
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
expect(fragment.sql).toBe('?? <@ ?::jsonb');
|
|
725
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
726
|
+
'string_field',
|
|
727
|
+
'{"theme":"dark","lang":"en"}',
|
|
728
|
+
]);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('generates JSON contained by for null and undefined values', () => {
|
|
732
|
+
const fragmentNull = SQLFragmentHelpers.jsonContainedBy('stringField', null);
|
|
733
|
+
const fragmentUndefined = SQLFragmentHelpers.jsonContainedBy('stringField', undefined);
|
|
734
|
+
|
|
735
|
+
expect(fragmentNull.sql).toBe('?? <@ ?::jsonb');
|
|
736
|
+
expect(fragmentNull.getKnexBindings(getColumnForField)).toEqual(['string_field', 'null']);
|
|
737
|
+
|
|
738
|
+
expect(fragmentUndefined.sql).toBe('?? <@ ?::jsonb');
|
|
739
|
+
expect(fragmentUndefined.getKnexBindings(getColumnForField)).toEqual([
|
|
740
|
+
'string_field',
|
|
741
|
+
undefined,
|
|
742
|
+
]);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('throws when value is not JSON-serializable', () => {
|
|
746
|
+
expect(() => SQLFragmentHelpers.jsonContainedBy('stringField', (() => {}) as any)).toThrow(
|
|
747
|
+
'jsonContainedBy: value is not JSON-serializable',
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe(SQLFragmentHelpers.jsonPath, () => {
|
|
753
|
+
it('generates JSON path access', () => {
|
|
754
|
+
const fragment = SQLFragmentHelpers.jsonPath('stringField', 'user');
|
|
755
|
+
|
|
756
|
+
expect(fragment.sql).toBe(`??->?`);
|
|
757
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', 'user']);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe(SQLFragmentHelpers.jsonPathText, () => {
|
|
762
|
+
it('generates JSON path text access', () => {
|
|
763
|
+
const fragment = SQLFragmentHelpers.jsonPathText('stringField', 'email');
|
|
764
|
+
|
|
765
|
+
expect(fragment.sql).toBe(`??->>?`);
|
|
766
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['string_field', 'email']);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
describe(SQLFragmentHelpers.and, () => {
|
|
771
|
+
it('combines conditions with AND', () => {
|
|
772
|
+
const cond1 = sql`age >= ${18}`;
|
|
773
|
+
const cond2 = sql`status = ${'active'}`;
|
|
774
|
+
const fragment = SQLFragmentHelpers.and(cond1, cond2);
|
|
775
|
+
|
|
776
|
+
expect(fragment.sql).toBe('(age >= ?) AND (status = ?)');
|
|
777
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([18, 'active']);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
it('handles single condition in AND', () => {
|
|
781
|
+
const cond = sql`age >= ${18}`;
|
|
782
|
+
const fragment = SQLFragmentHelpers.and(cond);
|
|
783
|
+
|
|
784
|
+
expect(fragment.sql).toBe('(age >= ?)');
|
|
785
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([18]);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('handles empty conditions in AND', () => {
|
|
789
|
+
const fragment = SQLFragmentHelpers.and();
|
|
790
|
+
|
|
791
|
+
expect(fragment.sql).toBe('1 = 1');
|
|
792
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([]);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe(SQLFragmentHelpers.or, () => {
|
|
797
|
+
it('combines conditions with OR', () => {
|
|
798
|
+
const cond1 = sql`status = ${'active'}`;
|
|
799
|
+
const cond2 = sql`status = ${'pending'}`;
|
|
800
|
+
const fragment = SQLFragmentHelpers.or(cond1, cond2);
|
|
801
|
+
|
|
802
|
+
expect(fragment.sql).toBe('(status = ?) OR (status = ?)');
|
|
803
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['active', 'pending']);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('handles single condition in OR', () => {
|
|
807
|
+
const cond = sql`status = ${'active'}`;
|
|
808
|
+
const fragment = SQLFragmentHelpers.or(cond);
|
|
809
|
+
|
|
810
|
+
expect(fragment.sql).toBe('(status = ?)');
|
|
811
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['active']);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('handles empty conditions in OR', () => {
|
|
815
|
+
const fragment = SQLFragmentHelpers.or();
|
|
816
|
+
|
|
817
|
+
expect(fragment.sql).toBe('1 = 0');
|
|
818
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([]);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
describe(SQLFragmentHelpers.not, () => {
|
|
823
|
+
it('negates conditions with NOT', () => {
|
|
824
|
+
const cond = sql`status = ${'deleted'}`;
|
|
825
|
+
const fragment = SQLFragmentHelpers.not(cond);
|
|
826
|
+
|
|
827
|
+
expect(fragment.sql).toBe('NOT (status = ?)');
|
|
828
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual(['deleted']);
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
describe(SQLFragmentHelpers.group, () => {
|
|
833
|
+
it('groups conditions with parentheses', () => {
|
|
834
|
+
const cond = sql`age >= ${18} AND age <= ${65}`;
|
|
835
|
+
const fragment = SQLFragmentHelpers.group(cond);
|
|
836
|
+
|
|
837
|
+
expect(fragment.sql).toBe('(age >= ? AND age <= ?)');
|
|
838
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([18, 65]);
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
describe('complex combinations', () => {
|
|
843
|
+
it('builds complex queries with multiple helpers', () => {
|
|
844
|
+
const fragment = SQLFragmentHelpers.and(
|
|
845
|
+
SQLFragmentHelpers.between('intField', 18, 65),
|
|
846
|
+
SQLFragmentHelpers.group(
|
|
847
|
+
SQLFragmentHelpers.or(
|
|
848
|
+
SQLFragmentHelpers.inArray('stringField', ['active', 'premium']),
|
|
849
|
+
sql`role = ${'admin'}`,
|
|
850
|
+
),
|
|
851
|
+
),
|
|
852
|
+
SQLFragmentHelpers.isNotNull('testIndexedField'),
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
expect(fragment.sql).toBe(
|
|
856
|
+
'(?? BETWEEN ? AND ?) AND (((?? IN (?, ?)) OR (role = ?))) AND (?? IS NOT NULL)',
|
|
857
|
+
);
|
|
858
|
+
expect(fragment.getKnexBindings(getColumnForField)).toEqual([
|
|
859
|
+
'number_field',
|
|
860
|
+
18,
|
|
861
|
+
65,
|
|
862
|
+
'string_field',
|
|
863
|
+
'active',
|
|
864
|
+
'premium',
|
|
865
|
+
'admin',
|
|
866
|
+
'test_index',
|
|
867
|
+
]);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
});
|