prostgles-server 2.0.185 → 2.0.188

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 (91) hide show
  1. package/dist/DboBuilder/insert.d.ts +5 -0
  2. package/dist/DboBuilder/insert.d.ts.map +1 -0
  3. package/dist/DboBuilder/insert.js +129 -0
  4. package/dist/DboBuilder/insert.js.map +1 -0
  5. package/dist/DboBuilder/insertDataParse.d.ts +11 -0
  6. package/dist/DboBuilder/insertDataParse.d.ts.map +1 -0
  7. package/dist/DboBuilder/insertDataParse.js +283 -0
  8. package/dist/DboBuilder/insertDataParse.js.map +1 -0
  9. package/dist/DboBuilder.d.ts +10 -5
  10. package/dist/DboBuilder.d.ts.map +1 -1
  11. package/dist/DboBuilder.js +121 -374
  12. package/dist/DboBuilder.js.map +1 -1
  13. package/dist/FileManager.d.ts.map +1 -1
  14. package/dist/FileManager.js +17 -12
  15. package/dist/FileManager.js.map +1 -1
  16. package/dist/Prostgles.d.ts +16 -14
  17. package/dist/Prostgles.d.ts.map +1 -1
  18. package/dist/Prostgles.js +7 -7
  19. package/dist/Prostgles.js.map +1 -1
  20. package/dist/PubSubManager.d.ts +1 -0
  21. package/dist/PubSubManager.d.ts.map +1 -1
  22. package/dist/PubSubManager.js +2 -1
  23. package/dist/PubSubManager.js.map +1 -1
  24. package/dist/QueryBuilder.d.ts +7 -3
  25. package/dist/QueryBuilder.d.ts.map +1 -1
  26. package/dist/QueryBuilder.js +7 -2
  27. package/dist/QueryBuilder.js.map +1 -1
  28. package/dist/TableConfig.d.ts +1 -4
  29. package/dist/TableConfig.d.ts.map +1 -1
  30. package/dist/TableConfig.js +16 -1
  31. package/dist/TableConfig.js.map +1 -1
  32. package/lib/DboBuilder/insert.d.ts +5 -0
  33. package/lib/DboBuilder/insert.d.ts.map +1 -0
  34. package/lib/DboBuilder/insert.js +128 -0
  35. package/lib/DboBuilder/insert.ts +138 -0
  36. package/lib/DboBuilder/insertDataParse.d.ts +11 -0
  37. package/lib/DboBuilder/insertDataParse.d.ts.map +1 -0
  38. package/lib/DboBuilder/insertDataParse.js +282 -0
  39. package/lib/DboBuilder/insertDataParse.ts +355 -0
  40. package/lib/DboBuilder.d.ts +10 -5
  41. package/lib/DboBuilder.d.ts.map +1 -1
  42. package/lib/DboBuilder.js +121 -374
  43. package/lib/DboBuilder.ts +138 -453
  44. package/lib/FileManager.d.ts.map +1 -1
  45. package/lib/FileManager.js +17 -12
  46. package/lib/FileManager.ts +18 -13
  47. package/lib/Prostgles.d.ts +16 -14
  48. package/lib/Prostgles.d.ts.map +1 -1
  49. package/lib/Prostgles.js +7 -7
  50. package/lib/Prostgles.ts +664 -650
  51. package/lib/PubSubManager.d.ts +1 -0
  52. package/lib/PubSubManager.d.ts.map +1 -1
  53. package/lib/PubSubManager.js +2 -1
  54. package/lib/PubSubManager.ts +3 -1
  55. package/lib/QueryBuilder.d.ts.map +1 -1
  56. package/lib/QueryBuilder.js +7 -2
  57. package/lib/QueryBuilder.ts +12 -7
  58. package/lib/SchemaWatchManager.ts +72 -0
  59. package/lib/TableConfig.d.ts +1 -4
  60. package/lib/TableConfig.d.ts.map +1 -1
  61. package/lib/TableConfig.js +16 -1
  62. package/lib/TableConfig.ts +21 -8
  63. package/package.json +3 -3
  64. package/tests/client/PID.txt +1 -1
  65. package/tests/client/package-lock.json +15 -15
  66. package/tests/client/package.json +1 -1
  67. package/tests/client/tsconfig.json +1 -1
  68. package/tests/client_only_queries.d.ts +1 -1
  69. package/tests/client_only_queries.d.ts.map +1 -1
  70. package/tests/client_only_queries.ts +1 -1
  71. package/tests/isomorphic_queries.d.ts +1 -1
  72. package/tests/isomorphic_queries.d.ts.map +1 -1
  73. package/tests/isomorphic_queries.js +49 -1
  74. package/tests/isomorphic_queries.ts +66 -4
  75. package/tests/manual_test/DBoGenerated.d.ts +398 -0
  76. package/tests/manual_test/index.d.ts +2 -0
  77. package/tests/manual_test/index.d.ts.map +1 -0
  78. package/tests/{config_test2 → manual_test}/index.html +14 -23
  79. package/tests/{config_test2 → manual_test}/index.js +21 -15
  80. package/tests/{config_test2 → manual_test}/index.ts +22 -17
  81. package/tests/{config_test2 → manual_test}/init.sql +36 -5
  82. package/tests/manual_test/package-lock.json +2483 -0
  83. package/tests/{config_test2 → manual_test}/package.json +6 -7
  84. package/tests/manual_test/tsconfig.json +21 -0
  85. package/tests/server/DBoGenerated.d.ts +70 -0
  86. package/tests/server/index.js +29 -2
  87. package/tests/server/index.ts +30 -4
  88. package/tests/server/init.sql +25 -0
  89. package/tests/server/package-lock.json +5 -5
  90. package/tests/config_test2/DBoGenerated.d.ts +0 -135
  91. package/tests/config_test2/tsconfig.json +0 -21
