prostgles-server 2.0.270 → 2.0.273

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 (81) hide show
  1. package/dist/AuthHandler.d.ts +11 -3
  2. package/dist/AuthHandler.d.ts.map +1 -1
  3. package/dist/AuthHandler.js +38 -20
  4. package/dist/AuthHandler.js.map +1 -1
  5. package/dist/DBSchemaBuilder.d.ts.map +1 -1
  6. package/dist/DBSchemaBuilder.js +4 -0
  7. package/dist/DBSchemaBuilder.js.map +1 -1
  8. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts +149 -0
  9. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -0
  10. package/{lib → dist/DboBuilder/QueryBuilder}/QueryBuilder.js +4 -223
  11. package/dist/DboBuilder/QueryBuilder/QueryBuilder.js.map +1 -0
  12. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.d.ts +5 -0
  13. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.d.ts.map +1 -0
  14. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.js +225 -0
  15. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.js.map +1 -0
  16. package/dist/DboBuilder/delete.d.ts.map +1 -1
  17. package/dist/DboBuilder/delete.js +6 -1
  18. package/dist/DboBuilder/delete.js.map +1 -1
  19. package/dist/DboBuilder.d.ts +1 -1
  20. package/dist/DboBuilder.d.ts.map +1 -1
  21. package/dist/DboBuilder.js +3 -3
  22. package/dist/DboBuilder.js.map +1 -1
  23. package/dist/FileManager.d.ts +4 -1
  24. package/dist/FileManager.d.ts.map +1 -1
  25. package/dist/FileManager.js +22 -3
  26. package/dist/FileManager.js.map +1 -1
  27. package/dist/Filtering.d.ts +1 -1
  28. package/dist/Filtering.d.ts.map +1 -1
  29. package/dist/Prostgles.d.ts +11 -1
  30. package/dist/Prostgles.d.ts.map +1 -1
  31. package/dist/Prostgles.js +2 -0
  32. package/dist/Prostgles.js.map +1 -1
  33. package/dist/TableConfig.d.ts +4 -1
  34. package/dist/TableConfig.d.ts.map +1 -1
  35. package/dist/TableConfig.js +8 -6
  36. package/dist/TableConfig.js.map +1 -1
  37. package/dist/validation.js +1 -2
  38. package/dist/validation.js.map +1 -1
  39. package/lib/AuthHandler.d.ts +11 -3
  40. package/lib/AuthHandler.d.ts.map +1 -1
  41. package/lib/AuthHandler.js +38 -20
  42. package/lib/AuthHandler.ts +47 -21
  43. package/lib/DBSchemaBuilder.d.ts.map +1 -1
  44. package/lib/DBSchemaBuilder.js +4 -0
  45. package/lib/DBSchemaBuilder.ts +2 -0
  46. package/lib/{QueryBuilder.d.ts → DboBuilder/QueryBuilder/QueryBuilder.d.ts} +2 -3
  47. package/lib/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -0
  48. package/lib/DboBuilder/QueryBuilder/QueryBuilder.js +1160 -0
  49. package/lib/{QueryBuilder.ts → DboBuilder/QueryBuilder/QueryBuilder.ts} +3 -282
  50. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.d.ts +5 -0
  51. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.d.ts.map +1 -0
  52. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.js +224 -0
  53. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.ts +284 -0
  54. package/lib/DboBuilder/delete.d.ts.map +1 -1
  55. package/lib/DboBuilder/delete.js +6 -1
  56. package/lib/DboBuilder/delete.ts +6 -1
  57. package/lib/DboBuilder.d.ts +1 -1
  58. package/lib/DboBuilder.d.ts.map +1 -1
  59. package/lib/DboBuilder.js +3 -3
  60. package/lib/DboBuilder.ts +3 -3
  61. package/lib/FileManager.d.ts +4 -1
  62. package/lib/FileManager.d.ts.map +1 -1
  63. package/lib/FileManager.js +22 -3
  64. package/lib/FileManager.ts +26 -4
  65. package/lib/Filtering.d.ts +1 -1
  66. package/lib/Filtering.d.ts.map +1 -1
  67. package/lib/Filtering.ts +1 -1
  68. package/lib/Prostgles.d.ts +11 -1
  69. package/lib/Prostgles.d.ts.map +1 -1
  70. package/lib/Prostgles.js +2 -0
  71. package/lib/Prostgles.ts +14 -3
  72. package/lib/TableConfig.d.ts +4 -1
  73. package/lib/TableConfig.d.ts.map +1 -1
  74. package/lib/TableConfig.js +8 -6
  75. package/lib/TableConfig.ts +11 -6
  76. package/lib/validation.js +1 -2
  77. package/lib/validation.ts +2 -2
  78. package/package.json +1 -1
  79. package/tests/client/PID.txt +1 -1
  80. package/tests/server/package-lock.json +1 -1
  81. package/lib/QueryBuilder.d.ts.map +0 -1
