pogi 2.11.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.
Files changed (82) hide show
  1. package/.vscode/launch.json +35 -0
  2. package/CHANGELOG.md +277 -0
  3. package/LICENSE +21 -0
  4. package/README.md +85 -0
  5. package/docs/API/PgDb.md +218 -0
  6. package/docs/API/PgSchema.md +91 -0
  7. package/docs/API/PgTable.md +365 -0
  8. package/docs/API/QueryOptions.md +77 -0
  9. package/docs/API/condition.md +133 -0
  10. package/docs/connection.md +91 -0
  11. package/docs/css/docs.css +164 -0
  12. package/docs/executingSqlFile.md +44 -0
  13. package/docs/faq.md +15 -0
  14. package/docs/functions.md +19 -0
  15. package/docs/generatingInterfaceForTables.md +35 -0
  16. package/docs/index.md +48 -0
  17. package/docs/logger.md +40 -0
  18. package/docs/mappingDatabaseTypes.md +89 -0
  19. package/docs/notification.md +19 -0
  20. package/docs/pitfalls.md +73 -0
  21. package/docs/streams.md +68 -0
  22. package/docs/transaction.md +65 -0
  23. package/lib/bin/generateInterface.d.ts +1 -0
  24. package/lib/bin/generateInterface.js +53 -0
  25. package/lib/bin/generateInterface.js.map +1 -0
  26. package/lib/connectionOptions.d.ts +25 -0
  27. package/lib/connectionOptions.js +3 -0
  28. package/lib/connectionOptions.js.map +1 -0
  29. package/lib/index.d.ts +6 -0
  30. package/lib/index.js +10 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/pgConverters.d.ts +10 -0
  33. package/lib/pgConverters.js +66 -0
  34. package/lib/pgConverters.js.map +1 -0
  35. package/lib/pgDb.d.ts +86 -0
  36. package/lib/pgDb.js +745 -0
  37. package/lib/pgDb.js.map +1 -0
  38. package/lib/pgDbLogger.d.ts +5 -0
  39. package/lib/pgDbLogger.js +3 -0
  40. package/lib/pgDbLogger.js.map +1 -0
  41. package/lib/pgDbOperators.d.ts +113 -0
  42. package/lib/pgDbOperators.js +44 -0
  43. package/lib/pgDbOperators.js.map +1 -0
  44. package/lib/pgSchema.d.ts +16 -0
  45. package/lib/pgSchema.js +16 -0
  46. package/lib/pgSchema.js.map +1 -0
  47. package/lib/pgTable.d.ts +131 -0
  48. package/lib/pgTable.js +322 -0
  49. package/lib/pgTable.js.map +1 -0
  50. package/lib/pgUtils.d.ts +31 -0
  51. package/lib/pgUtils.js +157 -0
  52. package/lib/pgUtils.js.map +1 -0
  53. package/lib/queryAble.d.ts +76 -0
  54. package/lib/queryAble.js +330 -0
  55. package/lib/queryAble.js.map +1 -0
  56. package/lib/queryWhere.d.ts +8 -0
  57. package/lib/queryWhere.js +249 -0
  58. package/lib/queryWhere.js.map +1 -0
  59. package/mkdocs.yml +25 -0
  60. package/package.json +65 -0
  61. package/spec/resources/init.sql +122 -0
  62. package/spec/resources/throw_exception.sql +5 -0
  63. package/spec/resources/tricky.sql +13 -0
  64. package/spec/run.js +5 -0
  65. package/spec/support/jasmine.json +9 -0
  66. package/src/bin/generateInterface.ts +54 -0
  67. package/src/connectionOptions.ts +42 -0
  68. package/src/index.ts +6 -0
  69. package/src/pgConverters.ts +55 -0
  70. package/src/pgDb.ts +820 -0
  71. package/src/pgDbLogger.ts +13 -0
  72. package/src/pgDbOperators.ts +62 -0
  73. package/src/pgSchema.ts +15 -0
  74. package/src/pgTable.ts +401 -0
  75. package/src/pgUtils.ts +176 -0
  76. package/src/queryAble.ts +393 -0
  77. package/src/queryWhere.ts +326 -0
  78. package/src/test/pgDbOperatorSpec.ts +492 -0
  79. package/src/test/pgDbSpec.ts +1339 -0
  80. package/src/test/pgServiceRestartTest.ts +1500 -0
  81. package/src/tsconfig.json +33 -0
  82. package/utils_sql/lower.sql +4 -0
