prostgles-server 4.2.544 → 4.2.546
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DboBuilder/DboBuilder.js.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.d.ts.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.js +19 -376
- package/dist/DboBuilder/QueryBuilder/Functions/Functions.js.map +1 -1
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js +107 -0
- package/dist/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.js.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.js +276 -0
- package/dist/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.js.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts +3 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.d.ts.map +1 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.js +6 -0
- package/dist/DboBuilder/QueryBuilder/Functions/utils.js.map +1 -0
- package/dist/DboBuilder/TableHandler/update.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/getExistsCondition.js +5 -5
- package/dist/DboBuilder/ViewHandler/getExistsCondition.js.map +1 -1
- package/dist/DboBuilder/ViewHandler/parseFieldFilter.js.map +1 -1
- package/dist/DboBuilder/runSql/runSQL.js +1 -1
- package/dist/DboBuilder/runSql/runSQL.js.map +1 -1
- package/dist/FileManager/FileManager.js.map +1 -1
- package/dist/Logging.d.ts +1 -1
- package/dist/Logging.d.ts.map +1 -1
- package/dist/PubSubManager/PubSubManager.d.ts +0 -1
- package/dist/PubSubManager/PubSubManager.d.ts.map +1 -1
- package/dist/PubSubManager/PubSubManager.js.map +1 -1
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.d.ts +7 -2
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.d.ts.map +1 -1
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.js +21 -16
- package/dist/PubSubManager/SyncReplication/getSyncUtilFunctions.js.map +1 -1
- package/dist/PubSubManager/SyncReplication/syncData.d.ts.map +1 -1
- package/dist/PubSubManager/SyncReplication/syncData.js +16 -14
- package/dist/PubSubManager/SyncReplication/syncData.js.map +1 -1
- package/dist/TableConfig/TableConfig.js.map +1 -1
- package/dist/runClientRequest.js +1 -1
- package/dist/runClientRequest.js.map +1 -1
- package/lib/DboBuilder/DboBuilder.ts +1 -1
- package/lib/DboBuilder/QueryBuilder/Functions/Functions.ts +142 -557
- package/lib/DboBuilder/QueryBuilder/Functions/HASHING_FUNCTIONS.ts +120 -0
- package/lib/DboBuilder/QueryBuilder/Functions/TEXT_FUNCTIONS.ts +302 -0
- package/lib/DboBuilder/QueryBuilder/Functions/utils.ts +3 -0
- package/lib/DboBuilder/TableHandler/update.ts +1 -1
- package/lib/DboBuilder/ViewHandler/getExistsCondition.ts +4 -4
- package/lib/DboBuilder/ViewHandler/parseFieldFilter.ts +1 -1
- package/lib/DboBuilder/runSql/runSQL.ts +1 -1
- package/lib/FileManager/FileManager.ts +1 -1
- package/lib/Logging.ts +1 -1
- package/lib/PubSubManager/PubSubManager.ts +0 -1
- package/lib/PubSubManager/SyncReplication/getSyncUtilFunctions.ts +28 -19
- package/lib/PubSubManager/SyncReplication/syncData.ts +16 -14
- package/lib/TableConfig/TableConfig.ts +1 -1
- package/lib/runClientRequest.ts +1 -1
- package/package.json +10 -10
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as pgPromise from "pg-promise";
|
|
2
|
+
import { asNameAlias } from "../../../utils/asNameAlias";
|
|
3
|
+
import type { FunctionSpec } from "./Functions";
|
|
4
|
+
|
|
5
|
+
const pgp = pgPromise();
|
|
6
|
+
|
|
7
|
+
const MAX_COL_NUM = 1600;
|
|
8
|
+
|
|
9
|
+
export const HASHING_FUNCTIONS: FunctionSpec[] = [
|
|
10
|
+
// Hashing
|
|
11
|
+
{
|
|
12
|
+
name: "$md5_multi",
|
|
13
|
+
description: ` :[...column_names] -> md5 hash of the column content`,
|
|
14
|
+
type: "function",
|
|
15
|
+
singleColArg: false,
|
|
16
|
+
numArgs: MAX_COL_NUM,
|
|
17
|
+
getFields: (args: any[]) => args,
|
|
18
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
19
|
+
const q = pgp.as.format(
|
|
20
|
+
"md5(" +
|
|
21
|
+
args
|
|
22
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + "::text, '' )")
|
|
23
|
+
.join(" || ") +
|
|
24
|
+
")",
|
|
25
|
+
);
|
|
26
|
+
return q;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "$md5_multi_agg",
|
|
31
|
+
description: ` :[...column_names] -> md5 hash of the string aggregation of column content`,
|
|
32
|
+
type: "aggregation",
|
|
33
|
+
singleColArg: false,
|
|
34
|
+
numArgs: MAX_COL_NUM,
|
|
35
|
+
getFields: (args: any[]) => args,
|
|
36
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
37
|
+
const q = pgp.as.format(
|
|
38
|
+
"md5(string_agg(" +
|
|
39
|
+
args
|
|
40
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + "::text, '' )")
|
|
41
|
+
.join(" || ") +
|
|
42
|
+
", ','))",
|
|
43
|
+
);
|
|
44
|
+
return q;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
name: "$sha256_multi",
|
|
50
|
+
description: ` :[...column_names] -> sha256 hash of the of column content`,
|
|
51
|
+
type: "function",
|
|
52
|
+
singleColArg: false,
|
|
53
|
+
numArgs: MAX_COL_NUM,
|
|
54
|
+
getFields: (args: any[]) => args,
|
|
55
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
56
|
+
const q = pgp.as.format(
|
|
57
|
+
"encode(sha256((" +
|
|
58
|
+
args
|
|
59
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
60
|
+
.join(" || ") +
|
|
61
|
+
")::text::bytea), 'hex')",
|
|
62
|
+
);
|
|
63
|
+
return q;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "$sha256_multi_agg",
|
|
68
|
+
description: ` :[...column_names] -> sha256 hash of the string aggregation of column content`,
|
|
69
|
+
type: "aggregation",
|
|
70
|
+
singleColArg: false,
|
|
71
|
+
numArgs: MAX_COL_NUM,
|
|
72
|
+
getFields: (args: any[]) => args,
|
|
73
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
74
|
+
const q = pgp.as.format(
|
|
75
|
+
"encode(sha256(string_agg(" +
|
|
76
|
+
args
|
|
77
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
78
|
+
.join(" || ") +
|
|
79
|
+
", ',')::text::bytea), 'hex')",
|
|
80
|
+
);
|
|
81
|
+
return q;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "$sha512_multi",
|
|
86
|
+
description: ` :[...column_names] -> sha512 hash of the of column content`,
|
|
87
|
+
type: "function",
|
|
88
|
+
singleColArg: false,
|
|
89
|
+
numArgs: MAX_COL_NUM,
|
|
90
|
+
getFields: (args: any[]) => args,
|
|
91
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
92
|
+
const q = pgp.as.format(
|
|
93
|
+
"encode(sha512((" +
|
|
94
|
+
args
|
|
95
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
96
|
+
.join(" || ") +
|
|
97
|
+
")::text::bytea), 'hex')",
|
|
98
|
+
);
|
|
99
|
+
return q;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "$sha512_multi_agg",
|
|
104
|
+
description: ` :[...column_names] -> sha512 hash of the string aggregation of column content`,
|
|
105
|
+
type: "aggregation",
|
|
106
|
+
singleColArg: false,
|
|
107
|
+
numArgs: MAX_COL_NUM,
|
|
108
|
+
getFields: (args: any[]) => args,
|
|
109
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
110
|
+
const q = pgp.as.format(
|
|
111
|
+
"encode(sha512(string_agg(" +
|
|
112
|
+
args
|
|
113
|
+
.map((fname) => "COALESCE( " + asNameAlias(fname, tableAlias) + ", '' )")
|
|
114
|
+
.join(" || ") +
|
|
115
|
+
", ',')::text::bytea), 'hex')",
|
|
116
|
+
);
|
|
117
|
+
return q;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
];
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { isEmpty, postgresToTsType } from "prostgles-types";
|
|
2
|
+
import { asValue } from "../../../PubSubManager/PubSubManagerUtils";
|
|
3
|
+
import { parseFieldFilter } from "../../ViewHandler/parseFieldFilter";
|
|
4
|
+
import { asFunction } from "./utils";
|
|
5
|
+
import type { FunctionSpec } from "./Functions";
|
|
6
|
+
import * as pgPromise from "pg-promise";
|
|
7
|
+
import { asNameAlias } from "../../../utils/asNameAlias";
|
|
8
|
+
const pgp = pgPromise();
|
|
9
|
+
|
|
10
|
+
export const TEXT_FUNCTIONS: FunctionSpec[] = [
|
|
11
|
+
{
|
|
12
|
+
name: "$left",
|
|
13
|
+
description: ` :[column_name, number] -> substring`,
|
|
14
|
+
type: "function",
|
|
15
|
+
numArgs: 2,
|
|
16
|
+
singleColArg: false,
|
|
17
|
+
getFields: (args: any[]) => [args[0]],
|
|
18
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
19
|
+
return pgp.as.format("LEFT(" + asNameAlias(args[0], tableAlias) + ", $1)", [args[1]]);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "$column",
|
|
24
|
+
description: ` :[column_name] -> Returns the column value as is`,
|
|
25
|
+
type: "function",
|
|
26
|
+
numArgs: 1,
|
|
27
|
+
singleColArg: false,
|
|
28
|
+
getFields: (args: any[]) => [args[0]],
|
|
29
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
30
|
+
const aliasedColumnName = args[0];
|
|
31
|
+
if (!aliasedColumnName) {
|
|
32
|
+
throw `$column: column_name is required`;
|
|
33
|
+
}
|
|
34
|
+
return pgp.as.format(asNameAlias(aliasedColumnName, tableAlias));
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "$unnest_words",
|
|
39
|
+
description: ` :[column_name] -> Splits string at spaces`,
|
|
40
|
+
type: "function",
|
|
41
|
+
numArgs: 1,
|
|
42
|
+
singleColArg: true,
|
|
43
|
+
getFields: (args: any[]) => [args[0]],
|
|
44
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
45
|
+
return pgp.as.format(
|
|
46
|
+
"unnest(string_to_array(" + asNameAlias(args[0], tableAlias) + "::TEXT , ' '))",
|
|
47
|
+
); //, [args[1]]
|
|
48
|
+
},
|
|
49
|
+
} satisfies FunctionSpec,
|
|
50
|
+
{
|
|
51
|
+
name: "$right",
|
|
52
|
+
description: ` :[column_name, number] -> substring`,
|
|
53
|
+
type: "function",
|
|
54
|
+
numArgs: 2,
|
|
55
|
+
singleColArg: false,
|
|
56
|
+
getFields: (args: any[]) => [args[0]],
|
|
57
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
58
|
+
return pgp.as.format("RIGHT(" + asNameAlias(args[0], tableAlias) + ", $1)", [args[1]]);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
name: "$to_char",
|
|
64
|
+
type: "function",
|
|
65
|
+
description: ` :[column_name, format<string>] -> format dates and strings. Eg: [current_timestamp, 'HH12:MI:SS']`,
|
|
66
|
+
singleColArg: false,
|
|
67
|
+
numArgs: 2,
|
|
68
|
+
getFields: (args: any[]) => [args[0]],
|
|
69
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
70
|
+
if (args.length === 3) {
|
|
71
|
+
return pgp.as.format("to_char(" + asNameAlias(args[0], tableAlias) + ", $2, $3)", [
|
|
72
|
+
args[0],
|
|
73
|
+
args[1],
|
|
74
|
+
args[2],
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
return pgp.as.format("to_char(" + asNameAlias(args[0], tableAlias) + ", $2)", [
|
|
78
|
+
args[0],
|
|
79
|
+
args[1],
|
|
80
|
+
]);
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/* Text col and value funcs */
|
|
85
|
+
...["position", "position_lower"].map((funcName) =>
|
|
86
|
+
asFunction({
|
|
87
|
+
name: "$" + funcName,
|
|
88
|
+
type: "function",
|
|
89
|
+
numArgs: 1,
|
|
90
|
+
singleColArg: false,
|
|
91
|
+
getFields: (args: any[]) => [args[1]],
|
|
92
|
+
getQuery: ({ args, tableAliasRaw: tableAlias }) => {
|
|
93
|
+
let a1 = asValue(args[0]),
|
|
94
|
+
a2 = asNameAlias(args[1], tableAlias);
|
|
95
|
+
if (funcName === "position_lower") {
|
|
96
|
+
a1 = `LOWER(${a1}::text)`;
|
|
97
|
+
a2 = `LOWER(${a2}::text)`;
|
|
98
|
+
}
|
|
99
|
+
return `position( ${a1} IN ${a2} )`;
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
...["template_string"].map((funcName) =>
|
|
104
|
+
asFunction({
|
|
105
|
+
name: "$" + funcName,
|
|
106
|
+
type: "function",
|
|
107
|
+
numArgs: 1,
|
|
108
|
+
minCols: 0,
|
|
109
|
+
singleColArg: false,
|
|
110
|
+
getFields: (args: any[]) => [] as string[], // Fields not validated because we'll use the allowed ones anyway
|
|
111
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias }) => {
|
|
112
|
+
if (typeof args[0] !== "string")
|
|
113
|
+
throw "First argument must be a string. E.g.: '{col1} ..text {col2} ...' ";
|
|
114
|
+
|
|
115
|
+
const rawValue = args[0];
|
|
116
|
+
let finalValue = rawValue;
|
|
117
|
+
const usedColumns = allowedFields.filter((fName) => rawValue.includes(`{${fName}}`));
|
|
118
|
+
usedColumns.forEach((colName, idx) => {
|
|
119
|
+
finalValue = finalValue.split(`{${colName}}`).join(`%${idx + 1}$s`);
|
|
120
|
+
});
|
|
121
|
+
finalValue = asValue(finalValue);
|
|
122
|
+
|
|
123
|
+
if (usedColumns.length) {
|
|
124
|
+
return `format(${finalValue}, ${usedColumns.map((c) => `${asNameAlias(c, tableAlias)}::TEXT`).join(", ")})`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `format(${finalValue})`;
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
|
|
132
|
+
/** Custom highlight -> myterm => ['some text and', ['myterm'], ' and some other text']
|
|
133
|
+
* (fields: "*" | string[], term: string, { edgeTruncate: number = -1; noFields: boolean = false }) => string | (string | [string])[]
|
|
134
|
+
* edgeTruncate = maximum extra characters left and right of matches
|
|
135
|
+
* noFields = exclude field names in search
|
|
136
|
+
* */
|
|
137
|
+
asFunction({
|
|
138
|
+
name: "$term_highlight" /* */,
|
|
139
|
+
description: ` :[column_names<string[] | "*">, search_term<string>, opts?<{ returnIndex?: number; edgeTruncate?: number; noFields?: boolean }>] -> get case-insensitive text match highlight`,
|
|
140
|
+
type: "function",
|
|
141
|
+
numArgs: 1,
|
|
142
|
+
singleColArg: true,
|
|
143
|
+
canBeUsedForFilter: true,
|
|
144
|
+
getFields: (args: any[]) => args[0],
|
|
145
|
+
getQuery: ({ allowedFields, args, tableAliasRaw: tableAlias, allColumns }) => {
|
|
146
|
+
const cols = parseFieldFilter(args[0], false, allowedFields);
|
|
147
|
+
let term = args[1];
|
|
148
|
+
const rawTerm = args[1];
|
|
149
|
+
const { edgeTruncate, noFields = false, returnType, matchCase = false } = args[2] || {};
|
|
150
|
+
if (!isEmpty(args[2])) {
|
|
151
|
+
const keys = Object.keys(args[2]);
|
|
152
|
+
const validKeys = ["edgeTruncate", "noFields", "returnType", "matchCase"];
|
|
153
|
+
const bad_keys = keys.filter((k) => !validKeys.includes(k));
|
|
154
|
+
if (bad_keys.length)
|
|
155
|
+
throw (
|
|
156
|
+
"Invalid options provided for $term_highlight. Expecting one of: " +
|
|
157
|
+
validKeys.join(", ")
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!cols.length) throw "Cols are empty/invalid";
|
|
161
|
+
if (typeof term !== "string") throw "Non string term provided: " + term;
|
|
162
|
+
if (edgeTruncate !== undefined && (!Number.isInteger(edgeTruncate) || edgeTruncate < -1))
|
|
163
|
+
throw "Invalid edgeTruncate. expecting a positive integer";
|
|
164
|
+
if (typeof noFields !== "boolean") throw "Invalid noFields. expecting boolean";
|
|
165
|
+
const RETURN_TYPES = ["index", "boolean", "object"];
|
|
166
|
+
if (returnType && !RETURN_TYPES.includes(returnType)) {
|
|
167
|
+
throw `returnType can only be one of: ${RETURN_TYPES}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const makeTextMatcherArray = (rawText: string, _term: string) => {
|
|
171
|
+
let matchText = rawText,
|
|
172
|
+
term = _term;
|
|
173
|
+
if (!matchCase) {
|
|
174
|
+
matchText = `LOWER(${rawText})`;
|
|
175
|
+
term = `LOWER(${term})`;
|
|
176
|
+
}
|
|
177
|
+
let leftStr = `substr(${rawText}, 1, position(${term} IN ${matchText}) - 1 )`,
|
|
178
|
+
rightStr = `substr(${rawText}, position(${term} IN ${matchText}) + length(${term}) )`;
|
|
179
|
+
if (edgeTruncate) {
|
|
180
|
+
leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
|
|
181
|
+
rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
|
|
182
|
+
}
|
|
183
|
+
return `
|
|
184
|
+
CASE WHEN position(${term} IN ${matchText}) > 0 AND ${term} <> ''
|
|
185
|
+
THEN array_to_json(ARRAY[
|
|
186
|
+
to_json( ${leftStr}::TEXT ),
|
|
187
|
+
array_to_json(
|
|
188
|
+
ARRAY[substr(${rawText}, position(${term} IN ${matchText}), length(${term}) )::TEXT ]
|
|
189
|
+
),
|
|
190
|
+
to_json(${rightStr}::TEXT )
|
|
191
|
+
])
|
|
192
|
+
ELSE
|
|
193
|
+
array_to_json(ARRAY[(${rawText})::TEXT])
|
|
194
|
+
END
|
|
195
|
+
`;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const colRaw =
|
|
199
|
+
"( " +
|
|
200
|
+
cols
|
|
201
|
+
.map(
|
|
202
|
+
(c) =>
|
|
203
|
+
`${noFields ? "" : asValue(c + ": ") + " || "} COALESCE(${asNameAlias(c, tableAlias)}::TEXT, '')`,
|
|
204
|
+
)
|
|
205
|
+
.join(" || ', ' || ") +
|
|
206
|
+
" )";
|
|
207
|
+
let col = colRaw;
|
|
208
|
+
term = asValue(term);
|
|
209
|
+
if (!matchCase) {
|
|
210
|
+
col = "LOWER" + col;
|
|
211
|
+
term = `LOWER(${term})`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let leftStr = `substr(${colRaw}, 1, position(${term} IN ${col}) - 1 )`,
|
|
215
|
+
rightStr = `substr(${colRaw}, position(${term} IN ${col}) + length(${term}) )`;
|
|
216
|
+
if (edgeTruncate) {
|
|
217
|
+
leftStr = `RIGHT(${leftStr}, ${asValue(edgeTruncate)})`;
|
|
218
|
+
rightStr = `LEFT(${rightStr}, ${asValue(edgeTruncate)})`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// console.log(col);
|
|
222
|
+
let res = "";
|
|
223
|
+
if (returnType === "index") {
|
|
224
|
+
res = `CASE WHEN position(${term} IN ${col}) > 0 THEN position(${term} IN ${col}) - 1 ELSE -1 END`;
|
|
225
|
+
|
|
226
|
+
// } else if(returnType === "boolean"){
|
|
227
|
+
// res = `CASE WHEN position(${term} IN ${col}) > 0 THEN TRUE ELSE FALSE END`;
|
|
228
|
+
} else if (returnType === "object" || returnType === "boolean") {
|
|
229
|
+
const hasChars = Boolean(rawTerm && /[a-z]/i.test(rawTerm));
|
|
230
|
+
const validCols = cols
|
|
231
|
+
.map((c) => {
|
|
232
|
+
const colInfo = allColumns.find((ac) => ac.name === c);
|
|
233
|
+
return {
|
|
234
|
+
key: c,
|
|
235
|
+
colInfo,
|
|
236
|
+
};
|
|
237
|
+
})
|
|
238
|
+
.filter((c) => c.colInfo && c.colInfo.udt_name !== "bytea");
|
|
239
|
+
|
|
240
|
+
const _cols = validCols.filter(
|
|
241
|
+
(c) =>
|
|
242
|
+
/** Exclude numeric columns when the search tern contains a character */
|
|
243
|
+
!hasChars || postgresToTsType(c.colInfo!.udt_name) !== "number",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
/** This will break GROUP BY (non-integer constant in GROUP BY) */
|
|
247
|
+
if (!_cols.length) {
|
|
248
|
+
if (validCols.length && hasChars)
|
|
249
|
+
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) `;
|
|
250
|
+
return returnType === "boolean" ? "FALSE" : "NULL";
|
|
251
|
+
}
|
|
252
|
+
res = `CASE
|
|
253
|
+
${_cols
|
|
254
|
+
.map((c) => {
|
|
255
|
+
const colNameEscaped = asNameAlias(c.key, tableAlias);
|
|
256
|
+
let colSelect = `${colNameEscaped}::TEXT`;
|
|
257
|
+
const isTstamp = c.colInfo?.udt_name.startsWith("timestamp");
|
|
258
|
+
if (isTstamp || c.colInfo?.udt_name === "date") {
|
|
259
|
+
colSelect = `( CASE WHEN ${colNameEscaped} IS NULL THEN ''
|
|
260
|
+
ELSE concat_ws(' ',
|
|
261
|
+
trim(to_char(${colNameEscaped}, 'YYYY-MM-DD HH24:MI:SS')),
|
|
262
|
+
trim(to_char(${colNameEscaped}, 'Day Month')),
|
|
263
|
+
'Q' || trim(to_char(${colNameEscaped}, 'Q')),
|
|
264
|
+
'WK' || trim(to_char(${colNameEscaped}, 'WW'))
|
|
265
|
+
) END)`;
|
|
266
|
+
}
|
|
267
|
+
const colTxt = `COALESCE(${colSelect}, '')`; // position(${term} IN ${colTxt}) > 0
|
|
268
|
+
if (returnType === "boolean") {
|
|
269
|
+
return `
|
|
270
|
+
WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue("%" + rawTerm + "%")}
|
|
271
|
+
THEN TRUE
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
return `
|
|
275
|
+
WHEN ${colTxt} ${matchCase ? "LIKE" : "ILIKE"} ${asValue("%" + rawTerm + "%")}
|
|
276
|
+
THEN json_build_object(
|
|
277
|
+
${asValue(c.key)},
|
|
278
|
+
${makeTextMatcherArray(colTxt, term)}
|
|
279
|
+
)::jsonb
|
|
280
|
+
`;
|
|
281
|
+
})
|
|
282
|
+
.join(" ")}
|
|
283
|
+
ELSE ${returnType === "boolean" ? "FALSE" : "NULL"}
|
|
284
|
+
|
|
285
|
+
END`;
|
|
286
|
+
|
|
287
|
+
// console.log(res)
|
|
288
|
+
} else {
|
|
289
|
+
/* If no match or empty search THEN return full row as string within first array element */
|
|
290
|
+
res = `CASE WHEN position(${term} IN ${col}) > 0 AND ${term} <> '' THEN array_to_json(ARRAY[
|
|
291
|
+
to_json( ${leftStr}::TEXT ),
|
|
292
|
+
array_to_json(
|
|
293
|
+
ARRAY[substr(${colRaw}, position(${term} IN ${col}), length(${term}) )::TEXT ]
|
|
294
|
+
),
|
|
295
|
+
to_json(${rightStr}::TEXT )
|
|
296
|
+
]) ELSE array_to_json(ARRAY[(${colRaw})::TEXT]) END`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return res;
|
|
300
|
+
},
|
|
301
|
+
} satisfies FunctionSpec),
|
|
302
|
+
];
|
|
@@ -112,7 +112,7 @@ export async function update(
|
|
|
112
112
|
}
|
|
113
113
|
await Promise.all(
|
|
114
114
|
nestedInserts.map(async (nestedInsert) => {
|
|
115
|
-
const nesedTableHandler = finalDBtx[nestedInsert.tableName]
|
|
115
|
+
const nesedTableHandler = finalDBtx[nestedInsert.tableName];
|
|
116
116
|
if (!nesedTableHandler)
|
|
117
117
|
throw `nestedInsert Tablehandler not found for ${nestedInsert.tableName}`;
|
|
118
118
|
const refTableRules =
|
|
@@ -21,7 +21,7 @@ export async function getExistsCondition(
|
|
|
21
21
|
if (Object.keys(targetTableFilter).find((fk) => EXISTS_KEYS.includes(fk as EXISTS_KEY))) {
|
|
22
22
|
throw {
|
|
23
23
|
stack: ["prepareExistCondition()"],
|
|
24
|
-
message: "Nested exists
|
|
24
|
+
message: "Nested exists disallowed",
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -35,16 +35,16 @@ export async function getExistsCondition(
|
|
|
35
35
|
}
|
|
36
36
|
const targetTable = eConfig.isJoined ? eConfig.parsedPath.at(-1)!.table : eConfig.targetTable;
|
|
37
37
|
if (localParams?.clientReq && this.dboBuilder.publishParser) {
|
|
38
|
-
t2Rules =
|
|
38
|
+
t2Rules = await this.dboBuilder.publishParser.getValidatedRequestRuleWusr(
|
|
39
39
|
{
|
|
40
40
|
tableName: targetTable,
|
|
41
41
|
command: "find",
|
|
42
42
|
clientReq: localParams.clientReq,
|
|
43
43
|
},
|
|
44
44
|
localParams.scope,
|
|
45
|
-
)
|
|
45
|
+
);
|
|
46
46
|
|
|
47
|
-
if (!t2Rules
|
|
47
|
+
if (!t2Rules.select) throw "Disallowed";
|
|
48
48
|
({ forcedFilter, filterFields } = t2Rules.select);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -37,7 +37,7 @@ export async function runSQL(
|
|
|
37
37
|
throw "Not allowed to run SQL";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const { returnType, allowListen, hasParams = true }: SQLOptions = options || ({}
|
|
40
|
+
const { returnType, allowListen, hasParams = true }: SQLOptions = options || ({});
|
|
41
41
|
const { socket } = localParams?.clientReq ?? {};
|
|
42
42
|
|
|
43
43
|
const db = localParams?.tx?.t || this.db;
|
|
@@ -340,7 +340,7 @@ export const getFileType = async (
|
|
|
340
340
|
/* Set correct/missing extension */
|
|
341
341
|
const nameExt = fileNameMime.ext;
|
|
342
342
|
if (["xml", "txt", "csv", "tsv", "svg", "sql"].includes(nameExt)) {
|
|
343
|
-
return fileNameMime
|
|
343
|
+
return fileNameMime;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
throw new Error("Could not get the file type from file buffer");
|
package/lib/Logging.ts
CHANGED
|
@@ -104,8 +104,8 @@ export const getSyncUtilFunctions = ({
|
|
|
104
104
|
getClientData = (from_synced: number | undefined, offset = 0): Promise<AnyObject[]> => {
|
|
105
105
|
return new Promise(async (resolve, reject) => {
|
|
106
106
|
const onPullRequest = {
|
|
107
|
-
from_synced
|
|
108
|
-
offset
|
|
107
|
+
from_synced,
|
|
108
|
+
offset,
|
|
109
109
|
limit: batch_size,
|
|
110
110
|
to_synced: undefined,
|
|
111
111
|
};
|
|
@@ -169,13 +169,13 @@ export const getSyncUtilFunctions = ({
|
|
|
169
169
|
/**
|
|
170
170
|
* Upserts the given client data where synced_field is higher than on server
|
|
171
171
|
*/
|
|
172
|
-
upsertData = async (data: AnyObject[]) => {
|
|
172
|
+
upsertData = async (data: AnyObject[], source: "WAL" | "client") => {
|
|
173
173
|
const start = Date.now();
|
|
174
174
|
const result = await pubSubManager.dboBuilder
|
|
175
175
|
.getTX(async (dbTX) => {
|
|
176
176
|
const tableHandlerTx = dbTX[table_name] as TableHandler;
|
|
177
|
-
const existingData = await tableHandlerTx.find(
|
|
178
|
-
{ $or: data.map((
|
|
177
|
+
const existingData = (await tableHandlerTx.find(
|
|
178
|
+
{ $or: data.map((row) => pickKeys(row, id_fields)) },
|
|
179
179
|
{
|
|
180
180
|
select: [synced_field, ...id_fields],
|
|
181
181
|
orderBy: orderByAsc,
|
|
@@ -183,11 +183,16 @@ export const getSyncUtilFunctions = ({
|
|
|
183
183
|
undefined,
|
|
184
184
|
table_rules,
|
|
185
185
|
{ clientReq: { socket } },
|
|
186
|
+
)) as AnyObject[];
|
|
187
|
+
let rowsToInsert = data.filter(
|
|
188
|
+
(incomingRow) =>
|
|
189
|
+
!existingData.find((existingRow) => rowsIdsMatch(existingRow, incomingRow)),
|
|
186
190
|
);
|
|
187
|
-
let
|
|
188
|
-
let rowsToUpdate = data.filter((d) =>
|
|
191
|
+
let rowsToUpdate = data.filter((incomingRow) =>
|
|
189
192
|
existingData.find(
|
|
190
|
-
(
|
|
193
|
+
(existingRow) =>
|
|
194
|
+
rowsIdsMatch(existingRow, incomingRow) &&
|
|
195
|
+
Number(existingRow[synced_field]) < Number(incomingRow[synced_field]),
|
|
191
196
|
),
|
|
192
197
|
);
|
|
193
198
|
|
|
@@ -227,7 +232,7 @@ export const getSyncUtilFunctions = ({
|
|
|
227
232
|
})
|
|
228
233
|
.then(({ inserts, updates }) => {
|
|
229
234
|
log(
|
|
230
|
-
`upsertData: inserted( ${inserts.length} ) updated( ${updates.length} ) total( ${data.length} ) \n last insert ${JSON.stringify(inserts.at(-1))} \n last update ${JSON.stringify(updates.at(-1))}`,
|
|
235
|
+
`upsertData (from ${source}): inserted( ${inserts.length} ) updated( ${updates.length} ) total( ${data.length} ) \n last insert ${JSON.stringify(inserts.at(-1))} \n last update ${JSON.stringify(updates.at(-1))}`,
|
|
231
236
|
);
|
|
232
237
|
return {
|
|
233
238
|
inserted: inserts.length,
|
|
@@ -263,17 +268,22 @@ export const getSyncUtilFunctions = ({
|
|
|
263
268
|
* Pushes the given data to client
|
|
264
269
|
* @param isSynced = true if
|
|
265
270
|
*/
|
|
266
|
-
pushData = async (
|
|
271
|
+
pushData = async (
|
|
272
|
+
request: { state: "syncing-data"; data: AnyObject[] } | { state: "synced" },
|
|
273
|
+
) => {
|
|
274
|
+
const items = request.state === "syncing-data" ? request.data : undefined;
|
|
267
275
|
const start = Date.now();
|
|
268
276
|
const result = await new Promise<{
|
|
269
277
|
pushed: number;
|
|
270
278
|
}>(async (resolve, reject) => {
|
|
271
279
|
const resp = await handlers.UpdateRequest(
|
|
272
|
-
|
|
280
|
+
request.state === "synced" ?
|
|
281
|
+
{ state: "synced", isSynced: true }
|
|
282
|
+
: { state: "syncing", data: request.data },
|
|
273
283
|
);
|
|
274
284
|
if (resp.success) {
|
|
275
285
|
// console.log("PUSHED to client: fr/lr", data[0], data[data.length - 1]);
|
|
276
|
-
resolve({ pushed:
|
|
286
|
+
resolve({ pushed: items?.length ?? 0 });
|
|
277
287
|
} else {
|
|
278
288
|
reject(resp);
|
|
279
289
|
console.error("Unexpected response");
|
|
@@ -284,7 +294,7 @@ export const getSyncUtilFunctions = ({
|
|
|
284
294
|
type: "sync",
|
|
285
295
|
command: "pushData",
|
|
286
296
|
tableName: sync.table_name,
|
|
287
|
-
rows:
|
|
297
|
+
rows: items?.length ?? 0,
|
|
288
298
|
socketId: socket_id,
|
|
289
299
|
duration: Date.now() - start,
|
|
290
300
|
sid: sync.sid,
|
|
@@ -392,7 +402,6 @@ export const getSyncUtilFunctions = ({
|
|
|
392
402
|
);
|
|
393
403
|
}
|
|
394
404
|
sync.lr = lastRow;
|
|
395
|
-
sync.last_synced = +sync.lr[synced_field];
|
|
396
405
|
},
|
|
397
406
|
/**
|
|
398
407
|
* Will push pull sync between client and server from a given from_synced value
|
|
@@ -415,7 +424,7 @@ export const getSyncUtilFunctions = ({
|
|
|
415
424
|
const clientData = await getClientData(min_synced, offset);
|
|
416
425
|
|
|
417
426
|
if (clientData.length) {
|
|
418
|
-
const res = await upsertData(clientData);
|
|
427
|
+
const res = await upsertData(clientData, "client");
|
|
419
428
|
inserted += res.inserted;
|
|
420
429
|
updated += res.updated;
|
|
421
430
|
}
|
|
@@ -425,7 +434,6 @@ export const getSyncUtilFunctions = ({
|
|
|
425
434
|
serverData = await getServerData(min_synced, offset);
|
|
426
435
|
} catch (e) {
|
|
427
436
|
console.trace("sync getServerData err", e);
|
|
428
|
-
// await pushData(undefined, undefined, "Internal error. Check server logs");
|
|
429
437
|
throw " d";
|
|
430
438
|
}
|
|
431
439
|
|
|
@@ -455,9 +463,10 @@ export const getSyncUtilFunctions = ({
|
|
|
455
463
|
);
|
|
456
464
|
});
|
|
457
465
|
if (forClient.length) {
|
|
458
|
-
const res = await pushData(
|
|
459
|
-
|
|
460
|
-
|
|
466
|
+
const res = await pushData({
|
|
467
|
+
state: "syncing-data",
|
|
468
|
+
data: forClient.filter((d) => !sync.wal || !sync.wal.isInHistory(d)),
|
|
469
|
+
});
|
|
461
470
|
pushed += res.pushed;
|
|
462
471
|
}
|
|
463
472
|
|