@@ -0,0 +1,1160 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Stefan L. All rights reserved.
4
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.getNewQuery = exports.SelectItemBuilder = exports.COMPUTED_FIELDS = exports.FUNCTIONS = exports.parseFunction = exports.parseFunctionObject = exports.asNameAlias = void 0;
8
+ const DboBuilder_1 = require("../../DboBuilder");
9
+ const prostgles_types_1 = require("prostgles-types");
10
+ const utils_1 = require("../../utils");
11
+ const asNameAlias = (field, tableAlias) => {
12
+ let result = (0, prostgles_types_1.asName)(field);
13
+ if (tableAlias)
14
+ return (0, prostgles_types_1.asName)(tableAlias) + "." + result;
15
+ return result;
16
+ };
17
+ exports.asNameAlias = asNameAlias;
18
+ const parseFunctionObject = (funcData) => {
19
+ const makeErr = (msg) => `Function not specified correctly. Expecting { $funcName: ["columnName",...] } object but got: ${JSON.stringify(funcData)} \n ${msg}`;
20
+ if (!(0, prostgles_types_1.isObject)(funcData))
21
+ throw makeErr("");
22
+ const keys = Object.keys(funcData);
23
+ if (keys.length !== 1)
24
+ throw makeErr("");
25
+ const funcName = keys[0];
26
+ const args = funcData[funcName];
27
+ if (!args || !Array.isArray(args)) {
28
+ throw makeErr("Arguments missing or invalid");
29
+ }
30
+ return { funcName, args };
31
+ };
32
+ exports.parseFunctionObject = parseFunctionObject;
33
+ const parseFunction = (funcData) => {
34
+ const { func, args, functions, allowedFields } = funcData;
35
+ /* Function is computed column. No checks needed */
36
+ if (typeof func !== "string") {
37
+ const computedCol = exports.COMPUTED_FIELDS.find(c => c.name === func.name);
38
+ if (!computedCol)
39
+ throw `Unexpected function: computed column spec not found for ${JSON.stringify(func.name)}`;
40
+ return func;
41
+ }
42
+ const funcName = func;
43
+ const makeErr = (msg) => {
44
+ return `Issue with function ${JSON.stringify({ [funcName]: args })}: \n${msg}`;
45
+ };
46
+ /* Find function */
47
+ const funcDef = functions.find(f => f.name === funcName);
48
+ if (!funcDef) {
49
+ const sf = functions.filter(f => f.name.toLowerCase().slice(1).startsWith(funcName.toLowerCase())).sort((a, b) => (a.name.length - b.name.length));
50
+ const hint = (sf.length ? `. \n Maybe you meant: \n | ${sf.map(s => s.name + " " + (s.description || "")).join(" \n | ")} ?` : "");
51
+ throw "\n Function " + funcName + " does not exist or is not allowed " + hint;
52
+ }
53
+ /* Validate fields */
54
+ const fields = funcDef.getFields(args);
55
+ if (fields !== "*") {
56
+ fields.forEach(fieldKey => {
57
+ if (typeof fieldKey !== "string" || !allowedFields.includes(fieldKey)) {
58
+ throw makeErr(`getFields() => field name ${JSON.stringify(fieldKey)} is invalid or disallowed`);
59
+ }
60
+ });
61
+ if ((funcDef.minCols ?? 0) > fields.length) {
62
+ throw makeErr(`Less columns provided than necessary (minCols=${funcDef.minCols})`);
63
+ }
64
+ }
65
+ if (funcDef.numArgs && funcDef.minCols !== 0 && fields !== "*" && Array.isArray(fields) && !fields.length) {
66
+ throw `\n Function "${funcDef.name}" expects at least a field name but has not been provided with one`;
67
+ }
68
+ return funcDef;
69
+ };
70
+ exports.parseFunction = parseFunction;
71
+ const MAX_COL_NUM = 1600;
72
+ const asValue = (v, castAs = "") => DboBuilder_1.pgp.as.format("$1" + castAs, [v]);
73
+ const FTS_Funcs =
74
+ /* Full text search
75
+ https://www.postgresql.org/docs/current/textsearch-dictionaries.html#TEXTSEARCH-SIMPLE-DICTIONARY
76
+ */
77
+ [
78
+ "simple",
79
+ // "synonym", // replace word with a synonym
80
+ "english",
81
+ // "english_stem",
82
+ // "english_hunspell",
83
+ ""
84
+ ].map(type => ({
85
+ name: "$ts_headline" + (type ? ("_" + type) : ""),
86
+ description: ` :[column_name <string>, search_term: <string | { to_tsquery: string } > ] -> sha512 hash of the of column content`,
87
+ type: "function",
88
+ singleColArg: true,
89
+ numArgs: 2,
90
+ getFields: (args) => [args[0]],
91
+ getQuery: ({ allColumns, args, tableAlias }) => {
92
+ const col = (0, prostgles_types_1.asName)(args[0]);
93
+ let qVal = args[1], qType = "to_tsquery";
94
+ let _type = type ? (asValue(type) + ",") : "";
95
+ const searchTypes = prostgles_types_1.TextFilter_FullTextSearchFilterKeys;
96
+ /* { to_tsquery: 'search term' } */
97
+ if ((0, DboBuilder_1.isPlainObject)(qVal)) {
98
+ const keys = Object.keys(qVal);
99
+ if (!keys.length)
100
+ throw "Bad arg";
101
+ if (keys.length !== 1 || !searchTypes.includes(keys[0]))
102
+ throw "Expecting a an object with a single key named one of: " + searchTypes.join(", ");
103
+ qType = keys[0];
104
+ qVal = asValue(qVal[qType]);
105
+ /* 'search term' */
106
+ }
107
+ else if (typeof qVal === "string") {
108
+ qVal = DboBuilder_1.pgp.as.format(qType + "($1)", [qVal]);
109
+ }
110
+ else
111
+ throw "Bad second arg. Exepcting search string or { to_tsquery: 'search string' }";
112
+ const res = `ts_headline(${_type} ${col}::text, ${qVal}, 'ShortWord=1 ' )`;
113
+ // console.log(res)
114
+ return res;
115
+ }
116
+ }));
117
+ let PostGIS_Funcs = [
118
+ {
119
+ fname: "ST_DWithin",
120
+ description: `:[column_name, { lat?: number; lng?: number; geojson?: object; srid?: number; use_spheroid?: boolean; distance: number; }]
121
+ -> Returns true if the geometries are within a given distance
122
+ For geometry: The distance is specified in units defined by the spatial reference system of the geometries. For this function to make sense, the source geometries must be in the same coordinate system (have the same SRID).
123
+ For geography: units are in meters and distance measurement defaults to use_spheroid=true. For faster evaluation use use_spheroid=false to measure on the sphere.
124
+ `
125
+ },
126
+ {
127
+ fname: "<->",
128
+ description: `:[column_name, { lat?: number; lng?: number; geojson?: object; srid?: number; use_spheroid?: boolean }]
129
+ -> The <-> operator returns the 2D distance between two geometries. Used in the "ORDER BY" clause provides index-assisted nearest-neighbor result sets. For PostgreSQL below 9.5 only gives centroid distance of bounding boxes and for PostgreSQL 9.5+, does true KNN distance search giving true distance between geometries, and distance sphere for geographies.`
130
+ },
131
+ {
132
+ fname: "ST_Distance",
133
+ description: ` :[column_name, { lat?: number; lng?: number; geojson?: object; srid?: number; use_spheroid?: boolean }]
134
+ -> For geometry types returns the minimum 2D Cartesian (planar) distance between two geometries, in projected units (spatial ref units).
135
+ -> For geography types defaults to return the minimum geodesic distance between two geographies in meters, compute on the spheroid determined by the SRID. If use_spheroid is false, a faster spherical calculation is used.
136
+ `,
137
+ }, {
138
+ fname: "ST_DistanceSpheroid",
139
+ description: ` :[column_name, { lat?: number; lng?: number; geojson?: object; srid?: number; spheroid?: string; }] -> Returns minimum distance in meters between two lon/lat geometries given a particular spheroid. See the explanation of spheroids given for ST_LengthSpheroid.
140
+
141
+ `,
142
+ }, {
143
+ fname: "ST_DistanceSphere",
144
+ description: ` :[column_name, { lat?: number; lng?: number; geojson?: object; srid?: number }] -> Returns linear distance in meters between two lon/lat points. Uses a spherical earth and radius of 6370986 meters. Faster than ST_DistanceSpheroid, but less accurate. Only implemented for points.`,
145
+ }
146
+ ].map(({ fname, description }) => ({
147
+ name: "$" + fname,
148
+ description,
149
+ type: "function",
150
+ singleColArg: true,
151
+ numArgs: 1,
152
+ getFields: (args) => [args[0]],
153
+ getQuery: ({ allColumns, args, tableAlias }) => {
154
+ const arg2 = args[1], mErr = () => { throw `${fname}: Expecting a second argument like: { lat?: number; lng?: number; geojson?: object; srid?: number; use_spheroid?: boolean }`; };
155
+ if (!(0, DboBuilder_1.isPlainObject)(arg2))
156
+ mErr();
157
+ const col = allColumns.find(c => c.name === args[0]);
158
+ if (!col)
159
+ throw new Error("Col not found: " + args[0]);
160
+ const { lat, lng, srid = 4326, geojson, text, use_spheroid, distance, spheroid = 'SPHEROID["WGS 84",6378137,298.257223563]', debug } = arg2;
161
+ let geomQ = "", extraParams = "";
162
+ if (typeof text === "string") {
163
+ geomQ = `ST_GeomFromText(${asValue(text)})`;
164
+ }
165
+ else if ([lat, lng].every(v => Number.isFinite(v))) {
166
+ geomQ = `ST_Point(${asValue(lng)}, ${asValue(lat)})`;
167
+ }
168
+ else if ((0, DboBuilder_1.isPlainObject)(geojson)) {
169
+ geomQ = `ST_GeomFromGeoJSON(${geojson})`;
170
+ }
171
+ else
172
+ mErr();
173
+ if (Number.isFinite(srid)) {
174
+ geomQ = `ST_SetSRID(${geomQ}, ${asValue(srid)})`;
175
+ }
176
+ let colCast = "";
177
+ const colIsGeog = col.udt_name === "geography";
178
+ let geomQCast = colIsGeog ? "::geography" : "::geometry";
179
+ /**
180
+ * float ST_Distance(geometry g1, geometry g2);
181
+ * float ST_Distance(geography geog1, geography geog2, boolean use_spheroid=true);
182
+ */
183
+ if (fname === "ST_Distance") {
184
+ if (typeof use_spheroid === "boolean") {
185
+ extraParams = ", " + asValue(use_spheroid);
186
+ }
187
+ colCast = (colIsGeog || use_spheroid) ? "::geography" : "::geometry";
188
+ geomQCast = (colIsGeog || use_spheroid) ? "::geography" : "::geometry";
189
+ /**
190
+ * boolean ST_DWithin(geometry g1, geometry g2, double precision distance_of_srid);
191
+ * boolean ST_DWithin(geography gg1, geography gg2, double precision distance_meters, boolean use_spheroid = true);
192
+ */
193
+ }
194
+ else if (fname === "ST_DWithin") {
195
+ colCast = colIsGeog ? "::geography" : "::geometry";
196
+ geomQCast = colIsGeog ? "::geography" : "::geometry";
197
+ if (typeof distance !== "number")
198
+ throw `ST_DWithin: distance param missing or not a number`;
199
+ extraParams = ", " + asValue(distance);
200
+ /**
201
+ * float ST_DistanceSpheroid(geometry geomlonlatA, geometry geomlonlatB, spheroid measurement_spheroid);
202
+ */
203
+ }
204
+ else if (fname === "ST_DistanceSpheroid") {
205
+ colCast = "::geometry";
206
+ geomQCast = "::geometry";
207
+ if (typeof spheroid !== "string")
208
+ throw `ST_DistanceSpheroid: spheroid param must be string`;
209
+ extraParams = `, ${asValue(spheroid)}`;
210
+ /**
211
+ * float ST_DistanceSphere(geometry geomlonlatA, geometry geomlonlatB);
212
+ */
213
+ }
214
+ else if (fname === "ST_DistanceSphere") {
215
+ colCast = "::geometry";
216
+ geomQCast = "::geometry";
217
+ extraParams = "";
218
+ /**
219
+ * double precision <->( geometry A , geometry B );
220
+ * double precision <->( geography A , geography B );
221
+ */
222
+ }
223
+ else if (fname === "<->") {
224
+ colCast = colIsGeog ? "::geography" : "::geometry";
225
+ geomQCast = colIsGeog ? "::geography" : "::geometry";
226
+ const q = DboBuilder_1.pgp.as.format(`${(0, exports.asNameAlias)(args[0], tableAlias)}${colCast} <-> ${geomQ}${geomQCast}`);
227
+ if (debug)
228
+ throw q;
229
+ return q;
230
+ }
231
+ const q = DboBuilder_1.pgp.as.format(`${fname}(${(0, exports.asNameAlias)(args[0], tableAlias)}${colCast} , ${geomQ}${geomQCast} ${extraParams})`);
232
+ if (debug)
233
+ throw q;
234
+ return q;
235
+ }
236
+ }));
237
+ PostGIS_Funcs = PostGIS_Funcs.concat([
238
+ "ST_AsText", "ST_AsEWKT", "ST_AsEWKB", "ST_AsBinary", "ST_AsMVT", "ST_AsMVTGeom",
239
+ "ST_AsGeoJSON", "ST_Simplify",
240
+ "ST_SnapToGrid",
241
+ ]
242
+ .map(fname => {
243
+ const res = {
244
+ name: "$" + fname,
245
+ description: ` :[column_name, precision?] -> json GeoJSON output of a geometry column`,
246
+ type: "function",
247
+ singleColArg: true,
248
+ numArgs: 1,
249
+ getFields: (args) => [args[0]],
250
+ getQuery: ({ allowedFields, args, tableAlias }) => {
251
+ let secondArg = "";
252
+ const otherArgs = args.slice(1);
253
+ if (otherArgs.length)
254
+ secondArg = ", " + otherArgs.map(arg => asValue(arg)).join(", ");
255
+ const escTabelName = (0, exports.asNameAlias)(args[0], tableAlias) + "::geometry";
256
+ let result = DboBuilder_1.pgp.as.format(fname + "(" + escTabelName + secondArg + (fname === "ST_AsGeoJSON" ? ")::jsonb" : ")"));
257
+ if (fname.startsWith("ST_SnapToGrid") || fname.startsWith("ST_Simplify")) {
258
+ let r = `ST_AsGeoJSON(${result})::jsonb`;
259
+ return r;
260
+ }
261
+ return result;
262
+ }
263
+ };
264
+ return res;
265
+ }));
266
+ PostGIS_Funcs = PostGIS_Funcs.concat(["ST_Extent", "ST_3DExtent", "ST_XMin_Agg", "ST_XMax_Agg", "ST_YMin_Agg", "ST_YMax_Agg", "ST_ZMin_Agg", "ST_ZMax_Agg"]
267
+ .map(fname => {
268
+ const res = {
269
+ name: "$" + fname,
270
+ description: ` :[column_name] -> ST_Extent returns a bounding box that encloses a set of geometries.
271
+ The ST_Extent function is an "aggregate" function in the terminology of SQL.
272
+ That means that it operates on lists of data, in the same way the SUM() and AVG() functions do.`,
273
+ type: "aggregation",
274
+ singleColArg: true,
275
+ numArgs: 1,
276
+ getFields: (args) => [args[0]],
277
+ getQuery: ({ allowedFields, args, tableAlias }) => {
278
+ const escTabelName = (0, exports.asNameAlias)(args[0], tableAlias) + "::geometry";
279
+ if (fname.includes("Extent")) {
280
+ return `${fname}(${escTabelName})`;
281
+ }
282
+ return `${fname.endsWith("_Agg") ? fname.slice(0, -4) : fname}(ST_Collect(${escTabelName}))`;
283
+ }
284
+ };
285
+ return res;
286
+ }));
287
+ PostGIS_Funcs = PostGIS_Funcs.concat(["ST_Length", "ST_X", "ST_Y", "ST_Z"].map(fname => ({
288
+ name: "$" + fname,
289
+ type: "function",
290
+ singleColArg: true,
291
+ numArgs: 1,
292
+ getFields: (args) => [args[0]],
293
+ getQuery: ({ allColumns, args, tableAlias }) => {
294
+ const colName = args[0];
295
+ const escapedColName = (0, exports.asNameAlias)(colName, tableAlias);
296
+ const col = allColumns.find(c => c.name === colName);
297
+ if (!col)
298
+ throw new Error("Col not found: " + colName);
299
+ return `${fname}(${escapedColName})`;
300
+ }
301
+ })));
302
+ /**
303
+ * Each function expects a column at the very least
304
+ */
305
+ exports.FUNCTIONS = [
306
+ // Hashing
307
+ {
308
+ name: "$md5_multi",
309
+ description: ` :[...column_names] -> md5 hash of the column content`,
310
+ type: "function",
311
+ singleColArg: false,
312
+ numArgs: MAX_COL_NUM,
313
+ getFields: (args) => args,
314
+ getQuery: ({ allowedFields, args, tableAlias }) => {
315
+ const q = DboBuilder_1.pgp.as.format("md5(" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + "::text, '' )").join(" || ") + ")");
316
+ return q;
317
+ }
318
+ },
319
+ {
320
+ name: "$md5_multi_agg",
321
+ description: ` :[...column_names] -> md5 hash of the string aggregation of column content`,
322
+ type: "aggregation",
323
+ singleColArg: false,
324
+ numArgs: MAX_COL_NUM,
325
+ getFields: (args) => args,
326
+ getQuery: ({ allowedFields, args, tableAlias }) => {
327
+ const q = DboBuilder_1.pgp.as.format("md5(string_agg(" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + "::text, '' )").join(" || ") + ", ','))");
328
+ return q;
329
+ }
330
+ },
331
+ {
332
+ name: "$sha256_multi",
333
+ description: ` :[...column_names] -> sha256 hash of the of column content`,
334
+ type: "function",
335
+ singleColArg: false,
336
+ numArgs: MAX_COL_NUM,
337
+ getFields: (args) => args,
338
+ getQuery: ({ allowedFields, args, tableAlias }) => {
339
+ const q = DboBuilder_1.pgp.as.format("encode(sha256((" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + ", '' )").join(" || ") + ")::text::bytea), 'hex')");
340
+ return q;
341
+ }
342
+ },
343
+ {
344
+ name: "$sha256_multi_agg",
345
+ description: ` :[...column_names] -> sha256 hash of the string aggregation of column content`,
346
+ type: "aggregation",
347
+ singleColArg: false,
348
+ numArgs: MAX_COL_NUM,
349
+ getFields: (args) => args,
350
+ getQuery: ({ allowedFields, args, tableAlias }) => {
351
+ const q = DboBuilder_1.pgp.as.format("encode(sha256(string_agg(" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + ", '' )").join(" || ") + ", ',')::text::bytea), 'hex')");
352
+ return q;
353
+ }
354
+ },
355
+ {
356
+ name: "$sha512_multi",
357
+ description: ` :[...column_names] -> sha512 hash of the of column content`,
358
+ type: "function",
359
+ singleColArg: false,
360
+ numArgs: MAX_COL_NUM,
361
+ getFields: (args) => args,
362
+ getQuery: ({ allowedFields, args, tableAlias }) => {
363
+ const q = DboBuilder_1.pgp.as.format("encode(sha512((" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + ", '' )").join(" || ") + ")::text::bytea), 'hex')");
364
+ return q;
365
+ }
366
+ },
367
+ {
368
+ name: "$sha512_multi_agg",
369
+ description: ` :[...column_names] -> sha512 hash of the string aggregation of column content`,
370
+ type: "aggregation",
371
+ singleColArg: false,
372
+ numArgs: MAX_COL_NUM,
373
+ getFields: (args) => args,
374
+ getQuery: ({ allowedFields, args, tableAlias }) => {
375
+ const q = DboBuilder_1.pgp.as.format("encode(sha512(string_agg(" + args.map(fname => "COALESCE( " + (0, exports.asNameAlias)(fname, tableAlias) + ", '' )").join(" || ") + ", ',')::text::bytea), 'hex')");
376
+ return q;
377
+ }
378
+ },
379
+ ...FTS_Funcs,
380
+ ...PostGIS_Funcs,
381
+ {
382
+ name: "$left",
383
+ description: ` :[column_name, number] -> substring`,
384
+ type: "function",
385
+ numArgs: 2,
386
+ singleColArg: false,
387
+ getFields: (args) => [args[0]],
388
+ getQuery: ({ allowedFields, args, tableAlias }) => {
389
+ return DboBuilder_1.pgp.as.format("LEFT(" + (0, exports.asNameAlias)(args[0], tableAlias) + ", $1)", [args[1]]);
390
+ }
391
+ },
392
+ {
393
+ name: "$unnest_words",
394
+ description: ` :[column_name] -> Splits string at spaces`,
395
+ type: "function",
396
+ numArgs: 1,
397
+ singleColArg: true,
398
+ getFields: (args) => [args[0]],
399
+ getQuery: ({ allowedFields, args, tableAlias }) => {
400
+ return DboBuilder_1.pgp.as.format("unnest(string_to_array(" + (0, exports.asNameAlias)(args[0], tableAlias) + "::TEXT , ' '))"); //, [args[1]]
401
+ }
402
+ },
403
+ {
404
+ name: "$right",
405
+ description: ` :[column_name, number] -> substring`,
406
+ type: "function",
407
+ numArgs: 2,
408
+ singleColArg: false,
409
+ getFields: (args) => [args[0]],
410
+ getQuery: ({ allowedFields, args, tableAlias }) => {
411
+ return DboBuilder_1.pgp.as.format("RIGHT(" + (0, exports.asNameAlias)(args[0], tableAlias) + ", $1)", [args[1]]);
412
+ }
413
+ },
414
+ {
415
+ name: "$to_char",
416
+ type: "function",
417
+ description: ` :[column_name, format<string>] -> format dates and strings. Eg: [current_timestamp, 'HH12:MI:SS']`,
418
+ singleColArg: false,
419
+ numArgs: 2,
420
+ getFields: (args) => [args[0]],
421
+ getQuery: ({ allowedFields, args, tableAlias }) => {
422
+ if (args.length === 3) {
423
+ return DboBuilder_1.pgp.as.format("to_char(" + (0, exports.asNameAlias)(args[0], tableAlias) + ", $2, $3)", [args[0], args[1], args[2]]);
424
+ }
425
+ return DboBuilder_1.pgp.as.format("to_char(" + (0, exports.asNameAlias)(args[0], tableAlias) + ", $2)", [args[0], args[1]]);
426
+ }
427
+ },
428
+ /**
429
+ * Date trunc utils
430
+ */
431
+ ...[
432
+ "microsecond",
433
+ "millisecond",
434
+ "second",
435
+ "minute",
436
+ "hour",
437
+ "day",
438
+ "week",
439
+ "month",
440
+ "quarter",
441
+ "year",
442
+ "decade",
443
+ "century",
444
+ "millennium"
445
+ ].map(k => ({ val: 0, unit: k }))
446
+ .concat([
447
+ { val: 6, unit: 'month' },
448
+ { val: 4, unit: 'month' },
449
+ { val: 2, unit: 'month' },
450
+ { val: 8, unit: 'hour' },
451
+ { val: 4, unit: 'hour' },
452
+ { val: 2, unit: 'hour' },
453
+ { val: 30, unit: 'minute' },
454
+ { val: 15, unit: 'minute' },
455
+ { val: 6, unit: 'minute' },
456
+ { val: 5, unit: 'minute' },
457
+ { val: 4, unit: 'minute' },
458
+ { val: 3, unit: 'minute' },
459
+ { val: 2, unit: 'minute' },
460
+ { val: 30, unit: 'second' },
461
+ { val: 15, unit: 'second' },
462
+ { val: 10, unit: 'second' },
463
+ { val: 8, unit: 'second' },
464
+ { val: 6, unit: 'second' },
465
+ { val: 5, unit: 'second' },
466
+ { val: 4, unit: 'second' },
467
+ { val: 3, unit: 'second' },
468
+ { val: 2, unit: 'second' },
469
+ { val: 500, unit: 'millisecond' },
470
+ { val: 250, unit: 'millisecond' },
471
+ { val: 100, unit: 'millisecond' },
472
+ { val: 50, unit: 'millisecond' },
473
+ { val: 25, unit: 'millisecond' },
474
+ { val: 10, unit: 'millisecond' },
475
+ { val: 5, unit: 'millisecond' },
476
+ { val: 2, unit: 'millisecond' },
477
+ ]).map(({ val, unit }) => ({
478
+ name: "$date_trunc_" + (val || "") + unit,
479
+ type: "function",
480
+ description: ` :[column_name] -> round down timestamp to closest ${val || ""} ${unit} `,
481
+ singleColArg: true,
482
+ numArgs: 1,
483
+ getFields: (args) => [args[0]],
484
+ getQuery: ({ allowedFields, args, tableAlias }) => {
485
+ const col = (0, exports.asNameAlias)(args[0], tableAlias);
486
+ if (!val)
487
+ return `date_trunc(${asValue(unit)}, ${col})`;
488
+ const prevInt = {
489
+ month: "year",
490
+ hour: "day",
491
+ minute: "hour",
492
+ second: "minute"
493
+ };
494
+ let res = `(date_trunc(${asValue(prevInt[unit] || "hour")}, ${col}) + date_part(${asValue(unit, "::text")}, ${col})::int / ${val} * interval ${asValue(val + " " + unit)})`;
495
+ // console.log(res);
496
+ return res;
497
+ }
498
+ })),
499
+ /* Date funcs date_part */
500
+ ...["date_trunc", "date_part"].map(funcName => ({
501
+ name: "$" + funcName,
502
+ type: "function",
503
+ numArgs: 2,
504
+ description: ` :[unit<string>, column_name] -> ` + (funcName === "date_trunc" ? ` round down timestamp to closest unit value. ` : ` extract date unit as float8. `) + ` E.g. ['hour', col] `,
505
+ singleColArg: false,
506
+ getFields: (args) => [args[1]],
507
+ getQuery: ({ allowedFields, args, tableAlias }) => {
508
+ return `${funcName}(${asValue(args[0])}, ${(0, exports.asNameAlias)(args[1], tableAlias)})`;
509
+ }
510
+ })),
511
+ /* Handy date funcs */
512
+ ...[
513
+ ["date", "YYYY-MM-DD"],
514
+ ["datetime", "YYYY-MM-DD HH24:MI"],
515
+ ["datetime_", "YYYY_MM_DD__HH24_MI"],
516
+ ["timedate", "HH24:MI YYYY-MM-DD"],
517
+ ["time", "HH24:MI"],
518
+ ["time12", "HH:MI"],
519
+ ["timeAM", "HH:MI AM"],
520
+ ["dy", "dy"],
521
+ ["Dy", "Dy"],
522
+ ["day", "day"],
523
+ ["Day", "Day"],
524
+ ["DayNo", "DD"],
525
+ ["DD", "DD"],
526
+ ["dowUS", "D"],
527
+ ["D", "D"],
528
+ ["dow", "ID"],
529
+ ["ID", "ID"],
530
+ ["MonthNo", "MM"],
531
+ ["MM", "MM"],
532
+ ["mon", "mon"],
533
+ ["Mon", "Mon"],
534
+ ["month", "month"],
535
+ ["Month", "Month"],
536
+ ["year", "yyyy"],
537
+ ["yyyy", "yyyy"],
538
+ ["yy", "yy"],
539
+ ["yr", "yy"],
540
+ ].map(([funcName, txt]) => ({
541
+ name: "$" + funcName,
542
+ type: "function",
543
+ description: ` :[column_name] -> get timestamp formated as ` + txt,
544
+ singleColArg: true,
545
+ numArgs: 1,
546
+ getFields: (args) => [args[0]],
547
+ getQuery: ({ allowedFields, args, tableAlias }) => {
548
+ return DboBuilder_1.pgp.as.format("trim(to_char(" + (0, exports.asNameAlias)(args[0], tableAlias) + ", $2))", [args[0], txt]);
549
+ }
550
+ })),
551
+ /* Basic 1 arg col funcs */
552
+ ...["upper", "lower", "length", "reverse", "trim", "initcap", "round", "ceil", "floor", "sign", "md5"].map(funcName => ({
553
+ name: "$" + funcName,
554
+ type: "function",
555
+ numArgs: 1,
556
+ singleColArg: true,
557
+ getFields: (args) => [args[0]],
558
+ getQuery: ({ allowedFields, args, tableAlias }) => {
559
+ return funcName + "(" + (0, exports.asNameAlias)(args[0], tableAlias) + ")";
560
+ }
561
+ })),
562
+ /* Interval funcs */
563
+ ...["age", "ageNow", "difference"].map(funcName => ({
564
+ name: "$" + funcName,
565
+ type: "function",
566
+ numArgs: 1,
567
+ singleColArg: true,
568
+ getFields: (args) => args.slice(0, 2).filter(a => typeof a === "string"),
569
+ getQuery: ({ allowedFields, args, tableAlias }) => {
570
+ const validCols = args.slice(0, 2).filter(a => typeof a === "string").length;
571
+ const trunc = args[2];
572
+ const allowedTruncs = ["second", "minute", "hour", "year"];
573
+ if (trunc && !allowedTruncs.includes(trunc))
574
+ throw new Error("Incorrect trunc provided. Allowed values: " + allowedTruncs);
575
+ if (funcName === "difference" && validCols !== 2)
576
+ throw new Error("Must have two column names");
577
+ if (![1, 2].includes(validCols))
578
+ throw new Error("Must have one or two column names");
579
+ const [leftField, rightField] = args;
580
+ const leftQ = (0, exports.asNameAlias)(leftField, tableAlias);
581
+ let rightQ = rightField ? (0, exports.asNameAlias)(rightField, tableAlias) : "";
582
+ let query = "";
583
+ if (funcName === "ageNow" && validCols === 1) {
584
+ query = `age(now(), ${leftQ})`;
585
+ }
586
+ else if (funcName === "age" || funcName === "ageNow") {
587
+ if (rightQ)
588
+ rightQ = ", " + rightQ;
589
+ query = `age(${leftQ} ${rightQ})`;
590
+ }
591
+ else {
592
+ query = `${leftQ} - ${rightQ}`;
593
+ }
594
+ return trunc ? `date_trunc(${asValue(trunc)}, ${query})` : query;
595
+ }
596
+ })),
597
+ /* pgcrypto funcs */
598
+ ...["crypt"].map(funcName => ({
599
+ name: "$" + funcName,
600
+ type: "function",
601
+ numArgs: 1,
602
+ singleColArg: false,
603
+ getFields: (args) => [args[1]],
604
+ getQuery: ({ allowedFields, args, tableAlias }) => {
605
+ const value = asValue(args[0]) + "", seedColumnName = (0, exports.asNameAlias)(args[1], tableAlias);
606
+ return `crypt(${value}, ${seedColumnName}::text)`;
607
+ }
608
+ })),
609
+ /* Text col and value funcs */
610
+ ...["position", "position_lower"].map(funcName => ({
611
+ name: "$" + funcName,
612
+ type: "function",
613
+ numArgs: 1,
614
+ singleColArg: false,
615
+ getFields: (args) => [args[1]],
616
+ getQuery: ({ allowedFields, args, tableAlias }) => {
617
+ let a1 = asValue(args[0]), a2 = (0, exports.asNameAlias)(args[1], tableAlias);
618
+ if (funcName === "position_lower") {
619
+ a1 = `LOWER(${a1}::text)`;
620
+ a2 = `LOWER(${a2}::text)`;
621
+ }
622
+ return `position( ${a1} IN ${a2} )`;
623
+ }
624
+ })),
625
+ ...["template_string"].map(funcName => ({
626
+ name: "$" + funcName,
627
+ type: "function",
628
+ numArgs: 1,
629
+ minCols: 0,
630
+ singleColArg: false,
631
+ getFields: (args) => [],
632
+ getQuery: ({ allowedFields, args, tableAlias }) => {
633
+ let value = asValue(args[0]);
634
+ if (typeof value !== "string")
635
+ throw "expecting string argument";
636
+ const usedColumns = allowedFields.filter(fName => value.includes(`{${fName}}`));
637
+ usedColumns.forEach((colName, idx) => {
638
+ value = value.split(`{${colName}}`).join(`%${idx + 1}$s`);
639
+ });
640
+ value = asValue(value);
641
+ if (usedColumns.length) {
642
+ return `format(${value}, ${usedColumns.map(c => `${(0, exports.asNameAlias)(c, tableAlias)}::TEXT`).join(", ")})`;
643
+ }
644
+ return `format(${value})`;
645
+ }
646
+ })),
647
+ /** Custom highlight -> myterm => ['some text and', ['myterm'], ' and some other text']
648
+ * (fields: "*" | string[], term: string, { edgeTruncate: number = -1; noFields: boolean = false }) => string | (string | [string])[]
649
+ * edgeTruncate = maximum extra characters left and right of matches
650
+ * noFields = exclude field names in search
651
+ * */
652
+ {
653
+ name: "$term_highlight",
654
+ description: ` :[column_names<string[] | "*">, search_term<string>, opts?<{ returnIndex?: number; edgeTruncate?: number; noFields?: boolean }>] -> get case-insensitive text match highlight`,
655
+ type: "function",
656
+ numArgs: 1,
657
+ singleColArg: true,
658
+ canBeUsedForFilter: true,
659
+ getFields: (args) => args[0],
660
+ getQuery: ({ allowedFields, args, tableAlias, allColumns }) => {
661
+ const cols = DboBuilder_1.ViewHandler._parseFieldFilter(args[0], false, allowedFields);
662
+ let term = args[1];
663
+ const rawTerm = args[1];
664
+ let { edgeTruncate, noFields = false, returnType, matchCase = false } = args[2] || {};
665
+ if (!(0, prostgles_types_1.isEmpty)(args[2])) {
666
+ const keys = Object.keys(args[2]);
667
+ const validKeys = ["edgeTruncate", "noFields", "returnType", "matchCase"];
668
+ const bad_keys = keys.filter(k => !validKeys.includes(k));
669
+ if (bad_keys.length)
670
+ throw "Invalid options provided for $term_highlight. Expecting one of: " + validKeys.join(", ");
671
+ }
672
+ if (!cols.length)
673
+ throw "Cols are empty/invalid";
674
+ if (typeof term !== "string")
675
+ throw "Non string term provided: " + term;
676
+ if (edgeTruncate !== undefined && (!Number.isInteger(edgeTruncate) || edgeTruncate < -1))
677
+ throw "Invalid edgeTruncate. expecting a positive integer";
678
+ if (typeof noFields !== "boolean")
679
+ throw "Invalid noFields. expecting boolean";
680
+ const RETURN_TYPES = ["index", "boolean", "object"];
681
+ if (returnType && !RETURN_TYPES.includes(returnType)) {
682
+ throw `returnType can only be one of: ${RETURN_TYPES}`;
683
+ }
684
+ const makeTextMatcherArray = (rawText, _term) => {
685
+ let matchText = rawText, term = _term;
686
+ if (!matchCase) {
687
+ matchText = `LOWER(${rawText})`;
688
+ term = `LOWER(${term})`;
689
+ }
690
+ let leftStr = `substr(${rawText}, 1, position(${term} IN ${matchText}) - 1 )`, rightStr = `substr(${rawText}, position(${term} IN ${matchText}) + length(${term}) )`;
691
+ if (edgeTruncate) {
692
+ leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
693
+ rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
694
+ }
695
+ return `
696
+ CASE WHEN position(${term} IN ${matchText}) > 0 AND ${term} <> ''
697
+ THEN array_to_json(ARRAY[
698
+ to_json( ${leftStr}::TEXT ),
699
+ array_to_json(
700
+ ARRAY[substr(${rawText}, position(${term} IN ${matchText}), length(${term}) )::TEXT ]
701
+ ),
702
+ to_json(${rightStr}::TEXT )
703
+ ])
704
+ ELSE
705
+ array_to_json(ARRAY[(${rawText})::TEXT])
706
+ END
707
+ `;
708
+ };
709
+ let colRaw = "( " + cols.map(c => `${noFields ? "" : (asValue(c + ": ") + " || ")} COALESCE(${(0, exports.asNameAlias)(c, tableAlias)}::TEXT, '')`).join(" || ', ' || ") + " )";
710
+ let col = colRaw;
711
+ term = asValue(term);
712
+ if (!matchCase) {
713
+ col = "LOWER" + col;
714
+ term = `LOWER(${term})`;
715
+ }
716
+ let leftStr = `substr(${colRaw}, 1, position(${term} IN ${col}) - 1 )`, rightStr = `substr(${colRaw}, position(${term} IN ${col}) + length(${term}) )`;
717
+ if (edgeTruncate) {
718
+ leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
719
+ rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
720
+ }
721
+ // console.log(col);
722
+ let res = "";
723
+ if (returnType === "index") {
724
+ res = `CASE WHEN position(${term} IN ${col}) > 0 THEN position(${term} IN ${col}) - 1 ELSE -1 END`;
725
+ // } else if(returnType === "boolean"){
726
+ // res = `CASE WHEN position(${term} IN ${col}) > 0 THEN TRUE ELSE FALSE END`;
727
+ }
728
+ else if (returnType === "object" || returnType === "boolean") {
729
+ const hasChars = Boolean(rawTerm && /[a-z]/i.test(rawTerm));
730
+ let validCols = cols.map(c => {
731
+ const colInfo = allColumns.find(ac => ac.name === c);
732
+ return {
733
+ key: c,
734
+ colInfo
735
+ };
736
+ })
737
+ .filter(c => c.colInfo && c.colInfo.udt_name !== "bytea");
738
+ let _cols = validCols.filter(c =>
739
+ /** Exclude numeric columns when the search tern contains a character */
740
+ !hasChars ||
741
+ (0, DboBuilder_1.postgresToTsType)(c.colInfo.udt_name) !== "number");
742
+ /** This will break GROUP BY (non-integer constant in GROUP BY) */
743
+ if (!_cols.length) {
744
+ if (validCols.length && hasChars)
745
+ throw `You're searching the impossible: characters in numeric fields. Use this to prevent making such a request in future: /[a-z]/i.test(your_term) `;
746
+ return (returnType === "boolean") ? "FALSE" : "NULL";
747
+ }
748
+ res = `CASE
749
+ ${_cols
750
+ .map(c => {
751
+ const colNameEscaped = (0, exports.asNameAlias)(c.key, tableAlias);
752
+ let colSelect = `${colNameEscaped}::TEXT`;
753
+ const isTstamp = c.colInfo?.udt_name.startsWith("timestamp");
754
+ if (isTstamp || c.colInfo?.udt_name === "date") {
755
+ colSelect = `( CASE WHEN ${colNameEscaped} IS NULL THEN ''
756
+ ELSE concat_ws(' ',
757
+ ${colNameEscaped}::TEXT,
758
+ ${isTstamp ? `'TZ' || trim(to_char(${colNameEscaped}, 'OF')), ` : ''}
759
+ trim(to_char(${colNameEscaped}, 'Day Month')),
760
+ 'Q' || trim(to_char(${colNameEscaped}, 'Q')),
761
+ 'WK' || trim(to_char(${colNameEscaped}, 'WW'))
762
+ ) END)`;
763
+ }
764
+ let colTxt = `COALESCE(${colSelect}, '')`; // position(${term} IN ${colTxt}) > 0
765
+ if (returnType === "boolean") {
766
+ return `
767
+ WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue('%' + rawTerm + '%')}
768
+ THEN TRUE
769
+ `;
770
+ }
771
+ return `
772
+ WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue('%' + rawTerm + '%')}
773
+ THEN json_build_object(
774
+ ${asValue(c.key)},
775
+ ${makeTextMatcherArray(colTxt, term)}
776
+ )::jsonb
777
+ `;
778
+ }).join(" ")}
779
+ ELSE ${(returnType === "boolean") ? "FALSE" : "NULL"}
780
+
781
+ END`;
782
+ // console.log(res)
783
+ }
784
+ else {
785
+ /* If no match or empty search THEN return full row as string within first array element */
786
+ res = `CASE WHEN position(${term} IN ${col}) > 0 AND ${term} <> '' THEN array_to_json(ARRAY[
787
+ to_json( ${leftStr}::TEXT ),
788
+ array_to_json(
789
+ ARRAY[substr(${colRaw}, position(${term} IN ${col}), length(${term}) )::TEXT ]
790
+ ),
791
+ to_json(${rightStr}::TEXT )
792
+ ]) ELSE array_to_json(ARRAY[(${colRaw})::TEXT]) END`;
793
+ }
794
+ return res;
795
+ }
796
+ },
797
+ /* Aggs */
798
+ ...["max", "min", "count", "avg", "json_agg", "jsonb_agg", "string_agg", "array_agg", "sum"].map(aggName => ({
799
+ name: "$" + aggName,
800
+ type: "aggregation",
801
+ numArgs: 1,
802
+ singleColArg: true,
803
+ getFields: (args) => [args[0]],
804
+ getQuery: ({ allowedFields, args, tableAlias }) => {
805
+ let extraArgs = "";
806
+ if (args.length > 1) {
807
+ extraArgs = DboBuilder_1.pgp.as.format(", $1:csv", args.slice(1));
808
+ }
809
+ return aggName + "(" + (0, exports.asNameAlias)(args[0], tableAlias) + `${extraArgs})`;
810
+ }
811
+ })),
812
+ /* More aggs */
813
+ {
814
+ name: "$countAll",
815
+ type: "aggregation",
816
+ description: `agg :[] COUNT of all rows `,
817
+ singleColArg: true,
818
+ numArgs: 0,
819
+ getFields: (args) => [],
820
+ getQuery: ({ allowedFields, args, tableAlias }) => {
821
+ return "COUNT(*)";
822
+ }
823
+ },
824
+ {
825
+ name: "$diff_perc",
826
+ type: "aggregation",
827
+ numArgs: 1,
828
+ singleColArg: true,
829
+ getFields: (args) => [args[0]],
830
+ getQuery: ({ allowedFields, args, tableAlias }) => {
831
+ const col = (0, exports.asNameAlias)(args[0], tableAlias);
832
+ return `round( ( ( MAX(${col}) - MIN(${col}) )::float/MIN(${col}) ) * 100, 2)`;
833
+ }
834
+ }
835
+ ];
836
+ /* The difference between a function and computed field is that the computed field does not require any arguments */
837
+ exports.COMPUTED_FIELDS = [
838
+ /**
839
+ * Used instead of row id. Must be used as a last resort. Use all non pseudo or domain data type columns first!
840
+ */
841
+ {
842
+ name: "$rowhash",
843
+ type: "computed",
844
+ // description: ` order hash of row content `,
845
+ getQuery: ({ allowedFields, tableAlias, ctidField }) => {
846
+ return "md5(" +
847
+ allowedFields
848
+ /* CTID not available in AFTER trigger */
849
+ // .concat(ctidField? [ctidField] : [])
850
+ .sort()
851
+ .map(f => (0, exports.asNameAlias)(f, tableAlias))
852
+ .map(f => `md5(coalesce(${f}::text, 'dd'))`)
853
+ .join(" || ") +
854
+ `)`;
855
+ }
856
+ }
857
+ // ,{
858
+ // name: "ctid",
859
+ // type: "computed",
860
+ // // description: ` order hash of row content `,
861
+ // getQuery: ({ allowedFields, tableAlias, ctidField }) => {
862
+ // return asNameAlias("ctid", tableAlias);
863
+ // }
864
+ // }
865
+ ];
866
+ class SelectItemBuilder {
867
+ constructor(params) {
868
+ this.select = [];
869
+ this.checkField = (f) => {
870
+ if (!this.allowedFieldsIncludingComputed.includes(f)) {
871
+ console.log(f, f === "name", this.allowedFieldsIncludingComputed.includes("name"), this.allowedFieldsIncludingComputed);
872
+ throw "Field " + f + " is invalid or dissallowed";
873
+ }
874
+ return f;
875
+ };
876
+ this.addItem = (item) => {
877
+ let fields = item.getFields();
878
+ // console.trace(fields)
879
+ if (fields === "*")
880
+ fields = this.allowedFields.slice(0); //.concat(fields.filter(f => f !== "*"));
881
+ fields.map(this.checkField);
882
+ if (this.select.find(s => s.alias === item.alias))
883
+ throw `Cannot specify duplicate columns ( ${item.alias} ). Perhaps you're using "*" with column names?`;
884
+ this.select.push(item);
885
+ };
886
+ this.addFunction = (func, args, alias) => {
887
+ const funcDef = (0, exports.parseFunction)({
888
+ func, args, functions: this.functions,
889
+ allowedFields: this.allowedFieldsIncludingComputed,
890
+ });
891
+ this.addItem({
892
+ type: funcDef.type,
893
+ alias,
894
+ getFields: () => funcDef.getFields(args),
895
+ getQuery: (tableAlias) => funcDef.getQuery({ allColumns: this.columns, allowedFields: this.allowedFields, args, tableAlias,
896
+ ctidField: undefined,
897
+ /* CTID not available in AFTER trigger */
898
+ // ctidField: this.isView? undefined : "ctid"
899
+ }),
900
+ selected: true
901
+ });
902
+ };
903
+ this.addColumn = (fieldName, selected) => {
904
+ /* Check if computed col */
905
+ if (selected) {
906
+ const compCol = exports.COMPUTED_FIELDS.find(cf => cf.name === fieldName);
907
+ if (compCol && !this.select.find(s => s.alias === fieldName)) {
908
+ const cf = {
909
+ ...compCol,
910
+ type: "computed",
911
+ numArgs: 0,
912
+ singleColArg: false,
913
+ getFields: (args) => []
914
+ };
915
+ this.addFunction(cf, [], compCol.name);
916
+ return;
917
+ }
918
+ }
919
+ const colDef = this.columns.find(c => c.name === fieldName);
920
+ let alias = selected ? fieldName : ("not_selected_" + fieldName);
921
+ this.addItem({
922
+ type: "column",
923
+ columnPGDataType: colDef?.data_type,
924
+ column_udt_type: colDef?.udt_name,
925
+ alias,
926
+ getQuery: () => (0, prostgles_types_1.asName)(fieldName),
927
+ getFields: () => [fieldName],
928
+ selected
929
+ });
930
+ };
931
+ this.parseUserSelect = async (userSelect, joinParse) => {
932
+ /* Array select */
933
+ if (Array.isArray(userSelect)) {
934
+ if (userSelect.find(key => typeof key !== "string"))
935
+ throw "Invalid array select. Expecting an array of strings";
936
+ userSelect.map(key => this.addColumn(key, true));
937
+ /* Empty select */
938
+ }
939
+ else if (userSelect === "") {
940
+ // select.push({
941
+ // type: "function",
942
+ // alias: "",
943
+ // getFields: () => [],
944
+ // getQuery: () => ""
945
+ // })
946
+ return [];
947
+ }
948
+ else if (userSelect === "*") {
949
+ this.allowedFields.map(key => this.addColumn(key, true));
950
+ }
951
+ else if ((0, DboBuilder_1.isPlainObject)(userSelect) && !(0, prostgles_types_1.isEmpty)(userSelect)) {
952
+ const selectKeys = Object.keys(userSelect), selectValues = Object.values(userSelect);
953
+ /* Cannot include and exclude at the same time */
954
+ if (selectValues.filter(v => [0, false].includes(v)).length) {
955
+ if (selectValues.filter(v => ![0, false].includes(v)).length) {
956
+ throw "\nCannot include and exclude fields at the same time";
957
+ }
958
+ /* Exclude only */
959
+ this.allowedFields.filter(f => !selectKeys.includes(f)).map(key => this.addColumn(key, true));
960
+ }
961
+ else {
962
+ await Promise.all(selectKeys.map(async (key) => {
963
+ const val = userSelect[key], throwErr = (extraErr = "") => {
964
+ console.trace(extraErr);
965
+ throw "Unexpected select -> " + JSON.stringify({ [key]: val }) + "\n" + extraErr;
966
+ };
967
+ /* Included fields */
968
+ if ([1, true].includes(val)) {
969
+ if (key === "*") {
970
+ this.allowedFields.map(key => this.addColumn(key, true));
971
+ }
972
+ else {
973
+ this.addColumn(key, true);
974
+ }
975
+ /* Aggs and functions */
976
+ }
977
+ else if (typeof val === "string" || (0, DboBuilder_1.isPlainObject)(val)) {
978
+ /* Function shorthand notation
979
+ { id: "$max" } === { id: { $max: ["id"] } } === SELECT MAX(id) AS id
980
+ */
981
+ if ((typeof val === "string" && val !== "*") ||
982
+ (0, DboBuilder_1.isPlainObject)(val) && Object.keys(val).length === 1 && Array.isArray(Object.values(val)[0]) // !isPlainObject(Object.values(val)[0])
983
+ ) {
984
+ // if(!Array.isArray(Object.values(val)[0])){
985
+ // throw `Could not parse selected item: ${JSON.stringify(val)}\nFunction arguments must be in an array`;
986
+ // }
987
+ let funcName, args;
988
+ if (typeof val === "string") {
989
+ /* Shorthand notation -> it is expected that the key is the column name used as the only argument */
990
+ try {
991
+ this.checkField(key);
992
+ }
993
+ catch (err) {
994
+ throwErr(` Shorthand function notation error: the specifield column ( ${key} ) is invalid or dissallowed. \n Use correct column name or full aliased function notation, e.g.: -> { alias: { $func_name: ["column_name"] } } `);
995
+ }
996
+ funcName = val;
997
+ args = [key];
998
+ /** Function full notation { $funcName: ["colName", ...args] } */
999
+ }
1000
+ else {
1001
+ ({ funcName, args } = (0, exports.parseFunctionObject)(val));
1002
+ }
1003
+ this.addFunction(funcName, args, key);
1004
+ /* Join */
1005
+ }
1006
+ else {
1007
+ if (!joinParse)
1008
+ throw "Joins dissalowed";
1009
+ await joinParse(key, val, throwErr);
1010
+ }
1011
+ }
1012
+ else
1013
+ throwErr();
1014
+ }));
1015
+ }
1016
+ }
1017
+ else
1018
+ throw "Unexpected select -> " + JSON.stringify(userSelect);
1019
+ };
1020
+ this.allFields = params.allFields;
1021
+ this.allowedFields = params.allowedFields;
1022
+ this.computedFields = params.computedFields;
1023
+ this.isView = params.isView;
1024
+ this.functions = params.functions;
1025
+ this.columns = params.columns;
1026
+ this.allowedFieldsIncludingComputed = this.allowedFields.concat(this.computedFields ? this.computedFields.map(cf => cf.name) : []);
1027
+ if (!this.allowedFields.length) {
1028
+ throw "allowedFields empty/missing";
1029
+ }
1030
+ /* Check for conflicting computed column names */
1031
+ const conflictingCol = this.allFields.find(fieldName => this.computedFields.find(cf => cf.name === fieldName));
1032
+ if (conflictingCol) {
1033
+ throw "INTERNAL ERROR: Cannot have duplicate column names ( " + conflictingCol + " ). One or more computed column names are colliding with table columns ones";
1034
+ }
1035
+ }
1036
+ }
1037
+ exports.SelectItemBuilder = SelectItemBuilder;
1038
+ async function getNewQuery(_this, filter, selectParams = {}, param3_unused = null, tableRules, localParams, columns) {
1039
+ if ((localParams?.socket || localParams?.httpReq) && !(0, utils_1.get)(tableRules, "select.fields")) {
1040
+ throw `INTERNAL ERROR: publish.${_this.name}.select.fields rule missing`;
1041
+ }
1042
+ // const all_columns: SelectItem[] = _this.column_names.slice(0).map(fieldName => ({
1043
+ // type: "column",
1044
+ // alias: fieldName,
1045
+ // getQuery: () => asName(fieldName),
1046
+ // getFields: () => [fieldName],
1047
+ // selected: false
1048
+ // } as SelectItem))
1049
+ // .concat(COMPUTED_FIELDS.map(c => ({
1050
+ // type: c.type,
1051
+ // alias: c.name,
1052
+ // getQuery: () => c.getQuery(),
1053
+ // getFields: c.getFields,
1054
+ // selected: false
1055
+ // })))
1056
+ // let select: SelectItem[] = [],
1057
+ let joinQueries = [];
1058
+ // const all_colnames = _this.column_names.slice(0).concat(COMPUTED_FIELDS.map(c => c.name));
1059
+ const { select: userSelect = "*" } = selectParams,
1060
+ // allCols = _this.column_names.slice(0),
1061
+ // allFieldsIncludingComputed = allCols.concat(COMPUTED_FIELDS.map(c => c.name)),
1062
+ allowedFields = _this.parseFieldFilter((0, utils_1.get)(tableRules, "select.fields")) || _this.column_names.slice(0),
1063
+ // allowedFieldsIncludingComputed = _this.parseFieldFilter(get(tableRules, "select.fields"), true, allFieldsIncludingComputed) || allFieldsIncludingComputed,
1064
+ sBuilder = new SelectItemBuilder({ allowedFields, computedFields: exports.COMPUTED_FIELDS, isView: _this.is_view, functions: exports.FUNCTIONS, allFields: _this.column_names.slice(0), columns });
1065
+ await sBuilder.parseUserSelect(userSelect, async (key, val, throwErr) => {
1066
+ // console.log({ key, val })
1067
+ let j_filter = {}, j_selectParams = {}, j_path, j_alias, j_tableRules, j_table, j_isLeftJoin = true;
1068
+ if (val === "*") {
1069
+ j_selectParams.select = "*";
1070
+ j_alias = key;
1071
+ j_table = key;
1072
+ }
1073
+ else {
1074
+ /* Full option join { field_name: db.innerJoin.table_name(filter, select) } */
1075
+ const JOIN_KEYS = ["$innerJoin", "$leftJoin"];
1076
+ const JOIN_PARAMS = ["select", "filter", "$path", "offset", "limit", "orderBy"];
1077
+ const joinKeys = Object.keys(val).filter(k => JOIN_KEYS.includes(k));
1078
+ if (joinKeys.length > 1) {
1079
+ throwErr("\nCannot specify more than one join type ( $innerJoin OR $leftJoin )");
1080
+ }
1081
+ else if (joinKeys.length === 1) {
1082
+ const invalidParams = Object.keys(val).filter(k => ![...JOIN_PARAMS, ...JOIN_KEYS].includes(k));
1083
+ if (invalidParams.length)
1084
+ throw "Invalid join params: " + invalidParams.join(", ");
1085
+ j_isLeftJoin = joinKeys[0] === "$leftJoin";
1086
+ j_table = val[joinKeys[0]];
1087
+ j_alias = key;
1088
+ if (typeof j_table !== "string")
1089
+ throw "\nIssue with select. \nJoin type must be a string table name but got -> " + JSON.stringify({ [key]: val });
1090
+ j_selectParams.select = val.select || "*";
1091
+ j_filter = val.filter || {};
1092
+ j_selectParams.limit = val.limit;
1093
+ j_selectParams.offset = val.offset;
1094
+ j_selectParams.orderBy = val.orderBy;
1095
+ j_path = val.$path;
1096
+ }
1097
+ else {
1098
+ j_selectParams.select = val;
1099
+ j_alias = key;
1100
+ j_table = key;
1101
+ }
1102
+ }
1103
+ if (!j_table)
1104
+ throw "j_table missing";
1105
+ const _thisJoinedTable = _this.dboBuilder.dbo[j_table];
1106
+ if (!_thisJoinedTable) {
1107
+ throw `Joined table ${JSON.stringify(j_table)} is disallowed or inexistent \nOr you've forgot to put the function arguments into an array`;
1108
+ }
1109
+ let isLocal = true;
1110
+ if (localParams && (localParams.socket || localParams.httpReq)) {
1111
+ isLocal = false;
1112
+ j_tableRules = await _this.dboBuilder.publishParser?.getValidatedRequestRuleWusr({ tableName: j_table, command: "find", localParams });
1113
+ }
1114
+ if (isLocal || j_tableRules) {
1115
+ const joinQuery = await getNewQuery(_thisJoinedTable, j_filter, { ...j_selectParams, alias: j_alias }, param3_unused, j_tableRules, localParams, columns);
1116
+ joinQuery.isLeftJoin = j_isLeftJoin;
1117
+ joinQuery.tableAlias = j_alias;
1118
+ joinQuery.$path = j_path;
1119
+ joinQueries.push(joinQuery);
1120
+ // console.log(joinQuery)
1121
+ }
1122
+ });
1123
+ /* Add non selected columns */
1124
+ /* WHY???? */
1125
+ allowedFields.map(key => {
1126
+ if (!sBuilder.select.find(s => s.alias === key && s.type === "column")) {
1127
+ sBuilder.addColumn(key, false);
1128
+ }
1129
+ });
1130
+ let select = sBuilder.select;
1131
+ // const validatedAggAliases = select
1132
+ // .filter(s => s.type !== "joinedColumn")
1133
+ // .map(s => s.alias);
1134
+ const filterOpts = await _this.prepareWhere({
1135
+ filter,
1136
+ select,
1137
+ forcedFilter: (0, utils_1.get)(tableRules, "select.forcedFilter"),
1138
+ filterFields: (0, utils_1.get)(tableRules, "select.filterFields"),
1139
+ tableAlias: selectParams.alias,
1140
+ localParams,
1141
+ tableRule: tableRules
1142
+ });
1143
+ const where = filterOpts.where;
1144
+ const p = _this.getValidatedRules(tableRules, localParams);
1145
+ let resQuery = {
1146
+ allFields: allowedFields,
1147
+ select,
1148
+ table: _this.name,
1149
+ joins: joinQueries,
1150
+ where,
1151
+ // having: cond.having,
1152
+ limit: _this.prepareLimitQuery(selectParams.limit, p),
1153
+ orderBy: [_this.prepareSort(selectParams.orderBy, allowedFields, selectParams.alias, undefined, select)],
1154
+ offset: _this.prepareOffsetQuery(selectParams.offset)
1155
+ };
1156
+ // console.log(resQuery);
1157
+ // console.log(buildJoinQuery(_this, resQuery));
1158
+ return resQuery;
1159
+ }
1160
+ exports.getNewQuery = getNewQuery;