prostgles-server 3.0.39 → 3.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/DBSchemaBuilder.d.ts.map +1 -1
  2. package/dist/DBSchemaBuilder.js.map +1 -1
  3. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts +2 -1
  4. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -1
  5. package/dist/DboBuilder/QueryBuilder/QueryBuilder.js +2 -1
  6. package/dist/DboBuilder/QueryBuilder/QueryBuilder.js.map +1 -1
  7. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.d.ts +1 -1
  8. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.d.ts.map +1 -1
  9. package/dist/DboBuilder/QueryBuilder/makeSelectQuery.js.map +1 -1
  10. package/dist/DboBuilder/TableHandler.d.ts +62 -0
  11. package/dist/DboBuilder/TableHandler.d.ts.map +1 -0
  12. package/dist/DboBuilder/TableHandler.js +304 -0
  13. package/dist/DboBuilder/TableHandler.js.map +1 -0
  14. package/dist/DboBuilder/ViewHandler.d.ts +137 -0
  15. package/dist/DboBuilder/ViewHandler.d.ts.map +1 -0
  16. package/dist/DboBuilder/ViewHandler.js +1292 -0
  17. package/dist/DboBuilder/ViewHandler.js.map +1 -0
  18. package/dist/DboBuilder/delete.d.ts +2 -1
  19. package/dist/DboBuilder/delete.d.ts.map +1 -1
  20. package/dist/DboBuilder/delete.js.map +1 -1
  21. package/dist/DboBuilder/getColumns.d.ts +12 -0
  22. package/dist/DboBuilder/getColumns.d.ts.map +1 -0
  23. package/dist/DboBuilder/getColumns.js +95 -0
  24. package/dist/DboBuilder/getColumns.js.map +1 -0
  25. package/dist/DboBuilder/insert.d.ts +2 -1
  26. package/dist/DboBuilder/insert.d.ts.map +1 -1
  27. package/dist/DboBuilder/insert.js +1 -1
  28. package/dist/DboBuilder/insert.js.map +1 -1
  29. package/dist/DboBuilder/insertDataParse.d.ts +2 -1
  30. package/dist/DboBuilder/insertDataParse.d.ts.map +1 -1
  31. package/dist/DboBuilder/insertDataParse.js +2 -3
  32. package/dist/DboBuilder/insertDataParse.js.map +1 -1
  33. package/dist/DboBuilder/parseUpdateRules.d.ts +18 -0
  34. package/dist/DboBuilder/parseUpdateRules.d.ts.map +1 -0
  35. package/dist/DboBuilder/parseUpdateRules.js +119 -0
  36. package/dist/DboBuilder/parseUpdateRules.js.map +1 -0
  37. package/dist/DboBuilder/update.d.ts +2 -1
  38. package/dist/DboBuilder/update.d.ts.map +1 -1
  39. package/dist/DboBuilder/update.js.map +1 -1
  40. package/dist/DboBuilder/uploadFile.d.ts +2 -1
  41. package/dist/DboBuilder/uploadFile.d.ts.map +1 -1
  42. package/dist/DboBuilder/uploadFile.js.map +1 -1
  43. package/dist/DboBuilder.d.ts +5 -185
  44. package/dist/DboBuilder.d.ts.map +1 -1
  45. package/dist/DboBuilder.js +7 -1774
  46. package/dist/DboBuilder.js.map +1 -1
  47. package/dist/FileManager.d.ts.map +1 -1
  48. package/dist/FileManager.js +4 -4
  49. package/dist/FileManager.js.map +1 -1
  50. package/dist/PublishParser.d.ts.map +1 -1
  51. package/dist/PublishParser.js.map +1 -1
  52. package/dist/index.js +0 -38
  53. package/dist/index.js.map +1 -1
  54. package/lib/DBSchemaBuilder.d.ts.map +1 -1
  55. package/lib/DBSchemaBuilder.ts +1 -1
  56. package/lib/DboBuilder/QueryBuilder/QueryBuilder.d.ts +2 -1
  57. package/lib/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -1
  58. package/lib/DboBuilder/QueryBuilder/QueryBuilder.js +2 -1
  59. package/lib/DboBuilder/QueryBuilder/QueryBuilder.ts +3 -1
  60. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.d.ts +1 -1
  61. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.d.ts.map +1 -1
  62. package/lib/DboBuilder/QueryBuilder/makeSelectQuery.ts +2 -1
  63. package/lib/DboBuilder/TableHandler.d.ts +54 -0
  64. package/lib/DboBuilder/TableHandler.d.ts.map +1 -0
  65. package/lib/DboBuilder/TableHandler.js +303 -0
  66. package/lib/DboBuilder/TableHandler.ts +365 -0
  67. package/lib/DboBuilder/ViewHandler.d.ts +133 -0
  68. package/lib/DboBuilder/ViewHandler.d.ts.map +1 -0
  69. package/lib/DboBuilder/ViewHandler.js +1291 -0
  70. package/lib/DboBuilder/ViewHandler.ts +1542 -0
  71. package/lib/DboBuilder/delete.d.ts +2 -1
  72. package/lib/DboBuilder/delete.d.ts.map +1 -1
  73. package/lib/DboBuilder/delete.ts +2 -1
  74. package/lib/DboBuilder/getColumns.d.ts +12 -0
  75. package/lib/DboBuilder/getColumns.d.ts.map +1 -0
  76. package/lib/DboBuilder/getColumns.js +94 -0
  77. package/lib/DboBuilder/getColumns.ts +133 -0
  78. package/lib/DboBuilder/insert.d.ts +2 -1
  79. package/lib/DboBuilder/insert.d.ts.map +1 -1
  80. package/lib/DboBuilder/insert.js +1 -1
  81. package/lib/DboBuilder/insert.ts +3 -2
  82. package/lib/DboBuilder/insertDataParse.d.ts +2 -1
  83. package/lib/DboBuilder/insertDataParse.d.ts.map +1 -1
  84. package/lib/DboBuilder/insertDataParse.js +2 -3
  85. package/lib/DboBuilder/insertDataParse.ts +6 -5
  86. package/lib/DboBuilder/parseUpdateRules.d.ts +18 -0
  87. package/lib/DboBuilder/parseUpdateRules.d.ts.map +1 -0
  88. package/lib/DboBuilder/parseUpdateRules.js +118 -0
  89. package/lib/DboBuilder/parseUpdateRules.ts +156 -0
  90. package/lib/DboBuilder/update.d.ts +2 -1
  91. package/lib/DboBuilder/update.d.ts.map +1 -1
  92. package/lib/DboBuilder/update.ts +2 -1
  93. package/lib/DboBuilder/uploadFile.d.ts +2 -1
  94. package/lib/DboBuilder/uploadFile.d.ts.map +1 -1
  95. package/lib/DboBuilder/uploadFile.ts +2 -1
  96. package/lib/DboBuilder.d.ts +5 -185
  97. package/lib/DboBuilder.d.ts.map +1 -1
  98. package/lib/DboBuilder.js +7 -1774
  99. package/lib/DboBuilder.ts +169 -2297
  100. package/lib/FileManager.d.ts.map +1 -1
  101. package/lib/FileManager.js +4 -4
  102. package/lib/FileManager.ts +3 -1
  103. package/lib/PublishParser.d.ts.map +1 -1
  104. package/lib/PublishParser.ts +3 -1
  105. package/lib/SyncReplication.ts +1 -1
  106. package/lib/index.js +0 -38
  107. package/lib/index.ts +1 -53
  108. package/package.json +1 -1
  109. package/tests/client/PID.txt +1 -1
  110. package/tests/server/package-lock.json +1 -1
package/lib/DboBuilder.ts CHANGED
@@ -105,6 +105,7 @@ import { insertDataParse } from "./DboBuilder/insertDataParse";
105
105
  import { insert } from "./DboBuilder/insert";
106
106
  import { update } from "./DboBuilder/update";
107
107
  import { _delete } from "./DboBuilder/delete";
108
+ import { JoinPaths, ViewHandler } from "./DboBuilder/ViewHandler";
108
109
 
109
110
  import { parseFilterItem } from "./Filtering";
110
111
 
@@ -191,2336 +192,222 @@ export type LocalParams = {
191
192
  referencingColumn?: string;
192
193
  }
193
194
  }
