prostgles-server 3.0.64 → 3.0.67

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 (123) hide show
  1. package/dist/AuthHandler.d.ts +11 -11
  2. package/dist/AuthHandler.d.ts.map +1 -1
  3. package/dist/DBSchemaBuilder.d.ts +3 -3
  4. package/dist/DBSchemaBuilder.d.ts.map +1 -1
  5. package/dist/DboBuilder/QueryBuilder/Functions.d.ts +3 -3
  6. package/dist/DboBuilder/QueryBuilder/Functions.d.ts.map +1 -1
  7. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts +3 -3
  8. package/dist/DboBuilder/QueryBuilder/QueryBuilder.d.ts.map +1 -1
  9. package/dist/DboBuilder/TableHandler.d.ts +1 -1
  10. package/dist/DboBuilder/TableHandler.d.ts.map +1 -1
  11. package/dist/DboBuilder/ViewHandler.d.ts +5 -4
  12. package/dist/DboBuilder/ViewHandler.d.ts.map +1 -1
  13. package/dist/DboBuilder/ViewHandler.js +7 -134
  14. package/dist/DboBuilder/ViewHandler.js.map +1 -1
  15. package/dist/DboBuilder/delete.js +1 -1
  16. package/dist/DboBuilder/delete.js.map +1 -1
  17. package/dist/DboBuilder/insert.js +1 -1
  18. package/dist/DboBuilder/insert.js.map +1 -1
  19. package/dist/DboBuilder/insertDataParse.js +1 -1
  20. package/dist/DboBuilder/insertDataParse.js.map +1 -1
  21. package/dist/DboBuilder/runSQL.js +1 -1
  22. package/dist/DboBuilder/runSQL.js.map +1 -1
  23. package/dist/DboBuilder/subscribe.d.ts +11 -0
  24. package/dist/DboBuilder/subscribe.d.ts.map +1 -0
  25. package/dist/DboBuilder/subscribe.js +190 -0
  26. package/dist/DboBuilder/subscribe.js.map +1 -0
  27. package/dist/DboBuilder/update.js +1 -1
  28. package/dist/DboBuilder/update.js.map +1 -1
  29. package/dist/DboBuilder.d.ts +24 -24
  30. package/dist/DboBuilder.d.ts.map +1 -1
  31. package/dist/DboBuilder.js +1 -1
  32. package/dist/DboBuilder.js.map +1 -1
  33. package/dist/FileManager.d.ts +6 -6
  34. package/dist/FileManager.d.ts.map +1 -1
  35. package/dist/FileManager.js +17 -17
  36. package/dist/FileManager.js.map +1 -1
  37. package/dist/Filtering.d.ts +1 -1
  38. package/dist/Filtering.d.ts.map +1 -1
  39. package/dist/PostgresNotifListenManager.d.ts +1 -1
  40. package/dist/PostgresNotifListenManager.d.ts.map +1 -1
  41. package/dist/Prostgles.d.ts +14 -14
  42. package/dist/Prostgles.d.ts.map +1 -1
  43. package/dist/Prostgles.js +12 -12
  44. package/dist/Prostgles.js.map +1 -1
  45. package/dist/{PubSubManager.d.ts → PubSubManager/PubSubManager.d.ts} +24 -21
  46. package/dist/PubSubManager/PubSubManager.d.ts.map +1 -0
  47. package/dist/PubSubManager/PubSubManager.js +754 -0
  48. package/dist/PubSubManager/PubSubManager.js.map +1 -0
  49. package/dist/PubSubManager/initPubSubManager.d.ts +3 -0
  50. package/dist/PubSubManager/initPubSubManager.d.ts.map +1 -0
  51. package/dist/PubSubManager/initPubSubManager.js +616 -0
  52. package/dist/PubSubManager/initPubSubManager.js.map +1 -0
  53. package/dist/PublishParser.d.ts +32 -32
  54. package/dist/PublishParser.d.ts.map +1 -1
  55. package/dist/PublishParser.js +1 -1
  56. package/dist/PublishParser.js.map +1 -1
  57. package/dist/SchemaWatch.d.ts +1 -1
  58. package/dist/SchemaWatch.d.ts.map +1 -1
  59. package/dist/SyncReplication.d.ts +6 -6
  60. package/dist/SyncReplication.d.ts.map +1 -1
  61. package/dist/SyncReplication.js +1 -1
  62. package/dist/SyncReplication.js.map +1 -1
  63. package/dist/TableConfig.d.ts +21 -21
  64. package/dist/TableConfig.d.ts.map +1 -1
  65. package/dist/TableConfig.js +13 -13
  66. package/dist/TableConfig.js.map +1 -1
  67. package/dist/shortestPath.d.ts +1 -1
  68. package/dist/shortestPath.d.ts.map +1 -1
  69. package/dist/validation.d.ts +9 -9
  70. package/dist/validation.d.ts.map +1 -1
  71. package/dist/validation.js +1 -1
  72. package/dist/validation.js.map +1 -1
  73. package/lib/DboBuilder/ViewHandler.d.ts +4 -3
  74. package/lib/DboBuilder/ViewHandler.d.ts.map +1 -1
  75. package/lib/DboBuilder/ViewHandler.js +7 -134
  76. package/lib/DboBuilder/ViewHandler.ts +15 -164
  77. package/lib/DboBuilder/delete.js +1 -1
  78. package/lib/DboBuilder/delete.ts +1 -1
  79. package/lib/DboBuilder/insert.js +1 -1
  80. package/lib/DboBuilder/insert.ts +1 -1
  81. package/lib/DboBuilder/insertDataParse.js +1 -1
  82. package/lib/DboBuilder/insertDataParse.ts +1 -1
  83. package/lib/DboBuilder/runSQL.js +1 -1
  84. package/lib/DboBuilder/runSQL.ts +1 -1
  85. package/lib/DboBuilder/subscribe.d.ts +11 -0
  86. package/lib/DboBuilder/subscribe.d.ts.map +1 -0
  87. package/lib/DboBuilder/subscribe.js +189 -0
  88. package/lib/DboBuilder/subscribe.ts +230 -0
  89. package/lib/DboBuilder/update.js +1 -1
  90. package/lib/DboBuilder/update.ts +1 -1
  91. package/lib/DboBuilder.d.ts +1 -1
  92. package/lib/DboBuilder.d.ts.map +1 -1
  93. package/lib/DboBuilder.js +1 -1
  94. package/lib/DboBuilder.ts +1 -1
  95. package/lib/Prostgles.js +1 -1
  96. package/lib/Prostgles.ts +1 -1
  97. package/lib/{PubSubManager.d.ts → PubSubManager/PubSubManager.d.ts} +20 -17
  98. package/lib/PubSubManager/PubSubManager.d.ts.map +1 -0
  99. package/lib/PubSubManager/PubSubManager.js +776 -0
  100. package/lib/PubSubManager/PubSubManager.ts +998 -0
  101. package/lib/PubSubManager/initPubSubManager.d.ts +3 -0
  102. package/lib/PubSubManager/initPubSubManager.d.ts.map +1 -0
  103. package/lib/PubSubManager/initPubSubManager.js +615 -0
  104. package/lib/PubSubManager/initPubSubManager.ts +630 -0
  105. package/lib/PublishParser.js +1 -1
  106. package/lib/PublishParser.ts +1 -1
  107. package/lib/SyncReplication.d.ts +1 -1
  108. package/lib/SyncReplication.d.ts.map +1 -1
  109. package/lib/SyncReplication.js +1 -1
  110. package/lib/SyncReplication.ts +1 -1
  111. package/lib/TableConfig.js +1 -1
  112. package/lib/TableConfig.ts +1 -1
  113. package/lib/validation.js +1 -1
  114. package/lib/validation.ts +1 -1
  115. package/package.json +6 -5
  116. package/tests/client/PID.txt +1 -1
  117. package/tests/server/package-lock.json +9 -7
  118. package/dist/PubSubManager.d.ts.map +0 -1
  119. package/dist/PubSubManager.js +0 -1398
  120. package/dist/PubSubManager.js.map +0 -1
  121. package/lib/PubSubManager.d.ts.map +0 -1
  122. package/lib/PubSubManager.js +0 -1420
  123. package/lib/PubSubManager.ts +0 -1655
