prostgles-server 2.0.178 → 2.0.181
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/AuthHandler.d.ts +4 -4
- package/dist/AuthHandler.d.ts.map +1 -1
- package/dist/DBSchemaBuilder.d.ts +6 -6
- package/dist/DBSchemaBuilder.d.ts.map +1 -1
- package/dist/DBSchemaBuilder.js +38 -15
- package/dist/DBSchemaBuilder.js.map +1 -1
- package/dist/DboBuilder.d.ts +20 -21
- package/dist/DboBuilder.d.ts.map +1 -1
- package/dist/DboBuilder.js +7 -2
- package/dist/DboBuilder.js.map +1 -1
- package/dist/Prostgles.d.ts +8 -10
- package/dist/Prostgles.d.ts.map +1 -1
- package/dist/Prostgles.js.map +1 -1
- package/dist/PubSubManager.js +1 -1
- package/dist/PubSubManager.js.map +1 -1
- package/dist/PublishParser.d.ts +37 -37
- package/dist/PublishParser.d.ts.map +1 -1
- package/dist/PublishParser.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/lib/AuthHandler.d.ts +148 -0
- package/lib/AuthHandler.d.ts.map +1 -0
- package/lib/AuthHandler.js +411 -0
- package/lib/AuthHandler.ts +3 -3
- package/lib/DBEventsManager.d.ts +38 -0
- package/lib/DBEventsManager.d.ts.map +1 -0
- package/lib/DBEventsManager.js +136 -0
- package/lib/DBSchemaBuilder.d.ts +11 -0
- package/lib/DBSchemaBuilder.d.ts.map +1 -0
- package/lib/DBSchemaBuilder.js +108 -0
- package/lib/DBSchemaBuilder.ts +75 -33
- package/lib/DboBuilder.d.ts +428 -0
- package/lib/DboBuilder.d.ts.map +1 -0
- package/lib/DboBuilder.js +3083 -0
- package/lib/DboBuilder.ts +33 -27
- package/lib/FileManager.d.ts +168 -0
- package/lib/FileManager.d.ts.map +1 -0
- package/lib/FileManager.js +474 -0
- package/lib/Filtering.d.ts +15 -0
- package/lib/Filtering.d.ts.map +1 -0
- package/lib/Filtering.js +299 -0
- package/lib/PostgresNotifListenManager.d.ts +27 -0
- package/lib/PostgresNotifListenManager.d.ts.map +1 -0
- package/lib/PostgresNotifListenManager.js +122 -0
- package/lib/Prostgles.d.ts +193 -0
- package/lib/Prostgles.d.ts.map +1 -0
- package/lib/Prostgles.js +579 -0
- package/lib/Prostgles.ts +6 -6
- package/lib/PubSubManager.d.ts +157 -0
- package/lib/PubSubManager.d.ts.map +1 -0
- package/lib/PubSubManager.js +1400 -0
- package/lib/PubSubManager.ts +1 -1
- package/lib/PublishParser.d.ts +262 -0
- package/lib/PublishParser.d.ts.map +1 -0
- package/lib/PublishParser.js +390 -0
- package/lib/PublishParser.ts +39 -38
- package/lib/QueryBuilder.d.ts +124 -0
- package/lib/QueryBuilder.d.ts.map +1 -0
- package/lib/QueryBuilder.js +1349 -0
- package/lib/SyncReplication.d.ts +34 -0
- package/lib/SyncReplication.d.ts.map +1 -0
- package/lib/SyncReplication.js +411 -0
- package/lib/TableConfig.d.ts +175 -0
- package/lib/TableConfig.d.ts.map +1 -0
- package/lib/TableConfig.js +231 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +45 -0
- package/lib/index.ts +3 -4
- package/lib/shortestPath.d.ts +10 -0
- package/lib/shortestPath.d.ts.map +1 -0
- package/lib/shortestPath.js +111 -0
- package/lib/utils.d.ts +2 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +5 -0
- package/package.json +3 -3
- package/tests/client/PID.txt +1 -1
- package/tests/client/index.d.ts +1 -1
- package/tests/client/index.d.ts.map +1 -1
- package/tests/client/package-lock.json +15 -15
- package/tests/client/package.json +1 -1
- package/tests/client_only_queries.d.ts +4 -0
- package/tests/client_only_queries.d.ts.map +1 -0
- package/tests/isomorphic_queries.d.ts +6 -0
- package/tests/isomorphic_queries.d.ts.map +1 -0
- package/tests/server/DBoGenerated.d.ts +227 -323
- package/tests/server/dboTypeCheck.d.ts +2 -0
- package/tests/server/dboTypeCheck.d.ts.map +1 -0
- package/tests/server/dboTypeCheck.js +16 -0
- package/tests/server/dboTypeCheck.ts +20 -0
- package/tests/server/index.d.ts +2 -0
- package/tests/server/index.d.ts.map +1 -0
- package/tests/server/index.js +11 -11
- package/tests/server/index.ts +23 -16
- package/tests/server/package-lock.json +5 -5
- package/tests/server/publishTypeCheck.d.ts +2 -0
- package/tests/server/publishTypeCheck.d.ts.map +1 -0
- package/tests/server/publishTypeCheck.js +120 -0
- package/tests/server/publishTypeCheck.ts +129 -0
- package/tests/server/tsconfig.json +4 -5
- package/tests/server_only_queries.d.ts +2 -0
- package/tests/server_only_queries.d.ts.map +1 -0
|
@@ -0,0 +1,3083 @@
|
|
|
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
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
23
|
+
if (mod && mod.__esModule) return mod;
|
|
24
|
+
var result = {};
|
|
25
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
26
|
+
__setModuleDefault(result, mod);
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
var _a;
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.postgresToTsType = exports.isPlainObject = exports.DboBuilder = exports.TableHandler = exports.ViewHandler = exports.EXISTS_KEYS = exports.escapeTSNames = exports.pgp = exports.getUpdateFilter = void 0;
|
|
32
|
+
const Bluebird = __importStar(require("bluebird"));
|
|
33
|
+
// declare global { export interface Promise<T> extends Bluebird<T> {} }
|
|
34
|
+
const pgPromise = __importStar(require("pg-promise"));
|
|
35
|
+
const { ParameterizedQuery: PQ } = require('pg-promise');
|
|
36
|
+
const prostgles_types_1 = require("prostgles-types");
|
|
37
|
+
const getUpdateFilter = (args) => {
|
|
38
|
+
const { filter, forcedFilter, $and_key } = args;
|
|
39
|
+
let result = { ...filter };
|
|
40
|
+
if (forcedFilter) {
|
|
41
|
+
return {
|
|
42
|
+
[$and_key]: [forcedFilter, filter].filter(prostgles_types_1.isDefined)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
exports.getUpdateFilter = getUpdateFilter;
|
|
48
|
+
const utils_1 = require("./utils");
|
|
49
|
+
const QueryBuilder_1 = require("./QueryBuilder");
|
|
50
|
+
const PubSubManager_1 = require("./PubSubManager");
|
|
51
|
+
const Filtering_1 = require("./Filtering");
|
|
52
|
+
exports.pgp = pgPromise({
|
|
53
|
+
promiseLib: Bluebird
|
|
54
|
+
// ,query: function (e) { console.log({psql: e.query, params: e.params}); }
|
|
55
|
+
});
|
|
56
|
+
function replaceNonAlphaNumeric(string, replacement = "_") {
|
|
57
|
+
return string.replace(/[\W_]+/g, replacement);
|
|
58
|
+
}
|
|
59
|
+
function capitalizeFirstLetter(string, nonalpha_replacement) {
|
|
60
|
+
const str = replaceNonAlphaNumeric(string, nonalpha_replacement);
|
|
61
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
62
|
+
}
|
|
63
|
+
function snakify(str, capitalize = false) {
|
|
64
|
+
return str.split("").map((c, i) => {
|
|
65
|
+
if (!i) {
|
|
66
|
+
if (capitalize)
|
|
67
|
+
c = c.toUpperCase();
|
|
68
|
+
if (c.match(/[^a-z_A-Z]/)) {
|
|
69
|
+
return ((capitalize) ? "D_" : "_") + c.charCodeAt(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (c.match(/[^a-zA-Z_0-9]/)) {
|
|
74
|
+
return "_" + c.charCodeAt(0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return c;
|
|
78
|
+
}).join("");
|
|
79
|
+
}
|
|
80
|
+
function canBeUsedAsIsInTypescript(str) {
|
|
81
|
+
if (!str)
|
|
82
|
+
return false;
|
|
83
|
+
const isAlphaNumericOrUnderline = str.match(/^[a-z0-9_]+$/i);
|
|
84
|
+
const startsWithCharOrUnderscore = str[0].match(/^[a-z_]+$/i);
|
|
85
|
+
return Boolean(isAlphaNumericOrUnderline && startsWithCharOrUnderscore);
|
|
86
|
+
}
|
|
87
|
+
function escapeTSNames(str, capitalize = false) {
|
|
88
|
+
let res = str;
|
|
89
|
+
res = (capitalize ? str[0].toUpperCase() : str[0]) + str.slice(1);
|
|
90
|
+
if (canBeUsedAsIsInTypescript(res))
|
|
91
|
+
return res;
|
|
92
|
+
return JSON.stringify(res);
|
|
93
|
+
}
|
|
94
|
+
exports.escapeTSNames = escapeTSNames;
|
|
95
|
+
const shortestPath_1 = require("./shortestPath");
|
|
96
|
+
/* DEBUG CLIENT ERRORS HERE */
|
|
97
|
+
function makeErr(err, localParams, view, allowedKeys) {
|
|
98
|
+
// console.trace(err)
|
|
99
|
+
if (process.env.TEST_TYPE || process.env.PRGL_DEBUG) {
|
|
100
|
+
console.trace(err);
|
|
101
|
+
}
|
|
102
|
+
const errObject = {
|
|
103
|
+
...((!localParams || !localParams.socket) ? err : {}),
|
|
104
|
+
...(0, PubSubManager_1.pickKeys)(err, ["column", "code", "table", "constraint"]),
|
|
105
|
+
...(err && err.toString ? { txt: err.toString() } : {}),
|
|
106
|
+
code_info: sqlErrCodeToMsg(err.code)
|
|
107
|
+
};
|
|
108
|
+
if (view?.dboBuilder?.constraints && errObject.constraint && !errObject.column) {
|
|
109
|
+
const constraint = view.dboBuilder.constraints
|
|
110
|
+
.find(c => c.conname === errObject.constraint && c.relname === view.name);
|
|
111
|
+
if (constraint) {
|
|
112
|
+
const cols = view.columns.filter(c => (!allowedKeys || allowedKeys.includes(c.name)) &&
|
|
113
|
+
constraint.conkey.includes(c.ordinal_position));
|
|
114
|
+
if (cols.length) {
|
|
115
|
+
errObject.column = cols[0].name;
|
|
116
|
+
errObject.columns = cols.map(c => c.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Promise.reject(errObject);
|
|
121
|
+
}
|
|
122
|
+
exports.EXISTS_KEYS = ["$exists", "$notExists", "$existsJoined", "$notExistsJoined"];
|
|
123
|
+
const FILTER_FUNCS = QueryBuilder_1.FUNCTIONS.filter(f => f.canBeUsedForFilter);
|
|
124
|
+
function parseError(e) {
|
|
125
|
+
// console.trace("INTERNAL ERROR: ", e);
|
|
126
|
+
let res = (!Object.keys(e || {}).length ? e : (e && e.toString) ? e.toString() : e);
|
|
127
|
+
if (isPlainObject(e))
|
|
128
|
+
res = JSON.stringify(e, null, 2);
|
|
129
|
+
return res;
|
|
130
|
+
}
|
|
131
|
+
class ColSet {
|
|
132
|
+
constructor(columns, tableName) {
|
|
133
|
+
this.opts = { columns, tableName, colNames: columns.map(c => c.name) };
|
|
134
|
+
}
|
|
135
|
+
async getRow(data, allowedCols, validate) {
|
|
136
|
+
const badCol = allowedCols.find(c => !this.opts.colNames.includes(c));
|
|
137
|
+
if (!allowedCols || badCol) {
|
|
138
|
+
throw "Missing or unexpected columns: " + badCol;
|
|
139
|
+
}
|
|
140
|
+
if ((0, prostgles_types_1.isEmpty)(data))
|
|
141
|
+
throw "No data";
|
|
142
|
+
let row = (0, PubSubManager_1.pickKeys)(data, allowedCols);
|
|
143
|
+
if (validate) {
|
|
144
|
+
row = await validate(row);
|
|
145
|
+
}
|
|
146
|
+
const rowKeys = Object.keys(row);
|
|
147
|
+
return rowKeys.map(key => {
|
|
148
|
+
const col = this.opts.columns.find(c => c.name === key);
|
|
149
|
+
if (!col)
|
|
150
|
+
throw "Unexpected missing col name";
|
|
151
|
+
const colIsJSON = ["json", "jsonb"].includes(col.data_type);
|
|
152
|
+
const colIsUUID = ["uuid"].includes(col.data_type);
|
|
153
|
+
/**
|
|
154
|
+
* Add utility functions for PostGIS data
|
|
155
|
+
*/
|
|
156
|
+
let escapedVal;
|
|
157
|
+
if (["geometry", "geography"].includes(col.udt_name) && row[key] && isPlainObject(row[key])) {
|
|
158
|
+
const basicFunc = (args) => {
|
|
159
|
+
return args.map(arg => (0, PubSubManager_1.asValue)(arg)).join(", ");
|
|
160
|
+
};
|
|
161
|
+
const basicFuncNames = ["ST_GeomFromText", "ST_Point", "ST_MakePoint", "ST_MakePointM", "ST_PointFromText", "ST_GeomFromEWKT", "ST_GeomFromGeoJSON"];
|
|
162
|
+
const dataKeys = Object.keys(row[key]);
|
|
163
|
+
const funcName = dataKeys[0];
|
|
164
|
+
const funcExists = basicFuncNames.includes(funcName);
|
|
165
|
+
const funcArgs = row[key]?.[funcName];
|
|
166
|
+
if (dataKeys.length !== 1 || !funcExists || !Array.isArray(funcArgs)) {
|
|
167
|
+
throw `Expecting only one function key (${basicFuncNames.join(", ")}) \nwith an array of arguments \n within column (${key}) data but got: ${JSON.stringify(row[key])} \nExample: { geo_col: { ST_GeomFromText: ["POINT(-71.064544 42.28787)", 4326] } }`;
|
|
168
|
+
}
|
|
169
|
+
escapedVal = `${funcName}(${basicFunc(funcArgs)})`;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
escapedVal = exports.pgp.as.format(colIsUUID ? "$1::uuid" : colIsJSON ? "$1:json" : "$1", [row[key]]);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
escapedCol: (0, prostgles_types_1.asName)(key),
|
|
176
|
+
escapedVal
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async getInsertQuery(data, allowedCols, validate) {
|
|
181
|
+
const res = (await Promise.all((Array.isArray(data) ? data : [data]).map(async (d) => {
|
|
182
|
+
const rowParts = await this.getRow(d, allowedCols, validate);
|
|
183
|
+
const select = rowParts.map(r => r.escapedCol).join(", "), values = rowParts.map(r => r.escapedVal).join(", ");
|
|
184
|
+
return `INSERT INTO ${(0, prostgles_types_1.asName)(this.opts.tableName)} (${select}) VALUES (${values})`;
|
|
185
|
+
}))).join(";\n") + " ";
|
|
186
|
+
return res;
|
|
187
|
+
}
|
|
188
|
+
async getUpdateQuery(data, allowedCols, validate) {
|
|
189
|
+
const res = (await Promise.all((Array.isArray(data) ? data : [data]).map(async (d) => {
|
|
190
|
+
const rowParts = await this.getRow(d, allowedCols, validate);
|
|
191
|
+
return `UPDATE ${(0, prostgles_types_1.asName)(this.opts.tableName)} SET ` + rowParts.map(r => `${r.escapedCol} = ${r.escapedVal} `).join(",\n");
|
|
192
|
+
}))).join(";\n") + " ";
|
|
193
|
+
return res;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
class ViewHandler {
|
|
197
|
+
constructor(db, tableOrViewInfo, dboBuilder, t, dbTX, joinPaths) {
|
|
198
|
+
this.tsColumnDefs = [];
|
|
199
|
+
this.is_view = true;
|
|
200
|
+
this.filterDef = "";
|
|
201
|
+
// pubSubManager: PubSubManager;
|
|
202
|
+
this.is_media = false;
|
|
203
|
+
if (!db || !tableOrViewInfo)
|
|
204
|
+
throw "";
|
|
205
|
+
this.db = db;
|
|
206
|
+
this.t = t;
|
|
207
|
+
this.dbTX = dbTX;
|
|
208
|
+
this.joinPaths = joinPaths;
|
|
209
|
+
this.tableOrViewInfo = tableOrViewInfo;
|
|
210
|
+
this.name = tableOrViewInfo.name;
|
|
211
|
+
this.escapedName = (0, prostgles_types_1.asName)(this.name);
|
|
212
|
+
this.columns = tableOrViewInfo.columns;
|
|
213
|
+
/* cols are sorted by name to reduce .d.ts schema rewrites */
|
|
214
|
+
this.columnsForTypes = tableOrViewInfo.columns.slice(0).sort((a, b) => a.name.localeCompare(b.name));
|
|
215
|
+
this.column_names = tableOrViewInfo.columns.map(c => c.name);
|
|
216
|
+
// this.pubSubManager = pubSubManager;
|
|
217
|
+
this.dboBuilder = dboBuilder;
|
|
218
|
+
this.joins = this.dboBuilder.joins ?? [];
|
|
219
|
+
// fix this
|
|
220
|
+
// and also make hot schema reload over ws
|
|
221
|
+
this.colSet = new ColSet(this.columns, this.name);
|
|
222
|
+
const { $and: $and_key, $or: $or_key } = this.dboBuilder.prostgles.keywords;
|
|
223
|
+
// this.tsDataName = snakify(this.name, true);
|
|
224
|
+
// if(this.tsDataName === "T") this.tsDataName = this.tsDataName + "_";
|
|
225
|
+
// this.tsDataDef = `export type ${this.tsDataName} = {\n`;
|
|
226
|
+
this.columnsForTypes.map(({ name, udt_name, is_nullable }) => {
|
|
227
|
+
this.tsColumnDefs.push(`${escapeTSNames(name)}?: ${postgresToTsType(udt_name)} ${is_nullable ? " | null " : ""};`);
|
|
228
|
+
});
|
|
229
|
+
// this.tsDataDef += "};";
|
|
230
|
+
// this.tsDataDef += "\n";
|
|
231
|
+
// this.tsDataDef += `export type ${this.tsDataName}_Filter = ${this.tsDataName} | object | { ${JSON.stringify($and_key)}: (${this.tsDataName} | object)[] } | { ${JSON.stringify($or_key)}: (${this.tsDataName} | object)[] } `;
|
|
232
|
+
// this.filterDef = ` ${this.tsDataName}_Filter `;
|
|
233
|
+
// const filterDef = this.filterDef;
|
|
234
|
+
// this.tsDboDefs = [
|
|
235
|
+
// ` getColumns: () => Promise<any[]>;`,
|
|
236
|
+
// ` find: (filter?: ${filterDef}, selectParams?: SelectParams) => Promise<Partial<${this.tsDataName} & { [x: string]: any }>[]>;`,
|
|
237
|
+
// ` findOne: (filter?: ${filterDef}, selectParams?: SelectParams) => Promise<Partial<${this.tsDataName} & { [x: string]: any }>>;`,
|
|
238
|
+
// ` subscribe: (filter: ${filterDef}, params: SelectParams, onData: (items: Partial<${this.tsDataName} & { [x: string]: any }>[]) => any) => Promise<{ unsubscribe: () => any }>;`,
|
|
239
|
+
// ` subscribeOne: (filter: ${filterDef}, params: SelectParams, onData: (item: Partial<${this.tsDataName} & { [x: string]: any }>) => any) => Promise<{ unsubscribe: () => any }>;`,
|
|
240
|
+
// ` count: (filter?: ${filterDef}) => Promise<number>;`
|
|
241
|
+
// ];
|
|
242
|
+
// this.makeDef();
|
|
243
|
+
}
|
|
244
|
+
// makeDef(){
|
|
245
|
+
// this.tsDboName = `DBO_${snakify(this.name)}`;
|
|
246
|
+
// this.tsDboDef = `export type ${this.tsDboName} = {\n ${this.tsDboDefs.join("\n")} \n};\n`;
|
|
247
|
+
// }
|
|
248
|
+
getRowHashSelect(allowedFields, alias, tableAlias) {
|
|
249
|
+
let allowed_cols = this.column_names;
|
|
250
|
+
if (allowedFields)
|
|
251
|
+
allowed_cols = this.parseFieldFilter(allowedFields);
|
|
252
|
+
return "md5(" +
|
|
253
|
+
allowed_cols
|
|
254
|
+
/* CTID not available in AFTER trigger */
|
|
255
|
+
// .concat(this.is_view? [] : ["ctid"])
|
|
256
|
+
.sort()
|
|
257
|
+
.map(f => (tableAlias ? ((0, prostgles_types_1.asName)(tableAlias) + ".") : "") + (0, prostgles_types_1.asName)(f))
|
|
258
|
+
.map(f => `md5(coalesce(${f}::text, 'dd'))`)
|
|
259
|
+
.join(" || ") +
|
|
260
|
+
`)` + (alias ? ` as ${(0, prostgles_types_1.asName)(alias)}` : "");
|
|
261
|
+
}
|
|
262
|
+
async validateViewRules(args) {
|
|
263
|
+
const { fields, filterFields, returningFields, forcedFilter, dynamicFields, rule, } = args;
|
|
264
|
+
/* Safely test publish rules */
|
|
265
|
+
if (fields) {
|
|
266
|
+
try {
|
|
267
|
+
const _fields = this.parseFieldFilter(fields);
|
|
268
|
+
if (this.is_media && rule === "insert" && !_fields.includes("id")) {
|
|
269
|
+
throw "Must allow id insert for media table";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
throw ` issue with publish.${this.name}.${rule}.fields: \nVALUE: ` + JSON.stringify(fields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (filterFields) {
|
|
277
|
+
try {
|
|
278
|
+
this.parseFieldFilter(filterFields);
|
|
279
|
+
}
|
|
280
|
+
catch (e) {
|
|
281
|
+
throw ` issue with publish.${this.name}.${rule}.filterFields: \nVALUE: ` + JSON.stringify(filterFields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (returningFields) {
|
|
285
|
+
try {
|
|
286
|
+
this.parseFieldFilter(returningFields);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
throw ` issue with publish.${this.name}.${rule}.returningFields: \nVALUE: ` + JSON.stringify(returningFields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (forcedFilter) {
|
|
293
|
+
try {
|
|
294
|
+
await this.find(forcedFilter, { limit: 0 });
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
throw ` issue with publish.${this.name}.${rule}.forcedFilter: \nVALUE: ` + JSON.stringify(forcedFilter, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (dynamicFields) {
|
|
301
|
+
for await (const dfieldRule of dynamicFields) {
|
|
302
|
+
try {
|
|
303
|
+
const { fields, filter } = dfieldRule;
|
|
304
|
+
this.parseFieldFilter(fields);
|
|
305
|
+
await this.find(filter, { limit: 0 });
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
throw ` issue with publish.${this.name}.${rule}.dynamicFields: \nVALUE: ` + JSON.stringify(dfieldRule, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
getShortestJoin(table1, table2, startAlias, isInner = false) {
|
|
315
|
+
// let searchedTables = [], result;
|
|
316
|
+
// while (!result && searchedTables.length <= this.joins.length * 2){
|
|
317
|
+
// }
|
|
318
|
+
let toOne = true, query = this.joins.map(({ tables, on, type }, i) => {
|
|
319
|
+
if (type.split("-")[1] === "many") {
|
|
320
|
+
toOne = false;
|
|
321
|
+
}
|
|
322
|
+
const tl = `tl${startAlias + i}`, tr = `tr${startAlias + i}`;
|
|
323
|
+
return `FROM ${tables[0]} ${tl} ${isInner ? "INNER" : "LEFT"} JOIN ${tables[1]} ${tr} ON ${Object.keys(on).map(lKey => `${tl}.${lKey} = ${tr}.${on[lKey]}`).join("\nAND ")}`;
|
|
324
|
+
}).join("\n");
|
|
325
|
+
return { query, toOne: false };
|
|
326
|
+
}
|
|
327
|
+
getJoins(source, target, path, checkTableConfig) {
|
|
328
|
+
let paths = [];
|
|
329
|
+
if (!this.joinPaths)
|
|
330
|
+
throw `${source} - ${target} Join info missing or dissallowed`;
|
|
331
|
+
if (path && !path.length)
|
|
332
|
+
throw `Empty join path ( $path ) specified for ${source} <-> ${target}`;
|
|
333
|
+
/* Find the join path between tables */
|
|
334
|
+
if (checkTableConfig) {
|
|
335
|
+
const tableConfigJoinInfo = this.dboBuilder?.prostgles?.tableConfigurator?.getJoinInfo(source, target);
|
|
336
|
+
if (tableConfigJoinInfo)
|
|
337
|
+
return tableConfigJoinInfo;
|
|
338
|
+
}
|
|
339
|
+
let jp;
|
|
340
|
+
if (!path) {
|
|
341
|
+
jp = this.joinPaths.find(j => j.t1 === source && j.t2 === target);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
jp = {
|
|
345
|
+
t1: source,
|
|
346
|
+
t2: target,
|
|
347
|
+
path
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
if (!jp || !this.joinPaths.find(j => path ? j.path.join() === path.join() : j.t1 === source && j.t2 === target))
|
|
351
|
+
throw `Joining ${source} <-...-> ${target} dissallowed or missing`;
|
|
352
|
+
/* Make the join chain info excluding root table */
|
|
353
|
+
paths = (path || jp.path).slice(1).map((t2, i, arr) => {
|
|
354
|
+
const t1 = i === 0 ? source : arr[i - 1];
|
|
355
|
+
if (!this.joins)
|
|
356
|
+
this.joins = JSON.parse(JSON.stringify(this.dboBuilder.joins));
|
|
357
|
+
/* Get join options */
|
|
358
|
+
const jo = this.joins.find(j => j.tables.includes(t1) && j.tables.includes(t2));
|
|
359
|
+
if (!jo)
|
|
360
|
+
throw `Joining ${t1} <-> ${t2} dissallowed or missing`;
|
|
361
|
+
;
|
|
362
|
+
let on = [];
|
|
363
|
+
Object.keys(jo.on).map(leftKey => {
|
|
364
|
+
const rightKey = jo.on[leftKey];
|
|
365
|
+
/* Left table is joining on keys */
|
|
366
|
+
if (jo.tables[0] === t1) {
|
|
367
|
+
on.push([leftKey, rightKey]);
|
|
368
|
+
/* Left table is joining on values */
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
on.push([rightKey, leftKey]);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
return {
|
|
375
|
+
source,
|
|
376
|
+
target,
|
|
377
|
+
table: t2,
|
|
378
|
+
on
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
let expectOne = false;
|
|
382
|
+
paths.map(({ source, target, on }, i) => {
|
|
383
|
+
// if(expectOne && on.length === 1){
|
|
384
|
+
// const sourceCol = on[0][1];
|
|
385
|
+
// const targetCol = on[0][0];
|
|
386
|
+
// const sCol = this.dboBuilder.dbo[source].columns.find(c => c.name === sourceCol)
|
|
387
|
+
// const tCol = this.dboBuilder.dbo[target].columns.find(c => c.name === targetCol)
|
|
388
|
+
// console.log({ sourceCol, targetCol, sCol, source, tCol, target, on})
|
|
389
|
+
// expectOne = sCol.is_pkey && tCol.is_pkey
|
|
390
|
+
// }
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
paths,
|
|
394
|
+
expectOne
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
checkFilter(filter) {
|
|
398
|
+
if (filter === null || filter && !isPojoObject(filter))
|
|
399
|
+
throw `invalid filter -> ${JSON.stringify(filter)} \nExpecting: undefined | {} | { field_name: "value" } | { field: { $gt: 22 } } ... `;
|
|
400
|
+
}
|
|
401
|
+
async getInfo(param1, param2, param3, tableRules, localParams) {
|
|
402
|
+
const p = this.getValidatedRules(tableRules, localParams);
|
|
403
|
+
if (!p.getInfo)
|
|
404
|
+
throw "Not allowed";
|
|
405
|
+
let has_media = undefined;
|
|
406
|
+
/**
|
|
407
|
+
* Media is directly related to this table (does not come from a deeply joined table)
|
|
408
|
+
*/
|
|
409
|
+
let has_direct_media = false;
|
|
410
|
+
const mediaTable = this.dboBuilder.prostgles?.opts?.fileTable?.tableName;
|
|
411
|
+
if (!this.is_media && mediaTable) {
|
|
412
|
+
if (this.dboBuilder.prostgles?.opts?.fileTable?.referencedTables?.[this.name]) {
|
|
413
|
+
has_media = this.dboBuilder.prostgles?.opts?.fileTable?.referencedTables?.[this.name];
|
|
414
|
+
has_direct_media = true;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const jp = this.dboBuilder.joinPaths.find(jp => jp.t1 === this.name && jp.t2 === mediaTable);
|
|
418
|
+
if (jp && jp.path.length <= 3) {
|
|
419
|
+
await Promise.all(jp.path.map(async (tableName) => {
|
|
420
|
+
const cols = (await this?.dboBuilder?.dbo?.[tableName]?.getColumns?.())?.filter(c => jp.path.includes(c?.references?.ftable));
|
|
421
|
+
if (cols && cols.length && has_media !== "many") {
|
|
422
|
+
if (cols.find(c => !c.is_pkey)) {
|
|
423
|
+
has_media = "many";
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
has_media = "one";
|
|
427
|
+
}
|
|
428
|
+
has_direct_media = jp.path.length === 2;
|
|
429
|
+
}
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
oid: this.tableOrViewInfo.oid,
|
|
436
|
+
comment: this.tableOrViewInfo.comment,
|
|
437
|
+
is_media: this.is_media,
|
|
438
|
+
has_media,
|
|
439
|
+
has_direct_media,
|
|
440
|
+
media_table_name: mediaTable,
|
|
441
|
+
dynamicRules: {
|
|
442
|
+
update: Boolean(tableRules?.update?.dynamicFields?.length)
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// TODO: fix renamed table trigger problem
|
|
447
|
+
async getColumns(lang, params, _param3, tableRules, localParams) {
|
|
448
|
+
try {
|
|
449
|
+
const p = this.getValidatedRules(tableRules, localParams);
|
|
450
|
+
if (!p.getColumns)
|
|
451
|
+
throw "Not allowed";
|
|
452
|
+
// console.log("getColumns", this.name, this.columns.map(c => c.name))
|
|
453
|
+
let dynamicUpdateFields;
|
|
454
|
+
if (params && "parseUpdateRules" in this && this.parseUpdateRules) {
|
|
455
|
+
if (!isPlainObject(params) || !isPlainObject(params.data) || !isPlainObject(params.filter) || params.rule !== "update") {
|
|
456
|
+
throw "params must be { rule: 'update', data, filter } but got: " + JSON.stringify(params);
|
|
457
|
+
}
|
|
458
|
+
const { data, filter } = params;
|
|
459
|
+
const updateRules = await this.parseUpdateRules(filter, data, undefined, tableRules, localParams);
|
|
460
|
+
dynamicUpdateFields = updateRules.fields;
|
|
461
|
+
}
|
|
462
|
+
let columns = this.columns
|
|
463
|
+
.filter(c => {
|
|
464
|
+
const { insert, select, update } = p || {};
|
|
465
|
+
return [
|
|
466
|
+
...(insert?.fields || []),
|
|
467
|
+
...(select?.fields || []),
|
|
468
|
+
...(update?.fields || []),
|
|
469
|
+
].includes(c.name);
|
|
470
|
+
})
|
|
471
|
+
.map(_c => {
|
|
472
|
+
let c = { ..._c };
|
|
473
|
+
let label = c.comment || capitalizeFirstLetter(c.name, " ");
|
|
474
|
+
const select = c.privileges.some(p => p.privilege_type === "SELECT"), insert = c.privileges.some(p => p.privilege_type === "INSERT"), update = c.privileges.some(p => p.privilege_type === "UPDATE"), _delete = this.tableOrViewInfo.privileges.delete; // c.privileges.some(p => p.privilege_type === "DELETE");
|
|
475
|
+
delete c.privileges;
|
|
476
|
+
let result = {
|
|
477
|
+
...c,
|
|
478
|
+
label,
|
|
479
|
+
tsDataType: postgresToTsType(c.udt_name),
|
|
480
|
+
insert: insert && Boolean(p.insert && p.insert.fields && p.insert.fields.includes(c.name)),
|
|
481
|
+
select: select && Boolean(p.select && p.select.fields && p.select.fields.includes(c.name)),
|
|
482
|
+
filter: Boolean(p.select && p.select.filterFields && p.select.filterFields.includes(c.name)),
|
|
483
|
+
update: update && Boolean(p.update && p.update.fields && p.update.fields.includes(c.name)),
|
|
484
|
+
delete: _delete && Boolean(p.delete && p.delete.filterFields && p.delete.filterFields.includes(c.name)),
|
|
485
|
+
...(this.dboBuilder?.prostgles?.tableConfigurator?.getColInfo({ table: this.name, col: c.name, lang }) || {})
|
|
486
|
+
};
|
|
487
|
+
if (dynamicUpdateFields) {
|
|
488
|
+
result.update = dynamicUpdateFields.includes(c.name);
|
|
489
|
+
}
|
|
490
|
+
return result;
|
|
491
|
+
}).filter(c => c.select || c.update || c.delete || c.insert);
|
|
492
|
+
//.sort((a, b) => a.ordinal_position - b.ordinal_position);
|
|
493
|
+
// const tblInfo = await this.getInfo();
|
|
494
|
+
// if(tblInfo && tblInfo.media_table_name && tblInfo.has_media){
|
|
495
|
+
// const mediaRules = this.dboBuilder.dbo[tblInfo.media_table_name]?.
|
|
496
|
+
// return columns.concat({
|
|
497
|
+
// comment: "",
|
|
498
|
+
// data_type: "file",
|
|
499
|
+
// delete: false,
|
|
500
|
+
// });
|
|
501
|
+
// }
|
|
502
|
+
return columns;
|
|
503
|
+
}
|
|
504
|
+
catch (e) {
|
|
505
|
+
console.trace(e);
|
|
506
|
+
throw "Something went wrong in " + `db.${this.name}.getColumns()`;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
getValidatedRules(tableRules, localParams) {
|
|
510
|
+
if ((0, utils_1.get)(localParams, "socket") && !tableRules) {
|
|
511
|
+
throw "INTERNAL ERROR: Unexpected case -> localParams && !tableRules";
|
|
512
|
+
}
|
|
513
|
+
/* Computed fields are allowed only if select is allowed */
|
|
514
|
+
const allColumns = this.column_names.slice(0).map(fieldName => ({
|
|
515
|
+
type: "column",
|
|
516
|
+
name: fieldName,
|
|
517
|
+
getQuery: ({ tableAlias }) => (0, QueryBuilder_1.asNameAlias)(fieldName, tableAlias),
|
|
518
|
+
selected: false
|
|
519
|
+
})).concat(QueryBuilder_1.COMPUTED_FIELDS.map(c => ({
|
|
520
|
+
type: c.type,
|
|
521
|
+
name: c.name,
|
|
522
|
+
getQuery: ({ tableAlias, allowedFields }) => c.getQuery({
|
|
523
|
+
allowedFields,
|
|
524
|
+
ctidField: undefined,
|
|
525
|
+
allColumns: this.columns,
|
|
526
|
+
/* CTID not available in AFTER trigger */
|
|
527
|
+
// ctidField: this.is_view? undefined : "ctid",
|
|
528
|
+
tableAlias
|
|
529
|
+
}),
|
|
530
|
+
selected: false
|
|
531
|
+
})));
|
|
532
|
+
if (tableRules) {
|
|
533
|
+
if ((0, prostgles_types_1.isEmpty)(tableRules))
|
|
534
|
+
throw "INTERNAL ERROR: Unexpected case -> Empty table rules for " + this.name;
|
|
535
|
+
const throwFieldsErr = (command, fieldType = "fields") => {
|
|
536
|
+
throw `Invalid publish.${this.name}.${command} rule -> ${fieldType} setting is missing.\nPlease specify allowed ${fieldType} in this format: "*" | { col_name: false } | { col1: true, col2: true }`;
|
|
537
|
+
}, getFirstSpecified = (...fieldParams) => {
|
|
538
|
+
const firstValid = fieldParams.find(fp => fp !== undefined);
|
|
539
|
+
return this.parseFieldFilter(firstValid);
|
|
540
|
+
};
|
|
541
|
+
let res = {
|
|
542
|
+
allColumns,
|
|
543
|
+
getColumns: tableRules?.getColumns ?? true,
|
|
544
|
+
getInfo: tableRules?.getColumns ?? true,
|
|
545
|
+
};
|
|
546
|
+
/* SELECT */
|
|
547
|
+
if (tableRules.select) {
|
|
548
|
+
if (!tableRules.select.fields)
|
|
549
|
+
return throwFieldsErr("select");
|
|
550
|
+
let maxLimit = null;
|
|
551
|
+
if (tableRules.select.maxLimit !== undefined && tableRules.select.maxLimit !== maxLimit) {
|
|
552
|
+
const ml = tableRules.select.maxLimit;
|
|
553
|
+
if (ml !== null && (!Number.isInteger(ml) || ml < 0))
|
|
554
|
+
throw ` Invalid publish.${this.name}.select.maxLimit -> expecting a positive integer OR null but got ` + ml;
|
|
555
|
+
maxLimit = ml;
|
|
556
|
+
}
|
|
557
|
+
res.select = {
|
|
558
|
+
fields: this.parseFieldFilter(tableRules.select.fields),
|
|
559
|
+
forcedFilter: { ...tableRules.select.forcedFilter },
|
|
560
|
+
filterFields: this.parseFieldFilter(tableRules.select.filterFields),
|
|
561
|
+
maxLimit
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/* UPDATE */
|
|
565
|
+
if (tableRules.update) {
|
|
566
|
+
if (!tableRules.update.fields)
|
|
567
|
+
return throwFieldsErr("update");
|
|
568
|
+
res.update = {
|
|
569
|
+
fields: this.parseFieldFilter(tableRules.update.fields),
|
|
570
|
+
forcedData: { ...tableRules.update.forcedData },
|
|
571
|
+
forcedFilter: { ...tableRules.update.forcedFilter },
|
|
572
|
+
returningFields: getFirstSpecified(tableRules.update?.returningFields, tableRules?.select?.fields, tableRules.update.fields),
|
|
573
|
+
filterFields: this.parseFieldFilter(tableRules.update.filterFields)
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/* INSERT */
|
|
577
|
+
if (tableRules.insert) {
|
|
578
|
+
if (!tableRules.insert.fields)
|
|
579
|
+
return throwFieldsErr("insert");
|
|
580
|
+
res.insert = {
|
|
581
|
+
fields: this.parseFieldFilter(tableRules.insert.fields),
|
|
582
|
+
forcedData: { ...tableRules.insert.forcedData },
|
|
583
|
+
returningFields: getFirstSpecified(tableRules.insert.returningFields, tableRules?.select?.fields, tableRules.insert.fields)
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/* DELETE */
|
|
587
|
+
if (tableRules.delete) {
|
|
588
|
+
if (!tableRules.delete.filterFields)
|
|
589
|
+
return throwFieldsErr("delete", "filterFields");
|
|
590
|
+
res.delete = {
|
|
591
|
+
forcedFilter: { ...tableRules.delete.forcedFilter },
|
|
592
|
+
filterFields: this.parseFieldFilter(tableRules.delete.filterFields),
|
|
593
|
+
returningFields: getFirstSpecified(tableRules.delete.returningFields, tableRules?.select?.fields, tableRules.delete.filterFields)
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
if (!tableRules.select && !tableRules.update && !tableRules.delete && !tableRules.insert) {
|
|
597
|
+
if ([null, false].includes(tableRules.getInfo))
|
|
598
|
+
res.getInfo = false;
|
|
599
|
+
if ([null, false].includes(tableRules.getColumns))
|
|
600
|
+
res.getColumns = false;
|
|
601
|
+
}
|
|
602
|
+
return res;
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
const all_cols = this.column_names.slice(0);
|
|
606
|
+
return {
|
|
607
|
+
allColumns,
|
|
608
|
+
getColumns: true,
|
|
609
|
+
getInfo: true,
|
|
610
|
+
select: {
|
|
611
|
+
fields: all_cols,
|
|
612
|
+
filterFields: all_cols,
|
|
613
|
+
forcedFilter: {},
|
|
614
|
+
maxLimit: null,
|
|
615
|
+
},
|
|
616
|
+
update: {
|
|
617
|
+
fields: all_cols,
|
|
618
|
+
filterFields: all_cols,
|
|
619
|
+
forcedFilter: {},
|
|
620
|
+
forcedData: {},
|
|
621
|
+
returningFields: all_cols
|
|
622
|
+
},
|
|
623
|
+
insert: {
|
|
624
|
+
fields: all_cols,
|
|
625
|
+
forcedData: {},
|
|
626
|
+
returningFields: all_cols
|
|
627
|
+
},
|
|
628
|
+
delete: {
|
|
629
|
+
filterFields: all_cols,
|
|
630
|
+
forcedFilter: {},
|
|
631
|
+
returningFields: all_cols
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async find(filter, selectParams, param3_unused, tableRules, localParams) {
|
|
637
|
+
try {
|
|
638
|
+
filter = filter || {};
|
|
639
|
+
const allowedReturnTypes = ["row", "value", "values"];
|
|
640
|
+
const { returnType } = selectParams || {};
|
|
641
|
+
if (returnType && !allowedReturnTypes.includes(returnType)) {
|
|
642
|
+
throw `returnType (${returnType}) can only be ${allowedReturnTypes.join(" OR ")}`;
|
|
643
|
+
}
|
|
644
|
+
const { testRule = false, returnQuery = false } = localParams || {};
|
|
645
|
+
if (testRule)
|
|
646
|
+
return [];
|
|
647
|
+
if (selectParams) {
|
|
648
|
+
const good_params = ["select", "orderBy", "offset", "limit", "returnType", "groupBy"];
|
|
649
|
+
const bad_params = Object.keys(selectParams).filter(k => !good_params.includes(k));
|
|
650
|
+
if (bad_params && bad_params.length)
|
|
651
|
+
throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
|
|
652
|
+
}
|
|
653
|
+
/* Validate publish */
|
|
654
|
+
if (tableRules) {
|
|
655
|
+
let fields, filterFields, forcedFilter, maxLimit;
|
|
656
|
+
if (!tableRules.select)
|
|
657
|
+
throw "select rules missing for " + this.name;
|
|
658
|
+
fields = tableRules.select.fields;
|
|
659
|
+
forcedFilter = tableRules.select.forcedFilter;
|
|
660
|
+
filterFields = tableRules.select.filterFields;
|
|
661
|
+
maxLimit = tableRules.select.maxLimit;
|
|
662
|
+
if (tableRules.select !== "*" && typeof tableRules.select !== "boolean" && !isPlainObject(tableRules.select))
|
|
663
|
+
throw `\nINVALID publish.${this.name}.select\nExpecting any of: "*" | { fields: "*" } | true | false`;
|
|
664
|
+
if (!fields)
|
|
665
|
+
throw ` invalid ${this.name}.select rule -> fields (required) setting missing.\nExpecting any of: "*" | { col_name: false } | { col1: true, col2: true }`;
|
|
666
|
+
if (maxLimit && !Number.isInteger(maxLimit))
|
|
667
|
+
throw ` invalid publish.${this.name}.select.maxLimit -> expecting integer but got ` + maxLimit;
|
|
668
|
+
}
|
|
669
|
+
let q = await (0, QueryBuilder_1.getNewQuery)(this, filter, selectParams, param3_unused, tableRules, localParams, this.columns), _query = (0, QueryBuilder_1.makeQuery)(this, q, undefined, undefined, selectParams);
|
|
670
|
+
// console.log(_query, JSON.stringify(q, null, 2))
|
|
671
|
+
if (testRule) {
|
|
672
|
+
try {
|
|
673
|
+
await this.db.any("EXPLAIN " + _query);
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
console.error(e);
|
|
678
|
+
throw `INTERNAL ERROR: Publish config is not valid for publish.${this.name}.select `;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (returnQuery)
|
|
682
|
+
return _query;
|
|
683
|
+
if (["row", "value"].includes(returnType)) {
|
|
684
|
+
return (this.t || this.db).oneOrNone(_query).then(data => {
|
|
685
|
+
return (data && returnType === "value") ? Object.values(data)[0] : data;
|
|
686
|
+
}).catch(err => makeErr(err, localParams, this));
|
|
687
|
+
}
|
|
688
|
+
else {
|
|
689
|
+
return (this.t || this.db).any(_query).then(data => {
|
|
690
|
+
if (returnType === "values") {
|
|
691
|
+
return data.map(d => Object.values(d)[0]);
|
|
692
|
+
}
|
|
693
|
+
return data;
|
|
694
|
+
}).catch(err => makeErr(err, localParams, this));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
// console.trace(e)
|
|
699
|
+
if (localParams && localParams.testRule)
|
|
700
|
+
throw e;
|
|
701
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.find(${JSON.stringify(filter || {}, null, 2)}, ${JSON.stringify(selectParams || {}, null, 2)})` };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
findOne(filter, selectParams, param3_unused, table_rules, localParams) {
|
|
705
|
+
try {
|
|
706
|
+
const { select = "*", orderBy, offset = 0 } = selectParams || {};
|
|
707
|
+
if (selectParams) {
|
|
708
|
+
const good_params = ["select", "orderBy", "offset"];
|
|
709
|
+
const bad_params = Object.keys(selectParams).filter(k => !good_params.includes(k));
|
|
710
|
+
if (bad_params && bad_params.length)
|
|
711
|
+
throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
|
|
712
|
+
}
|
|
713
|
+
return this.find(filter, { select, orderBy, limit: 1, offset, returnType: "row" }, undefined, table_rules, localParams);
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
if (localParams && localParams.testRule)
|
|
717
|
+
throw e;
|
|
718
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.findOne()` };
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async count(filter, param2_unused, param3_unused, table_rules, localParams = {}) {
|
|
722
|
+
filter = filter || {};
|
|
723
|
+
try {
|
|
724
|
+
return await this.find(filter, { select: "", limit: 0 }, undefined, table_rules, localParams)
|
|
725
|
+
.then(async (allowed) => {
|
|
726
|
+
const { filterFields, forcedFilter } = (0, utils_1.get)(table_rules, "select") || {};
|
|
727
|
+
const where = (await this.prepareWhere({ filter, forcedFilter, filterFields, addKeywords: true, localParams, tableRule: table_rules }));
|
|
728
|
+
let query = "SELECT COUNT(*) FROM " + this.escapedName + " " + where;
|
|
729
|
+
return (this.t || this.db).one(query, { _psqlWS_tableName: this.name }).then(({ count }) => +count);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
catch (e) {
|
|
733
|
+
if (localParams && localParams.testRule)
|
|
734
|
+
throw e;
|
|
735
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.count()` };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async size(filter, selectParams, param3_unused, table_rules, localParams = {}) {
|
|
739
|
+
filter = filter || {};
|
|
740
|
+
try {
|
|
741
|
+
return await this.find(filter, { ...selectParams, limit: 2 }, undefined, table_rules, localParams)
|
|
742
|
+
.then(async (_allowed) => {
|
|
743
|
+
// let rules: TableRule = table_rules || {};
|
|
744
|
+
// rules.select.maxLimit = Number.MAX_SAFE_INTEGER;
|
|
745
|
+
// rules.select.fields = rules.select.fields || "*";
|
|
746
|
+
const q = await this.find(filter, { ...selectParams, limit: selectParams?.limit ?? Number.MAX_SAFE_INTEGER }, undefined, table_rules, { ...localParams, returnQuery: true });
|
|
747
|
+
const query = `
|
|
748
|
+
SELECT sum(pg_column_size((prgl_size_query.*))) as size
|
|
749
|
+
FROM (
|
|
750
|
+
${q}
|
|
751
|
+
) prgl_size_query
|
|
752
|
+
`;
|
|
753
|
+
return (this.t || this.db).one(query, { _psqlWS_tableName: this.name }).then(({ size }) => size || '0');
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
catch (e) {
|
|
757
|
+
if (localParams && localParams.testRule)
|
|
758
|
+
throw e;
|
|
759
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.size()` };
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
getAllowedSelectFields(selectParams = "*", allowed_cols, allow_empty = true) {
|
|
763
|
+
let all_columns = this.column_names.slice(0), allowedFields = all_columns.slice(0), resultFields = [];
|
|
764
|
+
if (selectParams) {
|
|
765
|
+
resultFields = this.parseFieldFilter(selectParams, allow_empty);
|
|
766
|
+
}
|
|
767
|
+
if (allowed_cols) {
|
|
768
|
+
allowedFields = this.parseFieldFilter(allowed_cols, allow_empty);
|
|
769
|
+
}
|
|
770
|
+
let col_names = (resultFields || []).filter(f => !allowedFields || allowedFields.includes(f));
|
|
771
|
+
/* Maintain allowed cols order */
|
|
772
|
+
if (selectParams === "*" && allowedFields && allowedFields.length)
|
|
773
|
+
col_names = allowedFields;
|
|
774
|
+
return col_names;
|
|
775
|
+
}
|
|
776
|
+
prepareColumnSet(selectParams = "*", allowed_cols, allow_empty = true, onlyNames = true) {
|
|
777
|
+
let all_columns = this.column_names.slice(0);
|
|
778
|
+
let col_names = this.getAllowedSelectFields(selectParams, all_columns, allow_empty);
|
|
779
|
+
try {
|
|
780
|
+
let colSet = new exports.pgp.helpers.ColumnSet(col_names);
|
|
781
|
+
return onlyNames ? colSet.names : colSet;
|
|
782
|
+
}
|
|
783
|
+
catch (e) {
|
|
784
|
+
throw e;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
prepareSelect(selectParams = "*", allowed_cols, allow_empty = true, tableAlias) {
|
|
788
|
+
if (tableAlias) {
|
|
789
|
+
let cs = this.prepareColumnSet(selectParams, allowed_cols, true, false);
|
|
790
|
+
return cs.columns.map(col => `${this.escapedName}.${(0, prostgles_types_1.asName)(col.name)}`).join(", ");
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
return this.prepareColumnSet(selectParams, allowed_cols, true, true);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async prepareHaving(params) {
|
|
797
|
+
return "";
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Parses group or simple filter
|
|
801
|
+
*/
|
|
802
|
+
async prepareWhere(params) {
|
|
803
|
+
const { filter, select, forcedFilter, filterFields: ff, addKeywords = true, tableAlias, localParams, tableRule } = params;
|
|
804
|
+
const { $and: $and_key, $or: $or_key } = this.dboBuilder.prostgles.keywords;
|
|
805
|
+
let filterFields = ff;
|
|
806
|
+
/* Local update allow all. TODO -> FIX THIS */
|
|
807
|
+
if (!ff && !tableRule)
|
|
808
|
+
filterFields = "*";
|
|
809
|
+
const parseFullFilter = async (f, parentFilter = null) => {
|
|
810
|
+
if (!f)
|
|
811
|
+
throw "Invalid/missing group filter provided";
|
|
812
|
+
let result = "";
|
|
813
|
+
let keys = (0, prostgles_types_1.getKeys)(f);
|
|
814
|
+
if (!keys.length)
|
|
815
|
+
return result;
|
|
816
|
+
if ((keys.includes($and_key) || keys.includes($or_key))) {
|
|
817
|
+
if (keys.length > 1)
|
|
818
|
+
throw `\ngroup filter must contain only one array property. e.g.: { ${$and_key}: [...] } OR { ${$or_key}: [...] } `;
|
|
819
|
+
if (parentFilter && Object.keys(parentFilter).includes(""))
|
|
820
|
+
throw "group filter ($and/$or) can only be placed at the root or within another group filter";
|
|
821
|
+
}
|
|
822
|
+
const { [$and_key]: $and, [$or_key]: $or } = f, group = $and || $or;
|
|
823
|
+
if (group && group.length) {
|
|
824
|
+
const operand = $and ? " AND " : " OR ";
|
|
825
|
+
let conditions = (await Promise.all(group.map(async (gf) => await parseFullFilter(gf, group)))).filter(c => c);
|
|
826
|
+
if (conditions && conditions.length) {
|
|
827
|
+
if (conditions.length === 1)
|
|
828
|
+
return conditions.join(operand);
|
|
829
|
+
else
|
|
830
|
+
return ` ( ${conditions.sort().join(operand)} ) `;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
else if (!group) {
|
|
834
|
+
result = await this.getCondition({
|
|
835
|
+
filter: { ...f },
|
|
836
|
+
select,
|
|
837
|
+
allowed_colnames: this.parseFieldFilter(filterFields),
|
|
838
|
+
tableAlias,
|
|
839
|
+
localParams,
|
|
840
|
+
tableRules: tableRule
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return result;
|
|
844
|
+
};
|
|
845
|
+
if (!isPlainObject(filter))
|
|
846
|
+
throw "\nInvalid filter\nExpecting an object but got -> " + JSON.stringify(filter);
|
|
847
|
+
let _filter = (0, exports.getUpdateFilter)({ filter, forcedFilter, $and_key });
|
|
848
|
+
// let keys = Object.keys(filter);
|
|
849
|
+
// if(!keys.length) return result;
|
|
850
|
+
let cond = await parseFullFilter(_filter, null);
|
|
851
|
+
if (cond && addKeywords)
|
|
852
|
+
cond = "WHERE " + cond;
|
|
853
|
+
return cond || "";
|
|
854
|
+
}
|
|
855
|
+
async prepareExistCondition(eConfig, localParams) {
|
|
856
|
+
let res = "";
|
|
857
|
+
const thisTable = this.name;
|
|
858
|
+
const isNotExists = ["$notExists", "$notExistsJoined"].includes(eConfig.existType);
|
|
859
|
+
let { f2, tables, isJoined } = eConfig;
|
|
860
|
+
let t2 = tables[tables.length - 1];
|
|
861
|
+
tables.forEach(t => {
|
|
862
|
+
if (!this.dboBuilder.dbo[t])
|
|
863
|
+
throw "Invalid or dissallowed table: " + t;
|
|
864
|
+
});
|
|
865
|
+
/* Nested $exists not allowed */
|
|
866
|
+
if (f2 && Object.keys(f2).find(fk => exports.EXISTS_KEYS.includes(fk))) {
|
|
867
|
+
throw "Nested exists dissallowed";
|
|
868
|
+
}
|
|
869
|
+
const makeTableChain = (finalFilter) => {
|
|
870
|
+
let joinPaths = [];
|
|
871
|
+
let expectOne = true;
|
|
872
|
+
tables.map((t2, depth) => {
|
|
873
|
+
let t1 = depth ? tables[depth - 1] : thisTable;
|
|
874
|
+
let exactPaths = [t1, t2];
|
|
875
|
+
if (!depth && eConfig.shortestJoin)
|
|
876
|
+
exactPaths = undefined;
|
|
877
|
+
const jinf = this.getJoins(t1, t2, exactPaths, true);
|
|
878
|
+
expectOne = Boolean(expectOne && jinf.expectOne);
|
|
879
|
+
joinPaths = joinPaths.concat(jinf.paths);
|
|
880
|
+
});
|
|
881
|
+
let r = makeJoin({ paths: joinPaths, expectOne }, 0);
|
|
882
|
+
return r;
|
|
883
|
+
function makeJoin(joinInfo, ji) {
|
|
884
|
+
const { paths } = joinInfo;
|
|
885
|
+
const jp = paths[ji];
|
|
886
|
+
// let prevTable = ji? paths[ji - 1].table : jp.source;
|
|
887
|
+
let table = paths[ji].table;
|
|
888
|
+
let tableAlias = (0, prostgles_types_1.asName)(ji < paths.length - 1 ? `jd${ji}` : table);
|
|
889
|
+
let prevTableAlias = (0, prostgles_types_1.asName)(ji ? `jd${ji - 1}` : thisTable);
|
|
890
|
+
let cond = `${jp.on.map(([c1, c2]) => `${prevTableAlias}.${(0, prostgles_types_1.asName)(c1)} = ${tableAlias}.${(0, prostgles_types_1.asName)(c2)}`).join("\n AND ")}`;
|
|
891
|
+
let j = `SELECT 1 \n` +
|
|
892
|
+
`FROM ${(0, prostgles_types_1.asName)(table)} ${tableAlias} \n` +
|
|
893
|
+
`WHERE ${cond} \n`; //
|
|
894
|
+
if (ji === paths.length - 1 &&
|
|
895
|
+
finalFilter) {
|
|
896
|
+
j += `AND ${finalFilter} \n`;
|
|
897
|
+
}
|
|
898
|
+
const indent = (a, b) => a;
|
|
899
|
+
if (ji < paths.length - 1) {
|
|
900
|
+
j += `AND ${makeJoin(joinInfo, ji + 1)} \n`;
|
|
901
|
+
}
|
|
902
|
+
j = indent(j, ji + 1);
|
|
903
|
+
let res = `${isNotExists ? " NOT " : " "} EXISTS ( \n` +
|
|
904
|
+
j +
|
|
905
|
+
`) \n`;
|
|
906
|
+
return indent(res, ji);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
let t2Rules = undefined, forcedFilter, filterFields, tableAlias;
|
|
910
|
+
/* Check if allowed to view data */
|
|
911
|
+
if (localParams && (localParams.socket || localParams.httpReq) && this.dboBuilder.publishParser) {
|
|
912
|
+
/* Need to think about joining through dissallowed tables */
|
|
913
|
+
t2Rules = await this.dboBuilder.publishParser.getValidatedRequestRuleWusr({ tableName: t2, command: "find", localParams });
|
|
914
|
+
if (!t2Rules || !t2Rules.select)
|
|
915
|
+
throw "Dissallowed";
|
|
916
|
+
({ forcedFilter, filterFields } = t2Rules.select);
|
|
917
|
+
}
|
|
918
|
+
let finalWhere;
|
|
919
|
+
try {
|
|
920
|
+
finalWhere = (await this.dboBuilder.dbo[t2].prepareWhere({
|
|
921
|
+
filter: f2,
|
|
922
|
+
forcedFilter,
|
|
923
|
+
filterFields,
|
|
924
|
+
addKeywords: false,
|
|
925
|
+
tableAlias,
|
|
926
|
+
localParams,
|
|
927
|
+
tableRule: t2Rules //tableRules
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
// console.trace(err)
|
|
932
|
+
throw "Issue with preparing $exists query for table " + t2 + "\n->" + JSON.stringify(err);
|
|
933
|
+
}
|
|
934
|
+
if (!isJoined) {
|
|
935
|
+
res = `${isNotExists ? " NOT " : " "} EXISTS (SELECT 1 \nFROM ${(0, prostgles_types_1.asName)(t2)} \n${finalWhere ? `WHERE ${finalWhere}` : ""}) `;
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
res = makeTableChain(finalWhere);
|
|
939
|
+
}
|
|
940
|
+
return res;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* parses a single filter
|
|
944
|
+
* @example
|
|
945
|
+
* { fff: 2 } => "fff" = 2
|
|
946
|
+
* { fff: { $ilike: 'abc' } } => "fff" ilike 'abc'
|
|
947
|
+
*/
|
|
948
|
+
async getCondition(params) {
|
|
949
|
+
const { filter, select, allowed_colnames, tableAlias, localParams, tableRules } = params;
|
|
950
|
+
let data = { ...filter };
|
|
951
|
+
/* Exists join filter */
|
|
952
|
+
const ERR = "Invalid exists filter. \nExpecting somethibng like: { $exists: { tableName.tableName2: Filter } } | { $exists: { \"**.tableName3\": Filter } }\n";
|
|
953
|
+
const SP_WILDCARD = "**";
|
|
954
|
+
let existsKeys = Object.keys(data)
|
|
955
|
+
.filter(k => exports.EXISTS_KEYS.includes(k) && Object.keys(data[k] || {}).length)
|
|
956
|
+
.map(key => {
|
|
957
|
+
const isJoined = exports.EXISTS_KEYS.slice(-2).includes(key);
|
|
958
|
+
let firstKey = Object.keys(data[key])[0], tables = firstKey.split("."), f2 = data[key][firstKey], shortestJoin = false;
|
|
959
|
+
if (!isJoined) {
|
|
960
|
+
if (tables.length !== 1)
|
|
961
|
+
throw "Expecting single table in exists filter. Example: { $exists: { tableName: Filter } }";
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
/* First part can be the ** param meaning shortest join. Will be overriden by anything in tableConfig */
|
|
965
|
+
if (!tables.length)
|
|
966
|
+
throw ERR + "\nBut got: " + data[key];
|
|
967
|
+
if (tables[0] === SP_WILDCARD) {
|
|
968
|
+
tables = tables.slice(1);
|
|
969
|
+
shortestJoin = true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
key,
|
|
974
|
+
existType: key,
|
|
975
|
+
isJoined,
|
|
976
|
+
shortestJoin,
|
|
977
|
+
f2,
|
|
978
|
+
tables
|
|
979
|
+
};
|
|
980
|
+
});
|
|
981
|
+
/* Exists with exact path */
|
|
982
|
+
// Object.keys(data).map(k => {
|
|
983
|
+
// let isthis = isPlainObject(data[k]) && !this.column_names.includes(k) && !k.split(".").find(kt => !this.dboBuilder.dbo[kt]);
|
|
984
|
+
// if(isthis) {
|
|
985
|
+
// existsKeys.push({
|
|
986
|
+
// key: k,
|
|
987
|
+
// notJoined: false,
|
|
988
|
+
// exactPaths: k.split(".")
|
|
989
|
+
// });
|
|
990
|
+
// }
|
|
991
|
+
// });
|
|
992
|
+
let funcConds = [];
|
|
993
|
+
const funcFilterkeys = FILTER_FUNCS.filter(f => {
|
|
994
|
+
return f.name in data;
|
|
995
|
+
});
|
|
996
|
+
funcFilterkeys.map(f => {
|
|
997
|
+
const funcArgs = data[f.name];
|
|
998
|
+
if (!Array.isArray(funcArgs))
|
|
999
|
+
throw `A function filter must contain an array. E.g: { $funcFilterName: ["col1"] } \n but got: ${JSON.stringify((0, PubSubManager_1.pickKeys)(data, [f.name]))} `;
|
|
1000
|
+
const fields = this.parseFieldFilter(f.getFields(funcArgs), true, allowed_colnames);
|
|
1001
|
+
const dissallowedCols = fields.filter(fname => !allowed_colnames.includes(fname));
|
|
1002
|
+
if (dissallowedCols.length) {
|
|
1003
|
+
throw `Invalid/disallowed columns found in function filter: ${dissallowedCols}`;
|
|
1004
|
+
}
|
|
1005
|
+
funcConds.push(f.getQuery({ args: funcArgs, allColumns: this.columns, allowedFields: allowed_colnames, tableAlias }));
|
|
1006
|
+
});
|
|
1007
|
+
let existsCond = "";
|
|
1008
|
+
if (existsKeys.length) {
|
|
1009
|
+
existsCond = (await Promise.all(existsKeys.map(async (k) => await this.prepareExistCondition(k, localParams)))).join(" AND ");
|
|
1010
|
+
}
|
|
1011
|
+
/* Computed field queries */
|
|
1012
|
+
const p = this.getValidatedRules(tableRules, localParams);
|
|
1013
|
+
const computedFields = p.allColumns.filter(c => c.type === "computed");
|
|
1014
|
+
let computedColConditions = [];
|
|
1015
|
+
Object.keys(data || {}).map(key => {
|
|
1016
|
+
const compCol = computedFields.find(cf => cf.name === key);
|
|
1017
|
+
if (compCol) {
|
|
1018
|
+
computedColConditions.push(compCol.getQuery({
|
|
1019
|
+
tableAlias,
|
|
1020
|
+
allowedFields: p.select.fields,
|
|
1021
|
+
allColumns: this.columns,
|
|
1022
|
+
/* CTID not available in AFTER trigger */
|
|
1023
|
+
// ctidField: this.is_view? undefined : "ctid"
|
|
1024
|
+
ctidField: undefined,
|
|
1025
|
+
}) + ` = ${exports.pgp.as.format("$1", [data[key]])}`);
|
|
1026
|
+
delete data[key];
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
let allowedSelect = [];
|
|
1030
|
+
/* Select aliases take precedence over col names. This is to ensure filters work correctly and even on computed cols*/
|
|
1031
|
+
if (select) {
|
|
1032
|
+
/* Allow filtering by selected fields/funcs */
|
|
1033
|
+
allowedSelect = select.filter(s => {
|
|
1034
|
+
/* */
|
|
1035
|
+
if (["function", "computed", "column"].includes(s.type)) {
|
|
1036
|
+
if (s.type !== "column" || allowed_colnames.includes(s.alias)) {
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return false;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
/* Add remaining allowed fields */
|
|
1044
|
+
allowedSelect = allowedSelect.concat(p.allColumns.filter(c => allowed_colnames.includes(c.name) &&
|
|
1045
|
+
!allowedSelect.find(s => s.alias === c.name)).map(f => ({
|
|
1046
|
+
type: f.type,
|
|
1047
|
+
alias: f.name,
|
|
1048
|
+
getQuery: (tableAlias) => f.getQuery({
|
|
1049
|
+
tableAlias,
|
|
1050
|
+
allColumns: this.columns,
|
|
1051
|
+
allowedFields: allowed_colnames
|
|
1052
|
+
}),
|
|
1053
|
+
selected: false,
|
|
1054
|
+
getFields: () => [f.name],
|
|
1055
|
+
column_udt_type: f.type === "column" ? this.columns.find(c => c.name === f.name)?.udt_name : undefined
|
|
1056
|
+
})));
|
|
1057
|
+
/* Parse complex filters
|
|
1058
|
+
{ $filter: [{ $func: [...] }, "=", value | { $func: [..] }] }
|
|
1059
|
+
*/
|
|
1060
|
+
const complexFilters = [];
|
|
1061
|
+
const complexFilterKey = "$filter";
|
|
1062
|
+
const allowedComparators = [">", "<", "=", "<=", ">=", "<>", "!="];
|
|
1063
|
+
if (complexFilterKey in data) {
|
|
1064
|
+
const getFuncQuery = (funcData) => {
|
|
1065
|
+
const { funcName, args } = (0, QueryBuilder_1.parseFunctionObject)(funcData);
|
|
1066
|
+
const funcDef = (0, QueryBuilder_1.parseFunction)({ func: funcName, args, functions: QueryBuilder_1.FUNCTIONS, allowedFields: allowed_colnames });
|
|
1067
|
+
return funcDef.getQuery({ args, tableAlias, allColumns: this.columns, allowedFields: allowed_colnames });
|
|
1068
|
+
};
|
|
1069
|
+
const complexFilter = data[complexFilterKey];
|
|
1070
|
+
if (!Array.isArray(complexFilter))
|
|
1071
|
+
throw `Invalid $filter. Must contain an array of at least element but got: ${JSON.stringify(complexFilter)} `;
|
|
1072
|
+
const leftFilter = complexFilter[0];
|
|
1073
|
+
const comparator = complexFilter[1];
|
|
1074
|
+
const rightFilterOrValue = complexFilter[2];
|
|
1075
|
+
const leftVal = getFuncQuery(leftFilter);
|
|
1076
|
+
let result = leftVal;
|
|
1077
|
+
if (comparator) {
|
|
1078
|
+
if (!allowedComparators.includes(comparator))
|
|
1079
|
+
throw `Invalid $filter. comparator ${JSON.stringify(comparator)} is not valid. Expecting one of: ${allowedComparators}`;
|
|
1080
|
+
if (!rightFilterOrValue)
|
|
1081
|
+
throw "Invalid $filter. Expecting a value or function after the comparator";
|
|
1082
|
+
const rightVal = (0, prostgles_types_1.isObject)(rightFilterOrValue) ? getFuncQuery(rightFilterOrValue) : (0, PubSubManager_1.asValue)(rightFilterOrValue);
|
|
1083
|
+
if (leftVal === rightVal)
|
|
1084
|
+
throw "Invalid $filter. Cannot compare two identical function signatures: " + JSON.stringify(leftFilter);
|
|
1085
|
+
result += ` ${comparator} ${rightVal}`;
|
|
1086
|
+
}
|
|
1087
|
+
complexFilters.push(result);
|
|
1088
|
+
}
|
|
1089
|
+
/* Parse join filters
|
|
1090
|
+
{ $joinFilter: { $ST_DWithin: [table.col, foreignTable.col, distance] }
|
|
1091
|
+
will make an exists filter
|
|
1092
|
+
*/
|
|
1093
|
+
let filterKeys = Object.keys(data).filter(k => k !== complexFilterKey && !funcFilterkeys.find(ek => ek.name === k) && !computedFields.find(cf => cf.name === k) && !existsKeys.find(ek => ek.key === k));
|
|
1094
|
+
// if(allowed_colnames){
|
|
1095
|
+
// const aliasedColumns = (select || []).filter(s =>
|
|
1096
|
+
// ["function", "computed", "column"].includes(s.type) && allowed_colnames.includes(s.alias) ||
|
|
1097
|
+
// s.getFields().find(f => allowed_colnames.includes(f))
|
|
1098
|
+
// ).map(s => s.alias);
|
|
1099
|
+
// const validCols = [...allowed_colnames, ...aliasedColumns];
|
|
1100
|
+
// }
|
|
1101
|
+
const validFieldNames = allowedSelect.map(s => s.alias);
|
|
1102
|
+
const invalidColumn = filterKeys
|
|
1103
|
+
.find(fName => !validFieldNames.find(c => c === fName ||
|
|
1104
|
+
(fName.startsWith(c) && (fName.slice(c.length).includes("->") ||
|
|
1105
|
+
fName.slice(c.length).includes(".")))));
|
|
1106
|
+
if (invalidColumn) {
|
|
1107
|
+
throw `Table: ${this.name} -> disallowed/inexistent columns in filter: ${invalidColumn} \n Expecting one of: ${allowedSelect.map(s => s.type === "column" ? s.getQuery() : s.alias).join(", ")}`;
|
|
1108
|
+
}
|
|
1109
|
+
/* TODO: Allow filter funcs */
|
|
1110
|
+
// const singleFuncs = FUNCTIONS.filter(f => f.singleColArg);
|
|
1111
|
+
const f = (0, PubSubManager_1.pickKeys)(data, filterKeys);
|
|
1112
|
+
const q = (0, Filtering_1.parseFilterItem)({
|
|
1113
|
+
filter: f,
|
|
1114
|
+
tableAlias,
|
|
1115
|
+
pgp: exports.pgp,
|
|
1116
|
+
select: allowedSelect
|
|
1117
|
+
});
|
|
1118
|
+
let templates = [q].filter(q => q);
|
|
1119
|
+
if (existsCond)
|
|
1120
|
+
templates.push(existsCond);
|
|
1121
|
+
templates = templates.concat(funcConds);
|
|
1122
|
+
templates = templates.concat(computedColConditions);
|
|
1123
|
+
templates = templates.concat(complexFilters);
|
|
1124
|
+
return templates.sort() /* sorted to ensure duplicate subscription channels are not created due to different condition order */
|
|
1125
|
+
.join(" AND \n");
|
|
1126
|
+
// return templates; //pgp.as.format(template, data);
|
|
1127
|
+
/*
|
|
1128
|
+
SHOULD CHECK DATA TYPES TO AVOID "No operator matches the given data type" error
|
|
1129
|
+
console.log(table.columns)
|
|
1130
|
+
*/
|
|
1131
|
+
}
|
|
1132
|
+
/* This relates only to SELECT */
|
|
1133
|
+
prepareSort(orderBy, allowed_cols, tableAlias, excludeOrder = false, select) {
|
|
1134
|
+
let column_names = this.column_names.slice(0);
|
|
1135
|
+
const throwErr = () => {
|
|
1136
|
+
throw "\nInvalid orderBy option -> " + JSON.stringify(orderBy) +
|
|
1137
|
+
"Expecting: \
|
|
1138
|
+
{ key2: false, key1: true } \
|
|
1139
|
+
{ key1: 1, key2: -1 } \
|
|
1140
|
+
[{ key1: true }, { key2: false }] \
|
|
1141
|
+
[{ key: 'colName', asc: true, nulls: 'first', nullEmpty: true }]";
|
|
1142
|
+
}, parseOrderObj = (orderBy, expectOne = false) => {
|
|
1143
|
+
if (!isPlainObject(orderBy))
|
|
1144
|
+
return throwErr();
|
|
1145
|
+
const keys = Object.keys(orderBy);
|
|
1146
|
+
if (keys.length && keys.find(k => ["key", "asc", "nulls", "nullEmpty"].includes(k))) {
|
|
1147
|
+
const { key, asc, nulls, nullEmpty = false } = orderBy;
|
|
1148
|
+
if (!["string"].includes(typeof key) ||
|
|
1149
|
+
!["boolean"].includes(typeof asc) ||
|
|
1150
|
+
!["first", "last", undefined, null].includes(nulls) ||
|
|
1151
|
+
!["boolean"].includes(typeof nullEmpty)) {
|
|
1152
|
+
throw `Invalid orderBy option (${JSON.stringify(orderBy, null, 2)}) \n
|
|
1153
|
+
Expecting { key: string, asc?: boolean, nulls?: 'first' | 'last' | null | undefined, nullEmpty?: boolean } `;
|
|
1154
|
+
}
|
|
1155
|
+
return [{ key, asc, nulls, nullEmpty }];
|
|
1156
|
+
}
|
|
1157
|
+
if (expectOne && keys.length > 1) {
|
|
1158
|
+
throw "\nInvalid orderBy " + JSON.stringify(orderBy) +
|
|
1159
|
+
"\nEach orderBy array element cannot have more than one key";
|
|
1160
|
+
}
|
|
1161
|
+
/* { key2: true, key1: false } */
|
|
1162
|
+
if (!Object.values(orderBy).find(v => ![true, false].includes(v))) {
|
|
1163
|
+
return keys.map(key => ({ key, asc: Boolean(orderBy[key]) }));
|
|
1164
|
+
/* { key2: -1, key1: 1 } */
|
|
1165
|
+
}
|
|
1166
|
+
else if (!Object.values(orderBy).find(v => ![-1, 1].includes(v))) {
|
|
1167
|
+
return keys.map(key => ({ key, asc: orderBy[key] === 1 }));
|
|
1168
|
+
/* { key2: "asc", key1: "desc" } */
|
|
1169
|
+
}
|
|
1170
|
+
else if (!Object.values(orderBy).find(v => !["asc", "desc"].includes(v))) {
|
|
1171
|
+
return keys.map(key => ({ key, asc: orderBy[key] === "asc" }));
|
|
1172
|
+
}
|
|
1173
|
+
else
|
|
1174
|
+
return throwErr();
|
|
1175
|
+
};
|
|
1176
|
+
if (!orderBy)
|
|
1177
|
+
return "";
|
|
1178
|
+
let allowedFields = [];
|
|
1179
|
+
if (allowed_cols) {
|
|
1180
|
+
allowedFields = this.parseFieldFilter(allowed_cols);
|
|
1181
|
+
}
|
|
1182
|
+
let _ob = [];
|
|
1183
|
+
if (isPlainObject(orderBy)) {
|
|
1184
|
+
_ob = parseOrderObj(orderBy);
|
|
1185
|
+
}
|
|
1186
|
+
else if (typeof orderBy === "string") {
|
|
1187
|
+
/* string */
|
|
1188
|
+
_ob = [{ key: orderBy, asc: true }];
|
|
1189
|
+
}
|
|
1190
|
+
else if (Array.isArray(orderBy)) {
|
|
1191
|
+
/* Order by is formed of a list of ascending field names */
|
|
1192
|
+
let _orderBy = orderBy;
|
|
1193
|
+
if (_orderBy && !_orderBy.find(v => typeof v !== "string")) {
|
|
1194
|
+
/* [string] */
|
|
1195
|
+
_ob = _orderBy.map(key => ({ key, asc: true }));
|
|
1196
|
+
}
|
|
1197
|
+
else if (_orderBy.find(v => isPlainObject(v) && Object.keys(v).length)) {
|
|
1198
|
+
// if(_orderBy.find(v => typeof v.key === "string")){
|
|
1199
|
+
// /* [{ key, asc, nulls }] */
|
|
1200
|
+
// _ob = Object.freeze(_orderBy) as any;
|
|
1201
|
+
// } else {
|
|
1202
|
+
// /* [{ [key]: asc }] | [{ [key]: -1 }] */
|
|
1203
|
+
// _ob = _orderBy.map(v => parseOrderObj(v, true)[0]);
|
|
1204
|
+
// }
|
|
1205
|
+
_ob = _orderBy.map(v => parseOrderObj(v, true)[0]);
|
|
1206
|
+
}
|
|
1207
|
+
else
|
|
1208
|
+
return throwErr();
|
|
1209
|
+
}
|
|
1210
|
+
else
|
|
1211
|
+
return throwErr();
|
|
1212
|
+
if (!_ob || !_ob.length)
|
|
1213
|
+
return "";
|
|
1214
|
+
const validatedAggAliases = select.filter(s => s.type !== "joinedColumn").map(s => s.alias);
|
|
1215
|
+
let bad_param = _ob.find(({ key }) => !(validatedAggAliases || []).includes(key) &&
|
|
1216
|
+
(!column_names.includes(key) ||
|
|
1217
|
+
(allowedFields.length && !allowedFields.includes(key))));
|
|
1218
|
+
if (!bad_param) {
|
|
1219
|
+
const selectedAliases = select.filter(s => s.selected).map(s => s.alias);
|
|
1220
|
+
return (excludeOrder ? "" : " ORDER BY ") + (_ob.map(({ key, asc, nulls, nullEmpty = false }) => {
|
|
1221
|
+
/* Order by column index when possible to bypass name collision when ordering by a computed column.
|
|
1222
|
+
(Postgres will sort by existing columns wheundefined possible)
|
|
1223
|
+
*/
|
|
1224
|
+
const orderType = asc ? " ASC " : " DESC ";
|
|
1225
|
+
const index = selectedAliases.indexOf(key) + 1;
|
|
1226
|
+
const nullOrder = nulls ? ` NULLS ${nulls === "first" ? " FIRST " : " LAST "}` : "";
|
|
1227
|
+
let colKey = (index > 0 && !nullEmpty) ? index : [tableAlias, key].filter(prostgles_types_1.isDefined).map(prostgles_types_1.asName).join(".");
|
|
1228
|
+
if (nullEmpty) {
|
|
1229
|
+
colKey = `nullif(trim(${colKey}::text), '')`;
|
|
1230
|
+
}
|
|
1231
|
+
const res = `${colKey} ${orderType} ${nullOrder}`;
|
|
1232
|
+
return res;
|
|
1233
|
+
}).join(", "));
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
throw "Unrecognised orderBy fields or params: " + bad_param.key;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/* This relates only to SELECT */
|
|
1240
|
+
prepareLimitQuery(limit = 1000, p) {
|
|
1241
|
+
if (limit !== undefined && limit !== null && !Number.isInteger(limit)) {
|
|
1242
|
+
throw "Unexpected LIMIT. Must be null or an integer";
|
|
1243
|
+
}
|
|
1244
|
+
let _limit = limit;
|
|
1245
|
+
// if(_limit === undefined && p.select.maxLimit === null){
|
|
1246
|
+
// _limit = 1000;
|
|
1247
|
+
/* If no limit then set as the lesser of (100, maxLimit) */
|
|
1248
|
+
// } else
|
|
1249
|
+
if (_limit !== null && !Number.isInteger(_limit) && p.select.maxLimit !== null) {
|
|
1250
|
+
_limit = [100, p.select.maxLimit].filter(Number.isInteger).sort((a, b) => a - b)[0];
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
/* If a limit higher than maxLimit specified throw error */
|
|
1254
|
+
if (Number.isInteger(p.select.maxLimit) && _limit > p.select.maxLimit) {
|
|
1255
|
+
throw `Unexpected LIMIT ${_limit}. Must be less than the published maxLimit: ` + p.select.maxLimit;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return _limit;
|
|
1259
|
+
}
|
|
1260
|
+
/* This relates only to SELECT */
|
|
1261
|
+
prepareOffsetQuery(offset) {
|
|
1262
|
+
if (Number.isInteger(offset)) {
|
|
1263
|
+
return offset;
|
|
1264
|
+
}
|
|
1265
|
+
return 0;
|
|
1266
|
+
}
|
|
1267
|
+
intersectColumns(allowedFields, dissallowedFields, fixIssues = false) {
|
|
1268
|
+
let result = [];
|
|
1269
|
+
if (allowedFields) {
|
|
1270
|
+
result = this.parseFieldFilter(allowedFields);
|
|
1271
|
+
}
|
|
1272
|
+
if (dissallowedFields) {
|
|
1273
|
+
const _dissalowed = this.parseFieldFilter(dissallowedFields);
|
|
1274
|
+
if (!fixIssues) {
|
|
1275
|
+
throw `dissallowed/invalid field found for ${this.name}: `;
|
|
1276
|
+
}
|
|
1277
|
+
result = result.filter(key => !_dissalowed.includes(key));
|
|
1278
|
+
}
|
|
1279
|
+
return result;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Prepare and validate field object:
|
|
1283
|
+
* @example ({ item_id: 1 }, { user_id: 32 }) => { item_id: 1, user_id: 32 }
|
|
1284
|
+
* OR
|
|
1285
|
+
* ({ a: 1 }, { b: 32 }, ["c", "d"]) => throw "a field is not allowed"
|
|
1286
|
+
* @param {Object} obj - initial data
|
|
1287
|
+
* @param {Object} forcedData - set/override property
|
|
1288
|
+
* @param {string[]} allowed_cols - allowed columns (excluding forcedData) from table rules
|
|
1289
|
+
*/
|
|
1290
|
+
prepareFieldValues(obj = {}, forcedData = {}, allowed_cols, fixIssues = false) {
|
|
1291
|
+
let column_names = this.column_names.slice(0);
|
|
1292
|
+
if (!column_names || !column_names.length)
|
|
1293
|
+
throw "table column_names mising";
|
|
1294
|
+
let _allowed_cols = column_names.slice(0);
|
|
1295
|
+
let _obj = { ...obj };
|
|
1296
|
+
if (allowed_cols) {
|
|
1297
|
+
_allowed_cols = this.parseFieldFilter(allowed_cols, false);
|
|
1298
|
+
}
|
|
1299
|
+
let final_filter = { ..._obj }, filter_keys = Object.keys(final_filter);
|
|
1300
|
+
if (fixIssues && filter_keys.length) {
|
|
1301
|
+
final_filter = {};
|
|
1302
|
+
filter_keys
|
|
1303
|
+
.filter(col => _allowed_cols.includes(col))
|
|
1304
|
+
.map(col => {
|
|
1305
|
+
final_filter[col] = _obj[col];
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
/* If has keys check against allowed_cols */
|
|
1309
|
+
if (final_filter && Object.keys(final_filter).length && _allowed_cols) {
|
|
1310
|
+
validateObj(final_filter, _allowed_cols);
|
|
1311
|
+
}
|
|
1312
|
+
if (forcedData && Object.keys(forcedData).length) {
|
|
1313
|
+
final_filter = { ...final_filter, ...forcedData };
|
|
1314
|
+
}
|
|
1315
|
+
validateObj(final_filter, column_names.slice(0));
|
|
1316
|
+
return final_filter;
|
|
1317
|
+
}
|
|
1318
|
+
parseFieldFilter(fieldParams = "*", allow_empty = true, allowed_cols) {
|
|
1319
|
+
return ViewHandler._parseFieldFilter(fieldParams, allow_empty, allowed_cols || this.column_names.slice(0));
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Filter string array
|
|
1323
|
+
* @param {FieldFilter} fieldParams - { col1: 0, col2: 0 } | { col1: true, col2: true } | "*" | ["key1", "key2"] | []
|
|
1324
|
+
* @param {boolean} allow_empty - allow empty select. defaults to true
|
|
1325
|
+
*/
|
|
1326
|
+
static _parseFieldFilter(fieldParams = "*", allow_empty = true, all_cols) {
|
|
1327
|
+
if (!all_cols)
|
|
1328
|
+
throw "all_cols missing";
|
|
1329
|
+
const all_fields = all_cols; // || this.column_names.slice(0);
|
|
1330
|
+
let colNames = [], initialParams = JSON.stringify(fieldParams);
|
|
1331
|
+
if (fieldParams) {
|
|
1332
|
+
/*
|
|
1333
|
+
"field1, field2, field4" | "*"
|
|
1334
|
+
*/
|
|
1335
|
+
if (typeof fieldParams === "string") {
|
|
1336
|
+
fieldParams = fieldParams.split(",").map(k => k.trim());
|
|
1337
|
+
}
|
|
1338
|
+
/* string[] */
|
|
1339
|
+
if (Array.isArray(fieldParams) && !fieldParams.find(f => typeof f !== "string")) {
|
|
1340
|
+
/*
|
|
1341
|
+
["*"]
|
|
1342
|
+
*/
|
|
1343
|
+
if (fieldParams[0] === "*") {
|
|
1344
|
+
return all_fields.slice(0);
|
|
1345
|
+
/*
|
|
1346
|
+
[""]
|
|
1347
|
+
*/
|
|
1348
|
+
}
|
|
1349
|
+
else if (fieldParams[0] === "") {
|
|
1350
|
+
if (allow_empty) {
|
|
1351
|
+
return [""];
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
throw "Empty value not allowed";
|
|
1355
|
+
}
|
|
1356
|
+
/*
|
|
1357
|
+
["field1", "field2", "field3"]
|
|
1358
|
+
*/
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
colNames = fieldParams.slice(0);
|
|
1362
|
+
}
|
|
1363
|
+
/*
|
|
1364
|
+
{ field1: true, field2: true } = only field1 and field2
|
|
1365
|
+
{ field1: false, field2: false } = all fields except field1 and field2
|
|
1366
|
+
*/
|
|
1367
|
+
}
|
|
1368
|
+
else if (isPlainObject(fieldParams)) {
|
|
1369
|
+
if (Object.keys(fieldParams).length) {
|
|
1370
|
+
let keys = Object.keys(fieldParams);
|
|
1371
|
+
if (keys[0] === "") {
|
|
1372
|
+
if (allow_empty) {
|
|
1373
|
+
return [""];
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
throw "Empty value not allowed";
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
validate(keys);
|
|
1380
|
+
keys.forEach(key => {
|
|
1381
|
+
const allowedVals = [true, false, 0, 1];
|
|
1382
|
+
if (!allowedVals.includes(fieldParams[key]))
|
|
1383
|
+
throw `Invalid field selection value for: { ${key}: ${fieldParams[key]} }. \n Allowed values: ${allowedVals.join(" OR ")}`;
|
|
1384
|
+
});
|
|
1385
|
+
let allowed = keys.filter(key => fieldParams[key]), disallowed = keys.filter(key => !fieldParams[key]);
|
|
1386
|
+
if (disallowed && disallowed.length) {
|
|
1387
|
+
return all_fields.filter(col => !disallowed.includes(col));
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
return [...allowed];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
return all_fields.slice(0);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
else {
|
|
1398
|
+
throw " Unrecognised field filter.\nExpecting any of: string | string[] | { [field]: boolean } \n Received -> " + initialParams;
|
|
1399
|
+
}
|
|
1400
|
+
validate(colNames);
|
|
1401
|
+
}
|
|
1402
|
+
return colNames;
|
|
1403
|
+
function validate(cols) {
|
|
1404
|
+
let bad_keys = cols.filter(col => !all_fields.includes(col));
|
|
1405
|
+
if (bad_keys && bad_keys.length) {
|
|
1406
|
+
throw "\nUnrecognised or illegal fields: " + bad_keys.join(", ");
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
exports.ViewHandler = ViewHandler;
|
|
1412
|
+
function isPojoObject(obj) {
|
|
1413
|
+
if (obj && (typeof obj !== "object" || Array.isArray(obj) || obj instanceof Date)) {
|
|
1414
|
+
return false;
|
|
1415
|
+
}
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
class TableHandler extends ViewHandler {
|
|
1419
|
+
constructor(db, tableOrViewInfo, dboBuilder, t, dbTX, joinPaths) {
|
|
1420
|
+
super(db, tableOrViewInfo, dboBuilder, t, dbTX, joinPaths);
|
|
1421
|
+
this.prepareReturning = async (returning, allowedFields) => {
|
|
1422
|
+
let result = [];
|
|
1423
|
+
if (returning) {
|
|
1424
|
+
let sBuilder = new QueryBuilder_1.SelectItemBuilder({
|
|
1425
|
+
allFields: this.column_names.slice(0),
|
|
1426
|
+
allowedFields,
|
|
1427
|
+
computedFields: QueryBuilder_1.COMPUTED_FIELDS,
|
|
1428
|
+
functions: QueryBuilder_1.FUNCTIONS.filter(f => f.type === "function" && f.singleColArg),
|
|
1429
|
+
isView: this.is_view,
|
|
1430
|
+
columns: this.columns,
|
|
1431
|
+
});
|
|
1432
|
+
await sBuilder.parseUserSelect(returning);
|
|
1433
|
+
return sBuilder.select;
|
|
1434
|
+
}
|
|
1435
|
+
return result;
|
|
1436
|
+
};
|
|
1437
|
+
this.remove = this.delete;
|
|
1438
|
+
this.io_stats = {
|
|
1439
|
+
since: Date.now(),
|
|
1440
|
+
queries: 0,
|
|
1441
|
+
throttle_queries_per_sec: 500,
|
|
1442
|
+
batching: null
|
|
1443
|
+
};
|
|
1444
|
+
this.is_view = false;
|
|
1445
|
+
this.is_media = dboBuilder.prostgles.isMedia(this.name);
|
|
1446
|
+
}
|
|
1447
|
+
/* TO DO: Maybe finished query batching */
|
|
1448
|
+
willBatch(query) {
|
|
1449
|
+
const now = Date.now();
|
|
1450
|
+
if (this.io_stats.since < Date.now()) {
|
|
1451
|
+
this.io_stats.since = Date.now();
|
|
1452
|
+
this.io_stats.queries = 0;
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
this.io_stats.queries++;
|
|
1456
|
+
}
|
|
1457
|
+
if (this.io_stats.queries > this.io_stats.throttle_queries_per_sec) {
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async subscribe(filter, params = {}, localFunc, table_rules, localParams) {
|
|
1462
|
+
try {
|
|
1463
|
+
if (this.is_view)
|
|
1464
|
+
throw "Cannot subscribe to a view";
|
|
1465
|
+
if (this.t)
|
|
1466
|
+
throw "subscribe not allowed within transactions";
|
|
1467
|
+
if (!localParams && !localFunc)
|
|
1468
|
+
throw " missing data. provide -> localFunc | localParams { socket } ";
|
|
1469
|
+
if (localParams && localParams.socket && localFunc) {
|
|
1470
|
+
console.error({ localParams, localFunc });
|
|
1471
|
+
throw " Cannot have localFunc AND socket ";
|
|
1472
|
+
}
|
|
1473
|
+
const { filterFields, forcedFilter } = (0, utils_1.get)(table_rules, "select") || {}, condition = await this.prepareWhere({ filter, forcedFilter, addKeywords: false, filterFields, tableAlias: undefined, localParams, tableRule: table_rules }), throttle = (0, utils_1.get)(params, "throttle") || 0, selectParams = (0, PubSubManager_1.omitKeys)(params || {}, ["throttle"]);
|
|
1474
|
+
// const { subOne = false } = localParams || {};
|
|
1475
|
+
const filterSize = JSON.stringify(filter || {}).length;
|
|
1476
|
+
if (filterSize * 4 > 2704) {
|
|
1477
|
+
throw "filter too big. Might exceed the btree version 4 maximum 2704";
|
|
1478
|
+
}
|
|
1479
|
+
if (!localFunc) {
|
|
1480
|
+
if (!this.dboBuilder.prostgles.isSuperUser)
|
|
1481
|
+
throw "Subscribe not possible. Must be superuser to add triggers 1856";
|
|
1482
|
+
return await this.find(filter, { ...selectParams, limit: 0 }, undefined, table_rules, localParams)
|
|
1483
|
+
.then(async (isValid) => {
|
|
1484
|
+
const { socket } = localParams ?? {};
|
|
1485
|
+
const pubSubManager = await this.dboBuilder.getPubSubManager();
|
|
1486
|
+
return pubSubManager.addSub({
|
|
1487
|
+
table_info: this.tableOrViewInfo,
|
|
1488
|
+
socket,
|
|
1489
|
+
table_rules,
|
|
1490
|
+
condition: condition,
|
|
1491
|
+
func: undefined,
|
|
1492
|
+
filter: { ...filter },
|
|
1493
|
+
params: { ...selectParams },
|
|
1494
|
+
socket_id: socket?.id,
|
|
1495
|
+
table_name: this.name,
|
|
1496
|
+
throttle,
|
|
1497
|
+
last_throttled: 0,
|
|
1498
|
+
// subOne
|
|
1499
|
+
}).then(channelName => ({ channelName }));
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
const pubSubManager = await this.dboBuilder.getPubSubManager();
|
|
1504
|
+
pubSubManager.addSub({
|
|
1505
|
+
table_info: this.tableOrViewInfo,
|
|
1506
|
+
socket: undefined,
|
|
1507
|
+
table_rules,
|
|
1508
|
+
condition,
|
|
1509
|
+
func: localFunc,
|
|
1510
|
+
filter: { ...filter },
|
|
1511
|
+
params: { ...selectParams },
|
|
1512
|
+
socket_id: undefined,
|
|
1513
|
+
table_name: this.name,
|
|
1514
|
+
throttle,
|
|
1515
|
+
last_throttled: 0,
|
|
1516
|
+
// subOne
|
|
1517
|
+
}).then(channelName => ({ channelName }));
|
|
1518
|
+
const unsubscribe = async () => {
|
|
1519
|
+
const pubSubManager = await this.dboBuilder.getPubSubManager();
|
|
1520
|
+
pubSubManager.removeLocalSub(this.name, condition, localFunc);
|
|
1521
|
+
};
|
|
1522
|
+
let res = Object.freeze({ unsubscribe });
|
|
1523
|
+
return res;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
catch (e) {
|
|
1527
|
+
if (localParams && localParams.testRule)
|
|
1528
|
+
throw e;
|
|
1529
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.subscribe()` };
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
subscribeOne(filter, params = {}, localFunc, table_rules, localParams) {
|
|
1533
|
+
let func = localParams ? undefined : (rows) => localFunc(rows[0]);
|
|
1534
|
+
return this.subscribe(filter, { ...params, limit: 2 }, func, table_rules, localParams);
|
|
1535
|
+
}
|
|
1536
|
+
async updateBatch(data, params, tableRules, localParams) {
|
|
1537
|
+
try {
|
|
1538
|
+
const queries = await Promise.all(data.map(async ([filter, data]) => await this.update(filter, data, { ...(params || {}), returning: undefined }, tableRules, { ...(localParams || {}), returnQuery: true })));
|
|
1539
|
+
const keys = (data && data.length) ? Object.keys(data[0]) : [];
|
|
1540
|
+
return this.db.tx(t => {
|
|
1541
|
+
const _queries = queries.map(q => t.none(q));
|
|
1542
|
+
return t.batch(_queries);
|
|
1543
|
+
}).catch(err => makeErr(err, localParams, this, keys));
|
|
1544
|
+
}
|
|
1545
|
+
catch (e) {
|
|
1546
|
+
if (localParams && localParams.testRule)
|
|
1547
|
+
throw e;
|
|
1548
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.update()` };
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
async parseUpdateRules(filter, newData, params, tableRules, localParams) {
|
|
1552
|
+
const { testRule = false } = localParams ?? {};
|
|
1553
|
+
if (!testRule) {
|
|
1554
|
+
if (!newData || !Object.keys(newData).length)
|
|
1555
|
+
throw "no update data provided\nEXPECTING db.table.update(filter, updateData, options)";
|
|
1556
|
+
this.checkFilter(filter);
|
|
1557
|
+
}
|
|
1558
|
+
let forcedFilter = {}, forcedData = {}, validate, returningFields = "*", filterFields = "*", fields = "*";
|
|
1559
|
+
const { $and: $and_key } = this.dboBuilder.prostgles.keywords;
|
|
1560
|
+
let finalUpdateFilter = { ...filter };
|
|
1561
|
+
if (tableRules) {
|
|
1562
|
+
if (!tableRules.update)
|
|
1563
|
+
throw "update rules missing for " + this.name;
|
|
1564
|
+
({ forcedFilter, forcedData, fields, filterFields, validate } = tableRules.update);
|
|
1565
|
+
returningFields = tableRules.update.returningFields ?? (0, utils_1.get)(tableRules, "select.fields") ?? "";
|
|
1566
|
+
if (!returningFields && params?.returning) {
|
|
1567
|
+
throw "You are not allowed to return any fields from the update";
|
|
1568
|
+
}
|
|
1569
|
+
if (!fields)
|
|
1570
|
+
throw ` Invalid update rule for ${this.name}. fields missing `;
|
|
1571
|
+
finalUpdateFilter = (0, exports.getUpdateFilter)({ filter, forcedFilter, $and_key });
|
|
1572
|
+
if (tableRules.update.dynamicFields?.length) {
|
|
1573
|
+
/**
|
|
1574
|
+
* Ensure that dynamicFields.fields are less permissive than fields
|
|
1575
|
+
* This is because an update filter can target dynamicFields.filter AND also other records
|
|
1576
|
+
*/
|
|
1577
|
+
if (testRule) {
|
|
1578
|
+
const defaultFields = this.parseFieldFilter(fields);
|
|
1579
|
+
const morePermissiveRule = tableRules.update.dynamicFields.find(r => {
|
|
1580
|
+
const ruleFields = this.parseFieldFilter(r.fields);
|
|
1581
|
+
return defaultFields.length && defaultFields.every(f => ruleFields.includes(f)) && ruleFields.length > defaultFields.length;
|
|
1582
|
+
});
|
|
1583
|
+
if (morePermissiveRule) {
|
|
1584
|
+
throw `${this.name}.update.dynamicFields must be less permissive than the default ${this.name}.update.fields.
|
|
1585
|
+
This is because an update filter can target dynamicFields.filter AND also other records.
|
|
1586
|
+
Bad dynamicFields.fields: ${this.parseFieldFilter(morePermissiveRule.fields)}
|
|
1587
|
+
default fields: ${defaultFields}
|
|
1588
|
+
Bad dynamicFields: ${JSON.stringify(morePermissiveRule)}
|
|
1589
|
+
`;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
let found = false;
|
|
1593
|
+
for await (const dfRule of tableRules.update.dynamicFields) {
|
|
1594
|
+
if (!found) {
|
|
1595
|
+
const count = await this.count({ $and: [finalUpdateFilter, dfRule.filter].filter(prostgles_types_1.isDefined) });
|
|
1596
|
+
if (count && +count > 0) {
|
|
1597
|
+
found = true;
|
|
1598
|
+
fields = dfRule.fields;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/* Safely test publish rules */
|
|
1604
|
+
if (testRule) {
|
|
1605
|
+
await this.validateViewRules({ fields, filterFields, returningFields, forcedFilter, dynamicFields: tableRules.update.dynamicFields, rule: "update" });
|
|
1606
|
+
if (forcedData) {
|
|
1607
|
+
try {
|
|
1608
|
+
const { data, allowedCols } = this.validateNewData({ row: forcedData, forcedData: undefined, allowedFields: "*", tableRules, fixIssues: false });
|
|
1609
|
+
const updateQ = await this.colSet.getUpdateQuery(data, allowedCols, validate ? ((row) => validate({ update: row, filter: {} })) : undefined); //pgp.helpers.update(data, columnSet)
|
|
1610
|
+
let query = updateQ + " WHERE FALSE ";
|
|
1611
|
+
await this.db.any("EXPLAIN " + query);
|
|
1612
|
+
}
|
|
1613
|
+
catch (e) {
|
|
1614
|
+
throw " issue with forcedData: \nVALUE: " + JSON.stringify(forcedData, null, 2) + "\nERROR: " + e;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
/* Update all allowed fields (fields) except the forcedFilter (so that the user cannot change the forced filter values) */
|
|
1621
|
+
let _fields = this.parseFieldFilter(fields);
|
|
1622
|
+
/**
|
|
1623
|
+
* forcedFilter must not have any keys in common with the fields
|
|
1624
|
+
* A user is not allowed to change any fields found in the forcedFilter
|
|
1625
|
+
* A forced filter must be basic
|
|
1626
|
+
*/
|
|
1627
|
+
if (forcedFilter) {
|
|
1628
|
+
const _forcedFilterKeys = Object.keys(forcedFilter);
|
|
1629
|
+
const nonFields = _forcedFilterKeys.filter(key => !this.column_names.includes(key));
|
|
1630
|
+
if (nonFields.length)
|
|
1631
|
+
throw "forcedFilter must be a basic filter but it includes non field keys: " + nonFields;
|
|
1632
|
+
const clashingFields = _forcedFilterKeys.filter(key => _fields.includes(key));
|
|
1633
|
+
if (clashingFields.length)
|
|
1634
|
+
throw "forcedFilter must not include fields from the fields (otherwise the user can update data to bypass the forcedFilter). Clashing fields: " + nonFields;
|
|
1635
|
+
}
|
|
1636
|
+
const validateRow = validate ? (row) => validate({ update: row, filter: finalUpdateFilter }) : undefined;
|
|
1637
|
+
return {
|
|
1638
|
+
fields: _fields,
|
|
1639
|
+
validateRow,
|
|
1640
|
+
finalUpdateFilter,
|
|
1641
|
+
forcedData,
|
|
1642
|
+
forcedFilter,
|
|
1643
|
+
returningFields,
|
|
1644
|
+
filterFields,
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
async update(filter, newData, params, tableRules, localParams) {
|
|
1648
|
+
try {
|
|
1649
|
+
const parsedRules = await this.parseUpdateRules(filter, newData, params, tableRules, localParams);
|
|
1650
|
+
if (localParams?.testRule) {
|
|
1651
|
+
return parsedRules;
|
|
1652
|
+
}
|
|
1653
|
+
const { fields, validateRow, forcedData, finalUpdateFilter, returningFields, forcedFilter, filterFields } = parsedRules;
|
|
1654
|
+
let { returning, multi = true, onConflictDoNothing = false, fixIssues = false } = params || {};
|
|
1655
|
+
const { returnQuery = false } = localParams ?? {};
|
|
1656
|
+
if (params) {
|
|
1657
|
+
const good_params = ["returning", "multi", "onConflictDoNothing", "fixIssues"];
|
|
1658
|
+
const bad_params = Object.keys(params).filter(k => !good_params.includes(k));
|
|
1659
|
+
if (bad_params && bad_params.length)
|
|
1660
|
+
throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
|
|
1661
|
+
}
|
|
1662
|
+
const { data, allowedCols } = this.validateNewData({ row: newData, forcedData, allowedFields: fields, tableRules, fixIssues });
|
|
1663
|
+
/* Patch data */
|
|
1664
|
+
let patchedTextData = [];
|
|
1665
|
+
this.columns.map(c => {
|
|
1666
|
+
const d = data[c.name];
|
|
1667
|
+
if (c.data_type === "text" && d && isPlainObject(d) && !["from", "to"].find(key => typeof d[key] !== "number")) {
|
|
1668
|
+
const unrecProps = Object.keys(d).filter(k => !["from", "to", "text", "md5"].includes(k));
|
|
1669
|
+
if (unrecProps.length)
|
|
1670
|
+
throw "Unrecognised params in textPatch field: " + unrecProps.join(", ");
|
|
1671
|
+
patchedTextData.push({ ...d, fieldName: c.name });
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
if (patchedTextData && patchedTextData.length) {
|
|
1675
|
+
if (tableRules && !tableRules.select)
|
|
1676
|
+
throw "Select needs to be permitted to patch data";
|
|
1677
|
+
const rows = await this.find(filter, { select: patchedTextData.reduce((a, v) => ({ ...a, [v.fieldName]: 1 }), {}) }, undefined, tableRules);
|
|
1678
|
+
if (rows.length !== 1) {
|
|
1679
|
+
throw "Cannot patch data within a filter that affects more/less than 1 row";
|
|
1680
|
+
}
|
|
1681
|
+
patchedTextData.map(p => {
|
|
1682
|
+
data[p.fieldName] = (0, prostgles_types_1.unpatchText)(rows[0][p.fieldName], p);
|
|
1683
|
+
});
|
|
1684
|
+
// https://w3resource.com/PostgreSQL/overlay-function.p hp
|
|
1685
|
+
// overlay(coalesce(status, '') placing 'hom' from 2 for 0)
|
|
1686
|
+
}
|
|
1687
|
+
let nData = { ...data };
|
|
1688
|
+
// if(tableRules && tableRules.update && tableRules?.update?.validate){
|
|
1689
|
+
// nData = await tableRules.update.validate(nData);
|
|
1690
|
+
// }
|
|
1691
|
+
let query = await this.colSet.getUpdateQuery(nData, allowedCols, validateRow); //pgp.helpers.update(nData, columnSet) + " ";
|
|
1692
|
+
query += (await this.prepareWhere({
|
|
1693
|
+
filter,
|
|
1694
|
+
forcedFilter,
|
|
1695
|
+
filterFields,
|
|
1696
|
+
localParams,
|
|
1697
|
+
tableRule: tableRules
|
|
1698
|
+
}));
|
|
1699
|
+
if (onConflictDoNothing)
|
|
1700
|
+
query += " ON CONFLICT DO NOTHING ";
|
|
1701
|
+
let qType = "none";
|
|
1702
|
+
if (returning) {
|
|
1703
|
+
qType = multi ? "any" : "one";
|
|
1704
|
+
query += this.makeReturnQuery(await this.prepareReturning(returning, this.parseFieldFilter(returningFields)));
|
|
1705
|
+
}
|
|
1706
|
+
if (returnQuery)
|
|
1707
|
+
return query;
|
|
1708
|
+
if (this.t) {
|
|
1709
|
+
return this.t[qType](query).catch((err) => makeErr(err, localParams, this, fields));
|
|
1710
|
+
}
|
|
1711
|
+
return this.db.tx(t => t[qType](query)).catch(err => makeErr(err, localParams, this, fields));
|
|
1712
|
+
}
|
|
1713
|
+
catch (e) {
|
|
1714
|
+
if (localParams && localParams.testRule)
|
|
1715
|
+
throw e;
|
|
1716
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.update(${JSON.stringify(filter || {}, null, 2)}, ${JSON.stringify(newData || {}, null, 2)}, ${JSON.stringify(params || {}, null, 2)})` };
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
;
|
|
1720
|
+
validateNewData({ row, forcedData, allowedFields, tableRules, fixIssues = false }) {
|
|
1721
|
+
const synced_field = (0, utils_1.get)(tableRules ?? {}, "sync.synced_field");
|
|
1722
|
+
/* Update synced_field if sync is on and missing */
|
|
1723
|
+
if (synced_field && !row[synced_field]) {
|
|
1724
|
+
row[synced_field] = Date.now();
|
|
1725
|
+
}
|
|
1726
|
+
let data = this.prepareFieldValues(row, forcedData, allowedFields, fixIssues);
|
|
1727
|
+
const dataKeys = (0, prostgles_types_1.getKeys)(data);
|
|
1728
|
+
dataKeys.map(col => {
|
|
1729
|
+
this.dboBuilder.prostgles?.tableConfigurator?.checkColVal({ table: this.name, col, value: data[col] });
|
|
1730
|
+
const colConfig = this.dboBuilder.prostgles?.tableConfigurator?.getColumnConfig(this.name, col);
|
|
1731
|
+
if (colConfig && "isText" in colConfig && data[col]) {
|
|
1732
|
+
if (colConfig.lowerCased) {
|
|
1733
|
+
data[col] = data[col].toString().toLowerCase();
|
|
1734
|
+
}
|
|
1735
|
+
if (colConfig.trimmed) {
|
|
1736
|
+
data[col] = data[col].toString().trim();
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
return { data, allowedCols: this.columns.filter(c => dataKeys.includes(c.name)).map(c => c.name) };
|
|
1741
|
+
}
|
|
1742
|
+
async insertDataParse(data, param2, param3_unused, tableRules, _localParams) {
|
|
1743
|
+
const localParams = _localParams || {};
|
|
1744
|
+
let dbTX = localParams?.dbTX || this.dbTX;
|
|
1745
|
+
const isMultiInsert = Array.isArray(data);
|
|
1746
|
+
const getExtraKeys = (d) => Object.keys(d).filter(k => !this.columns.find(c => c.name === k));
|
|
1747
|
+
/* Nested insert is not allowed for the file table */
|
|
1748
|
+
const isNestedInsert = this.is_media ? false : (Array.isArray(data) ? data : [data]).some(d => getExtraKeys(d).length);
|
|
1749
|
+
/**
|
|
1750
|
+
* Make sure nested insert uses a transaction
|
|
1751
|
+
*/
|
|
1752
|
+
if (isNestedInsert && !dbTX) {
|
|
1753
|
+
return {
|
|
1754
|
+
insertResult: await this.dboBuilder.getTX((dbTX) => dbTX[this.name].insert(data, param2, param3_unused, tableRules, { dbTX, ...localParams }))
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
// if(!dbTX && this.t) dbTX = this.d;
|
|
1758
|
+
const preValidate = tableRules?.insert?.preValidate, validate = tableRules?.insert?.validate;
|
|
1759
|
+
let _data = await Promise.all((Array.isArray(data) ? data : [data]).map(async (row) => {
|
|
1760
|
+
if (preValidate) {
|
|
1761
|
+
row = await preValidate(row);
|
|
1762
|
+
}
|
|
1763
|
+
const dataKeys = Object.keys(row);
|
|
1764
|
+
const extraKeys = getExtraKeys(row);
|
|
1765
|
+
/* Upload file then continue insert */
|
|
1766
|
+
if (this.is_media) {
|
|
1767
|
+
if (!this.dboBuilder.prostgles?.fileManager)
|
|
1768
|
+
throw "fileManager not set up";
|
|
1769
|
+
const { data, name } = row;
|
|
1770
|
+
if (dataKeys.length !== 2)
|
|
1771
|
+
throw "Expecting only two properties: { name: string; data: File }";
|
|
1772
|
+
// if(!Buffer.isBuffer(data)) throw "data is not of type Buffer"
|
|
1773
|
+
if (!data)
|
|
1774
|
+
throw "data not provided";
|
|
1775
|
+
if (typeof name !== "string") {
|
|
1776
|
+
throw "name is not of type string";
|
|
1777
|
+
}
|
|
1778
|
+
const media_id = (await this.db.oneOrNone("SELECT gen_random_uuid() as name")).name;
|
|
1779
|
+
const type = await this.dboBuilder.prostgles.fileManager.getMIME(data, name);
|
|
1780
|
+
const media_name = `${media_id}.${type.ext}`;
|
|
1781
|
+
let media = {
|
|
1782
|
+
id: media_id,
|
|
1783
|
+
name: media_name,
|
|
1784
|
+
original_name: name,
|
|
1785
|
+
extension: type.ext,
|
|
1786
|
+
content_type: type.mime
|
|
1787
|
+
};
|
|
1788
|
+
if (validate) {
|
|
1789
|
+
media = await validate(media);
|
|
1790
|
+
}
|
|
1791
|
+
const _media = await this.dboBuilder.prostgles.fileManager.uploadAsMedia({
|
|
1792
|
+
item: {
|
|
1793
|
+
data,
|
|
1794
|
+
name: media.name ?? "????",
|
|
1795
|
+
content_type: media.content_type
|
|
1796
|
+
},
|
|
1797
|
+
// imageCompression: {
|
|
1798
|
+
// inside: {
|
|
1799
|
+
// width: 1100,
|
|
1800
|
+
// height: 630
|
|
1801
|
+
// }
|
|
1802
|
+
// }
|
|
1803
|
+
});
|
|
1804
|
+
return {
|
|
1805
|
+
...media,
|
|
1806
|
+
..._media,
|
|
1807
|
+
};
|
|
1808
|
+
/* Potentially a nested join */
|
|
1809
|
+
}
|
|
1810
|
+
else if (extraKeys.length) {
|
|
1811
|
+
/* Ensure we're using the same transaction */
|
|
1812
|
+
const _this = this.t ? this : dbTX[this.name];
|
|
1813
|
+
let rootData = Array.isArray(data) ? data.map(d => (0, PubSubManager_1.omitKeys)(d, extraKeys)) : (0, PubSubManager_1.omitKeys)(data, extraKeys);
|
|
1814
|
+
let insertedChildren;
|
|
1815
|
+
let targetTableRules;
|
|
1816
|
+
const fullRootResult = await _this.insert(rootData, { returning: "*" }, undefined, tableRules, localParams);
|
|
1817
|
+
let returnData;
|
|
1818
|
+
const returning = param2?.returning;
|
|
1819
|
+
if (returning) {
|
|
1820
|
+
returnData = {};
|
|
1821
|
+
const returningItems = await this.prepareReturning(returning, this.parseFieldFilter(tableRules?.insert?.returningFields));
|
|
1822
|
+
returningItems.filter(s => s.selected).map(rs => {
|
|
1823
|
+
returnData[rs.alias] = fullRootResult[rs.alias];
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
await Promise.all(extraKeys.map(async (targetTable) => {
|
|
1827
|
+
const childDataItems = Array.isArray(row[targetTable]) ? row[targetTable] : [row[targetTable]];
|
|
1828
|
+
/* Must be allowed to insert into media table */
|
|
1829
|
+
const canInsert = async (tbl) => {
|
|
1830
|
+
const childRules = await this.dboBuilder.publishParser?.getValidatedRequestRuleWusr({ tableName: tbl, command: "insert", localParams });
|
|
1831
|
+
if (!childRules || !childRules.insert)
|
|
1832
|
+
throw "Dissallowed nested insert into table " + childRules;
|
|
1833
|
+
return childRules;
|
|
1834
|
+
};
|
|
1835
|
+
// console.log(JSON.stringify(this.dboBuilder.joinPaths, null, 2))
|
|
1836
|
+
const jp = this.dboBuilder.joinPaths.find(jp => jp.t1 === this.name && jp.t2 === targetTable);
|
|
1837
|
+
if (!jp)
|
|
1838
|
+
throw `Could not find a valid table for the nested data { ${targetTable} } `;
|
|
1839
|
+
const thisInfo = await this.getInfo();
|
|
1840
|
+
const childInsert = async (cdata, tableName) => {
|
|
1841
|
+
// console.log("childInsert", {data, tableName})
|
|
1842
|
+
if (!cdata || !dbTX?.[tableName] || !("insert" in dbTX[tableName]))
|
|
1843
|
+
throw "childInsertErr: Child table handler missing for: " + tableName;
|
|
1844
|
+
const tableRules = await canInsert(tableName);
|
|
1845
|
+
if (thisInfo.has_media === "one" && thisInfo.media_table_name === tableName && Array.isArray(cdata) && cdata.length > 1) {
|
|
1846
|
+
throw "Constraint check fail: Cannot insert more than one record into " + JSON.stringify(tableName);
|
|
1847
|
+
}
|
|
1848
|
+
return Promise.all((Array.isArray(cdata) ? cdata : [cdata])
|
|
1849
|
+
.map(m => dbTX[tableName]
|
|
1850
|
+
.insert(m, { returning: "*" }, undefined, tableRules, localParams)
|
|
1851
|
+
.catch(e => {
|
|
1852
|
+
console.trace({ childInsertErr: e });
|
|
1853
|
+
return Promise.reject({ childInsertErr: e });
|
|
1854
|
+
})));
|
|
1855
|
+
};
|
|
1856
|
+
const { path } = jp;
|
|
1857
|
+
const [tbl1, tbl2, tbl3] = path;
|
|
1858
|
+
targetTableRules = await canInsert(targetTable); // tbl3
|
|
1859
|
+
const cols2 = this.dboBuilder.dbo[tbl2].columns || [];
|
|
1860
|
+
if (!this.dboBuilder.dbo[tbl2])
|
|
1861
|
+
throw "Invalid/disallowed table: " + tbl2;
|
|
1862
|
+
const colsRefT1 = cols2?.filter(c => c.references?.cols.length === 1 && c.references?.ftable === tbl1);
|
|
1863
|
+
if (!path.length) {
|
|
1864
|
+
throw "Nested inserts join path not found for " + [this.name, targetTable];
|
|
1865
|
+
}
|
|
1866
|
+
else if (path.length === 2) {
|
|
1867
|
+
if (targetTable !== tbl2)
|
|
1868
|
+
throw "Did not expect this";
|
|
1869
|
+
if (!colsRefT1.length)
|
|
1870
|
+
throw `Target table ${tbl2} does not reference any columns from the root table ${this.name}. Cannot do nested insert`;
|
|
1871
|
+
// console.log(childDataItems, JSON.stringify(colsRefT1, null, 2))
|
|
1872
|
+
insertedChildren = await childInsert(childDataItems.map((d) => {
|
|
1873
|
+
let result = { ...d };
|
|
1874
|
+
colsRefT1.map(col => {
|
|
1875
|
+
result[col.references.cols[0]] = fullRootResult[col.references.fcols[0]];
|
|
1876
|
+
});
|
|
1877
|
+
return result;
|
|
1878
|
+
}), targetTable);
|
|
1879
|
+
// console.log({ insertedChildren })
|
|
1880
|
+
}
|
|
1881
|
+
else if (path.length === 3) {
|
|
1882
|
+
if (targetTable !== tbl3)
|
|
1883
|
+
throw "Did not expect this";
|
|
1884
|
+
const colsRefT3 = cols2?.filter(c => c.references?.cols.length === 1 && c.references?.ftable === tbl3);
|
|
1885
|
+
if (!colsRefT1.length || !colsRefT3.length)
|
|
1886
|
+
throw "Incorrectly referenced or missing columns for nested insert";
|
|
1887
|
+
if (targetTable !== this.dboBuilder.prostgles.fileManager?.tableName) {
|
|
1888
|
+
throw "Only media allowed to have nested inserts more than 2 tables apart";
|
|
1889
|
+
}
|
|
1890
|
+
/* We expect tbl2 to have only 2 columns (media_id and foreign_id) */
|
|
1891
|
+
if (!cols2 || cols2.find(c => !["media_id", "foreign_id"].includes(c.name))) {
|
|
1892
|
+
throw "Second joining table not of expected format";
|
|
1893
|
+
}
|
|
1894
|
+
insertedChildren = await childInsert(childDataItems, targetTable);
|
|
1895
|
+
/* Insert in key_lookup table */
|
|
1896
|
+
await Promise.all(insertedChildren.map(async (t3Child) => {
|
|
1897
|
+
let tbl2Row = {};
|
|
1898
|
+
colsRefT3.map(col => {
|
|
1899
|
+
tbl2Row[col.name] = t3Child[col.references.fcols[0]];
|
|
1900
|
+
});
|
|
1901
|
+
colsRefT1.map(col => {
|
|
1902
|
+
tbl2Row[col.name] = fullRootResult[col.references.fcols[0]];
|
|
1903
|
+
});
|
|
1904
|
+
// console.log({ rootResult, tbl2Row, t3Child, colsRefT3, colsRefT1, t: this.t?.ctx?.start });
|
|
1905
|
+
await childInsert(tbl2Row, tbl2); //.then(() => {});
|
|
1906
|
+
}));
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
console.error(JSON.stringify({ path, thisTable: this.name, targetTable }, null, 2));
|
|
1910
|
+
throw "Unexpected path for Nested inserts";
|
|
1911
|
+
}
|
|
1912
|
+
/* Return also the nested inserted data */
|
|
1913
|
+
if (targetTableRules && insertedChildren?.length && returning) {
|
|
1914
|
+
const targetTableHandler = dbTX[targetTable];
|
|
1915
|
+
const targetReturning = await targetTableHandler.prepareReturning("*", targetTableHandler.parseFieldFilter(targetTableRules?.insert?.returningFields));
|
|
1916
|
+
let clientTargetInserts = insertedChildren.map(d => {
|
|
1917
|
+
let _d = { ...d };
|
|
1918
|
+
let res = {};
|
|
1919
|
+
targetReturning.map(r => {
|
|
1920
|
+
res[r.alias] = _d[r.alias];
|
|
1921
|
+
});
|
|
1922
|
+
return res;
|
|
1923
|
+
});
|
|
1924
|
+
returnData[targetTable] = clientTargetInserts.length === 1 ? clientTargetInserts[0] : clientTargetInserts;
|
|
1925
|
+
}
|
|
1926
|
+
}));
|
|
1927
|
+
return returnData;
|
|
1928
|
+
}
|
|
1929
|
+
return row;
|
|
1930
|
+
}));
|
|
1931
|
+
let result = isMultiInsert ? _data : _data[0];
|
|
1932
|
+
// if(validate && !isNestedInsert){
|
|
1933
|
+
// result = isMultiInsert? await Promise.all(_data.map(async d => await validate({ ...d }))) : await validate({ ..._data[0] });
|
|
1934
|
+
// }
|
|
1935
|
+
let res = isNestedInsert ?
|
|
1936
|
+
{ insertResult: result } :
|
|
1937
|
+
{ data: result };
|
|
1938
|
+
return res;
|
|
1939
|
+
}
|
|
1940
|
+
async insert(rowOrRows, param2, param3_unused, tableRules, _localParams) {
|
|
1941
|
+
const localParams = _localParams || {};
|
|
1942
|
+
const { dbTX } = localParams;
|
|
1943
|
+
try {
|
|
1944
|
+
const { returning, onConflictDoNothing, fixIssues = false } = param2 || {};
|
|
1945
|
+
const { testRule = false, returnQuery = false } = localParams || {};
|
|
1946
|
+
let returningFields, forcedData, fields;
|
|
1947
|
+
if (tableRules) {
|
|
1948
|
+
if (!tableRules.insert)
|
|
1949
|
+
throw "insert rules missing for " + this.name;
|
|
1950
|
+
returningFields = tableRules.insert.returningFields;
|
|
1951
|
+
forcedData = tableRules.insert.forcedData;
|
|
1952
|
+
fields = tableRules.insert.fields;
|
|
1953
|
+
/* If no returning fields specified then take select fields as returning */
|
|
1954
|
+
if (!returningFields)
|
|
1955
|
+
returningFields = (0, utils_1.get)(tableRules, "select.fields") || (0, utils_1.get)(tableRules, "insert.fields");
|
|
1956
|
+
if (!fields)
|
|
1957
|
+
throw ` invalid insert rule for ${this.name} -> fields missing `;
|
|
1958
|
+
/* Safely test publish rules */
|
|
1959
|
+
if (testRule) {
|
|
1960
|
+
// if(this.is_media && tableRules.insert.preValidate) throw "Media table cannot have a preValidate. It already is used internally by prostgles for file upload";
|
|
1961
|
+
await this.validateViewRules({ fields, returningFields, forcedFilter: forcedData, rule: "insert" });
|
|
1962
|
+
if (forcedData) {
|
|
1963
|
+
const keys = Object.keys(forcedData);
|
|
1964
|
+
if (keys.length) {
|
|
1965
|
+
try {
|
|
1966
|
+
const colset = new exports.pgp.helpers.ColumnSet(this.columns.filter(c => keys.includes(c.name)).map(c => ({ name: c.name, cast: c.udt_name === "uuid" ? c.udt_name : undefined }))), values = exports.pgp.helpers.values(forcedData, colset), colNames = this.prepareSelect(keys, this.column_names);
|
|
1967
|
+
await this.db.any("EXPLAIN INSERT INTO " + this.escapedName + " (${colNames:raw}) SELECT * FROM ( VALUES ${values:raw} ) t WHERE FALSE;", { colNames, values });
|
|
1968
|
+
}
|
|
1969
|
+
catch (e) {
|
|
1970
|
+
throw "\nissue with forcedData: \nVALUE: " + JSON.stringify(forcedData, null, 2) + "\nERROR: " + e;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return true;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
let conflict_query = "";
|
|
1978
|
+
if (typeof onConflictDoNothing === "boolean" && onConflictDoNothing) {
|
|
1979
|
+
conflict_query = " ON CONFLICT DO NOTHING ";
|
|
1980
|
+
}
|
|
1981
|
+
if (param2) {
|
|
1982
|
+
const good_params = ["returning", "multi", "onConflictDoNothing", "fixIssues"];
|
|
1983
|
+
const bad_params = Object.keys(param2).filter(k => !good_params.includes(k));
|
|
1984
|
+
if (bad_params && bad_params.length)
|
|
1985
|
+
throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
|
|
1986
|
+
}
|
|
1987
|
+
if (!rowOrRows)
|
|
1988
|
+
rowOrRows = {}; //throw "Provide data in param1";
|
|
1989
|
+
let returningSelect = this.makeReturnQuery(await this.prepareReturning(returning, this.parseFieldFilter(returningFields)));
|
|
1990
|
+
const makeQuery = async (_row, isOne = false) => {
|
|
1991
|
+
let row = { ..._row };
|
|
1992
|
+
if (!isPojoObject(row)) {
|
|
1993
|
+
console.trace(row);
|
|
1994
|
+
throw "\ninvalid insert data provided -> " + JSON.stringify(row);
|
|
1995
|
+
}
|
|
1996
|
+
const { data, allowedCols } = this.validateNewData({ row, forcedData, allowedFields: fields, tableRules, fixIssues });
|
|
1997
|
+
let _data = { ...data };
|
|
1998
|
+
let insertQ = "";
|
|
1999
|
+
if (!Object.keys(_data).length)
|
|
2000
|
+
insertQ = `INSERT INTO ${(0, prostgles_types_1.asName)(this.name)} DEFAULT VALUES `;
|
|
2001
|
+
else
|
|
2002
|
+
insertQ = await this.colSet.getInsertQuery(_data, allowedCols, tableRules?.insert?.validate); // pgp.helpers.insert(_data, columnSet);
|
|
2003
|
+
return insertQ + conflict_query + returningSelect;
|
|
2004
|
+
};
|
|
2005
|
+
let query = "";
|
|
2006
|
+
let queryType = "none";
|
|
2007
|
+
/**
|
|
2008
|
+
* If media it will: upload file and continue insert
|
|
2009
|
+
* If nested insert it will: make separate inserts and not continue main insert
|
|
2010
|
+
*/
|
|
2011
|
+
const insRes = await this.insertDataParse(rowOrRows, param2, param3_unused, tableRules, localParams);
|
|
2012
|
+
const { data, insertResult } = insRes;
|
|
2013
|
+
if ("insertResult" in insRes) {
|
|
2014
|
+
return insertResult;
|
|
2015
|
+
}
|
|
2016
|
+
if (Array.isArray(data)) {
|
|
2017
|
+
// if(returning) throw "Sorry but [returning] is dissalowed for multi insert";
|
|
2018
|
+
let queries = await Promise.all(data.map(async (p) => {
|
|
2019
|
+
const q = await makeQuery(p);
|
|
2020
|
+
return q;
|
|
2021
|
+
}));
|
|
2022
|
+
query = exports.pgp.helpers.concat(queries);
|
|
2023
|
+
if (returning)
|
|
2024
|
+
queryType = "many";
|
|
2025
|
+
}
|
|
2026
|
+
else {
|
|
2027
|
+
query = await makeQuery(data, true);
|
|
2028
|
+
if (returning)
|
|
2029
|
+
queryType = "one";
|
|
2030
|
+
}
|
|
2031
|
+
if (returnQuery)
|
|
2032
|
+
return query;
|
|
2033
|
+
let result;
|
|
2034
|
+
if (this.dboBuilder.prostgles.opts.DEBUG_MODE) {
|
|
2035
|
+
console.log(this.t?.ctx?.start, "insert in " + this.name, data);
|
|
2036
|
+
}
|
|
2037
|
+
const tx = dbTX?.[this.name]?.t || this.t;
|
|
2038
|
+
const allowedFieldKeys = this.parseFieldFilter(fields);
|
|
2039
|
+
if (tx) {
|
|
2040
|
+
result = tx[queryType](query).catch((err) => makeErr(err, localParams, this, allowedFieldKeys));
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
result = this.db.tx(t => t[queryType](query)).catch(err => makeErr(err, localParams, this, allowedFieldKeys));
|
|
2044
|
+
}
|
|
2045
|
+
return result;
|
|
2046
|
+
}
|
|
2047
|
+
catch (e) {
|
|
2048
|
+
if (localParams && localParams.testRule)
|
|
2049
|
+
throw e;
|
|
2050
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.insert(
|
|
2051
|
+
${JSON.stringify(rowOrRows || {}, null, 2)},
|
|
2052
|
+
${JSON.stringify(param2 || {}, null, 2)}
|
|
2053
|
+
)` };
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
;
|
|
2057
|
+
makeReturnQuery(items) {
|
|
2058
|
+
if (items?.length)
|
|
2059
|
+
return " RETURNING " + items.map(s => s.getQuery() + " AS " + (0, prostgles_types_1.asName)(s.alias)).join(", ");
|
|
2060
|
+
return "";
|
|
2061
|
+
}
|
|
2062
|
+
async delete(filter, params, param3_unused, table_rules, localParams) {
|
|
2063
|
+
try {
|
|
2064
|
+
const { returning } = params || {};
|
|
2065
|
+
filter = filter || {};
|
|
2066
|
+
this.checkFilter(filter);
|
|
2067
|
+
// table_rules = table_rules || {};
|
|
2068
|
+
let forcedFilter = {}, filterFields = "*", returningFields = "*", validate;
|
|
2069
|
+
const { testRule = false, returnQuery = false } = localParams || {};
|
|
2070
|
+
if (table_rules) {
|
|
2071
|
+
if (!table_rules.delete)
|
|
2072
|
+
throw "delete rules missing";
|
|
2073
|
+
forcedFilter = table_rules.delete.forcedFilter;
|
|
2074
|
+
filterFields = table_rules.delete.filterFields;
|
|
2075
|
+
returningFields = table_rules.delete.returningFields;
|
|
2076
|
+
validate = table_rules.delete.validate;
|
|
2077
|
+
if (!returningFields)
|
|
2078
|
+
returningFields = (0, utils_1.get)(table_rules, "select.fields");
|
|
2079
|
+
if (!returningFields)
|
|
2080
|
+
returningFields = (0, utils_1.get)(table_rules, "delete.filterFields");
|
|
2081
|
+
if (!filterFields)
|
|
2082
|
+
throw ` Invalid delete rule for ${this.name}. filterFields missing `;
|
|
2083
|
+
/* Safely test publish rules */
|
|
2084
|
+
if (testRule) {
|
|
2085
|
+
await this.validateViewRules({ filterFields, returningFields, forcedFilter, rule: "delete" });
|
|
2086
|
+
return true;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (params) {
|
|
2090
|
+
const good_params = ["returning"];
|
|
2091
|
+
const bad_params = Object.keys(params).filter(k => !good_params.includes(k));
|
|
2092
|
+
if (bad_params && bad_params.length)
|
|
2093
|
+
throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
|
|
2094
|
+
}
|
|
2095
|
+
let queryType = 'none';
|
|
2096
|
+
let _query = "DELETE FROM " + this.escapedName;
|
|
2097
|
+
_query += (await this.prepareWhere({
|
|
2098
|
+
filter,
|
|
2099
|
+
forcedFilter,
|
|
2100
|
+
filterFields,
|
|
2101
|
+
localParams,
|
|
2102
|
+
tableRule: table_rules
|
|
2103
|
+
}));
|
|
2104
|
+
if (validate) {
|
|
2105
|
+
const _filter = (0, exports.getUpdateFilter)({ filter, forcedFilter, $and_key: this.dboBuilder.prostgles.keywords.$and });
|
|
2106
|
+
await validate(_filter);
|
|
2107
|
+
}
|
|
2108
|
+
if (returning) {
|
|
2109
|
+
queryType = "any";
|
|
2110
|
+
if (!returningFields) {
|
|
2111
|
+
throw "Returning dissallowed";
|
|
2112
|
+
}
|
|
2113
|
+
_query += this.makeReturnQuery(await this.prepareReturning(returning, this.parseFieldFilter(returningFields)));
|
|
2114
|
+
}
|
|
2115
|
+
if (returnQuery)
|
|
2116
|
+
return _query;
|
|
2117
|
+
return (this.t || this.db)[queryType](_query).catch((err) => makeErr(err, localParams));
|
|
2118
|
+
}
|
|
2119
|
+
catch (e) {
|
|
2120
|
+
// console.trace(e)
|
|
2121
|
+
if (localParams && localParams.testRule)
|
|
2122
|
+
throw e;
|
|
2123
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.delete(${JSON.stringify(filter || {}, null, 2)}, ${JSON.stringify(params || {}, null, 2)})` };
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
;
|
|
2127
|
+
remove(filter, params, param3_unused, tableRules, localParams) {
|
|
2128
|
+
return this.delete(filter, params, param3_unused, tableRules, localParams);
|
|
2129
|
+
}
|
|
2130
|
+
async upsert(filter, newData, params, table_rules, localParams) {
|
|
2131
|
+
try {
|
|
2132
|
+
/* Do it within a transaction to ensure consisency */
|
|
2133
|
+
if (!this.t) {
|
|
2134
|
+
return this.dboBuilder.getTX(dbTX => _upsert(dbTX[this.name]));
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
return _upsert(this);
|
|
2138
|
+
}
|
|
2139
|
+
async function _upsert(tblH) {
|
|
2140
|
+
return tblH.find(filter, { select: "", limit: 1 }, undefined, table_rules, localParams)
|
|
2141
|
+
.then(exists => {
|
|
2142
|
+
if (exists && exists.length) {
|
|
2143
|
+
return tblH.update(filter, newData, params, table_rules, localParams);
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
return tblH.insert({ ...newData, ...filter }, params, undefined, table_rules, localParams);
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
catch (e) {
|
|
2152
|
+
if (localParams && localParams.testRule)
|
|
2153
|
+
throw e;
|
|
2154
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.upsert()` };
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
;
|
|
2158
|
+
/* External request. Cannot sync from server */
|
|
2159
|
+
async sync(filter, params, param3_unused, table_rules, localParams) {
|
|
2160
|
+
if (!localParams)
|
|
2161
|
+
throw "Sync not allowed within the same server code";
|
|
2162
|
+
const { socket } = localParams;
|
|
2163
|
+
if (!socket)
|
|
2164
|
+
throw "INTERNAL ERROR: socket missing";
|
|
2165
|
+
if (!table_rules || !table_rules.sync || !table_rules.select)
|
|
2166
|
+
throw "INTERNAL ERROR: sync or select rules missing";
|
|
2167
|
+
if (this.t)
|
|
2168
|
+
throw "Sync not allowed within transactions";
|
|
2169
|
+
const ALLOWED_PARAMS = ["select"];
|
|
2170
|
+
const invalidParams = Object.keys(params || {}).filter(k => !ALLOWED_PARAMS.includes(k));
|
|
2171
|
+
if (invalidParams.length)
|
|
2172
|
+
throw "Invalid or dissallowed params found: " + invalidParams.join(", ");
|
|
2173
|
+
try {
|
|
2174
|
+
let { id_fields, synced_field, allow_delete } = table_rules.sync;
|
|
2175
|
+
const syncFields = [...id_fields, synced_field];
|
|
2176
|
+
if (!id_fields || !synced_field) {
|
|
2177
|
+
const err = "INTERNAL ERROR: id_fields OR synced_field missing from publish";
|
|
2178
|
+
console.error(err);
|
|
2179
|
+
throw err;
|
|
2180
|
+
}
|
|
2181
|
+
id_fields = this.parseFieldFilter(id_fields, false);
|
|
2182
|
+
let allowedSelect = this.parseFieldFilter((0, utils_1.get)(table_rules, "select.fields"), false);
|
|
2183
|
+
if (syncFields.find(f => !allowedSelect.includes(f))) {
|
|
2184
|
+
throw `INTERNAL ERROR: sync field missing from publish.${this.name}.select.fields`;
|
|
2185
|
+
}
|
|
2186
|
+
let select = this.getAllowedSelectFields((0, utils_1.get)(params || {}, "select") || "*", allowedSelect, false);
|
|
2187
|
+
if (!select.length)
|
|
2188
|
+
throw "Empty select not allowed";
|
|
2189
|
+
/* Add sync fields if missing */
|
|
2190
|
+
syncFields.map(sf => {
|
|
2191
|
+
if (!select.includes(sf))
|
|
2192
|
+
select.push(sf);
|
|
2193
|
+
});
|
|
2194
|
+
/* Step 1: parse command and params */
|
|
2195
|
+
return this.find(filter, { select, limit: 0 }, undefined, table_rules, localParams)
|
|
2196
|
+
.then(async (isValid) => {
|
|
2197
|
+
const { filterFields, forcedFilter } = (0, utils_1.get)(table_rules, "select") || {};
|
|
2198
|
+
const condition = await this.prepareWhere({ filter, forcedFilter, filterFields, addKeywords: false, localParams, tableRule: table_rules });
|
|
2199
|
+
// let final_filter = getFindFilter(filter, table_rules);
|
|
2200
|
+
const pubSubManager = await this.dboBuilder.getPubSubManager();
|
|
2201
|
+
return pubSubManager.addSync({
|
|
2202
|
+
table_info: this.tableOrViewInfo,
|
|
2203
|
+
condition,
|
|
2204
|
+
id_fields, synced_field,
|
|
2205
|
+
allow_delete,
|
|
2206
|
+
socket,
|
|
2207
|
+
table_rules,
|
|
2208
|
+
filter: { ...filter },
|
|
2209
|
+
params: { select }
|
|
2210
|
+
}).then(channelName => ({ channelName, id_fields, synced_field }));
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
catch (e) {
|
|
2214
|
+
if (localParams && localParams.testRule)
|
|
2215
|
+
throw e;
|
|
2216
|
+
throw { err: parseError(e), msg: `Issue with dbo.${this.name}.sync()` };
|
|
2217
|
+
}
|
|
2218
|
+
/*
|
|
2219
|
+
REPLICATION
|
|
2220
|
+
|
|
2221
|
+
1 Sync proccess (NO DELETES ALLOWED):
|
|
2222
|
+
|
|
2223
|
+
Client sends:
|
|
2224
|
+
"sync-request"
|
|
2225
|
+
{ min_id, max_id, count, max_synced }
|
|
2226
|
+
|
|
2227
|
+
Server sends:
|
|
2228
|
+
"sync-pull"
|
|
2229
|
+
{ from_synced }
|
|
2230
|
+
|
|
2231
|
+
Client sends:
|
|
2232
|
+
"sync-push"
|
|
2233
|
+
{ data } -> WHERE synced >= from_synced
|
|
2234
|
+
|
|
2235
|
+
Server upserts:
|
|
2236
|
+
WHERE not exists synced = synced AND id = id
|
|
2237
|
+
UNTIL
|
|
2238
|
+
|
|
2239
|
+
Server sends
|
|
2240
|
+
"sync-push"
|
|
2241
|
+
{ data } -> WHERE synced >= from_synced
|
|
2242
|
+
*/
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
exports.TableHandler = TableHandler;
|
|
2246
|
+
const Prostgles_1 = require("./Prostgles");
|
|
2247
|
+
const DBSchemaBuilder_1 = require("./DBSchemaBuilder");
|
|
2248
|
+
class DboBuilder {
|
|
2249
|
+
constructor(prostgles) {
|
|
2250
|
+
this.schema = "public";
|
|
2251
|
+
this.getPubSubManager = async () => {
|
|
2252
|
+
if (!this._pubSubManager) {
|
|
2253
|
+
let onSchemaChange;
|
|
2254
|
+
if (this.prostgles.opts.watchSchema && this.prostgles.opts.watchSchemaType === "events") {
|
|
2255
|
+
if (!this.prostgles.isSuperUser) {
|
|
2256
|
+
console.warn(`watchSchemaType "events" cannot be used because db user is not a superuser. Will fallback to watchSchemaType "queries" `);
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
onSchemaChange = (event) => {
|
|
2260
|
+
this.prostgles.onSchemaChange(event);
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
if (this.prostgles.isSuperUser) {
|
|
2265
|
+
this._pubSubManager = await PubSubManager_1.PubSubManager.create({
|
|
2266
|
+
dboBuilder: this,
|
|
2267
|
+
db: this.db,
|
|
2268
|
+
dbo: this.dbo,
|
|
2269
|
+
onSchemaChange
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
else {
|
|
2273
|
+
console.warn(`subscribe and sync cannot be used because db user is not a superuser `);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (!this._pubSubManager)
|
|
2277
|
+
throw "Could not create this._pubSubManager";
|
|
2278
|
+
return this._pubSubManager;
|
|
2279
|
+
};
|
|
2280
|
+
this.joinPaths = [];
|
|
2281
|
+
this.init = async () => {
|
|
2282
|
+
/* If watchSchema then PubSubManager must be created */
|
|
2283
|
+
await this.build();
|
|
2284
|
+
if (this.prostgles.opts.watchSchema) {
|
|
2285
|
+
await this.getPubSubManager();
|
|
2286
|
+
}
|
|
2287
|
+
return this;
|
|
2288
|
+
};
|
|
2289
|
+
this.getTX = (cb) => {
|
|
2290
|
+
return this.db.tx((t) => {
|
|
2291
|
+
let dbTX = {};
|
|
2292
|
+
this.tablesOrViews?.map(tov => {
|
|
2293
|
+
if (tov.is_view) {
|
|
2294
|
+
dbTX[tov.name] = new ViewHandler(this.db, tov, this, t, dbTX, this.joinPaths);
|
|
2295
|
+
}
|
|
2296
|
+
else {
|
|
2297
|
+
dbTX[tov.name] = new TableHandler(this.db, tov, this, t, dbTX, this.joinPaths);
|
|
2298
|
+
/**
|
|
2299
|
+
* Pass only the transaction object to ensure consistency
|
|
2300
|
+
*/
|
|
2301
|
+
// dbTX[tov.name] = new ViewHandler(t, tov, this.pubSubManager, this, t, this.joinPaths);
|
|
2302
|
+
// } else {
|
|
2303
|
+
// dbTX[tov.name] = new TableHandler(t as any, tov, this.pubSubManager, this, t, this.joinPaths);
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
Object.keys(dbTX).map(k => {
|
|
2307
|
+
dbTX[k].dbTX = dbTX;
|
|
2308
|
+
});
|
|
2309
|
+
return cb(dbTX, t);
|
|
2310
|
+
});
|
|
2311
|
+
};
|
|
2312
|
+
this.prostgles = prostgles;
|
|
2313
|
+
if (!this.prostgles.db)
|
|
2314
|
+
throw "db missing";
|
|
2315
|
+
this.db = this.prostgles.db;
|
|
2316
|
+
this.schema = this.prostgles.opts.schema || "public";
|
|
2317
|
+
this.dbo = {};
|
|
2318
|
+
// this.joins = this.prostgles.joins;
|
|
2319
|
+
}
|
|
2320
|
+
destroy() {
|
|
2321
|
+
this._pubSubManager?.destroy();
|
|
2322
|
+
}
|
|
2323
|
+
getJoins() {
|
|
2324
|
+
return this.joins;
|
|
2325
|
+
}
|
|
2326
|
+
getJoinPaths() {
|
|
2327
|
+
return this.joinPaths;
|
|
2328
|
+
}
|
|
2329
|
+
async parseJoins() {
|
|
2330
|
+
if (this.prostgles.opts.joins) {
|
|
2331
|
+
let _joins = await this.prostgles.opts.joins;
|
|
2332
|
+
let inferredJoins = await getInferredJoins(this.db, this.prostgles.opts.schema);
|
|
2333
|
+
if (typeof _joins === "string" && _joins === "inferred") {
|
|
2334
|
+
_joins = inferredJoins;
|
|
2335
|
+
/* If joins are specified then include inferred joins except the explicit tables */
|
|
2336
|
+
}
|
|
2337
|
+
else if (Array.isArray(_joins)) {
|
|
2338
|
+
const joinTables = _joins.map(j => j.tables).flat();
|
|
2339
|
+
_joins = _joins.concat(inferredJoins.filter(j => !j.tables.find(t => joinTables.includes(t))));
|
|
2340
|
+
}
|
|
2341
|
+
let joins = JSON.parse(JSON.stringify(_joins));
|
|
2342
|
+
this.joins = joins;
|
|
2343
|
+
// Validate joins
|
|
2344
|
+
try {
|
|
2345
|
+
// 1 find duplicates
|
|
2346
|
+
const dup = joins.find(j => j.tables[0] === j.tables[1] ||
|
|
2347
|
+
joins.find(jj => j.tables.join() !== jj.tables.join() &&
|
|
2348
|
+
j.tables.slice().sort().join() === jj.tables.slice().sort().join()));
|
|
2349
|
+
if (dup) {
|
|
2350
|
+
throw "Duplicate join declaration for table: " + dup.tables[0];
|
|
2351
|
+
}
|
|
2352
|
+
const tovNames = this.tablesOrViews.map(t => t.name);
|
|
2353
|
+
// 2 find incorrect tables
|
|
2354
|
+
const missing = joins.flatMap(j => j.tables).find(t => !tovNames.includes(t));
|
|
2355
|
+
if (missing) {
|
|
2356
|
+
throw "Table not found: " + missing;
|
|
2357
|
+
}
|
|
2358
|
+
// 3 find incorrect fields
|
|
2359
|
+
joins.map(({ tables, on }) => {
|
|
2360
|
+
const t1 = tables[0], t2 = tables[1], f1s = Object.keys(on), f2s = Object.values(on);
|
|
2361
|
+
[[t1, f1s], [t2, f2s]].map(v => {
|
|
2362
|
+
var t = v[0], f = v[1];
|
|
2363
|
+
let tov = this.tablesOrViews.find(_t => _t.name === t);
|
|
2364
|
+
if (!tov)
|
|
2365
|
+
throw "Table not found: " + t;
|
|
2366
|
+
const m1 = f.filter(k => !tov.columns.map(c => c.name).includes(k));
|
|
2367
|
+
if (m1 && m1.length) {
|
|
2368
|
+
throw `Table ${t}(${tov.columns.map(c => c.name).join()}) has no fields named: ${m1.join()}`;
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2371
|
+
});
|
|
2372
|
+
// 4 find incorrect/missing join types
|
|
2373
|
+
const expected_types = " \n\n-> Expecting: " + Prostgles_1.JOIN_TYPES.map(t => JSON.stringify(t)).join(` | `);
|
|
2374
|
+
const mt = joins.find(j => !j.type);
|
|
2375
|
+
if (mt)
|
|
2376
|
+
throw "Join type missing for: " + JSON.stringify(mt, null, 2) + expected_types;
|
|
2377
|
+
const it = joins.find(j => !Prostgles_1.JOIN_TYPES.includes(j.type));
|
|
2378
|
+
if (it)
|
|
2379
|
+
throw "Incorrect join type for: " + JSON.stringify(it, null, 2) + expected_types;
|
|
2380
|
+
}
|
|
2381
|
+
catch (e) {
|
|
2382
|
+
console.error("JOINS VALIDATION ERROR \n-> ", e);
|
|
2383
|
+
}
|
|
2384
|
+
// Make joins graph
|
|
2385
|
+
this.joinGraph = {};
|
|
2386
|
+
this.joins.map(({ tables }) => {
|
|
2387
|
+
let _t = tables.slice().sort(), t1 = _t[0], t2 = _t[1];
|
|
2388
|
+
this.joinGraph[t1] = this.joinGraph[t1] || {};
|
|
2389
|
+
this.joinGraph[t1][t2] = 1;
|
|
2390
|
+
this.joinGraph[t2] = this.joinGraph[t2] || {};
|
|
2391
|
+
this.joinGraph[t2][t1] = 1;
|
|
2392
|
+
});
|
|
2393
|
+
const tables = this.joins.flatMap(t => t.tables);
|
|
2394
|
+
this.joinPaths = [];
|
|
2395
|
+
tables.map(t1 => {
|
|
2396
|
+
tables.map(t2 => {
|
|
2397
|
+
const spath = (0, shortestPath_1.findShortestPath)(this.joinGraph, t1, t2);
|
|
2398
|
+
if (spath && spath.distance < Infinity) {
|
|
2399
|
+
const existing1 = this.joinPaths.find(j => j.t1 === t1 && j.t2 === t2);
|
|
2400
|
+
if (!existing1) {
|
|
2401
|
+
this.joinPaths.push({ t1, t2, path: spath.path });
|
|
2402
|
+
}
|
|
2403
|
+
const existing2 = this.joinPaths.find(j => j.t2 === t1 && j.t1 === t2);
|
|
2404
|
+
if (!existing2) {
|
|
2405
|
+
this.joinPaths.push({ t1: t2, t2: t1, path: spath.path.slice().reverse() });
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
return this.joinPaths;
|
|
2412
|
+
}
|
|
2413
|
+
buildJoinPaths() {
|
|
2414
|
+
}
|
|
2415
|
+
async build() {
|
|
2416
|
+
// await this.pubSubManager.init()
|
|
2417
|
+
this.tablesOrViews = await getTablesForSchemaPostgresSQL(this.db); //, this.schema
|
|
2418
|
+
// console.log(this.tablesOrViews.map(t => `${t.name} (${t.columns.map(c => c.name).join(", ")})`))
|
|
2419
|
+
this.constraints = await getConstraints(this.db);
|
|
2420
|
+
await this.parseJoins();
|
|
2421
|
+
// const common_types =
|
|
2422
|
+
// `
|
|
2423
|
+
// import { ViewHandler, TableHandler, JoinMaker } from "prostgles-types";
|
|
2424
|
+
// export type TxCB = {
|
|
2425
|
+
// (t: DBObj): (any | void | Promise<(any | void)>)
|
|
2426
|
+
// };
|
|
2427
|
+
// `
|
|
2428
|
+
// this.dboDefinition = `export type DBObj = {\n`;
|
|
2429
|
+
// let joinTableNames: string[] = [];
|
|
2430
|
+
// let allDataDefs = "";
|
|
2431
|
+
this.tablesOrViews.map(tov => {
|
|
2432
|
+
const columnsForTypes = tov.columns.slice(0).sort((a, b) => a.name.localeCompare(b.name));
|
|
2433
|
+
const filterKeywords = Object.values(this.prostgles.keywords);
|
|
2434
|
+
const $filterCol = columnsForTypes.find(c => filterKeywords.includes(c.name));
|
|
2435
|
+
if ($filterCol) {
|
|
2436
|
+
throw `DboBuilder init error: \n\nTable ${JSON.stringify(tov.name)} column ${JSON.stringify($filterCol.name)} is colliding with Prostgles filtering functionality ($filter keyword)
|
|
2437
|
+
Please provide a replacement keyword name using the $filter_keyName init option.
|
|
2438
|
+
Alternatively you can rename the table column\n`;
|
|
2439
|
+
}
|
|
2440
|
+
const TSTableDataName = snakify(tov.name, true);
|
|
2441
|
+
const TSTableHandlerName = escapeTSNames(tov.name);
|
|
2442
|
+
if (tov.is_view) {
|
|
2443
|
+
this.dbo[tov.name] = new ViewHandler(this.db, tov, this, undefined, undefined, this.joinPaths);
|
|
2444
|
+
// this.dboDefinition += ` ${TSTableHandlerName}: ViewHandler<${TSTableDataName}> \n`;
|
|
2445
|
+
}
|
|
2446
|
+
else {
|
|
2447
|
+
this.dbo[tov.name] = new TableHandler(this.db, tov, this, undefined, undefined, this.joinPaths);
|
|
2448
|
+
// this.dboDefinition += ` ${TSTableHandlerName}: TableHandler<${TSTableDataName}> \n`;
|
|
2449
|
+
}
|
|
2450
|
+
// allDataDefs += `export type ${TSTableDataName} = { \n` +
|
|
2451
|
+
// (this.dbo[tov.name] as ViewHandler).tsColumnDefs.map(str => ` ` + str).join("\n") +
|
|
2452
|
+
// `\n}\n`;
|
|
2453
|
+
// this.dboDefinition += ` ${escapeTSNames(tov.name, false)}: ${(this.dbo[tov.name] as TableHandler).tsDboName};\n`;
|
|
2454
|
+
if (this.joinPaths && this.joinPaths.find(jp => [jp.t1, jp.t2].includes(tov.name))) {
|
|
2455
|
+
let table = tov.name;
|
|
2456
|
+
// joinTableNames.push(table);
|
|
2457
|
+
const makeJoin = (isLeft = true, filter, select, options) => {
|
|
2458
|
+
return {
|
|
2459
|
+
[isLeft ? "$leftJoin" : "$innerJoin"]: table,
|
|
2460
|
+
filter,
|
|
2461
|
+
select,
|
|
2462
|
+
...options
|
|
2463
|
+
};
|
|
2464
|
+
};
|
|
2465
|
+
this.dbo.innerJoin = this.dbo.innerJoin || {};
|
|
2466
|
+
this.dbo.leftJoin = this.dbo.leftJoin || {};
|
|
2467
|
+
this.dbo.innerJoinOne = this.dbo.innerJoinOne || {};
|
|
2468
|
+
this.dbo.leftJoinOne = this.dbo.leftJoinOne || {};
|
|
2469
|
+
this.dbo.leftJoin[table] = (filter, select, options = {}) => {
|
|
2470
|
+
return makeJoin(true, filter, select, options);
|
|
2471
|
+
};
|
|
2472
|
+
this.dbo.innerJoin[table] = (filter, select, options = {}) => {
|
|
2473
|
+
return makeJoin(false, filter, select, options);
|
|
2474
|
+
};
|
|
2475
|
+
this.dbo.leftJoinOne[table] = (filter, select, options = {}) => {
|
|
2476
|
+
return makeJoin(true, filter, select, { ...options, limit: 1 });
|
|
2477
|
+
};
|
|
2478
|
+
this.dbo.innerJoinOne[table] = (filter, select, options = {}) => {
|
|
2479
|
+
return makeJoin(false, filter, select, { ...options, limit: 1 });
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
// let joinBuilderDef = "";
|
|
2484
|
+
// if(joinTableNames.length){
|
|
2485
|
+
// joinBuilderDef += "export type JoinMakerTables = {\n";
|
|
2486
|
+
// joinTableNames.map(tname => {
|
|
2487
|
+
// joinBuilderDef += ` ${escapeTSNames(tname)}: JoinMaker<${snakify(tname, true)}>;\n`
|
|
2488
|
+
// })
|
|
2489
|
+
// joinBuilderDef += "};\n";
|
|
2490
|
+
// ["leftJoin", "innerJoin", "leftJoinOne", "innerJoinOne"].map(joinType => {
|
|
2491
|
+
// this.dboDefinition += ` ${joinType}: JoinMakerTables;\n`;
|
|
2492
|
+
// });
|
|
2493
|
+
// }
|
|
2494
|
+
if (this.prostgles.opts.transactions) {
|
|
2495
|
+
let txKey = "tx";
|
|
2496
|
+
if (typeof this.prostgles.opts.transactions === "string")
|
|
2497
|
+
txKey = this.prostgles.opts.transactions;
|
|
2498
|
+
this.dboDefinition += ` ${txKey}: (t: TxCB) => Promise<any | void> ;\n`;
|
|
2499
|
+
this.dbo[txKey] = (cb) => this.getTX(cb);
|
|
2500
|
+
}
|
|
2501
|
+
if (!this.dbo.sql) {
|
|
2502
|
+
let needType = true; // this.publishRawSQL && typeof this.publishRawSQL === "function";
|
|
2503
|
+
let DATA_TYPES = !needType ? [] : await this.db.any("SELECT oid, typname FROM pg_type");
|
|
2504
|
+
let USER_TABLES = !needType ? [] : await this.db.any("SELECT relid, relname FROM pg_catalog.pg_statio_user_tables");
|
|
2505
|
+
this.dbo.sql = async (query, params, options, localParams) => {
|
|
2506
|
+
const canRunSQL = async (localParams) => {
|
|
2507
|
+
if (!localParams)
|
|
2508
|
+
return true;
|
|
2509
|
+
const { socket } = localParams;
|
|
2510
|
+
const publishParams = await this.prostgles.publishParser.getPublishParams({ socket });
|
|
2511
|
+
let res = await this.prostgles.opts.publishRawSQL?.(publishParams);
|
|
2512
|
+
return Boolean(res && typeof res === "boolean" || res === "*");
|
|
2513
|
+
};
|
|
2514
|
+
if (!(await canRunSQL(localParams)))
|
|
2515
|
+
throw "Not allowed to run SQL";
|
|
2516
|
+
const { returnType, allowListen } = options || {};
|
|
2517
|
+
const { socket } = localParams || {};
|
|
2518
|
+
if (returnType === "noticeSubscription") {
|
|
2519
|
+
if (!socket)
|
|
2520
|
+
throw "Only allowed with client socket";
|
|
2521
|
+
return await this.prostgles.dbEventsManager?.addNotice(socket);
|
|
2522
|
+
}
|
|
2523
|
+
else if (returnType === "statement") {
|
|
2524
|
+
try {
|
|
2525
|
+
return exports.pgp.as.format(query, params);
|
|
2526
|
+
}
|
|
2527
|
+
catch (err) {
|
|
2528
|
+
throw err.toString();
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
else if (this.db) {
|
|
2532
|
+
let finalQuery = query + "";
|
|
2533
|
+
if (returnType === "arrayMode" && !["listen ", "notify "].find(c => query.toLowerCase().trim().startsWith(c))) {
|
|
2534
|
+
finalQuery = new PQ({ text: exports.pgp.as.format(query, params), rowMode: "array" });
|
|
2535
|
+
}
|
|
2536
|
+
let _qres = await this.db.result(finalQuery, params);
|
|
2537
|
+
const { fields, rows, command } = _qres;
|
|
2538
|
+
/**
|
|
2539
|
+
* Fallback for watchSchema in case not superuser and cannot add db event listener
|
|
2540
|
+
*/
|
|
2541
|
+
const { watchSchema, watchSchemaType } = this.prostgles?.opts || {};
|
|
2542
|
+
if (watchSchema &&
|
|
2543
|
+
(!this.prostgles.isSuperUser || watchSchemaType === "queries") &&
|
|
2544
|
+
(["CREATE", "ALTER", "DROP"].includes(command) ||
|
|
2545
|
+
// Cover this case: `CREATE TABLE mytable AS SELECT`
|
|
2546
|
+
query && query.toLowerCase().replace(/\s\s+/g, ' ').includes("create table"))) {
|
|
2547
|
+
this.prostgles.onSchemaChange({ command, query });
|
|
2548
|
+
}
|
|
2549
|
+
if (command === "LISTEN") {
|
|
2550
|
+
if (!allowListen)
|
|
2551
|
+
throw new Error(`Your query contains a LISTEN command. Set { allowListen: true } to get subscription hooks. Or ignore this message`);
|
|
2552
|
+
if (!socket)
|
|
2553
|
+
throw "Only allowed with client socket";
|
|
2554
|
+
return await this.prostgles.dbEventsManager?.addNotify(query, socket);
|
|
2555
|
+
}
|
|
2556
|
+
else if (returnType === "rows") {
|
|
2557
|
+
return rows;
|
|
2558
|
+
}
|
|
2559
|
+
else if (returnType === "row") {
|
|
2560
|
+
return rows[0];
|
|
2561
|
+
}
|
|
2562
|
+
else if (returnType === "value") {
|
|
2563
|
+
return Object.values(rows?.[0] || {})?.[0];
|
|
2564
|
+
}
|
|
2565
|
+
else if (returnType === "values") {
|
|
2566
|
+
return rows.map(r => Object.values(r[0]));
|
|
2567
|
+
}
|
|
2568
|
+
else {
|
|
2569
|
+
let qres = {
|
|
2570
|
+
duration: 0,
|
|
2571
|
+
..._qres,
|
|
2572
|
+
fields: fields?.map(f => {
|
|
2573
|
+
const dataType = DATA_TYPES.find(dt => +dt.oid === +f.dataTypeID)?.typname ?? "text", tableName = USER_TABLES.find(t => +t.relid === +f.tableID), tsDataType = postgresToTsType(dataType);
|
|
2574
|
+
return {
|
|
2575
|
+
...f,
|
|
2576
|
+
tsDataType,
|
|
2577
|
+
dataType,
|
|
2578
|
+
udt_name: dataType,
|
|
2579
|
+
tableName: tableName?.relname
|
|
2580
|
+
};
|
|
2581
|
+
}) ?? []
|
|
2582
|
+
};
|
|
2583
|
+
return qres;
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
else
|
|
2587
|
+
console.error("db missing");
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
else {
|
|
2591
|
+
console.warn(`Could not create dbo.sql handler because there is already a table named "sql"`);
|
|
2592
|
+
}
|
|
2593
|
+
this.dboDefinition += "};\n";
|
|
2594
|
+
this.tsTypesDefinition = [
|
|
2595
|
+
// common_types,
|
|
2596
|
+
`/* SCHEMA DEFINITON. Table names have been altered to work with Typescript */`,
|
|
2597
|
+
// allDataDefs,
|
|
2598
|
+
// joinBuilderDef,
|
|
2599
|
+
`/* DBO Definition. Isomorphic */`,
|
|
2600
|
+
// this.dboDefinition,
|
|
2601
|
+
(0, DBSchemaBuilder_1.getDBSchema)(this)
|
|
2602
|
+
].join("\n");
|
|
2603
|
+
return this.dbo;
|
|
2604
|
+
// let dbo = makeDBO(db, allTablesViews, pubSubManager, true);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
exports.DboBuilder = DboBuilder;
|
|
2608
|
+
_a = DboBuilder;
|
|
2609
|
+
DboBuilder.create = async (prostgles) => {
|
|
2610
|
+
let res = new DboBuilder(prostgles);
|
|
2611
|
+
return await res.init();
|
|
2612
|
+
};
|
|
2613
|
+
async function getConstraints(db, schema = "public") {
|
|
2614
|
+
return db.any(`
|
|
2615
|
+
SELECT rel.relname, con.conkey, con.conname, con.contype
|
|
2616
|
+
FROM pg_catalog.pg_constraint con
|
|
2617
|
+
INNER JOIN pg_catalog.pg_class rel
|
|
2618
|
+
ON rel.oid = con.conrelid
|
|
2619
|
+
INNER JOIN pg_catalog.pg_namespace nsp
|
|
2620
|
+
ON nsp.oid = connamespace
|
|
2621
|
+
WHERE nsp.nspname = ${(0, PubSubManager_1.asValue)(schema)}
|
|
2622
|
+
`);
|
|
2623
|
+
}
|
|
2624
|
+
async function getTablesForSchemaPostgresSQL(db, schema = "public") {
|
|
2625
|
+
const query = `
|
|
2626
|
+
SELECT jsonb_build_object(
|
|
2627
|
+
'insert', EXISTS (
|
|
2628
|
+
SELECT 1
|
|
2629
|
+
FROM information_schema.role_table_grants rg
|
|
2630
|
+
WHERE rg.table_name = t.table_name
|
|
2631
|
+
AND rg.privilege_type = 'INSERT'
|
|
2632
|
+
),
|
|
2633
|
+
'select', EXISTS (
|
|
2634
|
+
SELECT 1
|
|
2635
|
+
FROM information_schema.role_table_grants rg
|
|
2636
|
+
WHERE rg.table_name = t.table_name
|
|
2637
|
+
AND rg.privilege_type = 'SELECT'
|
|
2638
|
+
),
|
|
2639
|
+
'update', EXISTS (
|
|
2640
|
+
SELECT 1
|
|
2641
|
+
FROM information_schema.role_table_grants rg
|
|
2642
|
+
WHERE rg.table_name = t.table_name
|
|
2643
|
+
AND rg.privilege_type = 'UPDATE'
|
|
2644
|
+
),
|
|
2645
|
+
'delete', EXISTS (
|
|
2646
|
+
SELECT 1
|
|
2647
|
+
FROM information_schema.role_table_grants rg
|
|
2648
|
+
WHERE rg.table_name = t.table_name
|
|
2649
|
+
AND rg.privilege_type = 'DELETE'
|
|
2650
|
+
)
|
|
2651
|
+
) as privileges
|
|
2652
|
+
, t.table_schema as schema, t.table_name as name
|
|
2653
|
+
, cc.table_oid as oid
|
|
2654
|
+
, json_agg((SELECT x FROM (
|
|
2655
|
+
SELECT cc.column_name as name,
|
|
2656
|
+
cc.data_type,
|
|
2657
|
+
cc.udt_name,
|
|
2658
|
+
cc.element_type,
|
|
2659
|
+
cc.element_udt_name,
|
|
2660
|
+
cc.is_pkey,
|
|
2661
|
+
cc.comment,
|
|
2662
|
+
cc.ordinal_position,
|
|
2663
|
+
cc.is_nullable = 'YES' as is_nullable,
|
|
2664
|
+
cc.references,
|
|
2665
|
+
cc.has_default,
|
|
2666
|
+
cc.column_default,
|
|
2667
|
+
cc.privileges
|
|
2668
|
+
) as x) ORDER BY cc.ordinal_position ) as columns
|
|
2669
|
+
|
|
2670
|
+
, t.table_type = 'VIEW' as is_view
|
|
2671
|
+
, array_to_json(vr.table_names) as parent_tables
|
|
2672
|
+
, obj_description(cc.table_oid::regclass) as comment
|
|
2673
|
+
FROM information_schema.tables t
|
|
2674
|
+
INNER join (
|
|
2675
|
+
SELECT c.table_schema, c.table_name, c.column_name, c.data_type, c.udt_name
|
|
2676
|
+
, e.data_type as element_type
|
|
2677
|
+
, e.udt_name as element_udt_name
|
|
2678
|
+
, col_description(format('%I.%I', c.table_schema, c.table_name)::regclass::oid, c.ordinal_position) as comment
|
|
2679
|
+
, CASE WHEN fc.ftable IS NOT NULL THEN row_to_json((SELECT t FROM (SELECT fc.ftable, fc.fcols, fc.cols) t)) END as references
|
|
2680
|
+
, EXISTS (
|
|
2681
|
+
SELECT 1
|
|
2682
|
+
from information_schema.table_constraints as tc
|
|
2683
|
+
JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
2684
|
+
WHERE kcu.table_schema = c.table_schema AND kcu.table_name = c.table_name AND kcu.column_name = c.column_name AND tc.constraint_type IN ('PRIMARY KEY')
|
|
2685
|
+
) as is_pkey
|
|
2686
|
+
, c.ordinal_position
|
|
2687
|
+
, c.column_default IS NOT NULL as has_default
|
|
2688
|
+
, c.column_default
|
|
2689
|
+
, format('%I.%I', c.table_schema, c.table_name)::regclass::oid AS table_oid
|
|
2690
|
+
, c.is_nullable
|
|
2691
|
+
, cp.privileges
|
|
2692
|
+
FROM information_schema.columns c
|
|
2693
|
+
LEFT JOIN (SELECT * FROM information_schema.element_types ) e
|
|
2694
|
+
ON ((c.table_catalog, c.table_schema, c.table_name, 'TABLE', c.dtd_identifier)
|
|
2695
|
+
= (e.object_catalog, e.object_schema, e.object_name, e.object_type, e.collection_type_identifier)
|
|
2696
|
+
)
|
|
2697
|
+
LEFT JOIN (
|
|
2698
|
+
SELECT table_schema, table_name, column_name, json_agg(row_to_json((SELECT t FROM (SELECT cpp.privilege_type, cpp.is_grantable ) t))) as privileges
|
|
2699
|
+
FROM information_schema.column_privileges cpp
|
|
2700
|
+
GROUP BY table_schema, table_name, column_name
|
|
2701
|
+
) cp
|
|
2702
|
+
ON c.table_name = cp.table_name AND c.column_name = cp.column_name
|
|
2703
|
+
LEFT JOIN (
|
|
2704
|
+
SELECT *
|
|
2705
|
+
FROM (
|
|
2706
|
+
select
|
|
2707
|
+
(select r.relname from pg_class r where r.oid = c.conrelid) as table,
|
|
2708
|
+
(select array_agg(attname::text) from pg_attribute
|
|
2709
|
+
where attrelid = c.conrelid and ARRAY[attnum] <@ c.conkey) as cols,
|
|
2710
|
+
(select array_agg(attname::text) from pg_attribute
|
|
2711
|
+
where attrelid = c.confrelid and ARRAY[attnum] <@ c.confkey) as fcols,
|
|
2712
|
+
(select r.relname from pg_class r where r.oid = c.confrelid) as ftable
|
|
2713
|
+
from pg_constraint c
|
|
2714
|
+
) ft
|
|
2715
|
+
where ft.table IS NOT NULL
|
|
2716
|
+
AND ft.ftable IS NOT NULL
|
|
2717
|
+
-- where c.confrelid = 'users'::regclass::oid
|
|
2718
|
+
) fc
|
|
2719
|
+
ON fc.table = c.table_name
|
|
2720
|
+
AND c.column_name::text = ANY(fc.cols)
|
|
2721
|
+
) cc
|
|
2722
|
+
ON t.table_name = cc.table_name
|
|
2723
|
+
AND t.table_schema = cc.table_schema
|
|
2724
|
+
LEFT JOIN (
|
|
2725
|
+
SELECT cl_r.relname as view_name, array_agg(DISTINCT cl_d.relname) AS table_names
|
|
2726
|
+
FROM pg_rewrite AS r
|
|
2727
|
+
JOIN pg_class AS cl_r ON r.ev_class=cl_r.oid
|
|
2728
|
+
JOIN pg_depend AS d ON r.oid=d.objid
|
|
2729
|
+
JOIN pg_class AS cl_d ON d.refobjid=cl_d.oid
|
|
2730
|
+
WHERE cl_d.relkind IN ('r','v')
|
|
2731
|
+
AND cl_d.relname <> cl_r.relname
|
|
2732
|
+
GROUP BY cl_r.relname
|
|
2733
|
+
) vr
|
|
2734
|
+
ON t.table_name = vr.view_name
|
|
2735
|
+
WHERE t.table_schema = ${(0, PubSubManager_1.asValue)(schema)}
|
|
2736
|
+
GROUP BY t.table_schema, t.table_name, t.table_type, vr.table_names , cc.table_oid
|
|
2737
|
+
ORDER BY schema, name
|
|
2738
|
+
`;
|
|
2739
|
+
// console.log(pgp.as.format(query, { schema }), schema);
|
|
2740
|
+
let res = await db.any(query, { schema });
|
|
2741
|
+
// res = await Promise.all(res.map(async tbl => {
|
|
2742
|
+
// tbl.columns = await Promise.all(tbl.columns.map(async col => {
|
|
2743
|
+
// if(col.has_default){
|
|
2744
|
+
// col.column_default = !col.column_default.startsWith("nextval(")? (await db.oneOrNone(`SELECT ${col.column_default} as val`)).val : null;
|
|
2745
|
+
// }
|
|
2746
|
+
// return col;
|
|
2747
|
+
// }))
|
|
2748
|
+
// return tbl;
|
|
2749
|
+
// }))
|
|
2750
|
+
res = res.map(tbl => {
|
|
2751
|
+
tbl.columns = tbl.columns.map(col => {
|
|
2752
|
+
if (col.has_default) {
|
|
2753
|
+
col.column_default = (col.udt_name !== "uuid" && !col.is_pkey && !col.column_default.startsWith("nextval(")) ? col.column_default : null;
|
|
2754
|
+
}
|
|
2755
|
+
return col;
|
|
2756
|
+
}); //.slice(0).sort((a, b) => a.name.localeCompare(b.name))
|
|
2757
|
+
// .sort((a, b) => a.ordinal_position - b.ordinal_position)
|
|
2758
|
+
return tbl;
|
|
2759
|
+
});
|
|
2760
|
+
return res;
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* Throw error if illegal keys found in object
|
|
2764
|
+
* @param {Object} obj - Object to be checked
|
|
2765
|
+
* @param {string[]} allowedKeys - The name of the employee.
|
|
2766
|
+
*/
|
|
2767
|
+
function validateObj(obj, allowedKeys) {
|
|
2768
|
+
if (obj && Object.keys(obj).length) {
|
|
2769
|
+
const invalid_keys = Object.keys(obj).filter(k => !allowedKeys.includes(k));
|
|
2770
|
+
if (invalid_keys.length) {
|
|
2771
|
+
throw "Invalid/Illegal fields found: " + invalid_keys.join(", ");
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
return obj;
|
|
2775
|
+
}
|
|
2776
|
+
function isPlainObject(o) {
|
|
2777
|
+
return Object(o) === o && Object.getPrototypeOf(o) === Object.prototype;
|
|
2778
|
+
}
|
|
2779
|
+
exports.isPlainObject = isPlainObject;
|
|
2780
|
+
function postgresToTsType(udt_data_type) {
|
|
2781
|
+
return (0, prostgles_types_1.getKeys)(prostgles_types_1.TS_PG_Types).find(k => {
|
|
2782
|
+
// @ts-ignore
|
|
2783
|
+
return prostgles_types_1.TS_PG_Types[k].includes(udt_data_type);
|
|
2784
|
+
}) ?? "any";
|
|
2785
|
+
}
|
|
2786
|
+
exports.postgresToTsType = postgresToTsType;
|
|
2787
|
+
function sqlErrCodeToMsg(code) {
|
|
2788
|
+
const errs = {
|
|
2789
|
+
"00000": "successful_completion",
|
|
2790
|
+
"01000": "warning",
|
|
2791
|
+
"0100C": "dynamic_result_sets_returned",
|
|
2792
|
+
"01008": "implicit_zero_bit_padding",
|
|
2793
|
+
"01003": "null_value_eliminated_in_set_function",
|
|
2794
|
+
"01007": "privilege_not_granted",
|
|
2795
|
+
"01006": "privilege_not_revoked",
|
|
2796
|
+
"01004": "string_data_right_truncation",
|
|
2797
|
+
"01P01": "deprecated_feature",
|
|
2798
|
+
"02000": "no_data",
|
|
2799
|
+
"02001": "no_additional_dynamic_result_sets_returned",
|
|
2800
|
+
"03000": "sql_statement_not_yet_complete",
|
|
2801
|
+
"08000": "connection_exception",
|
|
2802
|
+
"08003": "connection_does_not_exist",
|
|
2803
|
+
"08006": "connection_failure",
|
|
2804
|
+
"08001": "sqlclient_unable_to_establish_sqlconnection",
|
|
2805
|
+
"08004": "sqlserver_rejected_establishment_of_sqlconnection",
|
|
2806
|
+
"08007": "transaction_resolution_unknown",
|
|
2807
|
+
"08P01": "protocol_violation",
|
|
2808
|
+
"09000": "triggered_action_exception",
|
|
2809
|
+
"0A000": "feature_not_supported",
|
|
2810
|
+
"0B000": "invalid_transaction_initiation",
|
|
2811
|
+
"0F000": "locator_exception",
|
|
2812
|
+
"0F001": "invalid_locator_specification",
|
|
2813
|
+
"0L000": "invalid_grantor",
|
|
2814
|
+
"0LP01": "invalid_grant_operation",
|
|
2815
|
+
"0P000": "invalid_role_specification",
|
|
2816
|
+
"0Z000": "diagnostics_exception",
|
|
2817
|
+
"0Z002": "stacked_diagnostics_accessed_without_active_handler",
|
|
2818
|
+
"20000": "case_not_found",
|
|
2819
|
+
"21000": "cardinality_violation",
|
|
2820
|
+
"22000": "data_exception",
|
|
2821
|
+
"2202E": "array_subscript_error",
|
|
2822
|
+
"22021": "character_not_in_repertoire",
|
|
2823
|
+
"22008": "datetime_field_overflow",
|
|
2824
|
+
"22012": "division_by_zero",
|
|
2825
|
+
"22005": "error_in_assignment",
|
|
2826
|
+
"2200B": "escape_character_conflict",
|
|
2827
|
+
"22022": "indicator_overflow",
|
|
2828
|
+
"22015": "interval_field_overflow",
|
|
2829
|
+
"2201E": "invalid_argument_for_logarithm",
|
|
2830
|
+
"22014": "invalid_argument_for_ntile_function",
|
|
2831
|
+
"22016": "invalid_argument_for_nth_value_function",
|
|
2832
|
+
"2201F": "invalid_argument_for_power_function",
|
|
2833
|
+
"2201G": "invalid_argument_for_width_bucket_function",
|
|
2834
|
+
"22018": "invalid_character_value_for_cast",
|
|
2835
|
+
"22007": "invalid_datetime_format",
|
|
2836
|
+
"22019": "invalid_escape_character",
|
|
2837
|
+
"2200D": "invalid_escape_octet",
|
|
2838
|
+
"22025": "invalid_escape_sequence",
|
|
2839
|
+
"22P06": "nonstandard_use_of_escape_character",
|
|
2840
|
+
"22010": "invalid_indicator_parameter_value",
|
|
2841
|
+
"22023": "invalid_parameter_value",
|
|
2842
|
+
"2201B": "invalid_regular_expression",
|
|
2843
|
+
"2201W": "invalid_row_count_in_limit_clause",
|
|
2844
|
+
"2201X": "invalid_row_count_in_result_offset_clause",
|
|
2845
|
+
"2202H": "invalid_tablesample_argument",
|
|
2846
|
+
"2202G": "invalid_tablesample_repeat",
|
|
2847
|
+
"22009": "invalid_time_zone_displacement_value",
|
|
2848
|
+
"2200C": "invalid_use_of_escape_character",
|
|
2849
|
+
"2200G": "most_specific_type_mismatch",
|
|
2850
|
+
"22004": "null_value_not_allowed",
|
|
2851
|
+
"22002": "null_value_no_indicator_parameter",
|
|
2852
|
+
"22003": "numeric_value_out_of_range",
|
|
2853
|
+
"2200H": "sequence_generator_limit_exceeded",
|
|
2854
|
+
"22026": "string_data_length_mismatch",
|
|
2855
|
+
"22001": "string_data_right_truncation",
|
|
2856
|
+
"22011": "substring_error",
|
|
2857
|
+
"22027": "trim_error",
|
|
2858
|
+
"22024": "unterminated_c_string",
|
|
2859
|
+
"2200F": "zero_length_character_string",
|
|
2860
|
+
"22P01": "floating_point_exception",
|
|
2861
|
+
"22P02": "invalid_text_representation",
|
|
2862
|
+
"22P03": "invalid_binary_representation",
|
|
2863
|
+
"22P04": "bad_copy_file_format",
|
|
2864
|
+
"22P05": "untranslatable_character",
|
|
2865
|
+
"2200L": "not_an_xml_document",
|
|
2866
|
+
"2200M": "invalid_xml_document",
|
|
2867
|
+
"2200N": "invalid_xml_content",
|
|
2868
|
+
"2200S": "invalid_xml_comment",
|
|
2869
|
+
"2200T": "invalid_xml_processing_instruction",
|
|
2870
|
+
"23000": "integrity_constraint_violation",
|
|
2871
|
+
"23001": "restrict_violation",
|
|
2872
|
+
"23502": "not_null_violation",
|
|
2873
|
+
"23503": "foreign_key_violation",
|
|
2874
|
+
"23505": "unique_violation",
|
|
2875
|
+
"23514": "check_violation",
|
|
2876
|
+
"23P01": "exclusion_violation",
|
|
2877
|
+
"24000": "invalid_cursor_state",
|
|
2878
|
+
"25000": "invalid_transaction_state",
|
|
2879
|
+
"25001": "active_sql_transaction",
|
|
2880
|
+
"25002": "branch_transaction_already_active",
|
|
2881
|
+
"25008": "held_cursor_requires_same_isolation_level",
|
|
2882
|
+
"25003": "inappropriate_access_mode_for_branch_transaction",
|
|
2883
|
+
"25004": "inappropriate_isolation_level_for_branch_transaction",
|
|
2884
|
+
"25005": "no_active_sql_transaction_for_branch_transaction",
|
|
2885
|
+
"25006": "read_only_sql_transaction",
|
|
2886
|
+
"25007": "schema_and_data_statement_mixing_not_supported",
|
|
2887
|
+
"25P01": "no_active_sql_transaction",
|
|
2888
|
+
"25P02": "in_failed_sql_transaction",
|
|
2889
|
+
"25P03": "idle_in_transaction_session_timeout",
|
|
2890
|
+
"26000": "invalid_sql_statement_name",
|
|
2891
|
+
"27000": "triggered_data_change_violation",
|
|
2892
|
+
"28000": "invalid_authorization_specification",
|
|
2893
|
+
"28P01": "invalid_password",
|
|
2894
|
+
"2B000": "dependent_privilege_descriptors_still_exist",
|
|
2895
|
+
"2BP01": "dependent_objects_still_exist",
|
|
2896
|
+
"2D000": "invalid_transaction_termination",
|
|
2897
|
+
"2F000": "sql_routine_exception",
|
|
2898
|
+
"2F005": "function_executed_no_return_statement",
|
|
2899
|
+
"2F002": "modifying_sql_data_not_permitted",
|
|
2900
|
+
"2F003": "prohibited_sql_statement_attempted",
|
|
2901
|
+
"2F004": "reading_sql_data_not_permitted",
|
|
2902
|
+
"34000": "invalid_cursor_name",
|
|
2903
|
+
"38000": "external_routine_exception",
|
|
2904
|
+
"38001": "containing_sql_not_permitted",
|
|
2905
|
+
"38002": "modifying_sql_data_not_permitted",
|
|
2906
|
+
"38003": "prohibited_sql_statement_attempted",
|
|
2907
|
+
"38004": "reading_sql_data_not_permitted",
|
|
2908
|
+
"39000": "external_routine_invocation_exception",
|
|
2909
|
+
"39001": "invalid_sqlstate_returned",
|
|
2910
|
+
"39004": "null_value_not_allowed",
|
|
2911
|
+
"39P01": "trigger_protocol_violated",
|
|
2912
|
+
"39P02": "srf_protocol_violated",
|
|
2913
|
+
"39P03": "event_trigger_protocol_violated",
|
|
2914
|
+
"3B000": "savepoint_exception",
|
|
2915
|
+
"3B001": "invalid_savepoint_specification",
|
|
2916
|
+
"3D000": "invalid_catalog_name",
|
|
2917
|
+
"3F000": "invalid_schema_name",
|
|
2918
|
+
"40000": "transaction_rollback",
|
|
2919
|
+
"40002": "transaction_integrity_constraint_violation",
|
|
2920
|
+
"40001": "serialization_failure",
|
|
2921
|
+
"40003": "statement_completion_unknown",
|
|
2922
|
+
"40P01": "deadlock_detected",
|
|
2923
|
+
"42000": "syntax_error_or_access_rule_violation",
|
|
2924
|
+
"42601": "syntax_error",
|
|
2925
|
+
"42501": "insufficient_privilege",
|
|
2926
|
+
"42846": "cannot_coerce",
|
|
2927
|
+
"42803": "grouping_error",
|
|
2928
|
+
"42P20": "windowing_error",
|
|
2929
|
+
"42P19": "invalid_recursion",
|
|
2930
|
+
"42830": "invalid_foreign_key",
|
|
2931
|
+
"42602": "invalid_name",
|
|
2932
|
+
"42622": "name_too_long",
|
|
2933
|
+
"42939": "reserved_name",
|
|
2934
|
+
"42804": "datatype_mismatch",
|
|
2935
|
+
"42P18": "indeterminate_datatype",
|
|
2936
|
+
"42P21": "collation_mismatch",
|
|
2937
|
+
"42P22": "indeterminate_collation",
|
|
2938
|
+
"42809": "wrong_object_type",
|
|
2939
|
+
"428C9": "generated_always",
|
|
2940
|
+
"42703": "undefined_column",
|
|
2941
|
+
"42883": "undefined_function",
|
|
2942
|
+
"42P01": "undefined_table",
|
|
2943
|
+
"42P02": "undefined_parameter",
|
|
2944
|
+
"42704": "undefined_object",
|
|
2945
|
+
"42701": "duplicate_column",
|
|
2946
|
+
"42P03": "duplicate_cursor",
|
|
2947
|
+
"42P04": "duplicate_database",
|
|
2948
|
+
"42723": "duplicate_function",
|
|
2949
|
+
"42P05": "duplicate_prepared_statement",
|
|
2950
|
+
"42P06": "duplicate_schema",
|
|
2951
|
+
"42P07": "duplicate_table",
|
|
2952
|
+
"42712": "duplicate_alias",
|
|
2953
|
+
"42710": "duplicate_object",
|
|
2954
|
+
"42702": "ambiguous_column",
|
|
2955
|
+
"42725": "ambiguous_function",
|
|
2956
|
+
"42P08": "ambiguous_parameter",
|
|
2957
|
+
"42P09": "ambiguous_alias",
|
|
2958
|
+
"42P10": "invalid_column_reference",
|
|
2959
|
+
"42611": "invalid_column_definition",
|
|
2960
|
+
"42P11": "invalid_cursor_definition",
|
|
2961
|
+
"42P12": "invalid_database_definition",
|
|
2962
|
+
"42P13": "invalid_function_definition",
|
|
2963
|
+
"42P14": "invalid_prepared_statement_definition",
|
|
2964
|
+
"42P15": "invalid_schema_definition",
|
|
2965
|
+
"42P16": "invalid_table_definition",
|
|
2966
|
+
"42P17": "invalid_object_definition",
|
|
2967
|
+
"44000": "with_check_option_violation",
|
|
2968
|
+
"53000": "insufficient_resources",
|
|
2969
|
+
"53100": "disk_full",
|
|
2970
|
+
"53200": "out_of_memory",
|
|
2971
|
+
"53300": "too_many_connections",
|
|
2972
|
+
"53400": "configuration_limit_exceeded",
|
|
2973
|
+
"54000": "program_limit_exceeded",
|
|
2974
|
+
"54001": "statement_too_complex",
|
|
2975
|
+
"54011": "too_many_columns",
|
|
2976
|
+
"54023": "too_many_arguments",
|
|
2977
|
+
"55000": "object_not_in_prerequisite_state",
|
|
2978
|
+
"55006": "object_in_use",
|
|
2979
|
+
"55P02": "cant_change_runtime_param",
|
|
2980
|
+
"55P03": "lock_not_available",
|
|
2981
|
+
"57000": "operator_intervention",
|
|
2982
|
+
"57014": "query_canceled",
|
|
2983
|
+
"57P01": "admin_shutdown",
|
|
2984
|
+
"57P02": "crash_shutdown",
|
|
2985
|
+
"57P03": "cannot_connect_now",
|
|
2986
|
+
"57P04": "database_dropped",
|
|
2987
|
+
"58000": "system_error",
|
|
2988
|
+
"58030": "io_error",
|
|
2989
|
+
"58P01": "undefined_file",
|
|
2990
|
+
"58P02": "duplicate_file",
|
|
2991
|
+
"72000": "snapshot_too_old",
|
|
2992
|
+
"F0000": "config_file_error",
|
|
2993
|
+
"F0001": "lock_file_exists",
|
|
2994
|
+
"HV000": "fdw_error",
|
|
2995
|
+
"HV005": "fdw_column_name_not_found",
|
|
2996
|
+
"HV002": "fdw_dynamic_parameter_value_needed",
|
|
2997
|
+
"HV010": "fdw_function_sequence_error",
|
|
2998
|
+
"HV021": "fdw_inconsistent_descriptor_information",
|
|
2999
|
+
"HV024": "fdw_invalid_attribute_value",
|
|
3000
|
+
"HV007": "fdw_invalid_column_name",
|
|
3001
|
+
"HV008": "fdw_invalid_column_number",
|
|
3002
|
+
"HV004": "fdw_invalid_data_type",
|
|
3003
|
+
"HV006": "fdw_invalid_data_type_descriptors",
|
|
3004
|
+
"HV091": "fdw_invalid_descriptor_field_identifier",
|
|
3005
|
+
"HV00B": "fdw_invalid_handle",
|
|
3006
|
+
"HV00C": "fdw_invalid_option_index",
|
|
3007
|
+
"HV00D": "fdw_invalid_option_name",
|
|
3008
|
+
"HV090": "fdw_invalid_string_length_or_buffer_length",
|
|
3009
|
+
"HV00A": "fdw_invalid_string_format",
|
|
3010
|
+
"HV009": "fdw_invalid_use_of_null_pointer",
|
|
3011
|
+
"HV014": "fdw_too_many_handles",
|
|
3012
|
+
"HV001": "fdw_out_of_memory",
|
|
3013
|
+
"HV00P": "fdw_no_schemas",
|
|
3014
|
+
"HV00J": "fdw_option_name_not_found",
|
|
3015
|
+
"HV00K": "fdw_reply_handle",
|
|
3016
|
+
"HV00Q": "fdw_schema_not_found",
|
|
3017
|
+
"HV00R": "fdw_table_not_found",
|
|
3018
|
+
"HV00L": "fdw_unable_to_create_execution",
|
|
3019
|
+
"HV00M": "fdw_unable_to_create_reply",
|
|
3020
|
+
"HV00N": "fdw_unable_to_establish_connection",
|
|
3021
|
+
"P0000": "plpgsql_error",
|
|
3022
|
+
"P0001": "raise_exception",
|
|
3023
|
+
"P0002": "no_data_found",
|
|
3024
|
+
"P0003": "too_many_rows",
|
|
3025
|
+
"P0004": "assert_failure",
|
|
3026
|
+
"XX000": "internal_error",
|
|
3027
|
+
"XX001": "data_corrupted",
|
|
3028
|
+
"XX002": "index_corrupted"
|
|
3029
|
+
}, c2 = { "20000": "case_not_found", "21000": "cardinality_violation", "22000": "data_exception", "22001": "string_data_right_truncation", "22002": "null_value_no_indicator_parameter", "22003": "numeric_value_out_of_range", "22004": "null_value_not_allowed", "22005": "error_in_assignment", "22007": "invalid_datetime_format", "22008": "datetime_field_overflow", "22009": "invalid_time_zone_displacement_value", "22010": "invalid_indicator_parameter_value", "22011": "substring_error", "22012": "division_by_zero", "22013": "invalid_preceding_or_following_size", "22014": "invalid_argument_for_ntile_function", "22015": "interval_field_overflow", "22016": "invalid_argument_for_nth_value_function", "22018": "invalid_character_value_for_cast", "22019": "invalid_escape_character", "22021": "character_not_in_repertoire", "22022": "indicator_overflow", "22023": "invalid_parameter_value", "22024": "unterminated_c_string", "22025": "invalid_escape_sequence", "22026": "string_data_length_mismatch", "22027": "trim_error", "22030": "duplicate_json_object_key_value", "22031": "invalid_argument_for_sql_json_datetime_function", "22032": "invalid_json_text", "22033": "invalid_sql_json_subscript", "22034": "more_than_one_sql_json_item", "22035": "no_sql_json_item", "22036": "non_numeric_sql_json_item", "22037": "non_unique_keys_in_a_json_object", "22038": "singleton_sql_json_item_required", "22039": "sql_json_array_not_found", "23000": "integrity_constraint_violation", "23001": "restrict_violation", "23502": "not_null_violation", "23503": "foreign_key_violation", "23505": "unique_violation", "23514": "check_violation", "24000": "invalid_cursor_state", "25000": "invalid_transaction_state", "25001": "active_sql_transaction", "25002": "branch_transaction_already_active", "25003": "inappropriate_access_mode_for_branch_transaction", "25004": "inappropriate_isolation_level_for_branch_transaction", "25005": "no_active_sql_transaction_for_branch_transaction", "25006": "read_only_sql_transaction", "25007": "schema_and_data_statement_mixing_not_supported", "25008": "held_cursor_requires_same_isolation_level", "26000": "invalid_sql_statement_name", "27000": "triggered_data_change_violation", "28000": "invalid_authorization_specification", "34000": "invalid_cursor_name", "38000": "external_routine_exception", "38001": "containing_sql_not_permitted", "38002": "modifying_sql_data_not_permitted", "38003": "prohibited_sql_statement_attempted", "38004": "reading_sql_data_not_permitted", "39000": "external_routine_invocation_exception", "39001": "invalid_sqlstate_returned", "39004": "null_value_not_allowed", "40000": "transaction_rollback", "40001": "serialization_failure", "40002": "transaction_integrity_constraint_violation", "40003": "statement_completion_unknown", "42000": "syntax_error_or_access_rule_violation", "42501": "insufficient_privilege", "42601": "syntax_error", "42602": "invalid_name", "42611": "invalid_column_definition", "42622": "name_too_long", "42701": "duplicate_column", "42702": "ambiguous_column", "42703": "undefined_column", "42704": "undefined_object", "42710": "duplicate_object", "42712": "duplicate_alias", "42723": "duplicate_function", "42725": "ambiguous_function", "42803": "grouping_error", "42804": "datatype_mismatch", "42809": "wrong_object_type", "42830": "invalid_foreign_key", "42846": "cannot_coerce", "42883": "undefined_function", "42939": "reserved_name", "44000": "with_check_option_violation", "53000": "insufficient_resources", "53100": "disk_full", "53200": "out_of_memory", "53300": "too_many_connections", "53400": "configuration_limit_exceeded", "54000": "program_limit_exceeded", "54001": "statement_too_complex", "54011": "too_many_columns", "54023": "too_many_arguments", "55000": "object_not_in_prerequisite_state", "55006": "object_in_use", "57000": "operator_intervention", "57014": "query_canceled", "58000": "system_error", "58030": "io_error", "72000": "snapshot_too_old", "00000": "successful_completion", "01000": "warning", "0100C": "dynamic_result_sets_returned", "01008": "implicit_zero_bit_padding", "01003": "null_value_eliminated_in_set_function", "01007": "privilege_not_granted", "01006": "privilege_not_revoked", "01004": "string_data_right_truncation", "01P01": "deprecated_feature", "02000": "no_data", "02001": "no_additional_dynamic_result_sets_returned", "03000": "sql_statement_not_yet_complete", "08000": "connection_exception", "08003": "connection_does_not_exist", "08006": "connection_failure", "08001": "sqlclient_unable_to_establish_sqlconnection", "08004": "sqlserver_rejected_establishment_of_sqlconnection", "08007": "transaction_resolution_unknown", "08P01": "protocol_violation", "09000": "triggered_action_exception", "0A000": "feature_not_supported", "0B000": "invalid_transaction_initiation", "0F000": "locator_exception", "0F001": "invalid_locator_specification", "0L000": "invalid_grantor", "0LP01": "invalid_grant_operation", "0P000": "invalid_role_specification", "0Z000": "diagnostics_exception", "0Z002": "stacked_diagnostics_accessed_without_active_handler", "2202E": "array_subscript_error", "2200B": "escape_character_conflict", "2201E": "invalid_argument_for_logarithm", "2201F": "invalid_argument_for_power_function", "2201G": "invalid_argument_for_width_bucket_function", "2200D": "invalid_escape_octet", "22P06": "nonstandard_use_of_escape_character", "2201B": "invalid_regular_expression", "2201W": "invalid_row_count_in_limit_clause", "2201X": "invalid_row_count_in_result_offset_clause", "2202H": "invalid_tablesample_argument", "2202G": "invalid_tablesample_repeat", "2200C": "invalid_use_of_escape_character", "2200G": "most_specific_type_mismatch", "2200H": "sequence_generator_limit_exceeded", "2200F": "zero_length_character_string", "22P01": "floating_point_exception", "22P02": "invalid_text_representation", "22P03": "invalid_binary_representation", "22P04": "bad_copy_file_format", "22P05": "untranslatable_character", "2200L": "not_an_xml_document", "2200M": "invalid_xml_document", "2200N": "invalid_xml_content", "2200S": "invalid_xml_comment", "2200T": "invalid_xml_processing_instruction", "2203A": "sql_json_member_not_found", "2203B": "sql_json_number_not_found", "2203C": "sql_json_object_not_found", "2203D": "too_many_json_array_elements", "2203E": "too_many_json_object_members", "2203F": "sql_json_scalar_required", "23P01": "exclusion_violation", "25P01": "no_active_sql_transaction", "25P02": "in_failed_sql_transaction", "25P03": "idle_in_transaction_session_timeout", "28P01": "invalid_password", "2B000": "dependent_privilege_descriptors_still_exist", "2BP01": "dependent_objects_still_exist", "2D000": "invalid_transaction_termination", "2F000": "sql_routine_exception", "2F005": "function_executed_no_return_statement", "2F002": "modifying_sql_data_not_permitted", "2F003": "prohibited_sql_statement_attempted", "2F004": "reading_sql_data_not_permitted", "39P01": "trigger_protocol_violated", "39P02": "srf_protocol_violated", "39P03": "event_trigger_protocol_violated", "3B000": "savepoint_exception", "3B001": "invalid_savepoint_specification", "3D000": "invalid_catalog_name", "3F000": "invalid_schema_name", "40P01": "deadlock_detected", "42P20": "windowing_error", "42P19": "invalid_recursion", "42P18": "indeterminate_datatype", "42P21": "collation_mismatch", "42P22": "indeterminate_collation", "428C9": "generated_always", "42P01": "undefined_table", "42P02": "undefined_parameter", "42P03": "duplicate_cursor", "42P04": "duplicate_database", "42P05": "duplicate_prepared_statement", "42P06": "duplicate_schema", "42P07": "duplicate_table", "42P08": "ambiguous_parameter", "42P09": "ambiguous_alias", "42P10": "invalid_column_reference", "42P11": "invalid_cursor_definition", "42P12": "invalid_database_definition", "42P13": "invalid_function_definition", "42P14": "invalid_prepared_statement_definition", "42P15": "invalid_schema_definition", "42P16": "invalid_table_definition", "42P17": "invalid_object_definition", "55P02": "cant_change_runtime_param", "55P03": "lock_not_available", "55P04": "unsafe_new_enum_value_usage", "57P01": "admin_shutdown", "57P02": "crash_shutdown", "57P03": "cannot_connect_now", "57P04": "database_dropped", "58P01": "undefined_file", "58P02": "duplicate_file", "F0000": "config_file_error", "F0001": "lock_file_exists", "HV000": "fdw_error", "HV005": "fdw_column_name_not_found", "HV002": "fdw_dynamic_parameter_value_needed", "HV010": "fdw_function_sequence_error", "HV021": "fdw_inconsistent_descriptor_information", "HV024": "fdw_invalid_attribute_value", "HV007": "fdw_invalid_column_name", "HV008": "fdw_invalid_column_number", "HV004": "fdw_invalid_data_type", "HV006": "fdw_invalid_data_type_descriptors", "HV091": "fdw_invalid_descriptor_field_identifier", "HV00B": "fdw_invalid_handle", "HV00C": "fdw_invalid_option_index", "HV00D": "fdw_invalid_option_name", "HV090": "fdw_invalid_string_length_or_buffer_length", "HV00A": "fdw_invalid_string_format", "HV009": "fdw_invalid_use_of_null_pointer", "HV014": "fdw_too_many_handles", "HV001": "fdw_out_of_memory", "HV00P": "fdw_no_schemas", "HV00J": "fdw_option_name_not_found", "HV00K": "fdw_reply_handle", "HV00Q": "fdw_schema_not_found", "HV00R": "fdw_table_not_found", "HV00L": "fdw_unable_to_create_execution", "HV00M": "fdw_unable_to_create_reply", "HV00N": "fdw_unable_to_establish_connection", "P0000": "plpgsql_error", "P0001": "raise_exception", "P0002": "no_data_found", "P0003": "too_many_rows", "P0004": "assert_failure", "XX000": "internal_error", "XX001": "data_corrupted", "XX002": "index_corrupted" };
|
|
3030
|
+
//@ts-ignore
|
|
3031
|
+
return c2[code] || errs[code] || code;
|
|
3032
|
+
/*
|
|
3033
|
+
https://www.postgresql.org/docs/13/errcodes-appendix.html
|
|
3034
|
+
JSON.stringify([...THE_table_$0.rows].map(t => [...t.children].map(u => u.innerText)).filter((d, i) => i && d.length > 1).reduce((a, v)=>({ ...a, [v[0]]: v[1] }), {}))
|
|
3035
|
+
*/
|
|
3036
|
+
}
|
|
3037
|
+
async function getInferredJoins(db, schema = "public") {
|
|
3038
|
+
let joins = [];
|
|
3039
|
+
let res = await db.any(`SELECT
|
|
3040
|
+
tc.table_schema,
|
|
3041
|
+
tc.constraint_name,
|
|
3042
|
+
tc.table_name,
|
|
3043
|
+
kcu.column_name,
|
|
3044
|
+
ccu.table_schema AS foreign_table_schema,
|
|
3045
|
+
ccu.table_name AS foreign_table_name,
|
|
3046
|
+
ccu.column_name AS foreign_column_name,
|
|
3047
|
+
tc.constraint_type IN ('UNIQUE', 'PRIMARY KEY') as foreign_is_unique
|
|
3048
|
+
FROM
|
|
3049
|
+
information_schema.table_constraints AS tc
|
|
3050
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
3051
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
3052
|
+
AND tc.table_schema = kcu.table_schema
|
|
3053
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
3054
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
3055
|
+
AND ccu.table_schema = tc.table_schema
|
|
3056
|
+
WHERE tc.table_schema=` + "${schema}" + `
|
|
3057
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
3058
|
+
AND tc.table_name <> ccu.table_name -- Exclude self-referencing tables
|
|
3059
|
+
`, { schema });
|
|
3060
|
+
res.map((d) => {
|
|
3061
|
+
let eIdx = joins.findIndex(j => j.tables.includes(d.table_name) && j.tables.includes(d.foreign_table_name));
|
|
3062
|
+
let existing = joins[eIdx];
|
|
3063
|
+
if (existing) {
|
|
3064
|
+
if (existing.tables[0] === d.table_name) {
|
|
3065
|
+
existing.on = { ...existing.on, [d.column_name]: d.foreign_column_name };
|
|
3066
|
+
}
|
|
3067
|
+
else {
|
|
3068
|
+
existing.on = { ...existing.on, [d.foreign_column_name]: d.column_name };
|
|
3069
|
+
}
|
|
3070
|
+
joins[eIdx] = existing;
|
|
3071
|
+
}
|
|
3072
|
+
else {
|
|
3073
|
+
joins.push({
|
|
3074
|
+
tables: [d.table_name, d.foreign_table_name],
|
|
3075
|
+
on: {
|
|
3076
|
+
[d.column_name]: d.foreign_column_name
|
|
3077
|
+
},
|
|
3078
|
+
type: "many-many"
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
});
|
|
3082
|
+
return joins;
|
|
3083
|
+
}
|