194
- function replaceNonAlphaNumeric(string: string, replacement = "_"): string {
195
- return string.replace(/[\W_]+/g, replacement);
196
- }
197
- function capitalizeFirstLetter(string: string, nonalpha_replacement?: string) : string {
198
- const str = replaceNonAlphaNumeric(string, nonalpha_replacement);
199
- return str.charAt(0).toUpperCase() + str.slice(1);
200
- }
201
-
202
- function snakify(str: string, capitalize = false) : string {
203
-
204
- return str.split("").map((c, i)=> {
205
-
206
- if(!i) {
207
- if(capitalize) c = c.toUpperCase();
208
- if(c.match(/[^a-z_A-Z]/)){
209
- return ((capitalize)? "D_" : "_") + c.charCodeAt(0);
210
- }
211
- } else {
212
- if(c.match(/[^a-zA-Z_0-9]/)){
213
- return "_" + c.charCodeAt(0);
214
- }
215
- }
216
-
217
- return c;
218
-
219
- }).join("");
220
- }
221
-
222
- function canBeUsedAsIsInTypescript(str: string): boolean {
223
- if(!str) return false;
224
- const isAlphaNumericOrUnderline = str.match(/^[a-z0-9_]+$/i);
225
- const startsWithCharOrUnderscore = str[0].match(/^[a-z_]+$/i);
226
- return Boolean(isAlphaNumericOrUnderline && startsWithCharOrUnderscore);
227
- }
228
-
229
- export function escapeTSNames(str: string, capitalize = false): string {
230
- let res = str;
231
- res = (capitalize? str[0].toUpperCase() : str[0]) + str.slice(1);
232
- if(canBeUsedAsIsInTypescript(res)) return res;
233
- return JSON.stringify(res);
234
- }
235
-
236
- export type Aggregation = {
237
- field: string,
238
- query: string,
239
- alias: string,
240
- getQuery: (alias: string) => string;
241
- };
242
-
243
- export type Filter = AnyObject | { $and: Filter[] } | { $or: Filter[] };
244
-
245
- type SelectFunc = {
246
- alias: string;
247
- getQuery: (alias: string, tableAlias?: string) => string;
248
- }
249
-
250
- type Query = {
251
- select: string[];
252
- selectFuncs: SelectFunc[];
253
- allFields: string[];
254
- aggs?: Aggregation[];
255
- table: string;
256
- where: string;
257
- orderBy: string[];
258
- limit: number;
259
- offset: number;
260
- isLeftJoin: boolean;
261
- joins?: Query[];
262
- joinAlias?: string;
263
- $path?: string[];
264
- }
265
-
266
- export type JoinInfo = {
267
- expectOne?: boolean,
268
- paths: {
269
-
270
- /**
271
- * The table that JOIN ON columns refer to.
272
- * columns in index = 1 refer to this table. index = 0 columns refer to previous JoinInfo.table
273
- */
274
- table: string,
275
-
276
- /**
277
- * Source and target JOIN ON columns
278
- * Each inner array group will be combined with AND and outer arrays with OR to allow multiple references to the same table
279
- * e.g.: [[source_table_column: string, table_column: string]]
280
- */
281
- on: [string, string][][],
282
-
283
- /**
284
- * Source table name
285
- */
286
- source: string,
287
-
288
- /**
289
- * Target table name
290
- */
291
- target: string
292
- }[]
293
- }
294
- type JoinPaths = {
295
- t1: string;
296
- t2: string;
297
- path: string[];
298
- }[];
299
-
300
- import { findShortestPath, Graph } from "./shortestPath";
301
-
302
- export type CommonTableRules = {
303
-
304
- /**
305
- * True by default. Allows clients to get column information on any columns that are allowed in (select, insert, update) field rules.
306
- */
307
- getColumns?: PublishAllOrNothing;
308
-
309
- /**
310
- * True by default. Allows clients to get table information (oid, comment, label, has_media).
311
- */
312
- getInfo?: PublishAllOrNothing
313
- }
314
-
315
- export type ValidatedTableRules = CommonTableRules & {
316
-
317
- /* All columns of the view/table. Includes computed fields as well */
318
- allColumns: FieldSpec[];
319
-
320
- select: {
321
- /* Fields you can select */
322
- fields: string[];
323
-
324
- /* Fields you can select */
325
- orderByFields: string[];
326
-
327
- /* Filter applied to every select */
328
- filterFields: string[];
329
-
330
- /* Filter applied to every select */
331
- forcedFilter: any;
332
-
333
- /* Max limit allowed for each select. 1000 by default. If null then an unlimited select is allowed when providing { limit: null } */
334
- maxLimit: number | null;
335
- },
336
- update: {
337
- /* Fields you can update */
338
- fields: string[];
339
-
340
- /* Fields you can return after updating */
341
- returningFields: string[];
342
-
343
- /* Fields you can use in filtering when updating */
344
- filterFields: string[];
345
-
346
- /* Filter applied to every update. Filter fields cannot be updated */
347
- forcedFilter: any;
348
-
349
- /* Data applied to every update */
350
- forcedData: any;
351
- },
352
- insert: {
353
- /* Fields you can insert */
354
- fields: string[];
355
-
356
- /* Fields you can return after inserting. Will return select.fields by default */
357
- returningFields: string[];
358
-
359
- /* Data applied to every insert */
360
- forcedData: any;
361
- },
362
- delete: {
363
- /* Fields to filter by when deleting */
364
- filterFields: string[];
365
-
366
- /* Filter applied to every deletes */
367
- forcedFilter: any;
368
-
369
- /* Fields you can return after deleting */
370
- returningFields: string[];
371
- }
372
- }
373
-
374
- /* DEBUG CLIENT ERRORS HERE */
375
- export function makeErr(err: any, localParams?: LocalParams, view?: ViewHandler, allowedKeys?: string[]){
376
- // console.trace(err)
377
- if(process.env.TEST_TYPE || process.env.PRGL_DEBUG) {
378
- console.trace(err)
379
- }
380
- const errObject = {
381
- ...((!localParams || !localParams.socket)? err : {}),
382
- ...pickKeys(err, ["column", "code", "table", "constraint"]),
383
- ...(err && err.toString? { txt: err.toString() } : {}),
384
- code_info: sqlErrCodeToMsg(err.code)
385
- };
386
- if(view?.dboBuilder?.constraints && errObject.constraint && !errObject.column){
387
- const constraint = view.dboBuilder.constraints
388
- .find(c => c.conname === errObject.constraint && c.relname === view.name);
389
- if(constraint){
390
- const cols = view.columns.filter(c =>
391
- (!allowedKeys || allowedKeys.includes(c.name)) &&
392
- constraint.conkey.includes(c.ordinal_position)
393
- );
394
- if(cols.length){
395
- errObject.column = cols[0].name;
396
- errObject.columns = cols.map(c => c.name);
397
- }
398
- }
399
- }
400
- return Promise.reject(errObject);
401
- }
402
- export const EXISTS_KEYS = ["$exists", "$notExists", "$existsJoined", "$notExistsJoined"] as const;
403
- export type EXISTS_KEY = typeof EXISTS_KEYS[number];
404
-
405
- const FILTER_FUNCS = FUNCTIONS.filter(f => f.canBeUsedForFilter);
406
-
407
- /**
408
- * Ensure the error is an Object and has
409
- */
410
- export function parseError(e: any, caller: string): ProstglesError {
411
-
412
- const errorObject = isObject(e)? e : undefined;
413
- const message = typeof e === "string"? e : e instanceof Error? e.message :
414
- isObject(errorObject)? (errorObject.message ?? errorObject.txt ?? JSON.stringify(errorObject) ?? "") : "";
415
- const stack = [
416
- ...(errorObject && Array.isArray(errorObject.stack)? errorObject.stack : []),
417
- caller
418
- ]
419
- const result: ProstglesError = {
420
- ...errorObject,
421
- message,
422
- stack,
423
- }
424
- return result;
425
- }
426
-
427
- class ColSet {
428
- opts: {
429
- columns: ColumnInfo[];
430
- tableName: string;
431
- colNames: string[];
432
- };
433
-
434
- constructor(columns: ColumnInfo[], tableName: string) {
435
- this.opts = { columns, tableName, colNames: columns.map(c => c.name) }
436
- }
437
-
438
- private async getRow(data: any, allowedCols: string[], dbTx: DBHandlerServer, validate?: ValidateRow): Promise<{ escapedCol: string; escapedVal: string; }[]> {
439
- const badCol = allowedCols.find(c => !this.opts.colNames.includes(c))
440
- if(!allowedCols || badCol){
441
- throw "Missing or unexpected columns: " + badCol;
442
- }
443
-
444
- if(isEmpty(data)) throw "No data";
445
-
446
- let row = pickKeys(data, allowedCols);
447
- if(validate){
448
- row = await validate(row, dbTx);
449
- }
450
- const rowKeys = Object.keys(row);
451
-
452
- return rowKeys.map(key => {
453
- const col = this.opts.columns.find(c => c.name === key);
454
- if(!col) throw "Unexpected missing col name";
455
-
456
- /**
457
- * Add utility functions for PostGIS data
458
- */
459
- let escapedVal: string = "";
460
- if(["geometry", "geography"].includes(col.udt_name) && row[key] && isPlainObject(row[key])){
461
-
462
- const basicFunc = (args: any[]) => {
463
- return args.map(arg => asValue(arg)).join(", ")
464
- }
465
- const basicFuncNames = ["ST_GeomFromText", "ST_Point", "ST_MakePoint", "ST_MakePointM", "ST_PointFromText", "ST_GeomFromEWKT", "ST_GeomFromGeoJSON"]
466
-
467
- const dataKeys = Object.keys(row[key]);
468
- const funcName = dataKeys[0];
469
- const funcExists = basicFuncNames.includes(funcName);
470
- const funcArgs = row[key]?.[funcName]
471
- if(dataKeys.length !== 1 || !funcExists || !Array.isArray(funcArgs)){
472
- throw `Expecting only one function key (${
473
- basicFuncNames.join(", ")}) \nwith an array of arguments \n within column (${key}) data but got: ${
474
- JSON.stringify(row[key])} \nExample: { geo_col: { ST_GeomFromText: ["POINT(-71.064544 42.28787)", 4326] } }`;
475
- }
476
- escapedVal = `${funcName}(${basicFunc(funcArgs)})`
477
-
478
- } else {
479
- /** Prevent pg-promise formatting jsonb */
480
- const colIsJSON = ["json", "jsonb"].includes(col.data_type);
481
- escapedVal = pgp.as.format(colIsJSON? "$1:json" : "$1", [row[key]])
482
- }
483
-
484
- /**
485
- * Cast to type to avoid array errors (they do not cast automatically)
486
- */
487
- escapedVal += `::${col.udt_name}`
488
-
489
- return {
490
- escapedCol: asName(key),
491
- escapedVal,
492
- }
493
- });
494
-
495
- }
496
-
497
- async getInsertQuery(data: any[], allowedCols: string[], dbTx: DBHandlerServer, validate: ValidateRow | undefined): Promise<string> {
498
- const res = (await Promise.all((Array.isArray(data)? data : [data]).map(async d => {
499
- const rowParts = await this.getRow(d, allowedCols, dbTx, validate);
500
- const select = rowParts.map(r => r.escapedCol).join(", "),
501
- values = rowParts.map(r => r.escapedVal).join(", ");
502
-
503
- return `INSERT INTO ${asName(this.opts.tableName)} (${select}) VALUES (${values})`;
504
- }))).join(";\n") + " ";
505
- return res;
506
- }
507
- async getUpdateQuery(data: any[], allowedCols: string[], dbTx: DBHandlerServer, validate: ValidateRow | undefined): Promise<string> {
508
- const res = (await Promise.all((Array.isArray(data)? data : [data]).map(async d => {
509
- const rowParts = await this.getRow(d, allowedCols, dbTx, validate);
510
- return `UPDATE ${asName(this.opts.tableName)} SET ` + rowParts.map(r => `${r.escapedCol} = ${r.escapedVal} `).join(",\n")
511
- }))).join(";\n") + " ";
512
- return res;
513
- }
514
- }
515
-
516
- export type ExistsFilterConfig = {
517
- key: string;
518
- f2: Filter;
519
- existType: EXISTS_KEY;
520
- tables: string[];
521
- isJoined: boolean;
522
- shortestJoin: boolean;
523
- };
524
-
525
- export class ViewHandler {
526
- db: DB;
527
- name: string;
528
- escapedName: string;
529
- columns: TableSchema["columns"];
530
- columnsForTypes: ColumnInfo[];
531
- column_names: string[];
532
- tableOrViewInfo: TableSchema;// TableOrViewInfo;
533
- colSet: ColSet;
534
- tsColumnDefs: string[] = [];
535
- joins: Join[];
536
- joinGraph?: Graph;
537
- joinPaths?: JoinPaths;
538
- dboBuilder: DboBuilder;
539
-
540
- t?: pgPromise.ITask<{}>;
541
- dbTX?: TableHandlers;
542
-
543
- is_view: boolean = true;
544
- filterDef: string = "";
545
-
546
- // pubSubManager: PubSubManager;
547
- is_media: boolean = false;
548
- constructor(db: DB, tableOrViewInfo: TableSchema, dboBuilder: DboBuilder, t?: pgPromise.ITask<{}>, dbTX?: TableHandlers, joinPaths?: JoinPaths){
549
- if(!db || !tableOrViewInfo) throw "";
550
-
551
- this.db = db;
552
- this.t = t;
553
- this.dbTX = dbTX;
554
- this.joinPaths = joinPaths;
555
- this.tableOrViewInfo = tableOrViewInfo;
556
- this.name = tableOrViewInfo.name;
557
- this.escapedName = asName(this.name);
558
- this.columns = tableOrViewInfo.columns;
559
-
560
- /* cols are sorted by name to reduce .d.ts schema rewrites */
561
- this.columnsForTypes = tableOrViewInfo.columns.slice(0).sort((a, b) => a.name.localeCompare(b.name));
562
-
563
- this.column_names = tableOrViewInfo.columns.map(c => c.name);
564
-
565
- // this.pubSubManager = pubSubManager;
566
- this.dboBuilder = dboBuilder;
567
- this.joins = this.dboBuilder.joins ?? [];
568
-
569
- // fix this
570
- // and also make hot schema reload over ws
571
- this.colSet = new ColSet(this.columns, this.name);
572
-
573
- const { $and: $and_key, $or: $or_key } = this.dboBuilder.prostgles.keywords;
574
-
575
- // this.tsDataName = snakify(this.name, true);
576
- // if(this.tsDataName === "T") this.tsDataName = this.tsDataName + "_";
577
- // this.tsDataDef = `export type ${this.tsDataName} = {\n`;
578
- this.columnsForTypes.map(({ name, udt_name, is_nullable }) => {
579
- this.tsColumnDefs.push(`${escapeTSNames(name)}?: ${postgresToTsType(udt_name) as string} ${is_nullable? " | null " : ""};`);
580
- });
581
- // this.tsDataDef += "};";
582
- // this.tsDataDef += "\n";
583
- // this.tsDataDef += `export type ${this.tsDataName}_Filter = ${this.tsDataName} | object | { ${JSON.stringify($and_key)}: (${this.tsDataName} | object)[] } | { ${JSON.stringify($or_key)}: (${this.tsDataName} | object)[] } `;
584
- // this.filterDef = ` ${this.tsDataName}_Filter `;
585
- // const filterDef = this.filterDef;
586
-
587
- // this.tsDboDefs = [
588
- // ` getColumns: () => Promise<any[]>;`,
589
- // ` find: (filter?: ${filterDef}, selectParams?: SelectParams) => Promise<Partial<${this.tsDataName} & { [x: string]: any }>[]>;`,
590
- // ` findOne: (filter?: ${filterDef}, selectParams?: SelectParams) => Promise<Partial<${this.tsDataName} & { [x: string]: any }>>;`,
591
- // ` subscribe: (filter: ${filterDef}, params: SelectParams, onData: (items: Partial<${this.tsDataName} & { [x: string]: any }>[]) => any) => Promise<{ unsubscribe: () => any }>;`,
592
- // ` subscribeOne: (filter: ${filterDef}, params: SelectParams, onData: (item: Partial<${this.tsDataName} & { [x: string]: any }>) => any) => Promise<{ unsubscribe: () => any }>;`,
593
- // ` count: (filter?: ${filterDef}) => Promise<number>;`
594
- // ];
595
- // this.makeDef();
596
- }
597
-
598
- // makeDef(){
599
- // this.tsDboName = `DBO_${snakify(this.name)}`;
600
- // this.tsDboDef = `export type ${this.tsDboName} = {\n ${this.tsDboDefs.join("\n")} \n};\n`;
601
- // }
602
-
603
- getRowHashSelect(allowedFields: FieldFilter, alias?: string, tableAlias?: string): string {
604
- let allowed_cols = this.column_names;
605
- if(allowedFields) allowed_cols = this.parseFieldFilter(allowedFields);
606
- return "md5(" +
607
- allowed_cols
608
- /* CTID not available in AFTER trigger */
609
- // .concat(this.is_view? [] : ["ctid"])
610
- .sort()
611
- .map(f => (tableAlias? (asName(tableAlias) + ".") : "") + asName(f))
612
- .map(f => `md5(coalesce(${f}::text, 'dd'))`)
613
- .join(" || ") +
614
- `)` + (alias? ` as ${asName(alias)}` : "");
615
- }
616
-
617
- async validateViewRules(args: {
618
- fields?: FieldFilter,
619
- filterFields?: FieldFilter,
620
- returningFields?: FieldFilter,
621
- forcedFilter?: AnyObject,
622
- dynamicFields?: UpdateRule["dynamicFields"],
623
- rule: "update" | "select" | "insert" | "delete"
624
- }){
625
- const {
626
- fields,
627
- filterFields,
628
- returningFields,
629
- forcedFilter,
630
- dynamicFields,
631
- rule,
632
- } = args;
633
-
634
- /* Safely test publish rules */
635
- if(fields) {
636
- try {
637
- const _fields = this.parseFieldFilter(fields);
638
- if(this.is_media && rule === "insert" && !_fields.includes("id")){
639
- throw "Must allow id insert for media table"
640
- }
641
- } catch(e){
642
- throw ` issue with publish.${this.name}.${rule}.fields: \nVALUE: ` + JSON.stringify(fields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
643
- }
644
- }
645
- if(filterFields) {
646
- try {
647
- this.parseFieldFilter(filterFields);
648
- } catch(e){
649
- throw ` issue with publish.${this.name}.${rule}.filterFields: \nVALUE: ` + JSON.stringify(filterFields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
650
- }
651
- }
652
- if(returningFields) {
653
- try {
654
- this.parseFieldFilter(returningFields);
655
- } catch(e){
656
- throw ` issue with publish.${this.name}.${rule}.returningFields: \nVALUE: ` + JSON.stringify(returningFields, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
657
- }
658
- }
659
- if(forcedFilter) {
660
- try {
661
- await this.find(forcedFilter, { limit: 0 });
662
- } catch(e){
663
- throw ` issue with publish.${this.name}.${rule}.forcedFilter: \nVALUE: ` + JSON.stringify(forcedFilter, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
664
- }
665
- }
666
- if(dynamicFields){
667
- for await(const dfieldRule of dynamicFields){
668
- try {
669
- const { fields, filter } = dfieldRule;
670
- this.parseFieldFilter(fields);
671
- await this.find(filter, { limit: 0 });
672
- } catch(e){
673
- throw ` issue with publish.${this.name}.${rule}.dynamicFields: \nVALUE: ` + JSON.stringify(dfieldRule, null, 2) + "\nERROR: " + JSON.stringify(e, null, 2);
674
- }
675
- }
676
- }
677
-
678
- return true;
679
- }
680
-
681
- getShortestJoin(table1: string, table2: string, startAlias: number, isInner: boolean = false): { query: string, toOne: boolean } {
682
- // let searchedTables = [], result;
683
- // while (!result && searchedTables.length <= this.joins.length * 2){
684
-
685
- // }
686
-
687
- const getJoinCondition = (on: Record<string, string>[], leftTable: string, rightTable: string) => {
688
- return on.map(cond => Object.keys(cond).map(lKey => `${leftTable}.${lKey} = ${rightTable}.${cond[lKey]}`).join("\nAND ")).join(" OR ")
689
- }
690
-
691
- let toOne = true,
692
- query = this.joins.map(({ tables, on, type }, i) => {
693
- if(type.split("-")[1] === "many"){
694
- toOne = false;
695
- }
696
- const tl = `tl${startAlias + i}`,
697
- tr = `tr${startAlias + i}`;
698
- return `FROM ${tables[0]} ${tl} ${isInner? "INNER" : "LEFT"} JOIN ${tables[1]} ${tr} ON ${getJoinCondition(on, tl, tr)}`;
699
- }).join("\n");
700
- return { query, toOne: false }
701
- }
702
-
703
- getJoins(source: string, target: string, path?: string[], checkTableConfig?: boolean): JoinInfo {
704
- let paths: JoinInfo["paths"] = [];
705
-
706
- if(!this.joinPaths) throw `${source} - ${target} Join info missing or dissallowed`;
707
-
708
- if(path && !path.length) throw `Empty join path ( $path ) specified for ${source} <-> ${target}`
709
-
710
- /* Find the join path between tables */
711
- if(checkTableConfig){
712
- const tableConfigJoinInfo = this.dboBuilder?.prostgles?.tableConfigurator?.getJoinInfo(source, target);
713
- if(tableConfigJoinInfo) return tableConfigJoinInfo;
714
- }
715
-
716
- let jp;
717
- if(!path){
718
- jp = this.joinPaths.find(j => j.t1 === source && j.t2 === target);
719
- } else {
720
- jp = {
721
- t1: source,
722
- t2: target,
723
- path
724
- }
725
- }
726
- /* Self join */
727
- if(source === target){
728
- const tableHandler = this.dboBuilder.tablesOrViews?.find(t => t.name === source);
729
- if(!tableHandler) throw `Table not found for joining ${source}`;
730
-
731
- const fcols = tableHandler.columns.filter(c => c.references?.some(({ ftable }) => ftable === this.name));
732
- if(fcols.length){
733
- throw "Self referencing not supported yet"
734
- // return {
735
- // paths: [{
736
- // source,
737
- // target,
738
- // table: target,
739
- // on: fcols.map(fc => fc.references!.some(({ fcols }) => fcols.map(fcol => [fc.name, fcol])))
740
- // }],
741
- // expectOne: false
742
- // }
743
- }
744
- }
745
- if(!jp || !this.joinPaths.find(j => path? j.path.join() === path.join() : j.t1 === source && j.t2 === target)){
746
- throw `Joining ${source} <-...-> ${target} dissallowed or missing`;
747
- }
748
-
749
- /* Make the join chain info excluding root table */
750
- paths = (path || jp.path).slice(1).map((t2, i, arr) => {
751
- const t1 = i === 0? source : arr[i-1];
752
-
753
- this.joins ??= this.dboBuilder.joins;
754
-
755
- /* Get join options */
756
- const jo = this.joins.find(j => j.tables.includes(t1) && j.tables.includes(t2));
757
- if(!jo) throw `Joining ${t1} <-> ${t2} dissallowed or missing`;;
758
-
759
- let on: [string, string][][] = [];
760
-
761
- jo.on.map(cond => {
762
- let condArr: [string, string][] = [];
763
- Object.keys(cond).map(leftKey => {
764
- const rightKey = cond[leftKey];
765
-
766
- /* Left table is joining on keys */
767
- if(jo.tables[0] === t1){
768
- condArr.push([leftKey, rightKey])
769
-
770
- /* Left table is joining on values */
771
- } else {
772
- condArr.push([rightKey, leftKey])
773
-
774
- }
775
- });
776
- on.push(condArr);
777
- })
778
-
779
-
780
- return {
781
- source,
782
- target,
783
- table: t2,
784
- on
785
- };
786
- });
787
- let expectOne = false;
788
- // paths.map(({ source, target, on }, i) => {
789
- // if(expectOne && on.length === 1){
790
- // const sourceCol = on[0][1];
791
- // const targetCol = on[0][0];
792
-
793
- // const sCol = this.dboBuilder.dbo[source].columns.find(c => c.name === sourceCol)
794
- // const tCol = this.dboBuilder.dbo[target].columns.find(c => c.name === targetCol)
795
- // console.log({ sourceCol, targetCol, sCol, source, tCol, target, on})
796
- // expectOne = sCol.is_pkey && tCol.is_pkey
797
- // }
798
- // })
799
- return {
800
- paths,
801
- expectOne
802
- };
803
- }
804
-
805
-
806
- checkFilter(filter: any){
807
- if(filter === null || filter && !isPojoObject(filter)) throw `invalid filter -> ${JSON.stringify(filter)} \nExpecting: undefined | {} | { field_name: "value" } | { field: { $gt: 22 } } ... `;
808
- }
809
-
810
- async getInfo(lang?: string, param2?: any, param3?: any, tableRules?: TableRule, localParams?: LocalParams): Promise<TInfo>{
811
- const p = this.getValidatedRules(tableRules, localParams);
812
- if(!p.getInfo) throw "Not allowed";
813
-
814
- let has_media: "one" | "many" | undefined = undefined;
815
-
816
- const mediaTable = this.dboBuilder.prostgles?.opts?.fileTable?.tableName;
817
-
818
- if(!this.is_media && mediaTable){
819
- const joinConf = this.dboBuilder.prostgles?.opts?.fileTable?.referencedTables?.[this.name]
820
- if(joinConf){
821
- has_media = typeof joinConf === "string"? joinConf : "one";
822
- } else {
823
- const jp = this.dboBuilder.joinPaths.find(jp => jp.t1 === this.name && jp.t2 === mediaTable);
824
- if(jp && jp.path.length <= 3){
825
- if(jp.path.length <= 2){
826
- has_media = "one"
827
- } else {
828
- await Promise.all(jp.path.map(async tableName => {
829
- const pkeyFcols = this?.dboBuilder?.dbo?.[tableName]?.columns?.filter(c => c.is_pkey).map(c => c.name);
830
- const cols = this?.dboBuilder?.dbo?.[tableName]?.columns?.filter(c => c?.references?.some(({ ftable }) => jp.path.includes(ftable)));
831
- if(cols && cols.length && has_media !== "many"){
832
- if(cols.some(c => !pkeyFcols?.includes(c.name))){
833
- has_media = "many"
834
- } else {
835
- has_media = "one"
836
- }
837
- }
838
- }));
839
- }
840
- }
841
- }
842
- }
843
-
844
- return {
845
- oid: this.tableOrViewInfo.oid,
846
- comment: this.tableOrViewInfo.comment,
847
- info: this.dboBuilder.prostgles?.tableConfigurator?.getTableInfo({ tableName: this.name, lang }),
848
- is_media: this.is_media, // this.name === this.dboBuilder.prostgles?.opts?.fileTable?.tableName
849
- is_view: this.is_view,
850
- has_media,
851
- media_table_name: mediaTable,
852
- dynamicRules: {
853
- update: Boolean(tableRules?.update?.dynamicFields?.length)
854
- }
855
- }
856
- }
857
-
858
- // TODO: fix renamed table trigger problem
859
-
860
- async getColumns(
861
- lang?: string,
862
- params?: { rule: "update", filter: AnyObject, data: AnyObject },
863
- _param3?: undefined,
864
- tableRules?: TableRule,
865
- localParams?: LocalParams
866
- ): Promise<ValidatedColumnInfo[]> {
867
-
868
- try {
869
- const p = this.getValidatedRules(tableRules, localParams);
870
-
871
- if(!p.getColumns) throw "Not allowed";
872
-
873
- // console.log("getColumns", this.name, this.columns.map(c => c.name))
874
-
875
-
876
- let dynamicUpdateFields: string[] | undefined;
877
- if(params && tableRules && "parseUpdateRules" in this && (this as TableHandler).parseUpdateRules){
878
- if(!isPlainObject(params) || !isPlainObject(params.data) || !isPlainObject(params.filter) || params.rule !== "update") {
879
- throw "params must be { rule: 'update', data, filter } but got: " + JSON.stringify(params);
880
- }
881
-
882
- if(!tableRules?.update){
883
- dynamicUpdateFields = [];
884
- } else {
885
- const { data, filter } = params;
886
- const updateRules = await (this as TableHandler).parseUpdateRules(filter, data, undefined, tableRules, localParams);
887
- dynamicUpdateFields = updateRules.fields;
888
- }
889
- }
890
-
891
- let columns = this.columns
892
- .filter(c => {
893
- const { insert, select, update } = p || {};
894
-
895
- return [
896
- ...(insert?.fields || []),
897
- ...(select?.fields || []),
898
- ...(update?.fields || []),
899
- ].includes(c.name)
900
- })
901
- .map(_c => {
902
- let c = { ..._c };
903
-
904
- let label = c.comment || capitalizeFirstLetter(c.name, " ");
905
-
906
- let select = c.privileges.some(p => p.privilege_type === "SELECT"),
907
- insert = c.privileges.some(p => p.privilege_type === "INSERT"),
908
- update = c.privileges.some(p => p.privilege_type === "UPDATE"),
909
- _delete = this.tableOrViewInfo.privileges.delete;// c.privileges.some(p => p.privilege_type === "DELETE");
910
-
911
- delete (c as any).privileges;
912
-
913
- const prostgles = this.dboBuilder?.prostgles;
914
- const fileConfig = prostgles.fileManager?.getColInfo({ colName: c.name, tableName: this.name });
915
-
916
- /** Do not allow updates to file table unless it's to delete fields */
917
- if(prostgles.fileManager?.config && prostgles.fileManager.tableName === this.name){
918
- update = false;
919
- }
920
-
921
- const nonOrderableUD_Types: PG_COLUMN_UDT_DATA_TYPE[] = [ ..._PG_geometric, "xml" as any];
922
-
923
- let result: ValidatedColumnInfo = {
924
- ...c,
925
- label,
926
- tsDataType: postgresToTsType(c.udt_name),
927
- insert: insert && Boolean(p.insert?.fields?.includes(c.name)) && tableRules?.insert?.forcedData?.[c.name] === undefined,
928
- select: select && Boolean(p.select?.fields?.includes(c.name)),
929
- orderBy: select && Boolean(p.select?.fields && p.select.orderByFields.includes(c.name)) && !nonOrderableUD_Types.includes(c.udt_name),
930
- filter: Boolean(p.select?.filterFields?.includes(c.name)),
931
- update: update && Boolean(p.update?.fields?.includes(c.name)) && tableRules?.update?.forcedData?.[c.name] === undefined,
932
- delete: _delete && Boolean(p.delete && p.delete.filterFields && p.delete.filterFields.includes(c.name)),
933
- ...(prostgles?.tableConfigurator?.getColInfo({ table: this.name, col: c.name, lang }) || {}),
934
- ...(fileConfig && { file: fileConfig })
935
- }
936
-
937
- if(dynamicUpdateFields){
938
- result.update = dynamicUpdateFields.includes(c.name);
939
- }
940
-
941
- return result;
942
- }).filter(c => c.select || c.update || c.delete || c.insert)
943
-
944
- //.sort((a, b) => a.ordinal_position - b.ordinal_position);
945
-
946
- // const tblInfo = await this.getInfo();
947
-
948
- // if(tblInfo && tblInfo.media_table_name && tblInfo.has_media){
949
- // const mediaRules = this.dboBuilder.dbo[tblInfo.media_table_name]?.
950
- // return columns.concat({
951
- // comment: "",
952
- // data_type: "file",
953
- // delete: false,
954
- // });
955
- // }
956
-
957
- return columns;
958
-
959
- } catch(e){
960
- throw parseError(e, `db.${this.name}.getColumns()`);
961
- // throw "Something went wrong in " + `db.${this.name}.getColumns()`;
962
- }
963
- }
964
-
965
- getValidatedRules(tableRules?: TableRule, localParams?: LocalParams): ValidatedTableRules {
966
-
967
- if(get(localParams, "socket") && !tableRules) {
968
- throw "INTERNAL ERROR: Unexpected case -> localParams && !tableRules";
969
- }
970
-
971
-
972
- /* Computed fields are allowed only if select is allowed */
973
- const allColumns: FieldSpec[] = this.column_names.slice(0).map(fieldName => ({
974
- type: "column",
975
- name: fieldName,
976
- getQuery: ({ tableAlias }) => asNameAlias(fieldName, tableAlias),
977
- selected: false
978
- } as FieldSpec)).concat(COMPUTED_FIELDS.map(c => ({
979
- type: c.type,
980
- name: c.name,
981
- getQuery: ({ tableAlias, allowedFields }) => c.getQuery({
982
- allowedFields,
983
- ctidField: undefined,
984
- allColumns: this.columns,
985
-
986
- /* CTID not available in AFTER trigger */
987
- // ctidField: this.is_view? undefined : "ctid",
988
- tableAlias
989
- }),
990
- selected: false
991
- })));
992
-
993
- if(tableRules){
994
- if(isEmpty(tableRules)) throw "INTERNAL ERROR: Unexpected case -> Empty table rules for " + this.name;
995
- const throwFieldsErr = (command: "select" | "update" | "delete" | "insert", fieldType: string = "fields") => {
996
- 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 }`;
997
- },
998
- getFirstSpecified = (...fieldParams: (FieldFilter | undefined)[]): string[] => {
999
- const firstValid = fieldParams.find(fp => fp !== undefined);
1000
- return this.parseFieldFilter(firstValid)
1001
- };
1002
-
1003
- let res: ValidatedTableRules = {
1004
- allColumns,
1005
- getColumns: tableRules?.getColumns ?? true,
1006
- getInfo: tableRules?.getColumns ?? true,
1007
- } as ValidatedTableRules;
1008
-
1009
- /* SELECT */
1010
- if(tableRules.select){
1011
- if(!tableRules.select.fields) return throwFieldsErr("select");
1012
-
1013
- let maxLimit = null;
1014
- if(tableRules.select.maxLimit !== undefined && tableRules.select.maxLimit !== maxLimit){
1015
- const ml = tableRules.select.maxLimit;
1016
- if(ml !== null && (!Number.isInteger(ml) || ml < 0)) throw ` Invalid publish.${this.name}.select.maxLimit -> expecting a positive integer OR null but got ` + ml;
1017
- maxLimit = ml;
1018
- }
1019
-
1020
- const fields = this.parseFieldFilter(tableRules.select.fields)
1021
- res.select = {
1022
- fields,
1023
- orderByFields: tableRules.select.orderByFields? this.parseFieldFilter(tableRules.select.orderByFields) : fields,
1024
- forcedFilter: { ...tableRules.select.forcedFilter },
1025
- filterFields: this.parseFieldFilter(tableRules.select.filterFields),
1026
- maxLimit
1027
- };
1028
- }
1029
-
1030
- /* UPDATE */
1031
- if(tableRules.update){
1032
- if(!tableRules.update.fields) return throwFieldsErr("update");
1033
-
1034
- res.update = {
1035
- fields: this.parseFieldFilter(tableRules.update.fields),
1036
- forcedData: { ...tableRules.update.forcedData },
1037
- forcedFilter: { ...tableRules.update.forcedFilter },
1038
- returningFields: getFirstSpecified(tableRules.update?.returningFields, tableRules?.select?.fields, tableRules.update.fields),
1039
- filterFields: this.parseFieldFilter(tableRules.update.filterFields)
1040
- }
1041
- }
1042
-
1043
- /* INSERT */
1044
- if(tableRules.insert){
1045
- if(!tableRules.insert.fields) return throwFieldsErr("insert");
1046
-
1047
- res.insert = {
1048
- fields: this.parseFieldFilter(tableRules.insert.fields),
1049
- forcedData: { ...tableRules.insert.forcedData },
1050
- returningFields: getFirstSpecified(tableRules.insert.returningFields, tableRules?.select?.fields, tableRules.insert.fields)
1051
- }
1052
- }
1053
-
1054
- /* DELETE */
1055
- if(tableRules.delete){
1056
- if(!tableRules.delete.filterFields) return throwFieldsErr("delete", "filterFields");
1057
-
1058
- res.delete = {
1059
- forcedFilter: { ...tableRules.delete.forcedFilter },
1060
- filterFields: this.parseFieldFilter(tableRules.delete.filterFields),
1061
- returningFields: getFirstSpecified(tableRules.delete.returningFields, tableRules?.select?.fields, tableRules.delete.filterFields)
1062
- }
1063
- }
1064
-
1065
- if(!tableRules.select && !tableRules.update && !tableRules.delete && !tableRules.insert){
1066
- if([null, false].includes(tableRules.getInfo as any)) res.getInfo = false;
1067
- if([null, false].includes(tableRules.getColumns as any)) res.getColumns = false;
1068
- }
1069
-
1070
- return res;
1071
- } else {
1072
- const all_cols = this.column_names.slice(0);
1073
- return {
1074
- allColumns,
1075
- getColumns: true,
1076
- getInfo: true,
1077
- select: {
1078
- fields: all_cols,
1079
- filterFields: all_cols,
1080
- orderByFields: all_cols,
1081
- forcedFilter: {},
1082
- maxLimit: null,
1083
- },
1084
- update: {
1085
- fields: all_cols,
1086
- filterFields: all_cols,
1087
- forcedFilter: {},
1088
- forcedData: {},
1089
- returningFields: all_cols
1090
- },
1091
- insert: {
1092
- fields: all_cols,
1093
- forcedData: {},
1094
- returningFields: all_cols
1095
- },
1096
- delete: {
1097
- filterFields: all_cols,
1098
- forcedFilter: {},
1099
- returningFields: all_cols
1100
- }
1101
- };
1102
-
1103
- }
1104
-
1105
- }
1106
-
1107
- async find(filter?: Filter, selectParams?: SelectParams , param3_unused?: undefined, tableRules?: TableRule, localParams?: LocalParams): Promise<any[]>{
1108
- try {
1109
- filter = filter || {};
1110
- const allowedReturnTypes: Array<SelectParams["returnType"]> = ["row", "value", "values", "statement"]
1111
- const { returnType } = selectParams || {};
1112
- if(returnType && !allowedReturnTypes.includes(returnType)){
1113
- throw `returnType (${returnType}) can only be ${allowedReturnTypes.join(" OR ")}`
1114
- }
1115
-
1116
- const { testRule = false, returnQuery = false } = localParams || {};
1117
-
1118
- if(testRule) return [];
1119
- if(selectParams){
1120
- const good_params: Array<keyof SelectParams> = ["select", "orderBy", "offset", "limit", "returnType", "groupBy"];
1121
- const bad_params = Object.keys(selectParams).filter(k => !good_params.includes(k as any));
1122
- if(bad_params && bad_params.length) throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
1123
- }
1124
-
1125
- /* Validate publish */
1126
- if(tableRules){
1127
-
1128
- let fields: FieldFilter,
1129
- filterFields: FieldFilter | undefined,
1130
- forcedFilter: AnyObject | undefined,
1131
- maxLimit: number | undefined | null;
1132
-
1133
- if(!tableRules.select) throw "select rules missing for " + this.name;
1134
- fields = tableRules.select.fields;
1135
- forcedFilter = tableRules.select.forcedFilter;
1136
- filterFields = tableRules.select.filterFields;
1137
- maxLimit = tableRules.select.maxLimit;
1138
-
1139
- if(<any>tableRules.select !== "*" && typeof tableRules.select !== "boolean" && !isPlainObject(tableRules.select)) throw `\nINVALID publish.${this.name}.select\nExpecting any of: "*" | { fields: "*" } | true | false`
1140
- if(!fields) throw ` invalid ${this.name}.select rule -> fields (required) setting missing.\nExpecting any of: "*" | { col_name: false } | { col1: true, col2: true }`;
1141
- if(maxLimit && !Number.isInteger(maxLimit)) throw ` invalid publish.${this.name}.select.maxLimit -> expecting integer but got ` + maxLimit;
1142
- }
1143
-
1144
- let q = await getNewQuery(this as unknown as TableHandler, filter, selectParams, param3_unused, tableRules, localParams, this.columns),
1145
- _query = makeSelectQuery(this as unknown as TableHandler, q, undefined, undefined, selectParams);
1146
- // console.log(_query, JSON.stringify(q, null, 2))
1147
- if(testRule){
1148
- try {
1149
- await this.db.any("EXPLAIN " + _query);
1150
- return [];
1151
- } catch(e) {
1152
- console.error(e);
1153
- throw `INTERNAL ERROR: Publish config is not valid for publish.${this.name}.select `
1154
- }
1155
- }
1156
-
1157
- if(returnQuery) return (_query as unknown as any[]);
1158
-
1159
- if(returnType === "statement"){
1160
- if(!(await canRunSQL(this.dboBuilder.prostgles, localParams))){
1161
- throw `Not allowed: {returnType: "statement"} requires sql privileges `
1162
- }
1163
- return _query as unknown as any[];
1164
- }
1165
-
1166
- if(["row", "value"].includes(returnType!)) {
1167
- return (this.t || this.db).oneOrNone(_query).then(data => {
1168
- return (data && returnType === "value")? Object.values(data)[0] : data;
1169
- }).catch(err => makeErr(err, localParams, this));
1170
- } else {
1171
- return (this.t || this.db).any(_query).then(data => {
1172
- if(returnType === "values"){
1173
- return data.map(d => Object.values(d)[0]);
1174
- }
1175
- return data;
1176
- }).catch(err => makeErr(err, localParams, this));
1177
- }
1178
-
1179
- } catch(e){
1180
- // console.trace(e)
1181
- if(localParams && localParams.testRule) throw e;
1182
- throw parseError(e, `dbo.${this.name}.find()`);
1183
- // throw { err: parseError(e), msg: `Issue with dbo.${this.name}.find()`, args: { filter, selectParams} };
1184
- }
1185
- }
1186
-
1187
- findOne(filter?: Filter, selectParams?: SelectParams, param3_unused?: undefined, table_rules?: TableRule, localParams?: LocalParams): Promise<any>{
1188
-
1189
- try {
1190
- const { select = "*", orderBy, offset = 0 } = selectParams || {};
1191
- if(selectParams){
1192
- const good_params = ["select", "orderBy", "offset"];
1193
- const bad_params = Object.keys(selectParams).filter(k => !good_params.includes(k));
1194
- if(bad_params && bad_params.length) throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
1195
- }
1196
- return this.find(filter, { select, orderBy, limit: 1, offset, returnType: "row" }, undefined, table_rules, localParams);
1197
- } catch(e){
1198
- if(localParams && localParams.testRule) throw e;
1199
- throw parseError(e, `Issue with dbo.${this.name}.findOne()`);
1200
- }
1201
- }
1202
-
1203
- async count(filter?: Filter, param2_unused?: undefined, param3_unused?: undefined, table_rules?: TableRule, localParams?: LocalParams): Promise<number>{
1204
- filter = filter || {};
1205
- try {
1206
- return await this.find(filter, { select: "", limit: 0 }, undefined, table_rules, localParams)
1207
- .then(async allowed => {
1208
- const { filterFields, forcedFilter } = get(table_rules, "select") || {};
1209
- const where = (await this.prepareWhere({ filter, forcedFilter, filterFields, addKeywords: true, localParams, tableRule: table_rules })).where;
1210
- let query = "SELECT COUNT(*) FROM " + this.escapedName + " " + where;
1211
- return (this.t || this.db).one(query, { _psqlWS_tableName: this.name }).then(({ count }) => +count);
1212
- });
1213
- } catch(e){
1214
- if(localParams && localParams.testRule) throw e;
1215
- throw parseError(e, `dbo.${this.name}.count()`)
1216
- }
1217
- }
1218
-
1219
- async size(filter?: Filter, selectParams?: SelectParams, param3_unused?: undefined, table_rules?: TableRule, localParams?: LocalParams): Promise<string>{
1220
- filter = filter || {};
1221
- try {
1222
- return await this.find(filter, { ...selectParams, limit: 2 }, undefined, table_rules, localParams)
1223
- .then(async _allowed => {
1224
- // let rules: TableRule = table_rules || {};
1225
- // rules.select.maxLimit = Number.MAX_SAFE_INTEGER;
1226
- // rules.select.fields = rules.select.fields || "*";
1227
-
1228
- const q: string = await this.find(
1229
- filter, { ...selectParams, limit: selectParams?.limit ?? Number.MAX_SAFE_INTEGER },
1230
- undefined,
1231
- table_rules,
1232
- { ...localParams, returnQuery: true }
1233
- ) as any;
1234
- const query = `
1235
- SELECT sum(pg_column_size((prgl_size_query.*))) as size
1236
- FROM (
1237
- ${q}
1238
- ) prgl_size_query
1239
- `;
1240
-
1241
- return (this.t || this.db).one(query, { _psqlWS_tableName: this.name }).then(({ size }) => size || '0');
1242
- });
1243
- } catch(e){
1244
- if(localParams && localParams.testRule) throw e;
1245
- throw parseError(e, `dbo.${this.name}.size()`);
1246
- }
1247
- }
1248
-
1249
- getAllowedSelectFields(selectParams: FieldFilter = "*", allowed_cols: FieldFilter, allow_empty: boolean = true): string[] {
1250
- let all_columns = this.column_names.slice(0),
1251
- allowedFields = all_columns.slice(0),
1252
- resultFields: string[] = [];
1253
-
1254
- if(selectParams){
1255
- resultFields = this.parseFieldFilter(selectParams, allow_empty);
1256
- }
1257
- if(allowed_cols){
1258
- allowedFields = this.parseFieldFilter(allowed_cols, allow_empty);
1259
- }
1260
- let col_names = (resultFields || []).filter(f => !allowedFields || allowedFields.includes(f));
1261
-
1262
- /* Maintain allowed cols order */
1263
- if(selectParams === "*" && allowedFields && allowedFields.length) col_names = allowedFields;
1264
-
1265
- return col_names;
1266
- }
1267
-
1268
- prepareColumnSet(selectParams: FieldFilter = "*", allowed_cols: FieldFilter, allow_empty: boolean = true, onlyNames: boolean = true): string | pgPromise.ColumnSet {
1269
- let all_columns = this.column_names.slice(0);
1270
- let col_names = this.getAllowedSelectFields(selectParams, all_columns, allow_empty);
1271
- /** Ensure order is maintained */
1272
- if(selectParams && Array.isArray(selectParams) && typeof selectParams[0] === "string"){
1273
- col_names = col_names.sort((a, b) => selectParams.indexOf(a) - selectParams.indexOf(b))
1274
- }
1275
- try{
1276
- let colSet = new pgp.helpers.ColumnSet(col_names);
1277
- return onlyNames? colSet.names : colSet;
1278
- } catch (e) {
1279
- throw e;
1280
- }
1281
- }
1282
-
1283
- prepareSelect(selectParams: FieldFilter = "*", allowed_cols: FieldFilter, allow_empty: boolean = true, tableAlias?: string): string {
1284
- if(tableAlias){
1285
- let cs = <pgPromise.ColumnSet>this.prepareColumnSet(selectParams, allowed_cols, true, false);
1286
- return cs.columns.map(col => `${this.escapedName}.${asName(col.name)}` ).join(", ");
1287
- } else {
1288
- return <string>this.prepareColumnSet(selectParams, allowed_cols, true, true);
1289
- }
1290
- }
1291
-
1292
- async prepareHaving(params: {
1293
- having: Filter;
1294
- select: SelectItem[];
1295
- forcedFilter: object;
1296
- filterFields: FieldFilter;
1297
- addKeywords?: boolean;
1298
- tableAlias?: string,
1299
- localParams: LocalParams,
1300
- tableRule: TableRule
1301
- }): Promise<string> {
1302
- return ""
1303
- }
1304
-
1305
- /**
1306
- * Parses group or simple filter
1307
- */
1308
- async prepareWhere(params: {
1309
- filter?: Filter;
1310
- select?: SelectItem[];
1311
- forcedFilter?: AnyObject;
1312
- filterFields?: FieldFilter;
1313
- addKeywords?: boolean;
1314
- tableAlias?: string,
1315
- localParams: LocalParams | undefined,
1316
- tableRule: TableRule | undefined
1317
- }): Promise<{ where: string; filter: AnyObject; }>
1318
- {
1319
- const { filter, select, forcedFilter, filterFields: ff, addKeywords = true, tableAlias, localParams, tableRule } = params;
1320
- const { $and: $and_key, $or: $or_key } = this.dboBuilder.prostgles.keywords;
1321
-
1322
- let filterFields = ff;
1323
- /* Local update allow all. TODO -> FIX THIS */
1324
- if(!ff && !tableRule) filterFields = "*";
1325
-
1326
- const parseFullFilter = async (f: any, parentFilter: any = null, isForcedFilterBypass: boolean): Promise<string> => {
1327
- if(!f) throw "Invalid/missing group filter provided";
1328
- let result = "";
1329
- let keys = getKeys(f);
1330
- if(!keys.length) return result;
1331
- if((keys.includes($and_key) || keys.includes($or_key))){
1332
- if(keys.length > 1) throw `\ngroup filter must contain only one array property. e.g.: { ${$and_key}: [...] } OR { ${$or_key}: [...] } `;
1333
- if(parentFilter && Object.keys(parentFilter).includes("")) throw "group filter ($and/$or) can only be placed at the root or within another group filter";
1334
- }
1335
-
1336
- const { [$and_key]: $and, [$or_key]: $or } = f,
1337
- group: AnyObject[] = $and || $or;
1338
-
1339
- if(group && group.length){
1340
- const operand = $and? " AND " : " OR ";
1341
- let conditions = (await Promise.all(group.map(async gf => await parseFullFilter(gf, group, isForcedFilterBypass)))).filter(c => c);
1342
- if(conditions && conditions.length){
1343
- if(conditions.length === 1) return conditions.join(operand);
1344
- else return ` ( ${conditions.sort().join(operand)} ) `;
1345
- }
1346
- } else if(!group) {
1347
-
1348
- /** forcedFilters do not get checked against publish and are treated as server-side requests */
1349
- result = await this.getCondition({
1350
- filter: { ...f },
1351
- select,
1352
- allowed_colnames: isForcedFilterBypass? this.column_names.slice(0) : this.parseFieldFilter(filterFields),
1353
- tableAlias,
1354
- localParams: isForcedFilterBypass? undefined : localParams,
1355
- tableRules: isForcedFilterBypass? undefined : tableRule
1356
- });
1357
- }
1358
- return result;
1359
- }
1360
-
1361
- if(!isPlainObject(filter)) throw "\nInvalid filter\nExpecting an object but got -> " + JSON.stringify(filter);
1362
-
1363
-
1364
- /* A forced filter condition will not check if the existsJoined filter tables have been published */
1365
- const forcedFilterCond = forcedFilter? await parseFullFilter(forcedFilter, null, true) : undefined;
1366
- const filterCond = await parseFullFilter(filter, null, false);
1367
- let cond = [
1368
- forcedFilterCond, filterCond
1369
- ].filter(c => c).join(" AND ");
1370
-
1371
- const finalFilter = forcedFilter? {
1372
- [$and_key]: [forcedFilter, filter].filter(isDefined)
1373
- } : { ...filter };
1374
-
1375
- if(cond && addKeywords) cond = "WHERE " + cond;
1376
- return { where: cond || "", filter: finalFilter };
1377
- }
1378
-
1379
- async prepareExistCondition(eConfig: ExistsFilterConfig, localParams: LocalParams | undefined): Promise<string> {
1380
- let res = "";
1381
- const thisTable = this.name;
1382
- const isNotExists = ["$notExists", "$notExistsJoined"].includes(eConfig.existType);
1383
-
1384
- let { f2, tables, isJoined } = eConfig;
1385
- let t2 = tables[tables.length - 1];
1386
-
1387
- tables.forEach(t => {
1388
- if(!this.dboBuilder.dbo[t]) throw { stack: ["prepareExistCondition()"], message: `Invalid or dissallowed table: ${t}` };
1389
- });
1390
-
1391
-
1392
- /* Nested $exists not allowed */
1393
- if(f2 && Object.keys(f2).find(fk => EXISTS_KEYS.includes(fk as EXISTS_KEY))){
1394
- throw { stack: ["prepareExistCondition()"], message: "Nested exists dissallowed" } ;
1395
- }
1396
-
1397
- const makeTableChain = (finalFilter: string) => {
1398
-
1399
- let joinPaths: JoinInfo["paths"] = [];
1400
- let expectOne = true;
1401
- tables.map((t2, depth) => {
1402
- let t1 = depth? tables[depth - 1] : thisTable;
1403
- let exactPaths: string[] | undefined = [t1, t2];
1404
-
1405
- if(!depth && eConfig.shortestJoin) exactPaths = undefined;
1406
- const jinf= this.getJoins(t1, t2, exactPaths, true);
1407
- expectOne = Boolean(expectOne && jinf.expectOne)
1408
- joinPaths = joinPaths.concat(jinf.paths);
1409
- });
1410
-
1411
- let r = makeJoin({ paths: joinPaths, expectOne }, 0);
1412
- return r;
1413
-
1414
- function makeJoin(joinInfo: JoinInfo, ji: number) {
1415
- const { paths } = joinInfo;
1416
- const jp = paths[ji];
1417
-
1418
- // let prevTable = ji? paths[ji - 1].table : jp.source;
1419
- let table = paths[ji].table;
1420
- let tableAlias = asName(ji < paths.length - 1? `jd${ji}` : table);
1421
- let prevTableAlias = asName(ji? `jd${ji - 1}` : thisTable);
1422
-
1423
- let cond = `${jp.on.map(c => {
1424
- return c.map(([c1, c2]) => `${prevTableAlias}.${asName(c1)} = ${tableAlias}.${asName(c2)}` ).join(" AND ")
1425
- }).join("\n OR ")
1426
- }`;
1427
-
1428
- let j = `SELECT 1 \n` +
1429
- `FROM ${asName(table)} ${tableAlias} \n` +
1430
- `WHERE ${cond} \n`;//
1431
- if(
1432
- ji === paths.length - 1 &&
1433
- finalFilter
1434
- ) {
1435
- j += `AND ${finalFilter} \n`;
1436
- }
1437
-
1438
- const indent = (a: any, b: any) => a;
1439
-
1440
- if(ji < paths.length - 1){
1441
- j += `AND ${makeJoin(joinInfo, ji + 1)} \n`
1442
- }
1443
-
1444
- j = indent(j, ji + 1);
1445
-
1446
- let res = `${isNotExists? " NOT " : " "} EXISTS ( \n` +
1447
- j +
1448
- `) \n`;
1449
- return indent(res, ji);
1450
- }
1451
-
1452
- }
1453
-
1454
- let finalWhere = "";
1455
-
1456
- let t2Rules: TableRule | undefined = undefined,
1457
- forcedFilter: AnyObject | undefined,
1458
- filterFields: FieldFilter | undefined,
1459
- tableAlias;
1460
-
1461
- /* Check if allowed to view data - forcedFilters will bypass this check through isForcedFilterBypass */
1462
- if(localParams?.isRemoteRequest && (!localParams?.socket && !localParams?.httpReq)) throw "Unexpected: localParams isRemoteRequest and missing socket/httpReq: ";
1463
- if(localParams && (localParams.socket || localParams.httpReq) && this.dboBuilder.publishParser){
1464
-
1465
- t2Rules = await this.dboBuilder.publishParser.getValidatedRequestRuleWusr({ tableName: t2, command: "find", localParams }) as TableRule;
1466
- if(!t2Rules || !t2Rules.select) throw "Dissallowed";
1467
- ({ forcedFilter, filterFields } = t2Rules.select);
1468
- }
1469
-
1470
- try {
1471
- finalWhere = (await (this.dboBuilder.dbo[t2] as TableHandler).prepareWhere({
1472
- filter: f2,
1473
- forcedFilter,
1474
- filterFields,
1475
- addKeywords: false,
1476
- tableAlias,
1477
- localParams,
1478
- tableRule: t2Rules
1479
- })).where
1480
- } catch(err) {
1481
- // console.trace(err)
1482
- throw err
1483
- }
1484
-
1485
- if(!isJoined){
1486
- res = `${isNotExists? " NOT " : " "} EXISTS (SELECT 1 \nFROM ${asName(t2)} \n${finalWhere? `WHERE ${finalWhere}` : ""}) `
1487
- } else {
1488
- res = makeTableChain(finalWhere);
1489
- }
1490
- return res;
1491
- }
1492
-
1493
- /**
1494
- * parses a single filter
1495
- * @example
1496
- * { fff: 2 } => "fff" = 2
1497
- * { fff: { $ilike: 'abc' } } => "fff" ilike 'abc'
1498
- */
1499
- async getCondition(params: { filter: any, select?: SelectItem[], allowed_colnames: string[], tableAlias?: string, localParams?: LocalParams, tableRules?: TableRule }){
1500
- const { filter, select, allowed_colnames, tableAlias, localParams, tableRules } = params;
1501
-
1502
-
1503
- let data = { ... (filter as any) } as any ;
1504
-
1505
- /* Exists join filter */
1506
- const ERR = "Invalid exists filter. \nExpecting somethibng like: { $exists: { tableName.tableName2: Filter } } | { $exists: { \"**.tableName3\": Filter } }\n"
1507
- const SP_WILDCARD = "**";
1508
- let existsKeys: ExistsFilterConfig[] = Object.keys(data)
1509
- .filter(k => EXISTS_KEYS.includes(k as EXISTS_KEY) && Object.keys(data[k] || {}).length)
1510
- .map(key => {
1511
-
1512
- const isJoined = EXISTS_KEYS.slice(-2).includes(key as EXISTS_KEY);
1513
- let firstKey = Object.keys(data[key])[0],
1514
- tables = firstKey.split("."),
1515
- f2 = data[key][firstKey],
1516
- shortestJoin = false;
1517
-
1518
- if(!isJoined){
1519
- if(tables.length !== 1) throw "Expecting single table in exists filter. Example: { $exists: { tableName: Filter } }"
1520
- } else {
1521
- /* First part can be the ** param meaning shortest join. Will be overriden by anything in tableConfig */
1522
-
1523
- if(!tables.length) throw ERR + "\nBut got: " + data[key];
1524
-
1525
- if(tables[0] === SP_WILDCARD){
1526
- tables = tables.slice(1);
1527
- shortestJoin = true;
1528
- }
1529
- }
1530
-
1531
- return {
1532
- key,
1533
- existType: key as EXISTS_KEY,
1534
- isJoined,
1535
- shortestJoin,
1536
- f2,
1537
- tables
1538
- }
1539
- });
1540
- /* Exists with exact path */
1541
- // Object.keys(data).map(k => {
1542
- // let isthis = isPlainObject(data[k]) && !this.column_names.includes(k) && !k.split(".").find(kt => !this.dboBuilder.dbo[kt]);
1543
- // if(isthis) {
1544
- // existsKeys.push({
1545
- // key: k,
1546
- // notJoined: false,
1547
- // exactPaths: k.split(".")
1548
- // });
1549
- // }
1550
- // });
1551
- let funcConds: string[] = [];
1552
- const funcFilterkeys = FILTER_FUNCS.filter(f => {
1553
- return f.name in data;
1554
- });
1555
- funcFilterkeys.map(f => {
1556
- const funcArgs = data[f.name];
1557
- if(!Array.isArray(funcArgs)) throw `A function filter must contain an array. E.g: { $funcFilterName: ["col1"] } \n but got: ${JSON.stringify(pickKeys(data, [f.name]))} `;
1558
- const fields = this.parseFieldFilter(f.getFields(funcArgs), true, allowed_colnames);
1559
-
1560
- const dissallowedCols = fields.filter(fname => !allowed_colnames.includes(fname))
1561
- if(dissallowedCols.length){
1562
- throw `Invalid/disallowed columns found in function filter: ${dissallowedCols}`
1563
- }
1564
- funcConds.push(f.getQuery({ args: funcArgs, allColumns: this.columns, allowedFields: allowed_colnames, tableAlias }));
1565
- })
1566
-
1567
-
1568
- let existsCond = "";
1569
- if(existsKeys.length){
1570
- existsCond = (await Promise.all(existsKeys.map(async k => await this.prepareExistCondition(k, localParams)))).join(" AND ");
1571
- }
1572
-
1573
- /* Computed field queries */
1574
- const p = this.getValidatedRules(tableRules, localParams);
1575
- const computedFields = p.allColumns.filter(c => c.type === "computed" );
1576
- let computedColConditions: string[] = [];
1577
- Object.keys(data || {}).map(key => {
1578
- const compCol = computedFields.find(cf => cf.name === key);
1579
- if(compCol){
1580
- computedColConditions.push(
1581
- compCol.getQuery({
1582
- tableAlias,
1583
- allowedFields: p.select.fields,
1584
- allColumns: this.columns,
1585
-
1586
- /* CTID not available in AFTER trigger */
1587
- // ctidField: this.is_view? undefined : "ctid"
1588
-
1589
- ctidField: undefined,
1590
- }) + ` = ${pgp.as.format("$1", [ (data as any)[key] ] )}`
1591
- );
1592
- delete (data as any)[key];
1593
- }
1594
- });
1595
-
1596
- let allowedSelect: SelectItem[] = [];
1597
- /* Select aliases take precedence over col names. This is to ensure filters work correctly and even on computed cols*/
1598
- if(select){
1599
- /* Allow filtering by selected fields/funcs */
1600
- allowedSelect = select.filter(s => {
1601
- /* */
1602
- if(["function", "computed", "column"].includes(s.type)){
1603
- if(s.type !== "column" || allowed_colnames.includes(s.alias)){
1604
- return true;
1605
- }
1606
- }
1607
- return false;
1608
- })
1609
- }
1610
-
1611
- /* Add remaining allowed fields */
1612
- allowedSelect = allowedSelect.concat(
1613
- p.allColumns.filter(c =>
1614
- allowed_colnames.includes(c.name) &&
1615
- !allowedSelect.find(s => s.alias === c.name)
1616
- ).map(f => ({
1617
- type: f.type,
1618
- alias: f.name,
1619
- getQuery: (tableAlias) => f.getQuery({
1620
- tableAlias,
1621
- allColumns: this.columns,
1622
- allowedFields: allowed_colnames
1623
- }),
1624
- selected: false,
1625
- getFields: () => [f.name],
1626
- column_udt_type: f.type === "column"? this.columns.find(c => c.name === f.name)?.udt_name : undefined
1627
- }))
1628
- );
1629
-
1630
- /* Parse complex filters
1631
- { $filter: [{ $func: [...] }, "=", value | { $func: [..] }] }
1632
- */
1633
- const complexFilters: string[] = [];
1634
- const complexFilterKey = "$filter";
1635
- const allowedComparators = [">", "<", "=", "<=", ">=", "<>", "!="]
1636
- if(complexFilterKey in data){
1637
- const getFuncQuery = (funcData: any): string => {
1638
- const { funcName, args } = parseFunctionObject(funcData);
1639
- const funcDef = parseFunction({ func: funcName, args, functions: FUNCTIONS, allowedFields: allowed_colnames });
1640
- return funcDef.getQuery({ args, tableAlias, allColumns: this.columns, allowedFields: allowed_colnames });
1641
- }
1642
-
1643
- const complexFilter = data[complexFilterKey];
1644
- if(!Array.isArray(complexFilter)) throw `Invalid $filter. Must contain an array of at least element but got: ${JSON.stringify(complexFilter)} `
1645
- const leftFilter = complexFilter[0];
1646
- const comparator = complexFilter[1];
1647
- const rightFilterOrValue = complexFilter[2];
1648
- const leftVal = getFuncQuery(leftFilter);
1649
- let result = leftVal;
1650
- if(comparator){
1651
- if(!allowedComparators.includes(comparator)) throw `Invalid $filter. comparator ${JSON.stringify(comparator)} is not valid. Expecting one of: ${allowedComparators}`
1652
- if(!rightFilterOrValue) throw "Invalid $filter. Expecting a value or function after the comparator";
1653
- const rightVal = isObject(rightFilterOrValue)? getFuncQuery(rightFilterOrValue) : asValue(rightFilterOrValue);
1654
- if(leftVal === rightVal) throw "Invalid $filter. Cannot compare two identical function signatures: " + JSON.stringify(leftFilter);
1655
- result += ` ${comparator} ${rightVal}`;
1656
- }
1657
- complexFilters.push(result);
1658
- }
1659
-
1660
-
1661
- /* Parse join filters
1662
- { $joinFilter: { $ST_DWithin: [table.col, foreignTable.col, distance] }
1663
- will make an exists filter
1664
- */
1665
-
1666
- 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));
1667
- // if(allowed_colnames){
1668
- // const aliasedColumns = (select || []).filter(s =>
1669
- // ["function", "computed", "column"].includes(s.type) && allowed_colnames.includes(s.alias) ||
1670
- // s.getFields().find(f => allowed_colnames.includes(f))
1671
- // ).map(s => s.alias);
1672
- // const validCols = [...allowed_colnames, ...aliasedColumns];
1673
-
1674
- // }
1675
- const validFieldNames = allowedSelect.map(s => s.alias);
1676
- const invalidColumn = filterKeys
1677
- .find(fName => !validFieldNames.find(c =>
1678
- c === fName ||
1679
- (
1680
- fName.startsWith(c) && (
1681
- fName.slice(c.length).includes("->") ||
1682
- fName.slice(c.length).includes(".")
1683
- )
1684
- )
1685
- ));
1686
-
1687
- if(invalidColumn){
1688
- 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(", ")}`;
1689
- }
1690
-
1691
- /* TODO: Allow filter funcs */
1692
- // const singleFuncs = FUNCTIONS.filter(f => f.singleColArg);
1693
-
1694
- const f = pickKeys(data, filterKeys);
1695
- const q = parseFilterItem({
1696
- filter: f,
1697
- tableAlias,
1698
- pgp,
1699
- select: allowedSelect
1700
- });
1701
-
1702
- let templates: string[] = [q].filter(q=>q);
1703
-
1704
- if(existsCond) templates.push(existsCond);
1705
- templates = templates.concat(funcConds);
1706
- templates = templates.concat(computedColConditions);
1707
- templates = templates.concat(complexFilters);
1708
-
1709
- /* sorted to ensure duplicate subscription channels are not created due to different condition order */
1710
- return templates.sort()
1711
- .join(" AND \n");
1712
-
1713
- }
1714
-
1715
- /* This relates only to SELECT */
1716
- prepareSortItems(orderBy: OrderBy | undefined, allowed_cols: string[], tableAlias: string | undefined, select: SelectItemValidated[]): SortItem[] {
1717
-
1718
- const throwErr = () => {
1719
- throw "\nInvalid orderBy option -> " + JSON.stringify(orderBy) +
1720
- "Expecting: \
1721
- { key2: false, key1: true } \
1722
- { key1: 1, key2: -1 } \
1723
- [{ key1: true }, { key2: false }] \
1724
- [{ key: 'colName', asc: true, nulls: 'first', nullEmpty: true }]"
1725
- },
1726
- parseOrderObj = (orderBy: any, expectOne = false): { key: string, asc: boolean, nulls?: "first" | "last", nullEmpty?: boolean }[] => {
1727
- if(!isPlainObject(orderBy)) return throwErr();
1728
-
1729
- const keys = Object.keys(orderBy);
1730
- if(keys.length && keys.find(k => ["key", "asc", "nulls", "nullEmpty"].includes(k))){
1731
- const { key, asc, nulls, nullEmpty = false } = orderBy;
1732
- if(
1733
- !["string"].includes(typeof key) ||
1734
- !["boolean"].includes(typeof asc) ||
1735
- !["first", "last", undefined, null].includes(nulls) ||
1736
- !["boolean"].includes(typeof nullEmpty)
1737
- ){
1738
- throw `Invalid orderBy option (${JSON.stringify(orderBy, null, 2)}) \n
1739
- Expecting { key: string, asc?: boolean, nulls?: 'first' | 'last' | null | undefined, nullEmpty?: boolean } `
1740
- }
1741
- return [{ key, asc, nulls, nullEmpty }];
1742
- }
1743
-
1744
- if(expectOne && keys.length > 1) {
1745
- throw "\nInvalid orderBy " + JSON.stringify(orderBy) +
1746
- "\nEach orderBy array element cannot have more than one key";
1747
- }
1748
- /* { key2: true, key1: false } */
1749
- if(!Object.values(orderBy).find(v => ![true, false].includes(<any>v))){
1750
- return keys.map(key => ({ key, asc: Boolean(orderBy[key]) }))
1751
-
1752
- /* { key2: -1, key1: 1 } */
1753
- } else if(!Object.values(orderBy).find(v => ![-1,1].includes(<any>v))){
1754
- return keys.map(key => ({ key, asc: orderBy[key] === 1 }))
1755
-
1756
- /* { key2: "asc", key1: "desc" } */
1757
- } else if(!Object.values(orderBy).find(v => !["asc", "desc"].includes(<any>v))){
1758
- return keys.map(key => ({ key, asc: orderBy[key] === "asc" }))
1759
- } else return throwErr();
1760
- };
1761
-
1762
- if(!orderBy) return [];
1763
-
1764
- let _ob: { key: string, asc: boolean, nulls?: "first" | "last", nullEmpty?: boolean }[] = [];
1765
- if(isPlainObject(orderBy)){
1766
- _ob = parseOrderObj(orderBy);
1767
- } else if(typeof orderBy === "string"){
1768
- /* string */
1769
- _ob = [{ key: orderBy, asc: true }];
1770
- } else if(Array.isArray(orderBy)){
1771
-
1772
- /* Order by is formed of a list of ascending field names */
1773
- let _orderBy = (orderBy as any[]);
1774
- if(_orderBy && !_orderBy.find(v => typeof v !== "string")){
1775
- /* [string] */
1776
- _ob = _orderBy.map(key => ({ key, asc: true }));
1777
- } else if(_orderBy.find(v => isPlainObject(v) && Object.keys(v).length)) {
1778
- _ob = _orderBy.map(v => parseOrderObj(v, true)[0]);
1779
- } else return throwErr();
1780
- } else return throwErr();
1781
-
1782
- if(!_ob || !_ob.length) return [];
1783
-
1784
- const validatedAggAliases = select.filter(s =>
1785
- s.type !== "joinedColumn" &&
1786
- (!s.fields.length || s.fields.every(f => allowed_cols.includes(f)))
1787
- ).map(s => s.alias)
1788
-
1789
- let bad_param = _ob.find(({ key }) =>
1790
- !(validatedAggAliases || []).includes(key) &&
1791
- !allowed_cols.includes(key)
1792
- );
1793
- if(!bad_param){
1794
- const selectedAliases = select.filter(s => s.selected).map(s => s.alias);
1795
- // return (excludeOrder? "" : " ORDER BY ") + (_ob.map(({ key, asc, nulls, nullEmpty = false }) => {
1796
- return _ob.map(({ key, asc, nulls, nullEmpty = false }) => {
1797
-
1798
- /* Order by column index when possible to bypass name collision when ordering by a computed column.
1799
- (Postgres will sort by existing columns wheundefined possible)
1800
- */
1801
- const orderType = asc? " ASC " : " DESC ";
1802
- const index = selectedAliases.indexOf(key) + 1;
1803
- const nullOrder = nulls? ` NULLS ${nulls === "first"? " FIRST " : " LAST "}` : "";
1804
- let colKey = (index > 0 && !nullEmpty)? index : [tableAlias, key].filter(isDefined).map(asName).join(".");
1805
- if(nullEmpty){
1806
- colKey = `nullif(trim(${colKey}::text), '')`
1807
- }
1808
-
1809
- const res = `${colKey} ${orderType} ${nullOrder}`;
1810
-
1811
- if(typeof colKey === "number"){
1812
- return {
1813
- asc,
1814
- fieldPosition: colKey
1815
- }
1816
- }
1817
-
1818
- return {
1819
- fieldQuery: colKey,
1820
- asc,
1821
- }
1822
- })
1823
- } else {
1824
- throw "Invalid/disallowed orderBy fields or params: " + bad_param.key;
1825
- }
1826
- }
1827
-
1828
- /* This relates only to SELECT */
1829
- prepareLimitQuery(limit = 1000, p: ValidatedTableRules): number {
1830
-
1831
- if(limit !== undefined && limit !== null && !Number.isInteger(limit)){
1832
- throw "Unexpected LIMIT. Must be null or an integer";
1833
- }
1834
-
1835
- let _limit = limit;
1836
-
1837
- // if(_limit === undefined && p.select.maxLimit === null){
1838
- // _limit = 1000;
1839
-
1840
- /* If no limit then set as the lesser of (100, maxLimit) */
1841
- // } else
1842
- if(_limit !== null && !Number.isInteger(_limit) && p.select.maxLimit !== null){
1843
- _limit = [100, p.select.maxLimit].filter(Number.isInteger).sort((a, b) => a - b)[0];
1844
- } else {
1845
-
1846
- /* If a limit higher than maxLimit specified throw error */
1847
- if(Number.isInteger(p.select.maxLimit) && _limit > p.select.maxLimit!){
1848
- throw `Unexpected LIMIT ${_limit}. Must be less than the published maxLimit: ` + p.select.maxLimit;
1849
- }
1850
- }
1851
-
1852
-
1853
- return _limit;
1854
- }
1855
-
1856
- /* This relates only to SELECT */
1857
- prepareOffsetQuery(offset?: number): number{
1858
- if(Number.isInteger(offset)){
1859
- return offset!;
1860
- }
1861
-
1862
- return 0;
1863
- }
1864
-
1865
-
1866
- intersectColumns(allowedFields: FieldFilter, dissallowedFields: FieldFilter, fixIssues: boolean = false): string[] {
1867
- let result: string[] = [];
1868
- if(allowedFields){
1869
- result = this.parseFieldFilter(allowedFields);
1870
- }
1871
- if(dissallowedFields){
1872
- const _dissalowed = this.parseFieldFilter(dissallowedFields);
1873
-
1874
- if(!fixIssues) {
1875
-
1876
- throw `dissallowed/invalid field found for ${this.name}: `
1877
- }
1878
- result = result.filter(key => !_dissalowed.includes(key));
1879
- }
1880
-
1881
- return result;
1882
- }
1883
-
1884
- /**
1885
- * Prepare and validate field object:
1886
- * @example ({ item_id: 1 }, { user_id: 32 }) => { item_id: 1, user_id: 32 }
1887
- * OR
1888
- * ({ a: 1 }, { b: 32 }, ["c", "d"]) => throw "a field is not allowed"
1889
- * @param {Object} obj - initial data
1890
- * @param {Object} forcedData - set/override property
1891
- * @param {string[]} allowed_cols - allowed columns (excluding forcedData) from table rules
1892
- */
1893
- prepareFieldValues(obj: Record<string, any> = {}, forcedData: object = {}, allowed_cols: FieldFilter | undefined, fixIssues = false): AnyObject {
1894
- let column_names = this.column_names.slice(0);
1895
- if(!column_names || !column_names.length) throw "table column_names mising";
1896
- let _allowed_cols = column_names.slice(0);
1897
- let _obj = { ...obj };
1898
-
1899
- if(allowed_cols){
1900
- _allowed_cols = this.parseFieldFilter(allowed_cols, false);
1901
- }
1902
- let final_filter = { ..._obj },
1903
- filter_keys: Array<keyof typeof final_filter> = Object.keys(final_filter);
1904
-
1905
- if(fixIssues && filter_keys.length){
1906
- final_filter = {};
1907
- filter_keys
1908
- .filter(col => _allowed_cols.includes(col))
1909
- .map(col => {
1910
- final_filter[col] = _obj[col];
1911
- });
1912
- }
1913
-
1914
- /* If has keys check against allowed_cols */
1915
- if(final_filter && Object.keys(final_filter).length && _allowed_cols){
1916
- validateObj(final_filter, _allowed_cols)
1917
- }
1918
-
1919
- if(forcedData && Object.keys(forcedData).length){
1920
- final_filter = { ...final_filter, ...forcedData };
1921
- }
1922
-
1923
- validateObj(final_filter, column_names.slice(0));
1924
- return final_filter;
1925
- }
1926
-
1927
-
1928
- parseFieldFilter(fieldParams: FieldFilter = "*", allow_empty: boolean = true, allowed_cols?: string[]): string[] {
1929
- return ViewHandler._parseFieldFilter(fieldParams, allow_empty, allowed_cols || this.column_names.slice(0))
1930
- }
1931
-
1932
- /**
1933
- * Filter string array
1934
- * @param {FieldFilter} fieldParams - { col1: 0, col2: 0 } | { col1: true, col2: true } | "*" | ["key1", "key2"] | []
1935
- * @param {boolean} allow_empty - allow empty select. defaults to true
1936
- */
1937
- static _parseFieldFilter<AllowedKeys extends string[]>(fieldParams: FieldFilter<Record<AllowedKeys[number], any>> = "*", allow_empty: boolean = true, all_cols: AllowedKeys): AllowedKeys | [""] {
1938
- if(!all_cols) throw "all_cols missing"
1939
- const all_fields = all_cols;// || this.column_names.slice(0);
1940
- let colNames: AllowedKeys = [] as any,
1941
- initialParams = JSON.stringify(fieldParams);
1942
-
1943
- if(fieldParams){
1944
-
1945
- /*
1946
- "field1, field2, field4" | "*"
1947
- */
1948
- if(typeof fieldParams === "string"){
1949
- fieldParams = fieldParams.split(",").map(k => k.trim());
1950
- }
1951
-
1952
- /* string[] */
1953
- if(Array.isArray(fieldParams) && !fieldParams.find(f => typeof f !== "string")){
1954
- /*
1955
- ["*"]
1956
- */
1957
- if(fieldParams[0] === "*"){
1958
- return all_fields.slice(0) as typeof all_fields;
1959
-
1960
- /*
1961
- [""]
1962
- */
1963
- } else if(fieldParams[0] === ""){
1964
- if(allow_empty){
1965
- return [""];
1966
- } else {
1967
- throw "Empty value not allowed";
1968
- }
1969
- /*
1970
- ["field1", "field2", "field3"]
1971
- */
1972
- } else {
1973
- colNames = fieldParams.slice(0) as AllowedKeys;
1974
- }
1975
-
1976
- /*
1977
- { field1: true, field2: true } = only field1 and field2
1978
- { field1: false, field2: false } = all fields except field1 and field2
1979
- */
1980
- } else if(isPlainObject(fieldParams)){
1981
-
1982
- if(!getKeys(fieldParams).length){
1983
- return [] as unknown as typeof all_fields; //all_fields.slice(0) as typeof all_fields;
1984
- }
1985
-
1986
- let keys = getKeys(fieldParams as {
1987
- [key: string]: boolean | 0 | 1;
1988
- }) as AllowedKeys;
1989
- if(keys[0] === ""){
1990
- if(allow_empty){
1991
- return [""];
1992
- } else {
1993
- throw "Empty value not allowed";
1994
- }
1995
- }
1996
-
1997
- validate(keys);
1998
-
1999
- keys.forEach(key => {
2000
- const allowedVals = [true, false, 0, 1];
2001
- if(!allowedVals.includes((fieldParams as any)[key])) throw `Invalid field selection value for: { ${key}: ${(fieldParams as any)[key]} }. \n Allowed values: ${allowedVals.join(" OR ")}`
2002
- })
2003
-
2004
- let allowed = keys.filter(key => (fieldParams as any)[key]),
2005
- disallowed = keys.filter(key => !(fieldParams as any)[key]);
2006
-
2007
-
2008
- if(disallowed && disallowed.length){
2009
- return all_fields.filter(col => !disallowed.includes(col)) as typeof all_fields;
2010
- } else {
2011
- return [...allowed] as any;
2012
- }
2013
-
2014
- } else {
2015
- throw " Unrecognised field filter.\nExpecting any of: string | string[] | { [field]: boolean } \n Received -> " + initialParams;
2016
- }
195
+ function snakify(str: string, capitalize = false) : string {
2017
196
 
2018
- validate(colNames);
2019
- }
2020
- return colNames as any;
197
+ return str.split("").map((c, i)=> {
2021
198
 
2022
- function validate(cols: AllowedKeys){
2023
- let bad_keys = cols.filter(col => !all_fields.includes(col));
2024
- if(bad_keys && bad_keys.length){
2025
- throw "\nUnrecognised or illegal fields: " + bad_keys.join(", ");
2026
- }
2027
- }
199
+ if(!i) {
200
+ if(capitalize) c = c.toUpperCase();
201
+ if(c.match(/[^a-z_A-Z]/)){
202
+ return ((capitalize)? "D_" : "_") + c.charCodeAt(0);
203
+ }
204
+ } else {
205
+ if(c.match(/[^a-zA-Z_0-9]/)){
206
+ return "_" + c.charCodeAt(0);
207
+ }
2028
208
  }
2029
- }
209
+
210
+ return c;
2030
211
 
2031
- export function isPojoObject<T>(obj: T): obj is Record<string, any> {
2032
- if(obj && (typeof obj !== "object" || Array.isArray(obj) || obj instanceof Date)){
2033
- return false;
2034
- }
2035
- return true;
212
+ }).join("");
2036
213
  }
2037
214
 
215
+ function canBeUsedAsIsInTypescript(str: string): boolean {
216
+ if(!str) return false;
217
+ const isAlphaNumericOrUnderline = str.match(/^[a-z0-9_]+$/i);
218
+ const startsWithCharOrUnderscore = str[0].match(/^[a-z_]+$/i);
219
+ return Boolean(isAlphaNumericOrUnderline && startsWithCharOrUnderscore);
220
+ }
2038
221
 
2039
- type ValidatedParams = {
2040
- row: AnyObject;
2041
- forcedData?: AnyObject;
2042
- allowedFields?: FieldFilter;
2043
- tableRules?: TableRule;
2044
- fixIssues: boolean;
222
+ export function escapeTSNames(str: string, capitalize = false): string {
223
+ let res = str;
224
+ res = (capitalize? str[0].toUpperCase() : str[0]) + str.slice(1);
225
+ if(canBeUsedAsIsInTypescript(res)) return res;
226
+ return JSON.stringify(res);
2045
227
  }
2046
228
 
2047
- export class TableHandler extends ViewHandler {
2048
- io_stats: {
2049
- throttle_queries_per_sec: number;
2050
- since: number,
2051
- queries: number,
2052
- batching: string[] | null
2053
- }
2054
-
2055
- constructor(db: DB, tableOrViewInfo: TableSchema, dboBuilder: DboBuilder, t?: pgPromise.ITask<{}>, dbTX?: TableHandlers, joinPaths?: JoinPaths){
2056
- super(db, tableOrViewInfo, dboBuilder, t, dbTX, joinPaths);
2057
-
2058
- this.remove = this.delete;
2059
-
2060
- this.io_stats = {
2061
- since: Date.now(),
2062
- queries: 0,
2063
- throttle_queries_per_sec: 500,
2064
- batching: null
2065
- };
2066
- this.is_view = false;
2067
- this.is_media = dboBuilder.prostgles.isMedia(this.name)
2068
- }
229
+ export type Aggregation = {
230
+ field: string,
231
+ query: string,
232
+ alias: string,
233
+ getQuery: (alias: string) => string;
234
+ };
2069
235
 
2070
- /* TO DO: Maybe finished query batching */
2071
- willBatch(query: string){
2072
- const now = Date.now();
2073
- if(this.io_stats.since < Date.now()){
2074
- this.io_stats.since = Date.now();
2075
- this.io_stats.queries = 0;
2076
- } else {
2077
- this.io_stats.queries++;
2078
- }
236
+ export type Filter = AnyObject | { $and: Filter[] } | { $or: Filter[] };
2079
237
 
2080
- if(this.io_stats.queries > this.io_stats.throttle_queries_per_sec){
238
+ type SelectFunc = {
239
+ alias: string;
240
+ getQuery: (alias: string, tableAlias?: string) => string;
241
+ }
2081
242
 
2082
- return true;
2083
- }
2084
- }
243
+ export type JoinInfo = {
244
+ expectOne?: boolean,
245
+ paths: {
2085
246
 
2086
- async subscribe(filter: Filter, params: SubscribeParams, localFunc: (items: AnyObject[]) => any): Promise<{ unsubscribe: () => any }>
2087
- async subscribe(filter: Filter, params: SubscribeParams, localFunc?: (items: AnyObject[]) => any, table_rules?: TableRule, localParams?: LocalParams): Promise<string>
2088
- async subscribe(filter: Filter, params: SubscribeParams = {}, localFunc?: (items: AnyObject[]) => any, table_rules?: TableRule, localParams?: LocalParams):
2089
- Promise<string | { unsubscribe: () => any }>
2090
- {
2091
- try {
2092
- if(this.is_view) throw "Cannot subscribe to a view";
2093
- if(this.t) throw "subscribe not allowed within transactions";
2094
- if(!localParams && !localFunc) throw " missing data. provide -> localFunc | localParams { socket } ";
2095
- if(localParams && localParams.socket && localFunc) {
2096
- console.error({ localParams, localFunc })
2097
- throw " Cannot have localFunc AND socket ";
2098
- }
247
+ /**
248
+ * The table that JOIN ON columns refer to.
249
+ * columns in index = 1 refer to this table. index = 0 columns refer to previous JoinInfo.table
250
+ */
251
+ table: string,
2099
252
 
2100
- const { filterFields, forcedFilter } = get(table_rules, "select") || {},
2101
- filterOpts = await this.prepareWhere({ filter, forcedFilter, addKeywords: false, filterFields, tableAlias: undefined, localParams, tableRule: table_rules }),
2102
- condition = filterOpts.where,
2103
- throttle = get(params, "throttle") || 0,
2104
- selectParams = omitKeys(params || {}, ["throttle"]);
253
+ /**
254
+ * Source and target JOIN ON columns
255
+ * Each inner array group will be combined with AND and outer arrays with OR to allow multiple references to the same table
256
+ * e.g.: [[source_table_column: string, table_column: string]]
257
+ */
258
+ on: [string, string][][],
2105
259
 
2106
- // const { subOne = false } = localParams || {};
2107
- const filterSize = JSON.stringify(filter || {}).length;
2108
- if(filterSize * 4 > 2704){
2109
- throw "filter too big. Might exceed the btree version 4 maximum 2704"
2110
- }
2111
-
2112
- if(!localFunc) {
2113
- if(!this.dboBuilder.prostgles.isSuperUser) throw "Subscribe not possible. Must be superuser to add triggers 1856";
2114
- return await this.find(filter, { ...selectParams, limit: 0 }, undefined, table_rules, localParams)
2115
- .then(async isValid => {
260
+ /**
261
+ * Source table name
262
+ */
263
+ source: string,
2116
264
 
2117
- const { socket } = localParams ?? {};
2118
- const pubSubManager = await this.dboBuilder.getPubSubManager();
2119
- return pubSubManager.addSub({
2120
- table_info: this.tableOrViewInfo,
2121
- socket,
2122
- table_rules,
2123
- condition: condition,
2124
- func: undefined,
2125
- filter: { ...filter },
2126
- params: { ...selectParams },
2127
- socket_id: socket?.id,
2128
- table_name: this.name,
2129
- throttle,
2130
- last_throttled: 0,
2131
- // subOne
2132
- }).then(channelName => ({ channelName }));
2133
- }) as string;
2134
- } else {
2135
- const pubSubManager = await this.dboBuilder.getPubSubManager();
2136
- pubSubManager.addSub({
2137
- table_info: this.tableOrViewInfo,
2138
- socket: undefined,
2139
- table_rules,
2140
- condition,
2141
- func: localFunc,
2142
- filter: { ...filter },
2143
- params: { ...selectParams },
2144
- socket_id: undefined,
2145
- table_name: this.name,
2146
- throttle,
2147
- last_throttled: 0,
2148
- // subOne
2149
- }).then(channelName => ({ channelName }));
2150
- const unsubscribe = async () => {
2151
- const pubSubManager = await this.dboBuilder.getPubSubManager();
2152
- pubSubManager.removeLocalSub(this.name, condition, localFunc)
2153
- };
2154
- let res: { unsubscribe: () => any } = Object.freeze({ unsubscribe })
2155
- return res;
2156
- }
2157
- } catch(e){
2158
- if(localParams && localParams.testRule) throw e;
2159
- throw parseError(e, `dbo.${this.name}.subscribe()`);
2160
- }
2161
- }
2162
-
2163
- /* This should only be called from server */
2164
- subscribeOne(filter: Filter, params: SubscribeParams, localFunc: (item: AnyObject) => any): Promise<{ unsubscribe: () => any }>
2165
- subscribeOne(filter: Filter, params: SubscribeParams, localFunc: (item: AnyObject) => any, table_rules?: TableRule, localParams?: LocalParams): Promise<string>
2166
- subscribeOne(filter: Filter, params: SubscribeParams = {}, localFunc: (item: AnyObject) => any, table_rules?: TableRule, localParams?: LocalParams):
2167
- Promise<string | { unsubscribe: () => any }>
2168
- {
2169
- let func = localParams? undefined : (rows: AnyObject[]) => localFunc(rows[0]);
2170
- return this.subscribe(filter, { ...params, limit: 2 }, func, table_rules, localParams);
2171
- }
2172
-
265
+ /**
266
+ * Target table name
267
+ */
268
+ target: string
269
+ }[]
270
+ }
2173
271
 
2174
- async updateBatch(data: [Filter, AnyObject][], params?: UpdateParams, tableRules?: TableRule, localParams?: LocalParams): Promise<any>{
2175
- try {
2176
- const queries = await Promise.all(
2177
- data.map(async ([filter, data]) =>
2178
- await this.update(
2179
- filter,
2180
- data,
2181
- { ...(params || {}), returning: undefined },
2182
- tableRules,
2183
- { ...(localParams || {}), returnQuery: true }
2184
- )
2185
- )
2186
- );
2187
- const keys = (data && data.length)? Object.keys(data[0]) : [];
2188
- return this.db.tx(t => {
2189
- const _queries = queries.map(q => t.none(q as unknown as string))
2190
- return t.batch(_queries)
2191
- }).catch(err => makeErr(err, localParams, this, keys));
2192
- } catch(e){
2193
- if(localParams && localParams.testRule) throw e;
2194
- throw parseError(e, `dbo.${this.name}.update()`);
2195
- }
2196
- }
272
+ import { findShortestPath, Graph } from "./shortestPath";
2197
273
 
2198
- async parseUpdateRules(filter: Filter, newData: AnyObject, params?: UpdateParams, tableRules?: TableRule, localParams?: LocalParams): Promise<{
2199
- fields: string[];
2200
- validateRow?: ValidateRow;
2201
- finalUpdateFilter: AnyObject;
2202
- forcedData?: AnyObject;
2203
- forcedFilter?: AnyObject;
2204
- returningFields: FieldFilter;
2205
- filterFields?: FieldFilter;
2206
- }> {
2207
- const { testRule = false } = localParams ?? {};
2208
- if(!testRule){
2209
- if(!newData || !Object.keys(newData).length) throw "no update data provided\nEXPECTING db.table.update(filter, updateData, options)";
2210
- this.checkFilter(filter);
2211
- }
274
+ export type CommonTableRules = {
2212
275
 
2213
- let forcedFilter: AnyObject | undefined = {},
2214
- forcedData: AnyObject | undefined = {},
2215
- validate: ValidateUpdateRow | undefined,
2216
- returningFields: FieldFilter = "*",
2217
- filterFields: FieldFilter | undefined = "*",
2218
- fields: FieldFilter = "*";
2219
-
2220
- let finalUpdateFilter = { ...filter };
2221
-
2222
- if(tableRules){
2223
- if(!tableRules.update) throw "update rules missing for " + this.name;
2224
- ({ forcedFilter, forcedData, fields, filterFields, validate } = tableRules.update);
2225
- returningFields = tableRules.update.returningFields ?? get(tableRules, "select.fields") ?? ""
2226
- if(!returningFields && params?.returning){
2227
- throw "You are not allowed to return any fields from the update"
2228
- }
276
+ /**
277
+ * True by default. Allows clients to get column information on any columns that are allowed in (select, insert, update) field rules.
278
+ */
279
+ getColumns?: PublishAllOrNothing;
280
+
281
+ /**
282
+ * True by default. Allows clients to get table information (oid, comment, label, has_media).
283
+ */
284
+ getInfo?: PublishAllOrNothing
285
+ }
2229
286
 
2230
- if(!fields) throw ` Invalid update rule fo r ${this.name}. fields missing `;
2231
- finalUpdateFilter = (await this.prepareWhere({ filter, forcedFilter, filterFields, localParams, tableRule: tableRules })).filter;
2232
- if(tableRules.update.dynamicFields?.length){
2233
-
2234
- /**
2235
- * dynamicFields.fields used to allow a custom list of fields for specific records
2236
- * dynamicFields.filter cannot overlap each other
2237
- * updates must target records from a specific dynamicFields.filter or not match any dynamicFields.filter
2238
- */
2239
- if(testRule){
2240
- for await(const [dfIndex, dfRule] of tableRules.update.dynamicFields.entries() ){
2241
- const condition = await this.getCondition({ allowed_colnames: this.column_names, filter: dfRule.filter });
2242
- if(!condition) throw "dynamicFields.filter cannot be empty: " + JSON.stringify(dfRule);
2243
- await this.find(dfRule.filter, { limit: 0 });
2244
-
2245
- /** Ensure dynamicFields filters do not overlap */
2246
- for await(const [_dfIndex, _dfRule] of tableRules.update.dynamicFields.entries() ){
2247
- if(dfIndex !== _dfIndex){
2248
- if(await this.findOne({ $and: [dfRule.filter, _dfRule.filter]}, { select: "" })){
2249
- throw `dynamicFields.filter cannot overlap each other. \n
2250
- Overlapping dynamicFields rules:
2251
- ${JSON.stringify(dfRule)}
2252
- AND
2253
- ${JSON.stringify(_dfRule)}
2254
- `;
2255
- }
2256
- }
2257
- }
2258
- }
2259
- }
287
+ export type ValidatedTableRules = CommonTableRules & {
2260
288
 
2261
- /** Pick dynamicFields.fields if matching filter */
2262
- let matchedRule: Required<UpdateRule>["dynamicFields"][number] | undefined;
2263
- for await(const dfRule of tableRules.update.dynamicFields){
2264
- const match = await this.findOne({ $and: [finalUpdateFilter, dfRule.filter].filter(isDefined) });
2265
-
2266
- if(match){
289
+ /* All columns of the view/table. Includes computed fields as well */
290
+ allColumns: FieldSpec[];
2267
291
 
2268
- /** Ensure it doesn't overlap with other dynamicFields.filter */
2269
- if(matchedRule){
2270
- throw "Your update is targeting multiple tableRules.update.dynamicFields. Restrict update filter to only target one rule";
2271
- }
292
+ select: {
293
+ /* Fields you can select */
294
+ fields: string[];
2272
295
 
2273
- matchedRule = dfRule;
2274
- fields = dfRule.fields;
2275
- }
2276
- }
2277
- }
296
+ /* Fields you can select */
297
+ orderByFields: string[];
2278
298
 
2279
- /* Safely test publish rules */
2280
- if(testRule){
2281
- await this.validateViewRules({ fields, filterFields, returningFields, forcedFilter, dynamicFields: tableRules.update.dynamicFields, rule: "update"});
2282
- if(forcedData) {
2283
- try {
2284
- const { data, allowedCols } = this.validateNewData({ row: forcedData, forcedData: undefined, allowedFields: "*", tableRules, fixIssues: false });
2285
- const updateQ = await this.colSet.getUpdateQuery(
2286
- data,
2287
- allowedCols,
2288
- this.dbTX || this.dboBuilder.dbo,
2289
- validate? ((row) => validate!({ update: row, filter: {}}, this.dbTX || this.dboBuilder.dbo)) : undefined
2290
- ) //pgp.helpers.update(data, columnSet)
2291
- let query = updateQ + " WHERE FALSE ";
2292
- await this.db.any("EXPLAIN " + query);
2293
- } catch(e){
2294
- throw " issue with forcedData: \nVALUE: " + JSON.stringify(forcedData, null, 2) + "\nERROR: " + e;
2295
- }
2296
- }
299
+ /* Filter applied to every select */
300
+ filterFields: string[];
2297
301
 
2298
- return true as unknown as any;
2299
- }
2300
- }
302
+ /* Filter applied to every select */
303
+ forcedFilter: any;
2301
304
 
2302
- /* Update all allowed fields (fields) except the forcedFilter (so that the user cannot change the forced filter values) */
2303
- let _fields = this.parseFieldFilter(fields);
305
+ /* Max limit allowed for each select. 1000 by default. If null then an unlimited select is allowed when providing { limit: null } */
306
+ maxLimit: number | null;
307
+ },
308
+ update: {
309
+ /* Fields you can update */
310
+ fields: string[];
2304
311
 
2305
- /**
2306
- * A forced filter must be basic
2307
- */
2308
- if(forcedFilter){
2309
- const _forcedFilterKeys = Object.keys(forcedFilter);
2310
- const nonFields = _forcedFilterKeys.filter(key => !this.column_names.includes(key));
2311
- if(nonFields.length) throw "forcedFilter must be a basic filter ( { col_name: 'value' } ). Invalid filter keys: " + nonFields;
2312
- // const clashingFields = _forcedFilterKeys.filter(key => _fields.includes(key));
2313
- }
2314
- const validateRow: ValidateRow | undefined = validate? (row) => validate!({ update: row, filter: finalUpdateFilter }, this.dbTX || this.dboBuilder.dbo) : undefined
2315
-
2316
- return {
2317
- fields: _fields,
2318
- validateRow,
2319
- finalUpdateFilter,
2320
- forcedData,
2321
- forcedFilter,
2322
- returningFields,
2323
- filterFields,
2324
- }
2325
- }
312
+ /* Fields you can return after updating */
313
+ returningFields: string[];
2326
314
 
2327
- async update(filter: Filter, newData: AnyObject, params?: UpdateParams, tableRules?: TableRule, localParams?: LocalParams): Promise<AnyObject | void>{
2328
- return update.bind(this)(filter, newData, params, tableRules, localParams);
2329
- };
315
+ /* Fields you can use in filtering when updating */
316
+ filterFields: string[];
2330
317
 
2331
- validateNewData({ row, forcedData, allowedFields, tableRules, fixIssues = false }: ValidatedParams): { data: any; allowedCols: string[] } {
2332
- const synced_field = get(tableRules ?? {}, "sync.synced_field");
318
+ /* Filter applied to every update. Filter fields cannot be updated */
319
+ forcedFilter: any;
2333
320
 
2334
- /* Update synced_field if sync is on and missing */
2335
- if(synced_field && !row[synced_field]){
2336
- row[synced_field] = Date.now();
2337
- }
321
+ /* Data applied to every update */
322
+ forcedData: any;
323
+ },
324
+ insert: {
325
+ /* Fields you can insert */
326
+ fields: string[];
2338
327
 
2339
- let data = this.prepareFieldValues(row, forcedData, allowedFields, fixIssues);
2340
- const dataKeys = getKeys(data);
2341
-
2342
- dataKeys.map(col => {
2343
- this.dboBuilder.prostgles?.tableConfigurator?.checkColVal({ table: this.name, col, value: data[col] });
2344
- const colConfig = this.dboBuilder.prostgles?.tableConfigurator?.getColumnConfig(this.name, col);
2345
- if(colConfig && isObject(colConfig) && "isText" in colConfig && data[col]){
2346
- if(colConfig.lowerCased){
2347
- data[col] = data[col].toString().toLowerCase()
2348
- }
2349
- if(colConfig.trimmed){
2350
- data[col] = data[col].toString().trim()
2351
- }
2352
- }
2353
- })
328
+ /* Fields you can return after inserting. Will return select.fields by default */
329
+ returningFields: string[];
2354
330
 
2355
- return { data, allowedCols: this.columns.filter(c => dataKeys.includes(c.name)).map(c => c.name) }
2356
- }
2357
-
2358
- insertDataParse = insertDataParse;
2359
- async insert(rowOrRows: (AnyObject | AnyObject[]), param2?: InsertParams, param3_unused?: undefined, tableRules?: TableRule, _localParams?: LocalParams): Promise<any | any[] | boolean>{
2360
- return insert.bind(this)(rowOrRows, param2, param3_unused, tableRules, _localParams)
2361
- }
331
+ /* Data applied to every insert */
332
+ forcedData: any;
333
+ },
334
+ delete: {
335
+ /* Fields to filter by when deleting */
336
+ filterFields: string[];
2362
337
 
2363
- prepareReturning = async (returning: Select | undefined, allowedFields: string[]): Promise<SelectItem[]> => {
2364
- let result: SelectItem[] = [];
2365
- if(returning){
2366
- let sBuilder = new SelectItemBuilder({
2367
- allFields: this.column_names.slice(0),
2368
- allowedFields,
2369
- allowedOrderByFields: allowedFields,
2370
- computedFields: COMPUTED_FIELDS,
2371
- functions: FUNCTIONS.filter(f => f.type === "function" && f.singleColArg),
2372
- isView: this.is_view,
2373
- columns: this.columns,
2374
- });
2375
- await sBuilder.parseUserSelect(returning);
338
+ /* Filter applied to every deletes */
339
+ forcedFilter: any;
2376
340
 
2377
- return sBuilder.select;
2378
- }
2379
-
2380
- return result;
341
+ /* Fields you can return after deleting */
342
+ returningFields: string[];
2381
343
  }
344
+ }
2382
345
 
2383
- makeReturnQuery(items?: SelectItem[]){
2384
- if(items?.length) return " RETURNING " + items.map(s => s.getQuery() + " AS " + asName(s.alias)).join(", ");
2385
- return "";
2386
- }
2387
-
2388
- async delete(filter?: Filter, params?: DeleteParams, param3_unused?: undefined, table_rules?: TableRule, localParams?: LocalParams): Promise<any> {
2389
- return _delete.bind(this)(filter, params, param3_unused, table_rules, localParams);
2390
- };
2391
-
2392
- remove(filter: Filter, params?: UpdateParams, param3_unused?: undefined, tableRules?: TableRule, localParams?: LocalParams){
2393
- return this.delete(filter, params, param3_unused , tableRules, localParams);
346
+ /* DEBUG CLIENT ERRORS HERE */
347
+ export function makeErr(err: any, localParams?: LocalParams, view?: ViewHandler, allowedKeys?: string[]){
348
+ // console.trace(err)
349
+ if(process.env.TEST_TYPE || process.env.PRGL_DEBUG) {
350
+ console.trace(err)
2394
351
  }
2395
-
2396
- async upsert(filter: Filter, newData: AnyObject, params?: UpdateParams, table_rules?: TableRule, localParams?: LocalParams): Promise<any> {
2397
- try {
2398
- /* Do it within a transaction to ensure consisency */
2399
- if(!this.t){
2400
- return this.dboBuilder.getTX(dbTX => _upsert(dbTX[this.name] as TableHandler))
2401
- } else {
2402
- return _upsert(this);
2403
- }
2404
-
2405
- async function _upsert(tblH: TableHandler){
2406
- return tblH.find(filter, { select: "", limit: 1 }, undefined, table_rules, localParams)
2407
- .then(exists => {
2408
- if(exists && exists.length){
2409
- return tblH.update(filter, newData, params, table_rules, localParams);
2410
- } else {
2411
- return tblH.insert({ ...newData, ...filter }, params, undefined, table_rules, localParams);
2412
- }
2413
- });
2414
- }
2415
- } catch(e){
2416
- if(localParams && localParams.testRule) throw e;
2417
- throw parseError(e, `dbo.${this.name}.upsert()`);
2418
- }
352
+ const errObject = {
353
+ ...((!localParams || !localParams.socket)? err : {}),
354
+ ...pickKeys(err, ["column", "code", "table", "constraint"]),
355
+ ...(err && err.toString? { txt: err.toString() } : {}),
356
+ code_info: sqlErrCodeToMsg(err.code)
2419
357
  };
2420
-
2421
- /* External request. Cannot sync from server */
2422
- async sync(filter: Filter, params: SelectParams, param3_unused: undefined, table_rules: TableRule, localParams: LocalParams){
2423
- if(!localParams) throw "Sync not allowed within the same server code";
2424
- const { socket } = localParams;
2425
- if(!socket) throw "INTERNAL ERROR: socket missing";
2426
-
2427
-
2428
- if(!table_rules || !table_rules.sync || !table_rules.select) throw "INTERNAL ERROR: sync or select rules missing";
2429
-
2430
- if(this.t) throw "Sync not allowed within transactions";
2431
-
2432
- const ALLOWED_PARAMS = ["select"];
2433
- const invalidParams = Object.keys(params || {}).filter(k => !ALLOWED_PARAMS.includes(k));
2434
- if(invalidParams.length) throw "Invalid or dissallowed params found: " + invalidParams.join(", ");
2435
-
2436
- try {
2437
-
2438
-
2439
- let { id_fields, synced_field, allow_delete }: SyncRule = table_rules.sync;
2440
- const syncFields = [...id_fields, synced_field];
2441
-
2442
- if(!id_fields || !synced_field){
2443
- const err = "INTERNAL ERROR: id_fields OR synced_field missing from publish";
2444
- console.error(err);
2445
- throw err;
2446
- }
2447
-
2448
- id_fields = this.parseFieldFilter(id_fields, false);
2449
-
2450
- let allowedSelect = this.parseFieldFilter(get(table_rules, "select.fields"), false);
2451
- if(syncFields.find(f => !allowedSelect.includes(f))){
2452
- throw `INTERNAL ERROR: sync field missing from publish.${this.name}.select.fields`;
358
+ if(view?.dboBuilder?.constraints && errObject.constraint && !errObject.column){
359
+ const constraint = view.dboBuilder.constraints
360
+ .find(c => c.conname === errObject.constraint && c.relname === view.name);
361
+ if(constraint){
362
+ const cols = view.columns.filter(c =>
363
+ (!allowedKeys || allowedKeys.includes(c.name)) &&
364
+ constraint.conkey.includes(c.ordinal_position)
365
+ );
366
+ if(cols.length){
367
+ errObject.column = cols[0].name;
368
+ errObject.columns = cols.map(c => c.name);
2453
369
  }
2454
- let select = this.getAllowedSelectFields(
2455
- get(params || {}, "select") || "*",
2456
- allowedSelect,
2457
- false
2458
- );
2459
- if(!select.length) throw "Empty select not allowed";
2460
-
2461
- /* Add sync fields if missing */
2462
- syncFields.map(sf => {
2463
- if(!select.includes(sf)) select.push(sf);
2464
- });
2465
-
2466
- /* Step 1: parse command and params */
2467
- return this.find(filter, { select, limit: 0 }, undefined, table_rules, localParams)
2468
- .then(async isValid => {
2469
-
2470
- const { filterFields, forcedFilter } = get(table_rules, "select") || {};
2471
- const condition = (await this.prepareWhere({ filter, forcedFilter, filterFields, addKeywords: false, localParams, tableRule: table_rules })).where;
2472
-
2473
- // let final_filter = getFindFilter(filter, table_rules);
2474
- const pubSubManager = await this.dboBuilder.getPubSubManager();
2475
- return pubSubManager.addSync({
2476
- table_info: this.tableOrViewInfo,
2477
- condition,
2478
- id_fields, synced_field,
2479
- allow_delete,
2480
- socket,
2481
- table_rules,
2482
- filter: { ...filter },
2483
- params: { select }
2484
- }).then(channelName => ({ channelName, id_fields, synced_field }));
2485
- });
2486
-
2487
- } catch(e){
2488
- if(localParams && localParams.testRule) throw e;
2489
- throw parseError(e, `dbo.${this.name}.sync()`);
2490
370
  }
371
+ }
372
+ return Promise.reject(errObject);
373
+ }
374
+ export const EXISTS_KEYS = ["$exists", "$notExists", "$existsJoined", "$notExistsJoined"] as const;
375
+ export type EXISTS_KEY = typeof EXISTS_KEYS[number];
2491
376
 
2492
- /*
2493
- REPLICATION
2494
-
2495
- 1 Sync proccess (NO DELETES ALLOWED):
2496
-
2497
- Client sends:
2498
- "sync-request"
2499
- { min_id, max_id, count, max_synced }
2500
-
2501
- Server sends:
2502
- "sync-pull"
2503
- { from_synced }
2504
-
2505
- Client sends:
2506
- "sync-push"
2507
- { data } -> WHERE synced >= from_synced
2508
-
2509
- Server upserts:
2510
- WHERE not exists synced = synced AND id = id
2511
- UNTIL
2512
377
 
2513
- Server sends
2514
- "sync-push"
2515
- { data } -> WHERE synced >= from_synced
2516
- */
378
+ /**
379
+ * Ensure the error is an Object and has
380
+ */
381
+ export function parseError(e: any, caller: string): ProstglesError {
382
+
383
+ const errorObject = isObject(e)? e : undefined;
384
+ const message = typeof e === "string"? e : e instanceof Error? e.message :
385
+ isObject(errorObject)? (errorObject.message ?? errorObject.txt ?? JSON.stringify(errorObject) ?? "") : "";
386
+ const stack = [
387
+ ...(errorObject && Array.isArray(errorObject.stack)? errorObject.stack : []),
388
+ caller
389
+ ]
390
+ const result: ProstglesError = {
391
+ ...errorObject,
392
+ message,
393
+ stack,
2517
394
  }
2518
-
395
+ return result;
2519
396
  }
2520
397
 
398
+ export type ExistsFilterConfig = {
399
+ key: string;
400
+ f2: Filter;
401
+ existType: EXISTS_KEY;
402
+ tables: string[];
403
+ isJoined: boolean;
404
+ shortestJoin: boolean;
405
+ };
406
+
2521
407
  import { JOIN_TYPES } from "./Prostgles";
2522
408
  import { BasicSession } from "./AuthHandler";
2523
409
  import { DBOFullyTyped, getDBSchema } from "./DBSchemaBuilder";
410
+ import { TableHandler } from "./DboBuilder/TableHandler";
2524
411
 
2525
412
  export class DboBuilder {
2526
413
  tablesOrViews?: TableSchema[]; //TableSchema TableOrViewInfo
@@ -3061,21 +948,6 @@ async function getTablesForSchemaPostgresSQL(db: DB, schema: string = "public"):
3061
948
  return res;
3062
949
  }
3063
950
 
3064
- /**
3065
- * Throw error if illegal keys found in object
3066
- */
3067
- function validateObj<T extends Record<string, any>>(obj: T, allowedKeys: string[]): T {
3068
- if(obj && Object.keys(obj).length){
3069
- const invalid_keys = Object.keys(obj).filter(k => !allowedKeys.includes(k));
3070
- if(invalid_keys.length){
3071
- throw "Invalid/Illegal fields found: " + invalid_keys.join(", ");
3072
- }
3073
- }
3074
-
3075
- return obj;
3076
- }
3077
-
3078
-
3079
951
  export function isPlainObject(o: any): o is Record<string, any> {
3080
952
  return Object(o) === o && Object.getPrototypeOf(o) === Object.prototype;
3081
953
  }