package/lib/DboBuilder.ts CHANGED
@@ -49,8 +49,8 @@ export type TX = {
49
49
  (t: TxCB): Promise<(any | void)>;
50
50
  }
51
51
 
52
- type TableHandlers = {
53
- [key: string]: Partial<TableHandler>;
52
+ export type TableHandlers = {
53
+ [key: string]: Partial<TableHandler> | TableHandler;
54
54
  }
55
55
 
56
56
  export type DBHandlerServer =
@@ -76,12 +76,14 @@ export const getUpdateFilter = (args: { filter?: AnyObject; forcedFilter?: AnyOb
76
76
  import { get } from "./utils";
77
77
  import { getNewQuery, makeQuery, COMPUTED_FIELDS, SelectItem, FieldSpec, asNameAlias, SelectItemBuilder, FUNCTIONS, parseFunction, parseFunctionObject } from "./QueryBuilder";
78
78
  import {
79
- Join, Prostgles, DB
79
+ Join, Prostgles, DB, isSuperUser
80
80
  } from "./Prostgles";
81
81
  import {
82
82
  TableRule, UpdateRule, SyncRule, PublishParser, ValidateRow, ValidateUpdateRow, PublishAllOrNothing, PublishParams, DeleteRule
83
83
  } from "./PublishParser";
84
84
  import { PubSubManager, asValue, BasicCallback, pickKeys, omitKeys } from "./PubSubManager";
85
+ import { insertDataParse } from "./DboBuilder/insertDataParse";
86
+ import { insert } from "./DboBuilder/insert";
85
87
 
86
88
  import { parseFilterItem } from "./Filtering";
87
89
 
@@ -242,9 +244,10 @@ export type JoinInfo = {
242
244
 
243
245
  /**
244
246
  * Source and target JOIN ON columns
245
- * e.g.: [source_table_column: string, table_column: string][]
247
+ * Each inner array group will be combined with AND and outer arrays with OR to allow multiple references to the same table
248
+ * e.g.: [[source_table_column: string, table_column: string]]
246
249
  */
247
- on: [string, string][],
250
+ on: [string, string][][],
248
251
 
249
252
  /**
250
253
  * Source table name
@@ -335,7 +338,7 @@ export type ValidatedTableRules = CommonTableRules & {
335
338
  }
336
339
 
337
340
  /* DEBUG CLIENT ERRORS HERE */
338
- function makeErr(err: any, localParams?: LocalParams, view?: ViewHandler, allowedKeys?: string[]){
341
+ export function makeErr(err: any, localParams?: LocalParams, view?: ViewHandler, allowedKeys?: string[]){
339
342
  // console.trace(err)
340
343
  if(process.env.TEST_TYPE || process.env.PRGL_DEBUG) {
341
344
  console.trace(err)
@@ -367,10 +370,10 @@ export type EXISTS_KEY = typeof EXISTS_KEYS[number];
367
370
 
368
371
  const FILTER_FUNCS = FUNCTIONS.filter(f => f.canBeUsedForFilter);
369
372
 
370
- function parseError(e: any){
373
+ export function parseError(e: any){
371
374
 
372
375
  // console.trace("INTERNAL ERROR: ", e);
373
- let res = (!Object.keys(e || {}).length? e : (e && e.toString)? e.toString() : e);
376
+ let res = e instanceof Error? e.message : (!Object.keys(e || {}).length? e : (e && e.toString)? e.toString() : e);
374
377
  if(isPlainObject(e)) res = JSON.stringify(e, null, 2)
375
378
  return res;
376
379
  }
@@ -627,6 +630,10 @@ export class ViewHandler {
627
630
  // while (!result && searchedTables.length <= this.joins.length * 2){
628
631
 
629
632
  // }
633
+
634
+ const getJoinCondition = (on: Record<string, string>[], leftTable: string, rightTable: string) => {
635
+ return on.map(cond => Object.keys(cond).map(lKey => `${leftTable}.${lKey} = ${rightTable}.${cond[lKey]}`).join("\nAND ")).join(" OR ")
636
+ }
630
637
 
631
638
  let toOne = true,
632
639
  query = this.joins.map(({ tables, on, type }, i) => {
@@ -635,7 +642,7 @@ export class ViewHandler {
635
642
  }
636
643
  const tl = `tl${startAlias + i}`,
637
644
  tr = `tr${startAlias + i}`;
638
- return `FROM ${tables[0]} ${tl} ${isInner? "INNER" : "LEFT"} JOIN ${tables[1]} ${tr} ON ${Object.keys(on).map(lKey => `${tl}.${lKey} = ${tr}.${on[lKey]}`).join("\nAND ")}`;
645
+ return `FROM ${tables[0]} ${tl} ${isInner? "INNER" : "LEFT"} JOIN ${tables[1]} ${tr} ON ${getJoinCondition(on, tl, tr)}`;
639
646
  }).join("\n");
640
647
  return { query, toOne: false }
641
648
  }
@@ -675,21 +682,26 @@ export class ViewHandler {
675
682
  const jo = this.joins.find(j => j.tables.includes(t1) && j.tables.includes(t2));
676
683
  if(!jo) throw `Joining ${t1} <-> ${t2} dissallowed or missing`;;
677
684
 
678
- let on: [string, string][] = [];
679
-
680
- Object.keys(jo.on).map(leftKey => {
681
- const rightKey = jo.on[leftKey];
682
-
683
- /* Left table is joining on keys */
684
- if(jo.tables[0] === t1){
685
- on.push([leftKey, rightKey])
686
-
687
- /* Left table is joining on values */
688
- } else {
689
- on.push([rightKey, leftKey])
685
+ let on: [string, string][][] = [];
690
686
 
691
- }
692
- });
687
+ jo.on.map(cond => {
688
+ let condArr: [string, string][] = [];
689
+ Object.keys(cond).map(leftKey => {
690
+ const rightKey = cond[leftKey];
691
+
692
+ /* Left table is joining on keys */
693
+ if(jo.tables[0] === t1){
694
+ condArr.push([leftKey, rightKey])
695
+
696
+ /* Left table is joining on values */
697
+ } else {
698
+ condArr.push([rightKey, leftKey])
699
+
700
+ }
701
+ });
702
+ on.push(condArr);
703
+ })
704
+
693
705
 
694
706
  return {
695
707
  source,
@@ -1065,7 +1077,8 @@ export class ViewHandler {
1065
1077
  } catch(e){
1066
1078
  // console.trace(e)
1067
1079
  if(localParams && localParams.testRule) throw e;
1068
- throw { err: parseError(e), msg: `Issue with dbo.${this.name}.find(${JSON.stringify(filter || {}, null, 2)}, ${JSON.stringify(selectParams || {}, null, 2)})` };
1080
+ // ${JSON.stringify(filter || {}, null, 2)}, ${JSON.stringify(selectParams || {}, null, 2)}
1081
+ throw { err: parseError(e), msg: `Issue with dbo.${this.name}.find()`, args: { filter, selectParams} };
1069
1082
  }
1070
1083
  }
1071
1084
 
@@ -1295,8 +1308,9 @@ export class ViewHandler {
1295
1308
  let tableAlias = asName(ji < paths.length - 1? `jd${ji}` : table);
1296
1309
  let prevTableAlias = asName(ji? `jd${ji - 1}` : thisTable);
1297
1310
 
1298
- let cond = `${jp.on.map(([c1, c2]) =>
1299
- `${prevTableAlias}.${asName(c1)} = ${tableAlias}.${asName(c2)}`).join("\n AND ")
1311
+ let cond = `${jp.on.map(c => {
1312
+ return c.map(([c1, c2]) => `${prevTableAlias}.${asName(c1)} = ${tableAlias}.${asName(c2)}` ).join(" AND ")
1313
+ }).join("\n OR ")
1300
1314
  }`;
1301
1315
 
1302
1316
  let j = `SELECT 1 \n` +
@@ -1907,7 +1921,7 @@ export class ViewHandler {
1907
1921
  }
1908
1922
  }
1909
1923
 
1910
- function isPojoObject<T>(obj: T): obj is Record<string, any> {
1924
+ export function isPojoObject<T>(obj: T): obj is Record<string, any> {
1911
1925
  if(obj && (typeof obj !== "object" || Array.isArray(obj) || obj instanceof Date)){
1912
1926
  return false;
1913
1927
  }
@@ -2305,380 +2319,10 @@ export class TableHandler extends ViewHandler {
2305
2319
  return { data, allowedCols: this.columns.filter(c => dataKeys.includes(c.name)).map(c => c.name) }
2306
2320
  }
2307
2321
 
2308
-
2309
- private async insertDataParse(data: (AnyObject | AnyObject[]), param2?: InsertParams, param3_unused?: undefined, tableRules?: TableRule, _localParams?: LocalParams): Promise<{
2310
- data?: AnyObject | AnyObject[];
2311
- insertResult?: AnyObject | AnyObject[];
2312
- }>{
2313
- const localParams = _localParams || {};
2314
- let dbTX = localParams?.dbTX || this.dbTX;
2315
-
2316
- const isMultiInsert = Array.isArray(data);
2317
- const getExtraKeys = (d: AnyObject)=> Object.keys(d).filter(k => !this.columns.find(c => c.name === k));
2318
-
2319
- /* Nested insert is not allowed for the file table */
2320
- const isNestedInsert = this.is_media? false : (Array.isArray(data)? data : [data]).some(d => getExtraKeys(d).length);
2321
-
2322
- /**
2323
- * Make sure nested insert uses a transaction
2324
- */
2325
- if(isNestedInsert && !dbTX){
2326
- return {
2327
- insertResult: await this.dboBuilder.getTX((dbTX) =>
2328
- (dbTX[this.name] as TableHandler).insert(
2329
- data,
2330
- param2,
2331
- param3_unused,
2332
- tableRules,
2333
- {dbTX, ...localParams}
2334
- )
2335
- )
2336
- }
2337
- }
2338
- // if(!dbTX && this.t) dbTX = this.d;
2339
-
2340
- const preValidate = tableRules?.insert?.preValidate,
2341
- validate = tableRules?.insert?.validate;
2342
-
2343
- let _data = await Promise.all((Array.isArray(data)? data : [data]).map(async row => {
2344
- if(preValidate){
2345
- row = await preValidate(row);
2346
- }
2347
-
2348
- const dataKeys = Object.keys(row);
2349
- const extraKeys = getExtraKeys(row);
2350
-
2351
- /* Upload file then continue insert */
2352
- if(this.is_media){
2353
- if(!this.dboBuilder.prostgles?.fileManager) throw "fileManager not set up";
2354
- const { data, name } = row;
2355
-
2356
- if(dataKeys.length !== 2) throw "Expecting only two properties: { name: string; data: File }";
2357
-
2358
- // if(!Buffer.isBuffer(data)) throw "data is not of type Buffer"
2359
- if(!data) throw "data not provided"
2360
- if(typeof name !== "string"){
2361
- throw "name is not of type string"
2362
- }
2363
-
2364
- const media_id = (await this.db.oneOrNone("SELECT gen_random_uuid() as name")).name;
2365
- const type = await this.dboBuilder.prostgles.fileManager.getMIME(data, name)
2366
- const media_name = `${media_id}.${type.ext}`;
2367
- let media: Media = {
2368
- id: media_id,
2369
- name: media_name,
2370
- original_name: name,
2371
- extension: type.ext,
2372
- content_type: type.mime
2373
- }
2374
-
2375
- if(validate){
2376
- media = await validate(media);
2377
- }
2378
-
2379
- const _media: Media = await this.dboBuilder.prostgles.fileManager.uploadAsMedia({
2380
- item: {
2381
- data,
2382
- name: media.name ?? "????",
2383
- content_type: media.content_type as any
2384
- },
2385
- // imageCompression: {
2386
- // inside: {
2387
- // width: 1100,
2388
- // height: 630
2389
- // }
2390
- // }
2391
- });
2392
-
2393
- return {
2394
- ...media,
2395
- ..._media,
2396
- };
2397
-
2398
- /* Potentially a nested join */
2399
- } else if(extraKeys.length){
2400
-
2401
- /* Ensure we're using the same transaction */
2402
- const _this = this.t? this : dbTX![this.name] as TableHandler;
2403
-
2404
- let rootData = Array.isArray(data)? data.map(d => omitKeys(d, extraKeys)) : omitKeys(data, extraKeys);
2405
-
2406
- let insertedChildren: AnyObject[];
2407
- let targetTableRules: TableRule;
2408
-
2409
- const fullRootResult = await _this.insert(rootData, { returning: "*" }, undefined, tableRules, localParams);
2410
- let returnData: AnyObject | undefined;
2411
- const returning = param2?.returning;
2412
- if(returning){
2413
- returnData = {}
2414
- const returningItems = await this.prepareReturning(returning, this.parseFieldFilter(tableRules?.insert?.returningFields));
2415
- returningItems.filter(s => s.selected).map(rs => {
2416
- returnData![rs.alias] = fullRootResult[rs.alias];
2417
- })
2418
- }
2419
-
2420
- await Promise.all(extraKeys.map(async targetTable => {
2421
- const childDataItems = Array.isArray(row[targetTable])? row[targetTable] : [row[targetTable]];
2422
-
2423
- /* Must be allowed to insert into media table */
2424
- const canInsert = async (tbl: string) => {
2425
- const childRules = await this.dboBuilder.publishParser?.getValidatedRequestRuleWusr({ tableName: tbl, command: "insert", localParams });
2426
- if(!childRules || !childRules.insert) throw "Dissallowed nested insert into table " + childRules;
2427
- return childRules;
2428
- }
2429
-
2430
- // console.log(JSON.stringify(this.dboBuilder.joinPaths, null, 2))
2431
- const jp = this.dboBuilder.joinPaths.find(jp => jp.t1 === this.name && jp.t2 === targetTable);
2432
- if(!jp) throw `Could not find a valid table for the nested data { ${targetTable} } `;
2433
-
2434
- const thisInfo = await this.getInfo();
2435
- const childInsert = async (cdata: AnyObject | AnyObject[], tableName: string) => {
2436
- // console.log("childInsert", {data, tableName})
2437
- if(!cdata || !dbTX?.[tableName] || !("insert" in dbTX[tableName])) throw "childInsertErr: Child table handler missing for: " + tableName;
2438
-
2439
- const tableRules = await canInsert(tableName);
2440
-
2441
- if(thisInfo.has_media === "one" && thisInfo.media_table_name === tableName && Array.isArray(cdata) && cdata.length > 1){
2442
- throw "Constraint check fail: Cannot insert more than one record into " + JSON.stringify(tableName);
2443
- }
2444
- return Promise.all(
2445
- (Array.isArray(cdata)? cdata : [cdata])
2446
- .map(m => (dbTX![tableName] as TableHandler)
2447
- .insert(m, { returning: "*" }, undefined, tableRules, localParams)
2448
- .catch(e => {
2449
- console.trace({ childInsertErr: e })
2450
- return Promise.reject({ childInsertErr: e });
2451
- })
2452
- )
2453
- );
2454
- }
2455
-
2456
- const { path } = jp;
2457
- const [tbl1, tbl2, tbl3] = path;
2458
- targetTableRules = await canInsert(targetTable); // tbl3
2459
-
2460
- const cols2 = this.dboBuilder.dbo[tbl2].columns || [];
2461
- if(!this.dboBuilder.dbo[tbl2]) throw "Invalid/disallowed table: " + tbl2;
2462
- const colsRefT1 = cols2?.filter(c => c.references?.cols.length === 1 && c.references?.ftable === tbl1);
2463
-
2464
-
2465
- if(!path.length) {
2466
- throw "Nested inserts join path not found for " + [this.name, targetTable];
2467
- } else if(path.length === 2){
2468
- if(targetTable !== tbl2) throw "Did not expect this";
2469
-
2470
- if(!colsRefT1.length) throw `Target table ${tbl2} does not reference any columns from the root table ${this.name}. Cannot do nested insert`;
2471
-
2472
- // console.log(childDataItems, JSON.stringify(colsRefT1, null, 2))
2473
- insertedChildren = await childInsert(
2474
- childDataItems.map((d: AnyObject) => {
2475
- let result = {...d};
2476
- colsRefT1.map(col => {
2477
- result[col.references!.cols[0]] = fullRootResult[col.references!.fcols[0]]
2478
- })
2479
- return result;
2480
- }),
2481
- targetTable
2482
- );
2483
- // console.log({ insertedChildren })
2484
-
2485
- } else if(path.length === 3){
2486
- if(targetTable !== tbl3) throw "Did not expect this";
2487
- const colsRefT3 = cols2?.filter(c => c.references?.cols.length === 1 && c.references?.ftable === tbl3);
2488
- if(!colsRefT1.length || !colsRefT3.length) throw "Incorrectly referenced or missing columns for nested insert";
2489
-
2490
- if(targetTable !== this.dboBuilder.prostgles.fileManager?.tableName){
2491
- throw "Only media allowed to have nested inserts more than 2 tables apart"
2492
- }
2493
-
2494
- /* We expect tbl2 to have only 2 columns (media_id and foreign_id) */
2495
- if(!cols2 || cols2.find(c => !["media_id", "foreign_id"].includes(c.name))){
2496
- throw "Second joining table not of expected format";
2497
- }
2498
-
2499
- insertedChildren = await childInsert(childDataItems, targetTable);
2500
-
2501
- /* Insert in key_lookup table */
2502
- await Promise.all(insertedChildren.map(async t3Child => {
2503
- let tbl2Row: AnyObject = {};
2504
-
2505
- colsRefT3.map(col => {
2506
- tbl2Row[col.name] = t3Child[col.references!.fcols[0]];
2507
- })
2508
- colsRefT1.map(col => {
2509
- tbl2Row[col.name] = fullRootResult[col.references!.fcols[0]];
2510
- })
2511
- // console.log({ rootResult, tbl2Row, t3Child, colsRefT3, colsRefT1, t: this.t?.ctx?.start });
2512
-
2513
- await childInsert(tbl2Row, tbl2);//.then(() => {});
2514
- }));
2515
-
2516
- } else {
2517
- console.error(JSON.stringify({ path, thisTable: this.name, targetTable }, null, 2));
2518
- throw "Unexpected path for Nested inserts";
2519
- }
2520
-
2521
- /* Return also the nested inserted data */
2522
- if(targetTableRules && insertedChildren?.length && returning){
2523
- const targetTableHandler = dbTX![targetTable] as TableHandler;
2524
- const targetReturning = await targetTableHandler.prepareReturning("*", targetTableHandler.parseFieldFilter(targetTableRules?.insert?.returningFields));
2525
- let clientTargetInserts = insertedChildren.map(d => {
2526
- let _d = { ...d };
2527
- let res: AnyObject = {};
2528
- targetReturning.map(r => {
2529
- res[r.alias] = _d[r.alias]
2530
- });
2531
- return res;
2532
- });
2533
-
2534
- returnData![targetTable] = clientTargetInserts.length === 1? clientTargetInserts[0] : clientTargetInserts;
2535
- }
2536
- }));
2537
-
2538
- return returnData
2539
- }
2540
-
2541
- return row;
2542
- }));
2543
-
2544
- let result = isMultiInsert? _data : _data[0];
2545
- // if(validate && !isNestedInsert){
2546
- // result = isMultiInsert? await Promise.all(_data.map(async d => await validate({ ...d }))) : await validate({ ..._data[0] });
2547
- // }
2548
- let res = isNestedInsert?
2549
- { insertResult: result } :
2550
- { data: result };
2551
-
2552
- return res;
2553
- }
2554
-
2322
+ insertDataParse = insertDataParse;
2555
2323
  async insert(rowOrRows: (AnyObject | AnyObject[]), param2?: InsertParams, param3_unused?: undefined, tableRules?: TableRule, _localParams?: LocalParams): Promise<any | any[] | boolean>{
2556
- const localParams = _localParams || {};
2557
- const {dbTX} = localParams
2558
- try {
2559
-
2560
- const { returning, onConflictDoNothing, fixIssues = false } = param2 || {};
2561
- const { testRule = false, returnQuery = false } = localParams || {};
2562
-
2563
- let returningFields: FieldFilter | undefined,
2564
- forcedData: AnyObject | undefined,
2565
- fields: FieldFilter | undefined;
2566
-
2567
- if(tableRules){
2568
- if(!tableRules.insert) throw "insert rules missing for " + this.name;
2569
- returningFields = tableRules.insert.returningFields;
2570
- forcedData = tableRules.insert.forcedData;
2571
- fields = tableRules.insert.fields;
2572
-
2573
- /* If no returning fields specified then take select fields as returning */
2574
- if(!returningFields) returningFields = get(tableRules, "select.fields") || get(tableRules, "insert.fields");
2575
-
2576
- if(!fields) throw ` invalid insert rule for ${this.name} -> fields missing `;
2577
-
2578
- /* Safely test publish rules */
2579
- if(testRule){
2580
- // if(this.is_media && tableRules.insert.preValidate) throw "Media table cannot have a preValidate. It already is used internally by prostgles for file upload";
2581
- await this.validateViewRules({ fields, returningFields, forcedFilter: forcedData, rule: "insert"});
2582
- if(forcedData) {
2583
- const keys = Object.keys(forcedData);
2584
- if(keys.length){
2585
- try {
2586
- const colset = new pgp.helpers.ColumnSet(this.columns.filter(c => keys.includes(c.name)).map(c => ({ name: c.name, cast: c.udt_name === "uuid"? c.udt_name : undefined }))),
2587
- values = pgp.helpers.values(forcedData, colset),
2588
- colNames = this.prepareSelect(keys, this.column_names);
2589
- await this.db.any("EXPLAIN INSERT INTO " + this.escapedName + " (${colNames:raw}) SELECT * FROM ( VALUES ${values:raw} ) t WHERE FALSE;", { colNames, values })
2590
- } catch(e){
2591
- throw "\nissue with forcedData: \nVALUE: " + JSON.stringify(forcedData, null, 2) + "\nERROR: " + e;
2592
- }
2593
- }
2594
- }
2595
-
2596
- return true;
2597
- }
2598
- }
2599
-
2600
- let conflict_query = "";
2601
- if(typeof onConflictDoNothing === "boolean" && onConflictDoNothing){
2602
- conflict_query = " ON CONFLICT DO NOTHING ";
2603
- }
2604
-
2605
- if(param2){
2606
- const good_params = ["returning", "multi", "onConflictDoNothing", "fixIssues"];
2607
- const bad_params = Object.keys(param2).filter(k => !good_params.includes(k));
2608
- if(bad_params && bad_params.length) throw "Invalid params: " + bad_params.join(", ") + " \n Expecting: " + good_params.join(", ");
2609
- }
2610
-
2611
- if(!rowOrRows) rowOrRows = {}; //throw "Provide data in param1";
2612
- let returningSelect = this.makeReturnQuery(await this.prepareReturning(returning, this.parseFieldFilter(returningFields)));
2613
- const makeQuery = async (_row: AnyObject | undefined, isOne = false) => {
2614
- let row = { ..._row };
2615
-
2616
- if(!isPojoObject(row)) {
2617
- console.trace(row)
2618
- throw "\ninvalid insert data provided -> " + JSON.stringify(row);
2619
- }
2620
-
2621
- const { data, allowedCols } = this.validateNewData({ row, forcedData, allowedFields: fields, tableRules, fixIssues });
2622
- let _data = { ...data };
2623
-
2624
- let insertQ = "";
2625
- if(!Object.keys(_data).length) insertQ = `INSERT INTO ${asName(this.name)} DEFAULT VALUES `;
2626
- else insertQ = await this.colSet.getInsertQuery(_data, allowedCols, tableRules?.insert?.validate) // pgp.helpers.insert(_data, columnSet);
2627
- return insertQ + conflict_query + returningSelect;
2628
- };
2629
-
2630
- let query = "";
2631
- let queryType: keyof pgPromise.ITask<{}> = "none";
2632
-
2633
- /**
2634
- * If media it will: upload file and continue insert
2635
- * If nested insert it will: make separate inserts and not continue main insert
2636
- */
2637
- const insRes = await this.insertDataParse(rowOrRows, param2, param3_unused, tableRules, localParams);
2638
- const { data, insertResult } = insRes;
2639
- if("insertResult" in insRes){
2640
- return insertResult;
2641
- }
2642
-
2643
- if(Array.isArray(data)){
2644
- // if(returning) throw "Sorry but [returning] is dissalowed for multi insert";
2645
- let queries = await Promise.all(data.map(async p => {
2646
- const q = await makeQuery(p);
2647
- return q;
2648
- }));
2649
-
2650
- query = pgp.helpers.concat(queries);
2651
- if(returning) queryType = "many";
2652
- } else {
2653
- query = await makeQuery(data, true);
2654
- if(returning) queryType = "one";
2655
- }
2656
-
2657
- if(returnQuery) return query;
2658
- let result;
2659
-
2660
- if(this.dboBuilder.prostgles.opts.DEBUG_MODE){
2661
- console.log(this.t?.ctx?.start, "insert in " + this.name, data);
2662
- }
2663
-
2664
- const tx = dbTX?.[this.name]?.t || this.t;
2665
-
2666
- const allowedFieldKeys = this.parseFieldFilter(fields);
2667
- if(tx) {
2668
- result = (tx as any)[queryType](query).catch((err: any) => makeErr(err, localParams, this, allowedFieldKeys));
2669
- } else {
2670
- result = this.db.tx(t => (t as any)[queryType](query)).catch(err => makeErr(err, localParams, this, allowedFieldKeys));
2671
- }
2672
-
2673
- return result;
2674
- } catch(e){
2675
- if(localParams && localParams.testRule) throw e;
2676
- throw { err: parseError(e), msg: `Issue with dbo.${this.name}.insert(
2677
- ${JSON.stringify(rowOrRows || {}, null, 2)},
2678
- ${JSON.stringify(param2 || {}, null, 2)}
2679
- )` };
2680
- }
2681
- };
2324
+ return insert.bind(this)(rowOrRows, param2, param3_unused, tableRules, _localParams)
2325
+ }
2682
2326
 
2683
2327
  prepareReturning = async (returning: Select | undefined, allowedFields: string[]): Promise<SelectItem[]> => {
2684
2328
  let result: SelectItem[] = [];
@@ -2928,9 +2572,9 @@ export class DboBuilder {
2928
2572
  if(!this._pubSubManager){
2929
2573
  let onSchemaChange;
2930
2574
 
2931
- if(this.prostgles.opts.watchSchema && this.prostgles.opts.watchSchemaType === "events"){
2575
+ if(this.prostgles.opts.watchSchema && this.prostgles.opts.watchSchemaType === "DDL_trigger"){
2932
2576
  if(!this.prostgles.isSuperUser){
2933
- console.warn(`watchSchemaType "events" cannot be used because db user is not a superuser. Will fallback to watchSchemaType "queries" `)
2577
+ console.warn(`watchSchemaType "events" cannot be used because db user is not a superuser. Will fallback to watchSchemaType "prostgles_queries" `)
2934
2578
  } else {
2935
2579
  onSchemaChange = (event: { command: string; query: string }) => {
2936
2580
  this.prostgles.onSchemaChange(event)
@@ -2949,7 +2593,10 @@ export class DboBuilder {
2949
2593
  console.warn(`subscribe and sync cannot be used because db user is not a superuser `)
2950
2594
  }
2951
2595
  }
2952
- if(!this._pubSubManager) throw "Could not create this._pubSubManager";
2596
+ if(!this._pubSubManager) {
2597
+ console.trace("Could not create this._pubSubManager")
2598
+ throw "Could not create this._pubSubManager";
2599
+ }
2953
2600
  return this._pubSubManager;
2954
2601
  }
2955
2602
 
@@ -2981,7 +2628,7 @@ export class DboBuilder {
2981
2628
 
2982
2629
  /* If watchSchema then PubSubManager must be created */
2983
2630
  await this.build();
2984
- if(this.prostgles.opts.watchSchema){
2631
+ if(this.prostgles.opts.watchSchema && (this.prostgles.opts.watchSchemaType === "DDL_trigger" || !this.prostgles.opts.watchSchemaType) && this.prostgles.isSuperUser){
2985
2632
  await this.getPubSubManager()
2986
2633
  }
2987
2634
 
@@ -3008,13 +2655,16 @@ export class DboBuilder {
3008
2655
  async parseJoins(): Promise<JoinPaths> {
3009
2656
  if(this.prostgles.opts.joins){
3010
2657
  let _joins = await this.prostgles.opts.joins;
3011
- let inferredJoins = await getInferredJoins(this.db, this.prostgles.opts.schema);
3012
- if(typeof _joins === "string" && _joins === "inferred"){
2658
+ if(!this.tablesOrViews) throw new Error("Could not create join config. this.tablesOrViews missing");
2659
+ let inferredJoins = await getInferredJoins2(this.tablesOrViews);
2660
+ if(_joins === "inferred"){
3013
2661
  _joins = inferredJoins
3014
2662
  /* If joins are specified then include inferred joins except the explicit tables */
3015
2663
  } else if(Array.isArray(_joins)){
3016
2664
  const joinTables = _joins.map(j => j.tables).flat();
3017
2665
  _joins = _joins.concat(inferredJoins.filter(j => !j.tables.find(t => joinTables.includes(t))))
2666
+ } else if(_joins){
2667
+ throw new Error("Unexpected joins init param. Expecting 'inferred' OR joinConfig but got: " + JSON.stringify(_joins))
3018
2668
  }
3019
2669
  let joins = JSON.parse(JSON.stringify(_joins)) as Join[];
3020
2670
  this.joins = joins;
@@ -3221,7 +2871,7 @@ export class DboBuilder {
3221
2871
  if(typeof this.prostgles.opts.transactions === "string") txKey = this.prostgles.opts.transactions;
3222
2872
  this.dboDefinition += ` ${txKey}: (t: TxCB) => Promise<any | void> ;\n`;
3223
2873
 
3224
- (this.dbo[txKey] as TX) = (cb: TxCB) => this.getTX(cb);
2874
+ (this.dbo[txKey] as unknown as TX) = (cb: TxCB) => this.getTX(cb);
3225
2875
  }
3226
2876
 
3227
2877
  if(!this.dbo.sql){
@@ -3272,15 +2922,16 @@ export class DboBuilder {
3272
2922
 
3273
2923
  if(
3274
2924
  watchSchema &&
3275
- (!this.prostgles.isSuperUser || watchSchemaType === "queries") &&
3276
- (
3277
- ["CREATE", "ALTER", "DROP"].includes(command) ||
3278
-
3279
- // Cover this case: `CREATE TABLE mytable AS SELECT`
3280
- query && query.toLowerCase().replace(/\s\s+/g, ' ').includes("create table")
3281
- )
2925
+ (!this.prostgles.isSuperUser || watchSchemaType === "prostgles_queries")
3282
2926
  ){
3283
- this.prostgles.onSchemaChange({ command, query })
2927
+ if(["CREATE", "ALTER", "DROP"].includes(command)){
2928
+ this.prostgles.onSchemaChange({ command, query })
2929
+ } else if(query) {
2930
+ const cleanedQuery = query.toLowerCase().replace(/\s\s+/g, ' ');
2931
+ if(PubSubManager.SCHEMA_ALTERING_QUERIES.some(q => cleanedQuery.includes(q.toLowerCase()))){
2932
+ this.prostgles.onSchemaChange({ command, query })
2933
+ }
2934
+ }
3284
2935
  }
3285
2936
 
3286
2937
  if(command === "LISTEN"){
@@ -3870,50 +3521,84 @@ function sqlErrCodeToMsg(code: string){
3870
3521
  JSON.stringify([...THE_table_$0.rows].map(t => [...t.children].map(u => u.innerText)).filter((d, i) => i && d.length > 1).reduce((a, v)=>({ ...a, [v[0]]: v[1] }), {}))
3871
3522
  */
3872
3523
  }
3873
- async function getInferredJoins(db: DB, schema: string = "public"): Promise<Join[]>{
3524
+
3525
+
3526
+ async function getInferredJoins2(schema: TableSchema[]): Promise<Join[]>{
3874
3527
  let joins: Join[] = [];
3875
- let res = await db.any(`SELECT
3876
- tc.table_schema,
3877
- tc.constraint_name,
3878
- tc.table_name,
3879
- kcu.column_name,
3880
- ccu.table_schema AS foreign_table_schema,
3881
- ccu.table_name AS foreign_table_name,
3882
- ccu.column_name AS foreign_column_name,
3883
- tc.constraint_type IN ('UNIQUE', 'PRIMARY KEY') as foreign_is_unique
3884
- FROM
3885
- information_schema.table_constraints AS tc
3886
- JOIN information_schema.key_column_usage AS kcu
3887
- ON tc.constraint_name = kcu.constraint_name
3888
- AND tc.table_schema = kcu.table_schema
3889
- JOIN information_schema.constraint_column_usage AS ccu
3890
- ON ccu.constraint_name = tc.constraint_name
3891
- AND ccu.table_schema = tc.table_schema
3892
- WHERE tc.table_schema=` + "${schema}" + `
3893
- AND tc.constraint_type = 'FOREIGN KEY'
3894
- AND tc.table_name <> ccu.table_name -- Exclude self-referencing tables
3895
- `, { schema });
3896
-
3897
- res.map((d: any) => {
3898
- let eIdx = joins.findIndex(j => j.tables.includes(d.table_name) && j.tables.includes(d.foreign_table_name));
3899
- let existing = joins[eIdx];
3528
+ const upsertJoin = (t1: string, t2: string, cols: { col1: string; col2: string }[]) => {
3529
+ let existingIdx = joins.findIndex(j => j.tables.slice(0).sort().join() === [t1, t2].sort().join());
3530
+ let existing = joins[existingIdx];
3531
+ const normalCond = cols.reduce((a, v) => ({ ...a, [v.col1]: v.col2 }), {});
3532
+ const revertedCond = cols.reduce((a, v) => ({ ...a, [v.col2]: v.col1 }), {});
3900
3533
  if(existing){
3901
- if(existing.tables[0] === d.table_name){
3902
- existing.on = { ...existing.on, [d.column_name]: d.foreign_column_name }
3903
- } else {
3904
- existing.on = { ...existing.on, [d.foreign_column_name]: d.column_name }
3534
+ const cond = existing.tables[0] === t1? normalCond : revertedCond;
3535
+ /** Avoid duplicates */
3536
+ if(!existing.on.some(_cond => JSON.stringify(_cond) === JSON.stringify(cond))){
3537
+ existing.on.push(cond);
3538
+ joins[existingIdx] = existing;
3905
3539
  }
3906
- joins[eIdx] = existing;
3907
3540
  } else {
3908
3541
  joins.push({
3909
- tables: [d.table_name, d.foreign_table_name],
3910
- on: {
3911
- [d.column_name]: d.foreign_column_name
3912
- },
3542
+ tables: [t1, t2],
3543
+ on: [normalCond],
3913
3544
  type: "many-many"
3914
3545
  })
3915
3546
  }
3916
- });
3917
-
3547
+ }
3548
+ schema.map(tov => {
3549
+ tov.columns.map(col => {
3550
+ if(col.references){
3551
+ const r = col.references;
3552
+ upsertJoin(tov.name, r.ftable, r.cols.map((c, i) => ({ col1: c, col2: r.fcols[i] })))
3553
+ }
3554
+ })
3555
+ })
3918
3556
  return joins;
3919
- }
3557
+ }
3558
+ // async function getInferredJoins(db: DB, schema: string = "public"): Promise<Join[]>{
3559
+ // let joins: Join[] = [];
3560
+ // let res = await db.any(`SELECT
3561
+ // tc.table_schema,
3562
+ // tc.constraint_name,
3563
+ // tc.table_name,
3564
+ // kcu.column_name,
3565
+ // ccu.table_schema AS foreign_table_schema,
3566
+ // ccu.table_name AS foreign_table_name,
3567
+ // ccu.column_name AS foreign_column_name,
3568
+ // tc.constraint_type IN ('UNIQUE', 'PRIMARY KEY') as foreign_is_unique
3569
+ // FROM
3570
+ // information_schema.table_constraints AS tc
3571
+ // JOIN information_schema.key_column_usage AS kcu
3572
+ // ON tc.constraint_name = kcu.constraint_name
3573
+ // AND tc.table_schema = kcu.table_schema
3574
+ // JOIN information_schema.constraint_column_usage AS ccu
3575
+ // ON ccu.constraint_name = tc.constraint_name
3576
+ // AND ccu.table_schema = tc.table_schema
3577
+ // WHERE tc.table_schema=` + "${schema}" + `
3578
+ // AND tc.constraint_type = 'FOREIGN KEY'
3579
+ // AND tc.table_name <> ccu.table_name -- Exclude self-referencing tables
3580
+ // `, { schema });
3581
+
3582
+ // res.map((d: any) => {
3583
+ // let eIdx = joins.findIndex(j => j.tables.includes(d.table_name) && j.tables.includes(d.foreign_table_name));
3584
+ // let existing = joins[eIdx];
3585
+ // if(existing){
3586
+ // if(existing.tables[0] === d.table_name){
3587
+ // existing.on = { ...existing.on, [d.column_name]: d.foreign_column_name }
3588
+ // } else {
3589
+ // existing.on = { ...existing.on, [d.foreign_column_name]: d.column_name }
3590
+ // }
3591
+ // joins[eIdx] = existing;
3592
+ // } else {
3593
+ // joins.push({
3594
+ // tables: [d.table_name, d.foreign_table_name],
3595
+ // on: {
3596
+ // [d.column_name]: d.foreign_column_name
3597
+ // },
3598
+ // type: "many-many"
3599
+ // })
3600
+ // }
3601
+ // });
3602
+
3603
+ // return joins;
3604
+ // }