@@ -0,0 +1,998 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Stefan L. All rights reserved.
3
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+
6
+ import { PostgresNotifListenManager } from "../PostgresNotifListenManager";
7
+ import { get } from "../utils";
8
+ import { TableOrViewInfo, TableInfo, DBHandlerServer, DboBuilder, PRGLIOSocket, canEXECUTE } from "../DboBuilder";
9
+ import { DB, isSuperUser } from "../Prostgles";
10
+ import { initPubSubManager } from "./initPubSubManager";
11
+
12
+ import * as Bluebird from "bluebird";
13
+ import * as pgPromise from 'pg-promise';
14
+ import pg from 'pg-promise/typescript/pg-subset';
15
+
16
+ import { SelectParams, FieldFilter, asName, WAL, isEmpty, AnyObject, getKeys } from "prostgles-types";
17
+
18
+ import { ClientExpressData, syncData } from "../SyncReplication";
19
+ import { TableRule } from "../PublishParser";
20
+
21
+
22
+ type PGP = pgPromise.IMain<{}, pg.IClient>;
23
+ let pgp: PGP = pgPromise({
24
+ promiseLib: Bluebird
25
+ });
26
+ export const asValue = (v: any) => pgp.as.format("$1", [v]);
27
+ export const DEFAULT_SYNC_BATCH_SIZE = 50;
28
+
29
+ export const log = (...args: any[]) => {
30
+ if (process.env.TEST_TYPE) {
31
+ console.log(...args)
32
+ }
33
+ }
34
+
35
+ export type BasicCallback = (err?: any, res?: any) => void
36
+
37
+ export type SyncParams = {
38
+ socket_id: string;
39
+ channel_name: string;
40
+ table_name: string;
41
+ table_rules?: TableRule;
42
+ synced_field: string;
43
+ allow_delete: boolean;
44
+ id_fields: string[];
45
+ batch_size: number;
46
+ filter: object;
47
+ params: {
48
+ select: FieldFilter
49
+ };
50
+ condition: string;
51
+ wal?: WAL,
52
+ throttle?: number;
53
+ lr?: AnyObject;
54
+ last_synced: number;
55
+ is_syncing: boolean;
56
+ }
57
+
58
+ type AddSyncParams = {
59
+ socket: any;
60
+ table_info: TableInfo;
61
+ table_rules: TableRule;
62
+ synced_field: string;
63
+ allow_delete?: boolean;
64
+ id_fields: string[];
65
+ filter: object;
66
+ params: {
67
+ select: FieldFilter
68
+ };
69
+ condition: string;
70
+ throttle?: number;
71
+ }
72
+
73
+ export type ViewSubscriptionOptions = {
74
+ viewName: string;
75
+ definition: string;
76
+ relatedTables: {
77
+ tableName: string;
78
+ tableNameEscaped: string;
79
+ condition: string;
80
+ }[];
81
+ }
82
+
83
+ type SubscriptionParams = {
84
+ socket_id?: string;
85
+ channel_name: string;
86
+ table_name: string;
87
+ socket: PRGLIOSocket | undefined;
88
+
89
+ /**
90
+ * If this is a view then an array with all related tables will be
91
+ * */
92
+ viewOptions?: ViewSubscriptionOptions;
93
+ parentSubParams: Omit<SubscriptionParams, "parentSubParams"> | undefined;
94
+
95
+ table_info: TableOrViewInfo;
96
+ table_rules?: TableRule;
97
+ filter: object;
98
+ params: SelectParams;
99
+ func?: (data: any) => any;
100
+ throttle?: number;
101
+ last_throttled: number;
102
+ is_throttling?: any;
103
+ is_ready?: boolean;
104
+ // subOne?: boolean;
105
+ }
106
+ type AddSubscriptionParams = SubscriptionParams & {
107
+ condition: string;
108
+ }
109
+
110
+ export type PubSubManagerOptions = {
111
+ dboBuilder: DboBuilder;
112
+ // db: DB;
113
+ // dbo: DBHandlerServer;
114
+ wsChannelNamePrefix?: string;
115
+ pgChannelName?: string;
116
+ onSchemaChange?: (event: { command: string; query: string }) => void;
117
+ }
118
+
119
+ export class PubSubManager {
120
+ static DELIMITER = '|$prstgls$|';
121
+
122
+ dboBuilder: DboBuilder;
123
+ get db(): DB {
124
+ return this.dboBuilder.db;
125
+ }
126
+ get dbo(): DBHandlerServer {
127
+ return this.dboBuilder.dbo;
128
+ }
129
+
130
+ _triggers?: Record<string, string[]>;
131
+ sockets: any;
132
+ subs: { [ke: string]: { [ke: string]: { subs: SubscriptionParams[] } } };
133
+ syncs: SyncParams[];
134
+ socketChannelPreffix: string;
135
+ onSchemaChange?: ((event: { command: string; query: string }) => void) = undefined;
136
+
137
+ postgresNotifListenManager?: PostgresNotifListenManager;
138
+
139
+ private constructor(options: PubSubManagerOptions) {
140
+ const { wsChannelNamePrefix, pgChannelName, onSchemaChange, dboBuilder } = options;
141
+ if (!dboBuilder.db || !dboBuilder.dbo) {
142
+ throw 'MISSING: db_pg, db';
143
+ }
144
+
145
+ this.onSchemaChange = onSchemaChange;
146
+ this.dboBuilder = dboBuilder;
147
+
148
+ this.sockets = {};
149
+ this.subs = {};
150
+ this.syncs = [];
151
+ this.socketChannelPreffix = wsChannelNamePrefix || "_psqlWS_";
152
+
153
+ log("Created PubSubManager");
154
+ }
155
+
156
+ NOTIF_TYPE = {
157
+ data: "data_has_changed",
158
+ schema: "schema_has_changed"
159
+ }
160
+ NOTIF_CHANNEL = {
161
+ preffix: 'prostgles_',
162
+ getFull: (appID?: string) => {
163
+ const finalAppId = appID ?? this.appID;
164
+ if (!finalAppId) throw "No appID";
165
+ return this.NOTIF_CHANNEL.preffix + finalAppId;
166
+ }
167
+ }
168
+
169
+ appID?: string;
170
+
171
+ appCheckFrequencyMS = 10 * 1000;
172
+ appCheck?: ReturnType<typeof setInterval>;
173
+
174
+
175
+
176
+ // ,datname
177
+ // ,usename
178
+ // ,client_hostname
179
+ // ,client_port
180
+ // ,backend_start
181
+ // ,query_start
182
+ // ,query
183
+ // ,state
184
+
185
+ // console.log(await _db.any(`
186
+ // SELECT pid, application_name, state
187
+ // FROM pg_stat_activity
188
+ // WHERE application_name IS NOT NULL AND application_name != '' -- state = 'active';
189
+ // `))
190
+
191
+ public static canCreate = async (db: DB) => {
192
+
193
+ const canExecute = await canEXECUTE(db);
194
+ const isSuperUs = await isSuperUser(db);
195
+ return { canExecute, isSuperUs, yes: canExecute && isSuperUs };
196
+ }
197
+
198
+ public static create = async (options: PubSubManagerOptions) => {
199
+ const res = new PubSubManager(options);
200
+ return await res.init();
201
+ }
202
+
203
+ destroyed = false;
204
+ destroy = () => {
205
+ this.destroyed = true;
206
+ if (this.appCheck) {
207
+ clearInterval(this.appCheck);
208
+ }
209
+ this.onSocketDisconnected();
210
+ // if(this.postgresNotifListenManager){
211
+ // this.postgresNotifListenManager.stopListening();
212
+ // }
213
+ if (!this.postgresNotifListenManager) throw "this.postgresNotifListenManager missing"
214
+ this.postgresNotifListenManager.destroy();
215
+ }
216
+
217
+ canContinue = () => {
218
+ if (this.destroyed) {
219
+ console.trace("Could not start destroyed instance");
220
+ return false
221
+ }
222
+ return true
223
+ }
224
+
225
+ appChecking = false;
226
+ init = initPubSubManager.bind(this);
227
+
228
+ DB_OBJ_NAMES = {
229
+ trigger_add_remove_func: "prostgles.trigger_add_remove_func",
230
+ data_watch_func: "prostgles.prostgles_trigger_function",
231
+ schema_watch_func: "prostgles.schema_watch_func",
232
+ schema_watch_trigger: "prostgles_schema_watch_trigger_new"
233
+ }
234
+
235
+ static SCHEMA_ALTERING_QUERIES = ['CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE VIEW', 'DROP VIEW', 'ALTER VIEW', 'CREATE TABLE AS', 'SELECT INTO'];
236
+
237
+ static EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID = "prostgles internal query that should be excluded from "
238
+ prepareTriggers = async () => {
239
+ // SELECT * FROM pg_catalog.pg_event_trigger WHERE evtname
240
+ if (!this.appID) throw "prepareTriggers failed: this.appID missing";
241
+ if (this.dboBuilder.prostgles.opts.watchSchema && !(await isSuperUser(this.db))) {
242
+ console.warn("prostgles watchSchema requires superuser db user. Will not watch using event triggers")
243
+ }
244
+
245
+ try {
246
+
247
+ await this.db.any(`
248
+ BEGIN;-- ISOLATION LEVEL SERIALIZABLE;
249
+
250
+ /** ${PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID}
251
+ * Drop stale triggers
252
+ * */
253
+ DO
254
+ $do$
255
+ DECLARE trg RECORD;
256
+ q TEXT;
257
+ ev_trg_needed BOOLEAN := FALSE;
258
+ ev_trg_exists BOOLEAN := FALSE;
259
+ is_super_user BOOLEAN := FALSE;
260
+ BEGIN
261
+ --SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
262
+
263
+ LOCK TABLE prostgles.app_triggers IN ACCESS EXCLUSIVE MODE;
264
+ EXECUTE format(
265
+ $q$
266
+
267
+ CREATE TEMP TABLE %1$I AS --ON COMMIT DROP AS
268
+ SELECT * FROM prostgles.app_triggers;
269
+
270
+ DELETE FROM prostgles.app_triggers;
271
+
272
+ INSERT INTO prostgles.app_triggers
273
+ SELECT * FROM %1$I;
274
+
275
+ DROP TABLE IF EXISTS %1$I;
276
+ $q$,
277
+ ${asValue('triggers_' + this.appID)}
278
+ );
279
+
280
+ is_super_user := EXISTS (select 1 from pg_user where usename = CURRENT_USER AND usesuper IS TRUE);
281
+ /**
282
+ * Delete stale app records
283
+ * */
284
+ DELETE FROM prostgles.apps
285
+ WHERE last_check < NOW() - 8 * check_frequency_ms * interval '1 millisecond';
286
+
287
+ DELETE FROM prostgles.app_triggers
288
+ WHERE app_id NOT IN (SELECT id FROM prostgles.apps);
289
+
290
+ /* DROP the old buggy schema watch trigger */
291
+ IF EXISTS (
292
+ SELECT 1 FROM pg_catalog.pg_event_trigger
293
+ WHERE evtname = 'prostgles_schema_watch_trigger'
294
+ ) AND is_super_user IS TRUE
295
+ THEN
296
+ DROP EVENT TRIGGER IF EXISTS prostgles_schema_watch_trigger;
297
+ END IF;
298
+
299
+ ev_trg_needed := EXISTS (SELECT 1 FROM prostgles.apps WHERE watching_schema IS TRUE);
300
+ ev_trg_exists := EXISTS (
301
+ SELECT 1 FROM pg_catalog.pg_event_trigger
302
+ WHERE evtname = ${asValue(this.DB_OBJ_NAMES.schema_watch_trigger)}
303
+ );
304
+
305
+ -- RAISE NOTICE ' ev_trg_needed %, ev_trg_exists %', ev_trg_needed, ev_trg_exists;
306
+
307
+ /**
308
+ * DROP stale event trigger
309
+ * */
310
+ IF is_super_user IS TRUE AND ev_trg_needed IS FALSE AND ev_trg_exists IS TRUE THEN
311
+
312
+ SELECT format(
313
+ $$ DROP EVENT TRIGGER IF EXISTS %I ; $$
314
+ , ${asValue(this.DB_OBJ_NAMES.schema_watch_trigger)}
315
+ )
316
+ INTO q;
317
+
318
+ --RAISE NOTICE ' DROP EVENT TRIGGER %', q;
319
+
320
+ EXECUTE q;
321
+
322
+ /**
323
+ * CREATE event trigger
324
+ * */
325
+ ELSIF
326
+ is_super_user IS TRUE
327
+ AND ev_trg_needed IS TRUE
328
+ AND ev_trg_exists IS FALSE
329
+ THEN
330
+
331
+ DROP EVENT TRIGGER IF EXISTS ${this.DB_OBJ_NAMES.schema_watch_trigger};
332
+ CREATE EVENT TRIGGER ${this.DB_OBJ_NAMES.schema_watch_trigger} ON ddl_command_end
333
+ WHEN TAG IN ('COMMENT', 'CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE VIEW', 'DROP VIEW', 'ALTER VIEW', 'CREATE TABLE AS', 'SELECT INTO')
334
+ --WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE TRIGGER', 'DROP TRIGGER')
335
+ EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.schema_watch_func}();
336
+
337
+ --RAISE NOTICE ' CREATED EVENT TRIGGER %', q;
338
+ END IF;
339
+
340
+
341
+ END
342
+ $do$;
343
+
344
+
345
+ COMMIT;
346
+ `).catch(e => {
347
+ console.error("prepareTriggers failed: ", e);
348
+ throw e;
349
+ });
350
+
351
+ return true;
352
+
353
+ } catch (e) {
354
+ console.error("prepareTriggers failed: ", e);
355
+ throw e;
356
+ }
357
+ }
358
+
359
+ isReady() {
360
+ if (!this.postgresNotifListenManager) throw "this.postgresNotifListenManager missing";
361
+ return this.postgresNotifListenManager.isListening();
362
+ }
363
+
364
+ getSubs(table_name: string, condition: string): SubscriptionParams[] {
365
+ return this.subs?.[table_name]?.[condition]?.subs
366
+ }
367
+
368
+ getSyncs(table_name: string, condition: string) {
369
+ return (this.syncs || [])
370
+ .filter((s: SyncParams) => s.table_name === table_name && s.condition === condition);
371
+ }
372
+
373
+ /* Relay relevant data to relevant subscriptions */
374
+ notifListener = async (data: { payload: string }) => {
375
+ const str = data.payload;
376
+
377
+ if (!str) {
378
+ console.error("Empty notif?")
379
+ return;
380
+ }
381
+ const dataArr = str.split(PubSubManager.DELIMITER),
382
+ notifType = dataArr[0];
383
+
384
+ log(str);
385
+
386
+ if (notifType === this.NOTIF_TYPE.schema) {
387
+ if (this.onSchemaChange) {
388
+ const command = dataArr[1],
389
+ event_type = dataArr[2],
390
+ query = dataArr[3];
391
+
392
+ if (query) {
393
+ this.onSchemaChange({ command, query })
394
+ }
395
+ }
396
+
397
+ return;
398
+ }
399
+
400
+ if (notifType !== this.NOTIF_TYPE.data) {
401
+ console.error("Unexpected notif type: ", notifType);
402
+ return;
403
+ }
404
+
405
+ const table_name = dataArr[1],
406
+ op_name = dataArr[2],
407
+ condition_ids_str = dataArr[3];
408
+
409
+ // const triggers = await this.db.any("SELECT * FROM prostgles.triggers WHERE table_name = $1 AND id IN ($2:csv)", [table_name, condition_ids_str.split(",").map(v => +v)]);
410
+ // const conditions: string[] = triggers.map(t => t.condition);
411
+
412
+ log("PG Trigger ->", dataArr.join("__"))
413
+ if (
414
+ condition_ids_str && condition_ids_str.startsWith("error") &&
415
+ this._triggers && this._triggers[table_name] && this._triggers[table_name].length
416
+ ) {
417
+ const pref = "INTERNAL ERROR. Schema might have changed";
418
+ console.error(`${pref}: ${condition_ids_str}`)
419
+ this._triggers[table_name].map(c => {
420
+ const subs = this.getSubs(table_name, c);
421
+ subs.map(s => {
422
+ this.pushSubData(s, pref + ". Check server logs");
423
+ })
424
+ });
425
+ } else if (
426
+ condition_ids_str?.split(",").length &&
427
+ condition_ids_str?.split(",").every((c: string) => Number.isInteger(+c)) &&
428
+ this._triggers?.[table_name]?.length
429
+ ) {
430
+
431
+
432
+ const idxs = condition_ids_str.split(",").map(v => +v);
433
+ const conditions = this._triggers[table_name].filter((c, i) => idxs.includes(i))
434
+
435
+ log("PG Trigger -> ", { table_name, op_name, condition_ids_str, conditions }, this._triggers[table_name]);
436
+
437
+ conditions.map(condition => {
438
+
439
+ const subs = this.getSubs(table_name, condition);
440
+ const syncs = this.getSyncs(table_name, condition);
441
+
442
+
443
+ syncs.map((s) => {
444
+ this.syncData(s, undefined, "trigger");
445
+ });
446
+
447
+ if (!subs) {
448
+
449
+ // console.error(`sub missing for ${table_name} ${condition}`, this.triggers);
450
+ // console.log(this.subs)
451
+ return;
452
+ }
453
+
454
+ /* Throttle the subscriptions */
455
+ for (var i = 0; i < subs.length; i++) {
456
+ var sub = subs[i];
457
+ if (
458
+ this.dbo[sub.table_name] &&
459
+ sub.is_ready &&
460
+ (sub.socket_id && this.sockets[sub.socket_id]) || sub.func
461
+ ) {
462
+ const throttle = sub.throttle || 0;
463
+ if (sub.last_throttled <= Date.now() - throttle) {
464
+
465
+ /* It is assumed the policy was checked before this point */
466
+ this.pushSubData(sub);
467
+ // sub.last_throttled = Date.now();
468
+ } else if (!sub.is_throttling) {
469
+
470
+
471
+ log("throttling sub")
472
+ sub.is_throttling = setTimeout(() => {
473
+ log("throttling finished. pushSubData...")
474
+ sub.is_throttling = null;
475
+ this.pushSubData(sub);
476
+ }, throttle);// sub.throttle);
477
+ }
478
+ }
479
+ }
480
+ });
481
+
482
+ } else {
483
+
484
+ // if(!this._triggers || !this._triggers[table_name] || !this._triggers[table_name].length){
485
+ // console.warn(190, "Trigger sub not found. DROPPING TRIGGER", table_name, condition_ids_str, this._triggers);
486
+ // this.dropTrigger(table_name);
487
+ // } else {
488
+ // }
489
+ console.warn(190, "Trigger sub issue: ", table_name, condition_ids_str, this._triggers);
490
+ }
491
+ }
492
+
493
+
494
+ pushSubData(sub: SubscriptionParams, err?: any) {
495
+ if (!sub) throw "pushSubData: invalid sub";
496
+ const { table_name, filter, params, table_rules, socket_id, channel_name, func } = sub; //, subOne = false
497
+
498
+ sub.last_throttled = Date.now();
499
+
500
+ if (err) {
501
+ if (socket_id) {
502
+ this.sockets[socket_id].emit(channel_name, { err });
503
+ }
504
+ return true;
505
+ }
506
+
507
+ return new Promise(async (resolve, reject) => {
508
+ /* TODO: Retire subOne -> it's redundant */
509
+ // this.dbo[table_name][subOne? "findOne" : "find"](filter, params, null, table_rules)
510
+ if (!this.dbo?.[table_name]?.find) {
511
+ throw new Error(`1107 this.dbo.${table_name}.find`);
512
+ }
513
+
514
+ this.dbo?.[table_name]?.find?.(filter, params, undefined, table_rules)
515
+ .then(data => {
516
+
517
+ if (socket_id && this.sockets[socket_id]) {
518
+ log("Pushed " + data.length + " records to sub")
519
+ this.sockets[socket_id].emit(channel_name, { data }, () => {
520
+ resolve(data);
521
+ });
522
+ /* TO DO: confirm receiving data or server will unsubscribe
523
+ { data }, (cb)=> { console.log(cb) });
524
+ */
525
+ } else if (func) {
526
+ func(data);
527
+ resolve(data);
528
+ }
529
+ sub.last_throttled = Date.now();
530
+ }).catch(err => {
531
+ const errObj = { _err_msg: err.toString(), err };
532
+ if (socket_id && this.sockets[socket_id]) {
533
+ this.sockets[socket_id].emit(channel_name, { err: errObj });
534
+ } else if (func) {
535
+ func({ err: errObj });
536
+ }
537
+ reject(errObj)
538
+ });
539
+ });
540
+ }
541
+
542
+ upsertSocket(socket: any, channel_name: string) {
543
+ if (socket && !this.sockets[socket.id]) {
544
+ this.sockets[socket.id] = socket;
545
+ socket.on("disconnect", () => this.onSocketDisconnected(socket));
546
+ }
547
+ }
548
+
549
+ syncTimeout?: ReturnType<typeof setTimeout>;
550
+ async syncData(sync: SyncParams, clientData: ClientExpressData | undefined, source: "trigger" | "client") {
551
+ return await syncData(this, sync, clientData, source);
552
+ }
553
+
554
+ /**
555
+ * Returns a sync channel
556
+ * A sync channel is unique per socket for each filter
557
+ */
558
+ async addSync(syncParams: AddSyncParams) {
559
+ const {
560
+ socket = null, table_info = null, table_rules, synced_field = null,
561
+ allow_delete = false, id_fields = [], filter = {},
562
+ params, condition = "", throttle = 0
563
+ } = syncParams || {};
564
+
565
+ let conditionParsed = parseCondition(condition);
566
+ if (!socket || !table_info) throw "socket or table_info missing";
567
+
568
+
569
+ const { name: table_name } = table_info,
570
+ channel_name = `${this.socketChannelPreffix}.${table_name}.${JSON.stringify(filter)}.sync`;
571
+
572
+ if (!synced_field) throw "synced_field missing from table_rules";
573
+
574
+ this.upsertSocket(socket, channel_name);
575
+
576
+ const upsertSync = () => {
577
+ let newSync = {
578
+ channel_name,
579
+ table_name,
580
+ filter,
581
+ condition: conditionParsed,
582
+ synced_field,
583
+ id_fields,
584
+ allow_delete,
585
+ table_rules,
586
+ throttle: Math.max(throttle || 0, table_rules?.sync?.throttle || 0),
587
+ batch_size: get(table_rules, "sync.batch_size") || DEFAULT_SYNC_BATCH_SIZE,
588
+ last_throttled: 0,
589
+ socket_id: socket.id,
590
+ is_sync: true,
591
+ last_synced: 0,
592
+ lr: undefined,
593
+ table_info,
594
+ is_syncing: false,
595
+ wal: undefined,
596
+ socket,
597
+ params
598
+ };
599
+
600
+ /* Only a sync per socket per table per condition allowed */
601
+ this.syncs = this.syncs || [];
602
+ let existing = this.syncs.find(s => s.socket_id === socket.id && s.channel_name === channel_name);
603
+ if (!existing) {
604
+ this.syncs.push(newSync);
605
+ // console.log("Added SYNC");
606
+
607
+ socket.removeAllListeners(channel_name + "unsync");
608
+ socket.once(channel_name + "unsync", (_data: any, cb: BasicCallback) => {
609
+ this.onSocketDisconnected(socket, channel_name);
610
+ cb(null, { res: "ok" })
611
+ });
612
+
613
+ socket.removeAllListeners(channel_name);
614
+ socket.on(channel_name, (data: any, cb: BasicCallback) => {
615
+
616
+ if (!data) {
617
+ cb({ err: "Unexpected request. Need data or onSyncRequest" });
618
+ return;
619
+ }
620
+
621
+ /*
622
+ */
623
+
624
+ /* Server will:
625
+ 1. Ask for last_synced emit(onSyncRequest)
626
+ 2. Ask for data >= server_synced emit(onPullRequest)
627
+ -> Upsert that data
628
+ 2. Push data >= last_synced emit(data.data)
629
+
630
+ Client will:
631
+ 1. Send last_synced on(onSyncRequest)
632
+ 2. Send data >= server_synced on(onPullRequest)
633
+ 3. Send data on CRUD emit(data.data | data.deleted)
634
+ 4. Upsert data.data | deleted on(data.data | data.deleted)
635
+ */
636
+
637
+ // if(data.data){
638
+ // console.error("THIS SHOUKD NEVER FIRE !! NEW DATA FROM SYNC");
639
+ // this.upsertClientData(newSync, data.data);
640
+ // } else
641
+ if (data.onSyncRequest) {
642
+ // console.log("syncData from socket")
643
+ this.syncData(newSync, data.onSyncRequest, "client");
644
+
645
+ // console.log("onSyncRequest ", socket._user)
646
+ } else {
647
+ console.error("Unexpected sync request data from client: ", data)
648
+ }
649
+ });
650
+
651
+ // socket.emit(channel_name, { onSyncRequest: true }, (response) => {
652
+ // console.log(response)
653
+ // });
654
+ } else {
655
+ console.error("UNCLOSED DUPLICATE SYNC FOUND");
656
+ }
657
+
658
+ return newSync;
659
+ };
660
+
661
+
662
+ // const { min_id, max_id, count, max_synced } = params;
663
+
664
+ let sync = upsertSync();
665
+
666
+ await this.addTrigger({ table_name, condition: conditionParsed });
667
+
668
+ return channel_name;
669
+ }
670
+
671
+
672
+ /* Must return a channel for socket */
673
+ /* The distinct list of channel names must have a corresponding trigger in the database */
674
+ async addSub(subscriptionParams: Omit<AddSubscriptionParams, "channel_name" | "parentSubParams">) {
675
+ const {
676
+ socket, func = null, table_info = null, table_rules, filter = {},
677
+ params = {}, condition = "", throttle = 0, //subOne = false,
678
+ viewOptions
679
+ } = subscriptionParams || {};
680
+
681
+ let validated_throttle = subscriptionParams.throttle || 10;
682
+ if ((!socket && !func) || !table_info) throw "socket/func or table_info missing";
683
+
684
+ const pubThrottle = get(table_rules, ["subscribe", "throttle"]) || 0;
685
+ if (pubThrottle && Number.isInteger(pubThrottle) && pubThrottle > 0) {
686
+ validated_throttle = pubThrottle;
687
+ }
688
+ if (throttle && Number.isInteger(throttle) && throttle >= pubThrottle) {
689
+ validated_throttle = throttle;
690
+ }
691
+
692
+ const channel_name = `${this.socketChannelPreffix}.${table_info.name}.${JSON.stringify(filter)}.${JSON.stringify(params)}.${"m"}.sub`;
693
+
694
+ this.upsertSocket(socket, channel_name);
695
+
696
+ const upsertSub = (newSubData: { table_name: string; condition: string; is_ready: boolean; parentSubParams: SubscriptionParams["parentSubParams"] }) => {
697
+ const { table_name, condition: _cond, is_ready = false, parentSubParams } = newSubData,
698
+ condition = parseCondition(_cond),
699
+ newSub: SubscriptionParams = {
700
+ socket,
701
+ table_name: table_info.name,
702
+ table_info,
703
+ filter,
704
+ params,
705
+ table_rules,
706
+ channel_name,
707
+ parentSubParams,
708
+ func: func ?? undefined,
709
+ socket_id: socket?.id,
710
+ throttle: validated_throttle,
711
+ is_throttling: null,
712
+ last_throttled: 0,
713
+ is_ready,
714
+ };
715
+
716
+ this.subs[table_name] = this.subs[table_name] ?? {};
717
+ this.subs[table_name][condition] = this.subs[table_name][condition] ?? { subs: [] };
718
+ this.subs[table_name][condition].subs = this.subs[table_name][condition].subs ?? [];
719
+
720
+ // console.log("1034 upsertSub", this.subs)
721
+ const sub_idx = this.subs[table_name][condition].subs.findIndex(s =>
722
+ s.channel_name === channel_name &&
723
+ (
724
+ socket && s.socket_id === socket.id ||
725
+ func && s.func === func
726
+ )
727
+ );
728
+ if (sub_idx < 0) {
729
+ this.subs[table_name][condition].subs.push(newSub);
730
+ if (socket) {
731
+ const chnUnsub = channel_name + "unsubscribe";
732
+ socket.removeAllListeners(chnUnsub);
733
+ socket.once(chnUnsub, (_data: any, cb: BasicCallback) => {
734
+ const res = this.onSocketDisconnected(socket, channel_name);
735
+ cb(null, { res });
736
+ });
737
+ }
738
+ } else {
739
+ this.subs[table_name][condition].subs[sub_idx] = newSub;
740
+ }
741
+
742
+ if (is_ready) {
743
+ this.pushSubData(newSub);
744
+ }
745
+ };
746
+
747
+
748
+ if (table_info.is_view) {
749
+ if (viewOptions?.relatedTables.length) {
750
+
751
+ viewOptions?.relatedTables.map(async relatedTable => {
752
+ const params: Omit<Parameters<typeof upsertSub>[0], "is_ready"> = {
753
+ table_name: relatedTable.tableName,
754
+ condition: relatedTable.condition,
755
+ parentSubParams: {
756
+ ...subscriptionParams,
757
+ channel_name
758
+ },
759
+ }
760
+
761
+ upsertSub({
762
+ ...params,
763
+ is_ready: false
764
+ });
765
+
766
+ await this.addTrigger(params);
767
+
768
+ upsertSub({
769
+ ...params,
770
+ is_ready: true
771
+ });
772
+ });
773
+
774
+ return channel_name
775
+ } else {
776
+ throw "PubSubManager: view parent_tables missing";
777
+ }
778
+ /* */
779
+ } else {
780
+ /* Just a table, add table + condition trigger */
781
+ // console.log(table_info, 202);
782
+
783
+ upsertSub({
784
+ table_name: table_info.name,
785
+ condition: parseCondition(condition),
786
+ parentSubParams: undefined,
787
+ is_ready: false
788
+ });
789
+ await this.addTrigger({
790
+ table_name: table_info.name,
791
+ condition: parseCondition(condition),
792
+ });
793
+ upsertSub({
794
+ table_name: table_info.name,
795
+ condition: parseCondition(condition),
796
+ parentSubParams: undefined,
797
+ is_ready: true
798
+ });
799
+
800
+ return channel_name
801
+ }
802
+ }
803
+
804
+ removeLocalSub(table_name: string, condition: string, func: (items: object[]) => any) {
805
+ let cond = parseCondition(condition);
806
+ if (get(this.subs, [table_name, cond, "subs"])) {
807
+ this.subs[table_name][cond].subs.map((sub, i) => {
808
+ if (
809
+ sub.func && sub.func === func
810
+ ) {
811
+ this.subs[table_name][cond].subs.splice(i, 1);
812
+ }
813
+ });
814
+ } else {
815
+ console.error("Could not unsubscribe. Subscription might not have initialised yet")
816
+ }
817
+ }
818
+
819
+ getActiveListeners = (): { table_name: string; condition: string }[] => {
820
+ let result: { table_name: string; condition: string }[] = [];
821
+ const upsert = (t: string, c: string) => {
822
+ if (!result.find(r => r.table_name === t && r.condition === c)) {
823
+ result.push({ table_name: t, condition: c });
824
+ }
825
+ }
826
+ (this.syncs || []).map(s => {
827
+ upsert(s.table_name, s.condition)
828
+ });
829
+ Object.keys(this.subs || {}).map(table_name => {
830
+ Object.keys(this.subs[table_name] || {}).map(condition => {
831
+ if (this.subs[table_name][condition].subs.length) {
832
+ upsert(table_name, condition);
833
+ }
834
+ });
835
+ });
836
+
837
+ return result;
838
+ }
839
+
840
+ onSocketDisconnected(socket?: PRGLIOSocket, channel_name?: string) {
841
+ // process.on('warning', e => {
842
+ // console.warn(e.stack)
843
+ // });
844
+ // console.log("onSocketDisconnected", channel_name, this.syncs)
845
+ if (this.subs) {
846
+ Object.keys(this.subs).map(table_name => {
847
+ Object.keys(this.subs[table_name]).map(condition => {
848
+ this.subs[table_name][condition].subs.map((sub, i) => {
849
+
850
+ /**
851
+ * If a channel name is specified then delete triggers
852
+ */
853
+ if (
854
+ (socket && sub.socket_id === socket.id) &&
855
+ (!channel_name || sub.channel_name === channel_name)
856
+ ) {
857
+ this.subs[table_name][condition].subs.splice(i, 1);
858
+ if (!this.subs[table_name][condition].subs.length) {
859
+ delete this.subs[table_name][condition];
860
+
861
+ if (isEmpty(this.subs[table_name])) {
862
+ delete this.subs[table_name];
863
+ }
864
+ }
865
+ }
866
+ });
867
+ })
868
+ });
869
+ }
870
+
871
+ if (this.syncs) {
872
+ this.syncs = this.syncs.filter(s => {
873
+ const matchesSocket = Boolean(socket && s.socket_id !== socket.id)
874
+ if (channel_name) {
875
+ return matchesSocket || s.channel_name !== channel_name
876
+ }
877
+
878
+ return matchesSocket;
879
+ });
880
+ }
881
+
882
+ if (!socket) {
883
+
884
+ } else if (!channel_name) {
885
+ delete this.sockets[socket.id];
886
+ } else {
887
+ socket.removeAllListeners(channel_name);
888
+ socket.removeAllListeners(channel_name + "unsync");
889
+ socket.removeAllListeners(channel_name + "unsubscribe");
890
+ }
891
+
892
+ return "ok";
893
+ }
894
+
895
+
896
+ checkIfTimescaleBug = async (table_name: string) => {
897
+ const schema = "_timescaledb_catalog",
898
+ res = await this.db.oneOrNone("SELECT EXISTS( \
899
+ SELECT * \
900
+ FROM information_schema.tables \
901
+ WHERE 1 = 1 \
902
+ AND table_schema = ${schema} \
903
+ AND table_name = 'hypertable' \
904
+ );", { schema });
905
+ if (res.exists) {
906
+ let isHyperTable = await this.db.any("SELECT * FROM " + asName(schema) + ".hypertable WHERE table_name = ${table_name};", { table_name, schema });
907
+ if (isHyperTable && isHyperTable.length) {
908
+ throw "Triggers do not work on timescaledb hypertables due to bug:\nhttps://github.com/timescale/timescaledb/issues/1084"
909
+ }
910
+ }
911
+ return true;
912
+ }
913
+
914
+ /*
915
+ A table will only have a trigger with all conditions (for different subs)
916
+ conditions = ["user_id = 1"]
917
+ fields = ["user_id"]
918
+ */
919
+
920
+ getMyTriggerQuery = async () => {
921
+ return pgp.as.format(`
922
+ SELECT * --, ROW_NUMBER() OVER(PARTITION BY table_name ORDER BY table_name, condition ) - 1 as id
923
+ FROM prostgles.v_triggers
924
+ WHERE app_id = $1
925
+ ORDER BY table_name, condition
926
+ `, [this.appID]
927
+ )
928
+ }
929
+
930
+ // waitingTriggers: { [key: string]: string[] } = undefined;
931
+ addingTrigger: any;
932
+ addTriggerPool?: Record<string, string[]> = undefined;
933
+ async addTrigger(params: { table_name: string; condition: string; }, viewOptions?: ViewSubscriptionOptions) {
934
+ try {
935
+
936
+ let { table_name, condition } = { ...params }
937
+ if (!table_name) throw "MISSING table_name";
938
+ if (!this.appID) throw "MISSING appID";
939
+
940
+ if (!condition || !condition.trim().length) {
941
+ condition = "TRUE";
942
+ }
943
+
944
+ // console.log(1623, { app_id, addTrigger: { table_name, condition } });
945
+
946
+ await this.checkIfTimescaleBug(table_name);
947
+
948
+ const trgVals = {
949
+ tbl: asValue(table_name),
950
+ cond: asValue(condition),
951
+ };
952
+
953
+ await this.db.any(`
954
+ BEGIN WORK;
955
+ LOCK TABLE prostgles.app_triggers IN ACCESS EXCLUSIVE MODE;
956
+
957
+ INSERT INTO prostgles.app_triggers (table_name, condition, app_id, related_view_name, related_view_def)
958
+ VALUES (${trgVals.tbl}, ${trgVals.cond}, ${asValue(this.appID)}, ${viewOptions?.viewName ?? null}, ${viewOptions?.definition ?? null})
959
+ ON CONFLICT DO NOTHING;
960
+
961
+ COMMIT WORK;
962
+ `);
963
+
964
+ log("addTrigger.. ", { table_name, condition });
965
+
966
+ const triggers: {
967
+ table_name: string;
968
+ condition: string;
969
+ }[] = await this.db.any(await this.getMyTriggerQuery());
970
+
971
+
972
+ this._triggers = {};
973
+ triggers.map(t => {
974
+ this._triggers = this._triggers || {};
975
+ this._triggers[t.table_name] = this._triggers[t.table_name] || [];
976
+ if (!this._triggers[t.table_name].includes(t.condition)) {
977
+ this._triggers[t.table_name].push(t.condition)
978
+ }
979
+ });
980
+ log("trigger added.. ", { table_name, condition });
981
+
982
+ return true;
983
+ // console.log("1612", JSON.stringify(triggers, null, 2))
984
+ // console.log("1613",JSON.stringify(this._triggers, null, 2))
985
+
986
+
987
+ } catch (e) {
988
+ console.trace("Failed adding trigger", e);
989
+ // throw e
990
+ }
991
+
992
+ }
993
+ }
994
+
995
+
996
+ const parseCondition = (condition: string): string => Boolean(condition && condition.trim().length) ? condition : "TRUE"
997
+
998
+ export { pickKeys, omitKeys } from "prostgles-types"