@@ -0,0 +1,326 @@
1
+ import operationsMap from "./pgDbOperators";
2
+ import {FieldType} from "./pgDb";
3
+
4
+ const _ = require("lodash");
5
+ const util = require("util");
6
+
7
+ class FieldAndOperator {
8
+ field: string;
9
+ quotedField: string;
10
+ operator: string;
11
+ originalOp: string;
12
+ mutator?: Function;
13
+ }
14
+
15
+ /** public */
16
+ function generateWhere(conditions, fieldTypes: { [index: string]: FieldType }, tableName: string, placeholderOffset = 0, skipUndefined): { where: string, params: Array<any> } {
17
+ let result = generate({
18
+ params: [],
19
+ predicates: [],
20
+ offset: placeholderOffset,
21
+ }, conditions, fieldTypes, tableName, skipUndefined);
22
+
23
+ return {
24
+ where: result.predicates.length > 0 ? ' WHERE ' + result.predicates.join(' AND ') : '',
25
+ params: result.params
26
+ };
27
+ }
28
+
29
+ /** private */
30
+ function generate(result, conditions, fieldTypes: { [index: string]: FieldType }, tableName: string, skipUndefined) {
31
+ _.each(conditions, (value, key) => {
32
+ //get the column field and the operator if specified
33
+ let fieldAndOperator = parseKey(key);
34
+
35
+ if (value === undefined) { //null is ok, but undefined is skipped if requested
36
+ if (skipUndefined === true) return;
37
+ throw new Error('Invalid conditions! Field value undefined: "' + fieldAndOperator.field + '". Either delete the field, set it to null or use the options.skipUndefined parameter.');
38
+ }
39
+ else if (fieldAndOperator.field === 'or' || fieldAndOperator.field === 'and') {
40
+ result = handleOrAnd(result, fieldAndOperator, value, fieldTypes, tableName, skipUndefined);
41
+ }
42
+ else if (value === null) {
43
+ result = handleNullValue(result, fieldAndOperator, value);
44
+ }
45
+ else if (Array.isArray(value)) {
46
+ result = handleArrayValue(result, fieldAndOperator, value, fieldTypes);
47
+ }
48
+ else {
49
+ result = handleSingleValue(result, fieldAndOperator, value, fieldTypes, tableName);
50
+ }
51
+ });
52
+
53
+ return result;
54
+ }
55
+
56
+ function handleOrAnd(result, fieldAndOperator, value, fieldTypes: { [index: string]: FieldType }, tableName: string, skipUndefined) {
57
+ if (!Array.isArray(value)) {
58
+ value = [value];
59
+ }
60
+
61
+ let groupResult = _.reduce(value, (acc, v) => {
62
+ // assemble predicates for each subgroup in this 'or' array
63
+ let subResult = generate({
64
+ params: [],
65
+ predicates: [],
66
+ offset: result.params.length + acc.offset // ensure the offset from predicates outside the subgroup is counted
67
+ }, v, fieldTypes, tableName, skipUndefined);
68
+
69
+ // encapsulate and join the individual predicates with AND to create the complete subgroup predicate
70
+ acc.predicates.push(util.format('(%s)', subResult.predicates.join(' AND ')));
71
+ acc.params = acc.params.concat(subResult.params);
72
+ acc.offset += subResult.params.length;
73
+
74
+ return acc;
75
+ }, {
76
+ params: [],
77
+ predicates: [],
78
+ offset: result.offset
79
+ });
80
+
81
+ // join the compiled subgroup predicates with OR, encapsulate, and push the
82
+ // complex predicate ("((x = $1 AND y = $2) OR (z = $3))") onto the result object
83
+ result.params = result.params.concat(groupResult.params);
84
+ if (groupResult.predicates.length) {
85
+ if (fieldAndOperator.field === 'and') {
86
+ result.predicates.push(util.format('(%s)', groupResult.predicates.join(' AND ')));
87
+ } else {
88
+ result.predicates.push(util.format('(%s)', groupResult.predicates.join(' OR ')));
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function handleNullValue(result, fieldAndOperator, value) {
95
+ fieldAndOperator.operator = fieldAndOperator.operator === '=' ? 'IS' : 'IS NOT';
96
+ result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
97
+ return result;
98
+ }
99
+
100
+ function handleArrayValue(result, fieldAndOperator, value, fieldTypes: { [index: string]: FieldType }) {
101
+ if (fieldAndOperator.mutator) {
102
+ value = value.map(v => fieldAndOperator.mutator(v));
103
+ }
104
+ let fieldType = fieldTypes[fieldAndOperator.field];
105
+
106
+ if (fieldType == FieldType.JSON &&
107
+ ['?|', '?&'].indexOf(fieldAndOperator.operator) != -1) {
108
+ result.params.push(value);
109
+ value = util.format("$%s", result.params.length + result.offset);
110
+ result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
111
+ return result;
112
+ }
113
+ else if (fieldType == FieldType.JSON &&
114
+ ['@>', '<@', '&&'].indexOf(fieldAndOperator.operator) != -1) {
115
+ result.params.push(JSON.stringify(value));
116
+ value = util.format("$%s", result.params.length + result.offset);
117
+ result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
118
+ return result;
119
+ }
120
+ else if ((!fieldType || fieldType == FieldType.TIME) &&
121
+ ['=', '<>', 'IN', 'NOT IN'].includes(fieldAndOperator.operator)) {
122
+ if (fieldAndOperator.operator === '=' || fieldAndOperator.operator === 'IN') {
123
+ fieldAndOperator.operator = '= ANY';
124
+ } else {
125
+ fieldAndOperator.operator = '<> ALL';
126
+ }
127
+
128
+ if (value.length === 0) { // avoid empty "[NOT] IN ()"
129
+ throw new Error('Invalid conditions! empty array for field:"' + fieldAndOperator.field + '" and operator:"' + fieldAndOperator.operator + '"');
130
+ //return result;
131
+ }
132
+
133
+ result.params.push(value
134
+ .map(v => (fieldType == FieldType.TIME && !(value instanceof Date)) ? new Date(v) : v)
135
+ );
136
+ value = util.format("$%s", result.params.length + result.offset);
137
+ result.predicates.push(util.format('%s %s (%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
138
+ return result;
139
+ }
140
+ else if (!fieldType && ['LIKE', 'ILIKE', 'SIMILAR TO', '~', '~*'].indexOf(fieldAndOperator.operator) != -1) {
141
+ //defaults to any
142
+ result.params.push(value);
143
+ value = util.format("$%s", result.params.length + result.offset);
144
+ result.predicates.push(util.format('%s %s ANY(%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
145
+ return result;
146
+ }
147
+ else if (!fieldType && ['NOT LIKE', 'NOT ILIKE', 'NOT SIMILAR TO', '!~', '!~*'].indexOf(fieldAndOperator.operator) != -1) {
148
+ //defaults to all
149
+ result.params.push(value);
150
+ value = util.format("$%s", result.params.length + result.offset);
151
+ result.predicates.push(util.format('%s %s ALL(%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
152
+ return result;
153
+ }
154
+ else if (fieldType == FieldType.ARRAY &&
155
+ ['=', '<>', '<', '>', '<=', '>=', '@>', '<@', '&&'].indexOf(fieldAndOperator.operator) != -1) {
156
+ result.params.push(value);
157
+ value = util.format("$%s", result.params.length + result.offset);
158
+ result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
159
+ return result;
160
+ }
161
+
162
+ throw new Error('[325] Not implemented operator: "' + fieldAndOperator.operator + '" for field ' + fieldAndOperator.field + ' with type ' + fieldType);
163
+ }
164
+
165
+
166
+ function handleSingleValue(result, fieldAndOperator: FieldAndOperator, value, fieldTypes: { [index: string]: FieldType }, tableName) {
167
+ if (fieldAndOperator.mutator) {
168
+ value = fieldAndOperator.mutator(value);
169
+ }
170
+ let fieldType = fieldTypes[fieldAndOperator.field];
171
+ if (fieldAndOperator.operator === '@@') {
172
+ /**
173
+ * field can be string -> to_tsquery(string)
174
+ * or object {lang:'english', txt:string} -> to_tsquery(o.lang, o.txt)
175
+ */
176
+ if (typeof value == 'object') {
177
+ if (!(value.lang || value.language) || !(value.query || value.plainquery)) {
178
+ throw new Error('[499] only "lang"/"language" and "query/plainquery" properties are supported!');
179
+ }
180
+ if (fieldType == FieldType.TSVECTOR) {
181
+ //language is already set
182
+ result.params.push(value.lang || value.language);
183
+ result.params.push(value.query || value.plainquery);
184
+ let template = value.query ? "%s %s to_tsquery($%s, $%s)" : "%s %s plainto_tsquery($%s, $%s)";
185
+ result.predicates.push(util.format(template,
186
+ fieldAndOperator.quotedField,
187
+ fieldAndOperator.operator,
188
+ result.params.length - 1 + result.offset, //lang
189
+ result.params.length + result.offset //query
190
+ ));
191
+ } else {
192
+ result.params.push(value.lang || value.language);
193
+ result.params.push(value.lang || value.language);
194
+ result.params.push(value.query || value.plainquery);
195
+ let template = value.query ? "to_tsvector($%s, %s) %s to_tsquery($%s, $%s)" : "to_tsvector($%s, %s) %s plainto_tsquery($%s, $%s)";
196
+ result.predicates.push(util.format(template,
197
+ result.params.length - 2 + result.offset, //lang
198
+ fieldAndOperator.quotedField,
199
+ fieldAndOperator.operator,
200
+ result.params.length - 1 + result.offset, //lang
201
+ result.params.length + result.offset //query
202
+ ));
203
+ }
204
+ } else {
205
+ result.params.push(value);
206
+ let template = fieldType == FieldType.TSVECTOR ? "%s %s plainto_tsquery($%s)" : "to_tsvector(%s) %s plainto_tsquery($%s)";
207
+ result.predicates.push(util.format(template, fieldAndOperator.quotedField, fieldAndOperator.operator, result.params.length + result.offset));
208
+ }
209
+ }
210
+ else if (fieldType == FieldType.ARRAY) {
211
+ if (['=', '<>'].indexOf(fieldAndOperator.operator) != -1) {
212
+ if (fieldAndOperator.originalOp == '=*') {
213
+ result.params.push([value]);
214
+ value = util.format("$%s", result.params.length + result.offset);
215
+ result.predicates.push(util.format('%s && %s', fieldAndOperator.quotedField, value));
216
+ } else {
217
+ result.params.push(value);
218
+ value = util.format("$%s", result.params.length + result.offset);
219
+ result.predicates.push(util.format('%s %s ANY(%s)', value, fieldAndOperator.operator, fieldAndOperator.quotedField));
220
+ }
221
+ }
222
+ else if (['LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE', 'SIMILAR TO', 'NOT SIMILAR TO'].indexOf(fieldAndOperator.operator) != -1) {
223
+ result.params.push(value);
224
+ value = util.format("$%s", result.params.length + result.offset);
225
+
226
+ let q = 'EXISTS (SELECT * FROM (SELECT UNNEST(' + tableName + '.%s) _el) _arr WHERE _arr._el %s %s)';
227
+ result.predicates.push(util.format(q, fieldAndOperator.quotedField, fieldAndOperator.operator, value));
228
+ } else {
229
+ throw new Error('[326] Not implemented operator: "' + fieldAndOperator.operator + '" for type ' + fieldType);
230
+ }
231
+ } else {
232
+ result.params.push((fieldType == FieldType.TIME && !(value instanceof Date)) ? new Date(value) : value);
233
+ value = util.format("$%s", result.params.length + result.offset);
234
+ result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
235
+ }
236
+ return result;
237
+ }
238
+
239
+
240
+ function strip(arr) {
241
+ return arr.map((s) => s.trim()).filter(v => v != '');
242
+ }
243
+
244
+ function getOp(str) {
245
+ for (let i = 0; i < str.length; i++) {
246
+ if (operationsMap[str.substr(i)]) {
247
+ return str.substr(i);
248
+ }
249
+ }
250
+ return '';
251
+ }
252
+
253
+ /**
254
+ * Parse out a criterion key into something more intelligible. Supports quoted
255
+ * field names and whitespace between components. If a function is applied on the field
256
+ * it takes the first quoted string to assume to be the column. (e.g. max("score") -> field: "score"
257
+ * in order to be able to recognize the field type)
258
+ *
259
+ * 'createdTs >' => {
260
+ * field: 'createdTs',
261
+ * quotedField: '"createdTs"',
262
+ * operator:'>',
263
+ * mutator:null
264
+ * }
265
+ *
266
+ * @param {String} key Key in a format resembling "field [JSON operation+path] operation"
267
+ * @return {Object} [description]
268
+ */
269
+ function parseKey(key): FieldAndOperator {
270
+ key = key.trim();
271
+
272
+ let userOp = getOp(key);
273
+ if (userOp) {
274
+ key = key.substr(0, key.length - userOp.length)
275
+ }
276
+ let operation = operationsMap[userOp] || {};
277
+ let jsonRegexp = /(->[^>]|->>|#>[^>]|#>>)/;
278
+
279
+ let field;
280
+ let quotedField;
281
+
282
+ let quotedByUser = key.indexOf('"') > -1; //key[0]=='"'; -> lets make it possible to write transformed columns, e.g. LOWER("field")
283
+ if (quotedByUser) {
284
+ quotedField = key;
285
+ //field is used for find out the type of the field, so lets restore it if possible, grab the first quoted string
286
+ field = /[^"]*"([^"]*)".*/.exec(key)[1];
287
+ if (!quotedField || !field) {
288
+ console.error("Parsing error!");
289
+ }
290
+ } else {
291
+ let parts = strip(key.split(jsonRegexp));
292
+
293
+ field = parts.shift();
294
+ quotedField = util.format('"%s"', field);
295
+
296
+ if (parts.length > 1) {
297
+ let jsonOp = parts.shift();
298
+ let jsonKey = parts.shift();
299
+
300
+ // treat numeric json keys as array indices, otherwise quote it
301
+ if (isNaN(jsonKey) && jsonKey.indexOf("'") == -1) {
302
+ jsonKey = util.format("'%s'", jsonKey);
303
+ }
304
+
305
+ quotedField = util.format('%s%s%s', quotedField, jsonOp, jsonKey);
306
+ }
307
+ }
308
+
309
+
310
+ if (operation.fieldMutator) {
311
+ quotedField = operation.fieldMutator(field, quotedField);
312
+ }
313
+
314
+
315
+ return {
316
+ field: field,
317
+ quotedField: quotedField,
318
+ operator: (operation.operator || '=').toUpperCase(),
319
+ mutator: operation.mutator,
320
+ originalOp: userOp
321
+ };
322
+ }
323
+
324
+
325
+ export default generateWhere;
326
+
@@ -0,0 +1,492 @@
1
+ /// <reference types="jasmine"/>
2
+ import {PgDb} from "../pgDb";
3
+ import {PgTable} from "../pgTable";
4
+
5
+ const util = require('util');
6
+
7
+ function w(func) {
8
+ return function (done) {
9
+ return (async () => {
10
+ try {
11
+ await func();
12
+ } catch (e) {
13
+ console.log('------------------------------');
14
+ console.error(e.message, e.stack);
15
+ console.log('------------------------------');
16
+ expect('Exception: ' + e.message).toBeFalsy();
17
+ }
18
+ return done();
19
+ })();
20
+ }
21
+ }
22
+
23
+ describe("pgdb", () => {
24
+ let pgdb: PgDb;
25
+ let schema = 'pgdb_test';
26
+ let tableUsers: PgTable<any>;
27
+ let viewUsers: PgTable<any>;
28
+
29
+ beforeAll(w(async () => {
30
+ jasmine.DEFAULT_TIMEOUT_INTERVAL = 800000;
31
+
32
+ /**
33
+ * Using environment variables, e.g.
34
+ * PGUSER (defaults USER env var, so optional)
35
+ * PGDATABASE (defaults USER env var, so optional)
36
+ * PGPASSWORD
37
+ * PGPORT
38
+ * etc...
39
+ */
40
+ try {
41
+ pgdb = await PgDb.connect({connectionString: "postgres://"});
42
+ } catch (e) {
43
+ console.error("connection failed! Are you specified PGUSER/PGDATABASE/PGPASSWORD correctly?");
44
+ console.error(e);
45
+ process.exit(1);
46
+ }
47
+ //await pgdb.run('DROP SCHEMA IF EXISTS "' + schema + '" CASCADE ');
48
+ await pgdb.run('CREATE SCHEMA IF NOT EXISTS "' + schema + '"');
49
+ await pgdb.execute('spec/resources/init.sql', (cmd) => cmd.replace(/__SCHEMA__/g, '"' + schema + '"'));
50
+ await pgdb.reload();
51
+
52
+ pgdb.setLogger(console);
53
+ tableUsers = pgdb.schemas[schema]['users'];
54
+ viewUsers = pgdb.schemas[schema]['users_view'];
55
+
56
+ return Promise.resolve();
57
+ }));
58
+
59
+ beforeEach(w(async () => {
60
+ await tableUsers.run('DELETE FROM ' + tableUsers);
61
+ }));
62
+
63
+ it("Testing Array operators", w(async () => {
64
+ await tableUsers.insert({name: 'S', favourites: ['sport']});
65
+ await tableUsers.insert({name: 'SF', favourites: ['sport', 'food']});
66
+ await tableUsers.insert({name: 'TF', favourites: ['tech', 'food']});
67
+
68
+ let res;
69
+
70
+ res = await tableUsers.find({'favourites': 'sport'}, {fields: ['name']}); //=> 'sport' = ANY("favourites")
71
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
72
+
73
+ res = await tableUsers.find({'favourites': ['sport', 'food']}, {fields: ['name']}); //=> favourites = '{sport,food}'
74
+ expect(res.map(r => r.name)).toEqual(['SF']);
75
+
76
+ res = await tableUsers.find({'favourites @>': ['sport']}); //contains
77
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
78
+
79
+ res = await tableUsers.find({'favourites <@': ['sport', 'food', 'tech']});//contained by
80
+ expect(res.map(r => r.name)).toEqual(['S', 'SF', 'TF']);
81
+
82
+ res = await tableUsers.find({'favourites &&': ['sport', 'music']});//overlap
83
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
84
+
85
+ res = await tableUsers.find({'favourites !=': ['sport']});//
86
+ expect(res.map(r => r.name)).toEqual(['SF', 'TF']);
87
+ }));
88
+
89
+ it("Testing Array operators on view", w(async () => {
90
+ await viewUsers.insert({name: 'S', favourites: ['sport']});
91
+ await viewUsers.insert({name: 'SF', favourites: ['sport', 'food']});
92
+ await viewUsers.insert({name: 'TF', favourites: ['tech', 'food']});
93
+
94
+ let res;
95
+
96
+ res = await viewUsers.find({'favourites': 'sport'}, {fields: ['name']}); //=> 'sport' = ANY("favourites")
97
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
98
+
99
+ res = await viewUsers.find({'favourites': ['sport', 'food']}, {fields: ['name']}); //=> favourites = '{sport,food}'
100
+ expect(res.map(r => r.name)).toEqual(['SF']);
101
+
102
+ res = await viewUsers.find({'favourites @>': ['sport']}); //contains
103
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
104
+
105
+ res = await viewUsers.find({'favourites <@': ['sport', 'food', 'tech']});//contained by
106
+ expect(res.map(r => r.name)).toEqual(['S', 'SF', 'TF']);
107
+
108
+ res = await viewUsers.find({'favourites &&': ['sport', 'music']});//overlap
109
+ expect(res.map(r => r.name)).toEqual(['S', 'SF']);
110
+
111
+ res = await viewUsers.find({'favourites !=': ['sport']});//
112
+ expect(res.map(r => r.name)).toEqual(['SF', 'TF']);
113
+ }));
114
+
115
+ it("Searching in elements in jsonList", w(async () => {
116
+ await tableUsers.insert({name: 'Medium and high risk', jsonList: [{risk: 'H'}, {risk: 'M'}]});
117
+
118
+ let query1 = {
119
+ or: [{'"jsonList" @>': [{"risk": "H"}]}, {'"jsonList" @>': [{"risk": "L"}]}]
120
+ };
121
+ let query2 = {
122
+ or: [{'jsonList @>': [{"risk": "H"}]}, {'jsonList @>': [{"risk": "L"}]}]
123
+ };
124
+ let query3 = {
125
+ or: [{'jsonList @>': '[{"risk": "H"}]'}, {'jsonList @>': '[{"risk": "L"}]'}]
126
+ };
127
+
128
+ let res;
129
+ res = await tableUsers.find(query1, {fields: ['name']});
130
+ expect(res.map(r => r.name)).toEqual(['Medium and high risk']);
131
+
132
+ res = await tableUsers.find(query2, {fields: ['name']});
133
+ expect(res.map(r => r.name)).toEqual(['Medium and high risk']);
134
+
135
+ res = await tableUsers.find(query3, {fields: ['name']});
136
+ expect(res.map(r => r.name)).toEqual(['Medium and high risk']);
137
+ }));
138
+
139
+ it("Free text search", w(async () => {
140
+ await tableUsers.insert({name: 'Medium and high risk', jsonList: [{name: "The return of the Jedi"}]});
141
+
142
+ let res;
143
+ for (let searchCol of ['"name"||"jsonList" @@', 'tsv @@']) {
144
+ res = await tableUsers.find({[searchCol]: 'risk & return'}, {fields: ['name']});
145
+ expect(res.map(r => r.name)).toEqual(['Medium and high risk']);
146
+
147
+ res = await tableUsers.find({[searchCol]: 'risk & future'}, {fields: ['name']});
148
+ expect(res.length).toEqual(0);
149
+
150
+ res = await tableUsers.find({
151
+ [searchCol]: {
152
+ lang: 'english',
153
+ query: 'risk & return'
154
+ }
155
+ }, {fields: ['name']});
156
+ expect(res.map(r => r.name)).toEqual(['Medium and high risk']);
157
+ }
158
+ }));
159
+
160
+ it("Testing Jsonb list selector operators", w(async () => {
161
+ //@formatter:off
162
+ await tableUsers.insert({ name: 'Somebody', jsonList: ['sport', 'season'] });
163
+ await tableUsers.insert({ name: 'Anybody', jsonList: ['art', 'age'] });
164
+ await tableUsers.insert({ name: 'Nobody', jsonList: ['neverending', 'nearby'] });
165
+ await tableUsers.insert({ name: 'Noone', jsonList: null, });
166
+ await tableUsers.insert({ name: 'Anonymous', jsonList: [] });
167
+ await tableUsers.insert({ name: 'Obi1', jsonObject: {a:{b:3}}});
168
+ await tableUsers.insert({ name: 'Obi2', jsonObject: {a:{b:[3,4,5,6]}, d:'c', g:'e' }});
169
+ //@formatter:on
170
+
171
+ let res;
172
+
173
+ res = await tableUsers.find({'"jsonObject" ?|': ['d', 'f']}, {fields: ['name']});
174
+ expect(res.map(r => r.name)).toEqual(['Obi2']);
175
+
176
+ res = await tableUsers.find({'jsonList @>': ['sport']}, {fields: ['name']}); //=>
177
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
178
+
179
+ res = await tableUsers.find({'jsonList <@': ['sport', 'tech', 'season']}, {fields: ['name']}); //=>
180
+ expect(res.map(r => r.name)).toEqual(['Somebody', 'Anonymous']);
181
+
182
+ res = await tableUsers.find({'jsonList ?': 0}, {fields: ['name']}); //=> doesnt work
183
+ expect(res.map(r => r.name)).toEqual([]);
184
+
185
+ res = await tableUsers.find({'jsonList ?': '0'}, {fields: ['name']}); //=> doesnt work
186
+ expect(res.map(r => r.name)).toEqual([]);
187
+
188
+ res = await tableUsers.find({'jsonObject -> a': {b: 3}}, {fields: ['name']});
189
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
190
+
191
+ res = await tableUsers.find({"jsonObject -> 'a'": {b: 3}}, {fields: ['name']});
192
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
193
+
194
+ res = await tableUsers.find({'jsonList ->> 0': 'art'}, {fields: ['name']});
195
+ expect(res.map(r => r.name)).toEqual(['Anybody']);
196
+
197
+ res = await tableUsers.find({'jsonObject ->> a': '{"b": 3}'}, {fields: ['name']}); //->> return as a text
198
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
199
+
200
+ res = await tableUsers.find({"jsonObject ->> 'a'": '{"b": 3}'}, {fields: ['name']}); //->> return as a text
201
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
202
+
203
+ res = await tableUsers.find({"jsonList ->> '0'": 'art'}, {fields: ['name']}); //=> doesnt work
204
+ expect(res.map(r => r.name)).toEqual([]);
205
+
206
+ res = await tableUsers.find({"jsonObject #> {a}": {b: 3}}, {fields: ['name']});
207
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
208
+
209
+ res = await tableUsers.find({"jsonObject #>> {a}": '{"b": 3}'}, {fields: ['name']});
210
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
211
+
212
+ res = await tableUsers.find({"jsonObject #>> {a,b}": 3}, {fields: ['name']});
213
+ expect(res.map(r => r.name)).toEqual(['Obi1']);
214
+
215
+ res = await tableUsers.find({"jsonObject #>> {a,b,1}": 4}, {fields: ['name']});
216
+ expect(res.map(r => r.name)).toEqual(['Obi2']);
217
+
218
+ res = await tableUsers.find({"jsonObject #>> {a,b,1}": '4'}, {fields: ['name']});
219
+ expect(res.map(r => r.name)).toEqual(['Obi2']);
220
+ }));
221
+
222
+ it("Testing Jsonb list update", w(async () => {
223
+ await tableUsers.insert({name: 'Somebody', jsonList: ['sport', 'season']});
224
+ await tableUsers.update({name: 'Somebody'}, {jsonList: [{name: 'sport'}, {name: 'season'}]});
225
+
226
+ let res = await tableUsers.findAll();
227
+ expect(res.map(r => r.jsonList.map(e => e.name))[0]).toEqual(['sport', 'season']);
228
+ }));
229
+
230
+ it("Testing Jsonb list operators", w(async () => {
231
+ //@formatter:off
232
+ await tableUsers.insert({ name: 'Somebody', jsonObject: {realName: 'somebody', webpage: 's.com'} });
233
+ await tableUsers.insert({ name: 'Anybody', jsonObject: {realName: 'anybody', webpage: 'a.com'} });
234
+ await tableUsers.insert({ name: 'Nobody', jsonObject: {realName: 'nobody', email: 'nobody@nowhere.com'}});
235
+ await tableUsers.insert({ name: 'Noone', jsonObject: null});
236
+ await tableUsers.insert({ name: 'Anonymous', jsonObject: {}});
237
+ //@formatter:on
238
+ let res;
239
+
240
+ res = await tableUsers.find({'jsonObject ?': 'email'}, {fields: ['name']}); //=> has the key ..
241
+ expect(res.map(r => r.name)).toEqual(['Nobody']);
242
+
243
+ res = await tableUsers.find({'jsonObject ?&': ['email', 'realName']}, {fields: ['name']}); //=> has all key ..
244
+ expect(res.map(r => r.name)).toEqual(['Nobody']);
245
+
246
+ res = await tableUsers.find({'jsonObject ?|': ['email', 'webpage']}, {fields: ['name']}); //=> has any of the key..
247
+ expect(res.map(r => r.name)).toEqual(['Somebody', 'Anybody', 'Nobody']);
248
+
249
+ res = await tableUsers.find({'jsonObject @>': {realName: 'somebody'}}, {fields: ['name']}); //=> contains substructure
250
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
251
+
252
+ res = await tableUsers.find({'jsonObject ->> realName': 'somebody'}, {fields: ['name']}); //=> has the key + equals to
253
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
254
+
255
+ res = await tableUsers.find({'jsonObject ->> \'realName\'': 'somebody'}, {fields: ['name']}); //=> has the key + equals to
256
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
257
+
258
+ res = await tableUsers.find({'"jsonObject" ->> \'realName\'': 'somebody'}, {fields: ['name']}); //=> has the key + equals to
259
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
260
+
261
+ res = await tableUsers.find({'jsonObject ->> realName': ['somebody', 'anybody']}, {fields: ['name']}); //=> has the key + in array
262
+ expect(res.map(r => r.name)).toEqual(['Somebody', 'Anybody']);
263
+
264
+ res = await tableUsers.find({'jsonObject ->> realName like': '%body'}, {fields: ['name']}); //=> has the key + like
265
+ expect(res.map(r => r.name)).toEqual(['Somebody', 'Anybody', 'Nobody']);
266
+
267
+ res = await tableUsers.find({'jsonObject ->> realName ~~': '%body'}, {fields: ['name']}); //=> has the key + like
268
+ expect(res.map(r => r.name)).toEqual(['Somebody', 'Anybody', 'Nobody']);
269
+
270
+ }));
271
+
272
+ it("Test deep jsonb @>", w(async () => {
273
+ //@formatter:off
274
+ await tableUsers.insert({ name: 'Somebody', jsonObject: {service: {indexing: {by: [1, 2, 3]}, database: true}, paid: true}});
275
+ await tableUsers.insert({ name: 'Anybody', jsonList: [{realName: 'anyone'}, {realName: 'anybody'}]});
276
+ //@formatter:on
277
+
278
+ let res;
279
+ res = await tableUsers.find({'jsonObject @>': {service: {indexing: {by: [1]}}, paid: true}});
280
+ expect(res.map(r => r.name)).toEqual(['Somebody']);
281
+
282
+ res = await tableUsers.find({'jsonList @>': [{realName: 'anybody'}]});
283
+ expect(res.map(r => r.name)).toEqual(['Anybody']);
284
+ }))
285
+
286
+
287
+ it("Testing is (not) null", w(async () => {
288
+ await tableUsers.insert({name: 'Noone', jsonObject: null});
289
+ await tableUsers.insert({name: 'Anonymous', jsonObject: {}});
290
+
291
+ let count;
292
+
293
+ count = await tableUsers.count({'jsonObject !': null});
294
+ expect(count).toEqual(1);
295
+
296
+ count = await tableUsers.count({'jsonObject': null});
297
+ expect(count).toEqual(1);
298
+
299
+ count = await tableUsers.count({});
300
+ expect(count).toEqual(2);
301
+ }));
302
+
303
+ it("Test regexp ~,~*,!~,!~*", w(async () => {
304
+ await tableUsers.insert({name: "All' o Phoibe"});
305
+ await tableUsers.insert({name: "I've got that tune"});
306
+
307
+ let res = await tableUsers.find({'name ~': "\\so\\s"});
308
+ expect(res.map(r => r.name)).toEqual(["All' o Phoibe"]);
309
+
310
+ res = await tableUsers.find({'name ~*': "\\sO\\s"});
311
+ expect(res.map(r => r.name)).toEqual(["All' o Phoibe"]);
312
+
313
+ res = await tableUsers.find({'name !~': "\\so\\s"});
314
+ expect(res.map(r => r.name)).toEqual(["I've got that tune"]);
315
+
316
+ res = await tableUsers.find({'name !~*': "\\sO\\s"});
317
+ expect(res.map(r => r.name)).toEqual(["I've got that tune"]);
318
+
319
+ }));
320
+
321
+ it("Test regexp " +
322
+ "~/~* ANY" +
323
+ "!~/!~* ALL", w(async () => {
324
+ await tableUsers.insert({name: "All' o Phoibe"});
325
+ await tableUsers.insert({name: "I've got that tune"});
326
+
327
+ let res = await tableUsers.find({'name ~': ["\\so\\s", '\\d+']});
328
+ expect(res.map(r => r.name)).toEqual(["All' o Phoibe"]);
329
+
330
+ res = await tableUsers.find({'name ~*': ["\\sO\\s", '\\d+']});
331
+ expect(res.map(r => r.name)).toEqual(["All' o Phoibe"]);
332
+
333
+ res = await tableUsers.find({'name !~': ["\\so\\s", '\\d+']});
334
+ expect(res.map(r => r.name)).toEqual(["I've got that tune"]);
335
+
336
+ res = await tableUsers.find({'name !~*': ["\\sO\\s", '\\d+']});
337
+ expect(res.map(r => r.name)).toEqual(["I've got that tune"]);
338
+
339
+ }));
340
+
341
+
342
+ it("Test like ~~, like, ~~*, ilike, !~~, not like, !~~*, not ilike", w(async () => {
343
+ await tableUsers.insert({name: 'Iced lemonade'});
344
+ await tableUsers.insert({name: 'Cucumber pear juice'});
345
+ let res;
346
+
347
+ res = await tableUsers.find({'name ~~': '%lemon%'});
348
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
349
+
350
+ res = await tableUsers.find({'name like': '%lemon%'});
351
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
352
+
353
+ res = await tableUsers.find({'name ~~*': '%LEMON%'});
354
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
355
+
356
+ res = await tableUsers.find({'name ilike': '%LEMON%'});
357
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
358
+
359
+ res = await tableUsers.find({'name !~~': '%lemon%'});
360
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
361
+
362
+ res = await tableUsers.find({'name not like': '%lemon%'});
363
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
364
+
365
+ res = await tableUsers.find({'name !~~*': '%LEMON%'});
366
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
367
+
368
+ res = await tableUsers.find({'name not ilike': '%LEMON%'});
369
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
370
+
371
+ }));
372
+
373
+ it("Test " +
374
+ "like/~~/ilike/~~* ANY(), " +
375
+ "not like/!~~/not ilike/!~~* ALL()", w(async () => {
376
+
377
+ await tableUsers.insert({name: 'Iced lemonade'});
378
+ await tableUsers.insert({name: 'Cucumber pear juice'});
379
+ let res;
380
+
381
+ res = await tableUsers.find({'name ~~': ['BB', '%lemon%']});
382
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
383
+
384
+ res = await tableUsers.find({'name like': ['BB', '%lemon%']});
385
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
386
+
387
+ res = await tableUsers.find({'name ~~*': ['bb', '%LEMON%']});
388
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
389
+
390
+ res = await tableUsers.find({'name ilike': ['bb', '%LEMON%']});
391
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
392
+
393
+ res = await tableUsers.find({'name !~~': ['BB', '%lemon%']});
394
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
395
+
396
+ res = await tableUsers.find({'name not like': ['BB', '%lemon%']});
397
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
398
+
399
+ res = await tableUsers.find({'name !~~*': ['bb', '%LEMON%']});
400
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
401
+
402
+ res = await tableUsers.find({'name not ilike': ['bb', '%LEMON%']});
403
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
404
+ }));
405
+
406
+ it("Special added operators =*, icontains", w(async () => {
407
+ await tableUsers.insert({name: 'Iced lemonade'});
408
+ await tableUsers.insert({name: 'Cucumber pear juice', textList: ['good', 'better', 'best']});
409
+ let res;
410
+
411
+ res = await tableUsers.find({'name =*': 'ICED LEMONADE'});
412
+ expect(res.map(r => r.name)).toEqual(['Iced lemonade']);
413
+
414
+ res = await tableUsers.find({'textList icontains': 'GOOD'});
415
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
416
+
417
+ res = await tableUsers.find({'textList =*': 'GOOD'});
418
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
419
+
420
+ res = await tableUsers.find({'textList &&*': ['GOOD', 'Bad']});
421
+ expect(res.map(r => r.name)).toEqual(['Cucumber pear juice']);
422
+ }));
423
+
424
+
425
+ });
426
+
427
+ /*
428
+
429
+ console.log('--- TEST 17 jsonb [{}] update ---------');
430
+ res = await pdb.schemas['dev']['fsZones'].updateAndGetOne({id:14}, {zoneAdmins:[], buildings:[]});
431
+ if (!Array.isArray(res.buildings) || !Array.isArray(res.zoneAdmins)) {
432
+ throw Error('both should be an array!');
433
+ }
434
+ console.log(util.inspect(res, false, null));
435
+
436
+ console.log('--- TEST 18 array<int> ---------');
437
+ dbTable = pdb.schemas['dev']['fsZones'];
438
+ res = await dbTable.findAll();
439
+ console.log(util.inspect(res, false, null));
440
+
441
+
442
+ console.log('--- TEST 20 parsing array fields ----------');
443
+ res = await pdb.schemas['dev']['chemicals'].updateAndGetOne({id:167}, {extMedia:["test",'a b', 'c,d', '"e,f"','', null, null]});
444
+ console.log(res);
445
+
446
+
447
+ await pdb.close();
448
+ console.log('end');
449
+
450
+ })().catch(e => console.error(e.message, e.stack));
451
+ */
452
+
453
+ /**
454
+ * {,ads,"asdf""fd,",",3," "}
455
+ *
456
+ s = ',ads,"asdf""fd,",",3," "';
457
+ e =
458
+ e.exec(s)
459
+ */
460
+ function parseComplexTypeArray(str) {
461
+ let list = JSON.parse('[' + str.substring(1, str.length - 1) + ']');
462
+
463
+ let result = [];
464
+ for (let elementStr of list) {
465
+ result.push(parseComplexType(elementStr));
466
+ }
467
+ return result;
468
+ }
469
+
470
+ function parseComplexType(str) {
471
+ //cut of '(', ')'
472
+ str = str.substring(1, str.length - 1);
473
+ let e = /"((?:[^"]|"")*)"(?:,|$)|([^,]*)(?:,|$)/g;
474
+ let valList = [];
475
+ let parsingResult;
476
+ let valStr;
477
+ let hasNextValue;
478
+ /**
479
+ * parsingResult.index<str.length check for finish is not reliable
480
+ * as if the last value is null it goes undetected, e.g. (,,)
481
+ */
482
+ do {
483
+ parsingResult = e.exec(str);
484
+ valStr = (parsingResult[0] == '' || parsingResult[0] == ',' || parsingResult[2] == 'null') ? null : parsingResult[1] || parsingResult[2];
485
+ if (parsingResult[0] == '"",' || parsingResult[0] == '""') {
486
+ valStr = '';
487
+ }
488
+ valList.push(valStr ? valStr.replace(/""/g, '"') : valStr);
489
+ hasNextValue = parsingResult[0].substring(parsingResult[0].length - 1, parsingResult[0].length) == ',';
490
+ } while (hasNextValue);
491
+ return valList;
492
+ }