prostgles-server 3.0.64 → 3.0.66

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/{lib → dist/PubSubManager}/PubSubManager.d.ts +24 -19
  46. package/dist/PubSubManager/PubSubManager.d.ts.map +1 -0
  47. package/dist/PubSubManager/PubSubManager.js +770 -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/{dist → lib/PubSubManager}/PubSubManager.d.ts +19 -14
  98. package/lib/PubSubManager/PubSubManager.d.ts.map +1 -0
  99. package/lib/PubSubManager/PubSubManager.js +792 -0
  100. package/lib/PubSubManager/PubSubManager.ts +1016 -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 +5 -4
  116. package/tests/client/PID.txt +1 -1
  117. package/tests/server/package-lock.json +7 -5
  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
@@ -1,1655 +0,0 @@
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
-
11
- import * as Bluebird from "bluebird";
12
- import * as pgPromise from 'pg-promise';
13
- import pg from 'pg-promise/typescript/pg-subset';
14
-
15
- import { SelectParams, FieldFilter, asName, WAL, isEmpty, AnyObject, getKeys } from "prostgles-types";
16
-
17
- import { ClientExpressData, syncData } from "./SyncReplication";
18
- import { TableRule } from "./PublishParser";
19
-
20
- const REALTIME_TRIGGER_CHECK_QUERY = "prostgles-server internal query used to manage realtime triggers" as const;
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
- type SubscriptionParams = {
74
- socket_id?: string;
75
- channel_name: string;
76
- table_name: string;
77
- socket: PRGLIOSocket | undefined;
78
-
79
- /**
80
- * If this is a view then an array with all related tables will be
81
- * */
82
- relatedTableSubscriptions?: {
83
- tableName: string;
84
- tableNameEscaped: string;
85
- condition: string;
86
- }[];
87
- parentSubParams: Omit<SubscriptionParams, "parentSubParams"> | undefined;
88
-
89
- table_info: TableOrViewInfo;
90
- table_rules?: TableRule;
91
- filter: object;
92
- params: SelectParams;
93
- func?: (data: any) => any;
94
- throttle?: number;
95
- last_throttled: number;
96
- is_throttling?: any;
97
- is_ready?: boolean;
98
- // subOne?: boolean;
99
- }
100
- type AddSubscriptionParams = SubscriptionParams & {
101
- condition: string;
102
- }
103
-
104
- export type PubSubManagerOptions = {
105
- dboBuilder: DboBuilder;
106
- // db: DB;
107
- // dbo: DBHandlerServer;
108
- wsChannelNamePrefix?: string;
109
- pgChannelName?: string;
110
- onSchemaChange?: (event: { command: string; query: string }) => void;
111
- }
112
-
113
- export class PubSubManager {
114
- static DELIMITER = '|$prstgls$|';
115
-
116
- dboBuilder: DboBuilder;
117
- get db(): DB {
118
- return this.dboBuilder.db;
119
- }
120
- get dbo(): DBHandlerServer {
121
- return this.dboBuilder.dbo;
122
- }
123
-
124
- _triggers?: Record<string, string[]>;
125
- sockets: any;
126
- subs: { [ke: string]: { [ke: string]: { subs: SubscriptionParams[] } } };
127
- syncs: SyncParams[];
128
- socketChannelPreffix: string;
129
- onSchemaChange?: ((event: { command: string; query: string }) => void) = undefined;
130
-
131
- postgresNotifListenManager?: PostgresNotifListenManager;
132
-
133
- private constructor(options: PubSubManagerOptions) {
134
- const { wsChannelNamePrefix, pgChannelName, onSchemaChange, dboBuilder } = options;
135
- if (!dboBuilder.db || !dboBuilder.dbo) {
136
- throw 'MISSING: db_pg, db';
137
- }
138
-
139
- this.onSchemaChange = onSchemaChange;
140
- this.dboBuilder = dboBuilder;
141
-
142
- this.sockets = {};
143
- this.subs = {};
144
- this.syncs = [];
145
- this.socketChannelPreffix = wsChannelNamePrefix || "_psqlWS_";
146
-
147
- log("Created PubSubManager");
148
- }
149
-
150
- NOTIF_TYPE = {
151
- data: "data_has_changed",
152
- schema: "schema_has_changed"
153
- }
154
- NOTIF_CHANNEL = {
155
- preffix: 'prostgles_',
156
- getFull: (appID?: string) => {
157
- const finalAppId = appID ?? this.appID;
158
- if (!finalAppId) throw "No appID";
159
- return this.NOTIF_CHANNEL.preffix + finalAppId;
160
- }
161
- }
162
-
163
- private appID?: string;
164
-
165
- appCheckFrequencyMS = 10 * 1000;
166
- appCheck?: ReturnType<typeof setInterval>;
167
-
168
-
169
-
170
- // ,datname
171
- // ,usename
172
- // ,client_hostname
173
- // ,client_port
174
- // ,backend_start
175
- // ,query_start
176
- // ,query
177
- // ,state
178
-
179
- // console.log(await _db.any(`
180
- // SELECT pid, application_name, state
181
- // FROM pg_stat_activity
182
- // WHERE application_name IS NOT NULL AND application_name != '' -- state = 'active';
183
- // `))
184
-
185
- public static canCreate = async (db: DB) => {
186
-
187
- const canExecute = await canEXECUTE(db);
188
- const isSuperUs = await isSuperUser(db);
189
- return { canExecute, isSuperUs, yes: canExecute && isSuperUs };
190
- }
191
-
192
- public static create = async (options: PubSubManagerOptions) => {
193
- const res = new PubSubManager(options);
194
- return await res.init();
195
- }
196
-
197
- destroyed = false;
198
- destroy = () => {
199
- this.destroyed = true;
200
- if (this.appCheck) {
201
- clearInterval(this.appCheck);
202
- }
203
- this.onSocketDisconnected();
204
- // if(this.postgresNotifListenManager){
205
- // this.postgresNotifListenManager.stopListening();
206
- // }
207
- if (!this.postgresNotifListenManager) throw "this.postgresNotifListenManager missing"
208
- this.postgresNotifListenManager.destroy();
209
- }
210
-
211
- canContinue = () => {
212
- if (this.destroyed) {
213
- console.trace("Could not start destroyed instance");
214
- return false
215
- }
216
- return true
217
- }
218
-
219
- appChecking = false;
220
- init = async (): Promise<PubSubManager | undefined> => {
221
- if (!this.canContinue()) return undefined;
222
-
223
- try {
224
- const schema_version = 5;
225
-
226
- const q = `
227
- BEGIN; -- ISOLATION LEVEL SERIALIZABLE;-- TRANSACTION ISOLATION LEVEL SERIALIZABLE;
228
-
229
- --SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
230
-
231
- /*
232
- * ${PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID}
233
- */
234
-
235
- DO
236
- $do$
237
- BEGIN
238
-
239
- /* Reduce deadlocks */
240
- PERFORM pg_sleep(random());
241
-
242
- /* Drop older version */
243
- IF EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'prostgles') THEN
244
-
245
- IF
246
- NOT EXISTS (
247
- SELECT 1
248
- FROM information_schema.tables
249
- WHERE table_schema = 'prostgles'
250
- AND table_name = 'versions'
251
- )
252
- THEN
253
- DROP SCHEMA IF EXISTS prostgles CASCADE;
254
- ELSE
255
- IF NOT EXISTS(SELECT 1 FROM prostgles.versions WHERE version >= ${schema_version}) THEN
256
- DROP SCHEMA IF EXISTS prostgles CASCADE;
257
- END IF;
258
- END IF;
259
-
260
- END IF;
261
-
262
-
263
- IF NOT EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = 'prostgles')
264
- THEN
265
- --RAISE NOTICE 'CREATE SCHEMA IF NOT EXISTS prostgles';
266
-
267
- CREATE SCHEMA IF NOT EXISTS prostgles;
268
-
269
- CREATE TABLE IF NOT EXISTS prostgles.versions(
270
- version NUMERIC PRIMARY KEY
271
- );
272
- INSERT INTO prostgles.versions(version) VALUES(${schema_version}) ON CONFLICT DO NOTHING;
273
-
274
- CREATE OR REPLACE FUNCTION prostgles.random_string(length INTEGER DEFAULT 33) RETURNS TEXT AS $$
275
- DECLARE
276
- chars TEXT[] := '{0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z}';
277
- result TEXT := '';
278
- i INTEGER := 0;
279
- BEGIN
280
- IF length < 0 THEN
281
- RAISE exception 'Given length cannot be less than 0';
282
- END IF;
283
- FOR i IN 1..length LOOP
284
- result := result || chars[1+random()*(array_length(chars, 1)-1)];
285
- END LOOP;
286
- RETURN result;
287
- END;
288
- $$ language plpgsql;
289
- COMMENT ON FUNCTION prostgles.random_string IS 'UUIDs without installing pgcrypto';
290
-
291
-
292
- CREATE OR REPLACE FUNCTION prostgles.debug(VARIADIC args TEXT[]) RETURNS VOID AS $$
293
- BEGIN
294
-
295
- --PERFORM pg_notify('debug', concat_ws(' ', args));
296
- IF
297
- NOT EXISTS (
298
- SELECT 1
299
- FROM information_schema.tables
300
- WHERE table_schema = 'prostgles'
301
- AND table_name = 'debug'
302
- )
303
- THEN
304
- CREATE TABLE IF NOT EXISTS prostgles.debug(m TEXT);
305
- END IF;
306
-
307
- INSERT INTO prostgles.debug(m) VALUES(concat_ws(' ', args));
308
-
309
- END;
310
- $$ LANGUAGE plpgsql;
311
- COMMENT ON FUNCTION prostgles.debug IS 'Used for internal debugging';
312
-
313
-
314
- CREATE TABLE IF NOT EXISTS prostgles.apps (
315
- id TEXT PRIMARY KEY DEFAULT prostgles.random_string(),
316
- added TIMESTAMP DEFAULT NOW(),
317
- application_name TEXT,
318
- last_check TIMESTAMP NOT NULL DEFAULT NOW(),
319
- last_check_ended TIMESTAMP NOT NULL DEFAULT NOW(),
320
- watching_schema BOOLEAN DEFAULT FALSE,
321
- check_frequency_ms INTEGER NOT NULL
322
- );
323
- COMMENT ON TABLE prostgles.apps IS 'Keep track of prostgles server apps connected to db to combine common triggers. Heartbeat used due to no logout triggers in postgres';
324
-
325
- CREATE TABLE IF NOT EXISTS prostgles.app_triggers (
326
- app_id TEXT NOT NULL,
327
- table_name TEXT NOT NULL,
328
- condition TEXT NOT NULL,
329
- inserted TIMESTAMP NOT NULL DEFAULT NOW(),
330
- last_used TIMESTAMP NOT NULL DEFAULT NOW(),
331
- PRIMARY KEY (app_id, table_name, condition) -- This unqique index limits the condition column value to be less than 'SELECT current_setting('block_size');'
332
- );
333
- COMMENT ON TABLE prostgles.app_triggers IS 'Tables and conditions that are currently subscribed/synced';
334
-
335
-
336
- CREATE OR REPLACE VIEW prostgles.v_triggers AS
337
- SELECT *
338
- , (ROW_NUMBER() OVER( ORDER BY table_name, condition ))::text AS id
339
- -- , concat_ws('-', table_name, condition) AS id
340
- , ROW_NUMBER() OVER(PARTITION BY app_id, table_name ORDER BY table_name, condition ) - 1 AS c_id
341
- FROM prostgles.app_triggers;
342
- COMMENT ON VIEW prostgles.v_triggers IS 'Augment trigger table with natural IDs and per app IDs';
343
-
344
-
345
- /*
346
- CREATE OR REPLACE VIEW prostgles.v_triggers_unnested AS
347
- SELECT *
348
- , ROW_NUMBER() OVER(PARTITION BY app_id, table_name ORDER BY table_name, condition ) - 1 AS c_id
349
- FROM (
350
- SELECT *, unnest(app_ids) as app_id
351
- FROM prostgles.v_triggers
352
- ) t;
353
-
354
- -- Force table into cache
355
- IF EXISTS (select * from pg_extension where extname = 'pg_prewarm') THEN
356
- CREATE EXTENSION IF NOT EXISTS pg_prewarm;
357
- PERFORM pg_prewarm('prostgles.app_triggers');
358
- END IF;
359
- */
360
-
361
-
362
- CREATE OR REPLACE FUNCTION ${this.DB_OBJ_NAMES.data_watch_func}() RETURNS TRIGGER
363
- AS $$
364
-
365
- DECLARE t_ids TEXT[];
366
- DECLARE c_ids INTEGER[];
367
- DECLARE err_c_ids INTEGER[];
368
- DECLARE unions TEXT := '';
369
- DECLARE query TEXT := '';
370
- DECLARE nrw RECORD;
371
- DECLARE erw RECORD;
372
- DECLARE has_errors BOOLEAN := FALSE;
373
-
374
- DECLARE err_text TEXT;
375
- DECLARE err_detail TEXT;
376
- DECLARE err_hint TEXT;
377
-
378
- BEGIN
379
-
380
- -- PERFORM pg_notify('debug', concat_ws(' ', 'TABLE', TG_TABLE_NAME, TG_OP));
381
-
382
- SELECT string_agg(
383
- concat_ws(
384
- E' UNION \n ',
385
- CASE WHEN (TG_OP = 'DELETE' OR TG_OP = 'UPDATE') THEN (p1 || ' old_table ' || p2) END,
386
- CASE WHEN (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN (p1 || ' new_table ' || p2) END
387
- ),
388
- E' UNION \n '::text
389
- )
390
- INTO unions
391
- FROM (
392
- SELECT
393
- $z$
394
- SELECT CASE WHEN EXISTS( SELECT 1 FROM
395
- $z$ AS p1,
396
- format(
397
- $c$
398
- as %I WHERE %s ) THEN %s::text END AS t_ids
399
- $c$,
400
- table_name, condition, id
401
- ) AS p2
402
- FROM prostgles.v_triggers
403
- WHERE table_name = TG_TABLE_NAME
404
- ) t;
405
-
406
- /*
407
- PERFORM pg_notify(
408
- ${asValue(this.NOTIF_CHANNEL.preffix)} || (SELECT id FROM prostgles.apps LIMIT 1) ,
409
- concat_ws(
410
- ${asValue(PubSubManager.DELIMITER)},
411
-
412
- ${asValue(this.NOTIF_TYPE.data)},
413
- COALESCE(TG_TABLE_NAME, 'MISSING'),
414
- COALESCE(TG_OP, 'MISSING'),
415
- unions
416
- )
417
- );
418
- RAISE 'unions: % , cids: %', unions, c_ids;
419
- */
420
-
421
- IF unions IS NOT NULL THEN
422
- query = format(
423
- $s$
424
- SELECT ARRAY_AGG(DISTINCT t.t_ids)
425
- FROM ( %s ) t
426
- $s$,
427
- unions
428
- );
429
-
430
- BEGIN
431
- EXECUTE query INTO t_ids;
432
-
433
- --RAISE NOTICE 'trigger fired ok';
434
-
435
- EXCEPTION WHEN OTHERS THEN
436
-
437
- has_errors := TRUE;
438
-
439
- GET STACKED DIAGNOSTICS
440
- err_text = MESSAGE_TEXT,
441
- err_detail = PG_EXCEPTION_DETAIL,
442
- err_hint = PG_EXCEPTION_HINT;
443
-
444
-
445
- END;
446
-
447
- --RAISE NOTICE 'has_errors: % ', has_errors;
448
- --RAISE NOTICE 'unions: % , cids: %', unions, c_ids;
449
-
450
- IF (t_ids IS NOT NULL OR has_errors) THEN
451
-
452
- FOR nrw IN
453
- SELECT app_id, string_agg(c_id::text, ',') as cids
454
- FROM prostgles.v_triggers
455
- WHERE id = ANY(t_ids)
456
- OR has_errors
457
- GROUP BY app_id
458
- LOOP
459
-
460
- PERFORM pg_notify(
461
- ${asValue(this.NOTIF_CHANNEL.preffix)} || nrw.app_id ,
462
- concat_ws(
463
- ${asValue(PubSubManager.DELIMITER)},
464
-
465
- ${asValue(this.NOTIF_TYPE.data)},
466
- COALESCE(TG_TABLE_NAME, 'MISSING'),
467
- COALESCE(TG_OP, 'MISSING'),
468
- CASE WHEN has_errors
469
- THEN concat_ws('; ', 'error', err_text, err_detail, err_hint )
470
- ELSE COALESCE(nrw.cids, '')
471
- END
472
- ${this.dboBuilder.prostgles.opts.DEBUG_MODE? (", (select json_agg(t)::TEXT FROM (SELECT * from old_table) t), query") : ""}
473
- )
474
- );
475
- END LOOP;
476
-
477
-
478
- IF has_errors THEN
479
-
480
- DELETE FROM prostgles.app_triggers;
481
- RAISE NOTICE 'trigger dropped due to exception: % % %', err_text, err_detail, err_hint;
482
-
483
- END IF;
484
-
485
-
486
- END IF;
487
- END IF;
488
-
489
-
490
- RETURN NULL;
491
-
492
- /*
493
- EXCEPTION WHEN OTHERS THEN
494
- DELETE FROM prostgles.app_triggers; -- delete all or will need to loop through all conditions to find issue;
495
- RAISE NOTICE 'trigger dropped due to exception';
496
- ${"--EXCEPTION_WHEN_COLUMN_WAS_RENAMED_THEN_DROP_TRIGGER"};
497
-
498
-
499
-
500
- RETURN NULL;
501
- */
502
- END;
503
-
504
- --COMMIT;
505
- $$ LANGUAGE plpgsql;
506
- COMMENT ON FUNCTION ${this.DB_OBJ_NAMES.data_watch_func} IS 'Prostgles internal function used to notify when data in the table changed';
507
-
508
-
509
-
510
- CREATE OR REPLACE FUNCTION ${this.DB_OBJ_NAMES.trigger_add_remove_func}() RETURNS TRIGGER
511
- AS $$
512
-
513
- DECLARE operations TEXT[] := ARRAY['insert', 'update', 'delete'];
514
- DECLARE op TEXT;
515
- DECLARE query TEXT;
516
- DECLARE trw RECORD;
517
-
518
- BEGIN
519
-
520
-
521
- --RAISE NOTICE 'prostgles.app_triggers % ', TG_OP;
522
-
523
- /* If no other listeners on table then DROP triggers */
524
- IF TG_OP = 'DELETE' THEN
525
-
526
- --RAISE NOTICE 'DELETE trigger_add_remove_func table: % ', ' ' || COALESCE((SELECT concat_ws(' ', string_agg(table_name, ' & '), count(*), min(inserted) ) FROM prostgles.app_triggers) , ' 0 ');
527
- --RAISE NOTICE 'DELETE trigger_add_remove_func old_table: % ', '' || COALESCE((SELECT concat_ws(' ', string_agg(table_name, ' & '), count(*), min(inserted) ) FROM old_table), ' 0 ');
528
-
529
-
530
- /* Drop actual triggers if needed */
531
- FOR trw IN
532
- SELECT DISTINCT table_name FROM old_table ot
533
- WHERE NOT EXISTS (
534
- SELECT 1 FROM prostgles.app_triggers t
535
- WHERE t.table_name = ot.table_name
536
- )
537
- LOOP
538
-
539
- FOREACH op IN ARRAY operations
540
- LOOP
541
- --RAISE NOTICE ' DROP DATA TRIGGER FOR: % ', trw.table_name;
542
- EXECUTE format(' DROP TRIGGER IF EXISTS %I ON %I ;' , 'prostgles_triggers_' || trw.table_name || '_' || op, trw.table_name);
543
- END LOOP;
544
-
545
- END LOOP;
546
-
547
- /* If newly added listeners on table then CREATE triggers */
548
- ELSIF TG_OP = 'INSERT' THEN
549
-
550
-
551
- --RAISE NOTICE 'INSERT trigger_add_remove_func table: % ', ' ' || COALESCE((SELECT concat_ws(' ', string_agg(table_name, ' & '), count(*), min(inserted) ) FROM prostgles.triggers) , ' 0 ');
552
- --RAISE NOTICE 'INSERT trigger_add_remove_func new_table: % ', '' || COALESCE((SELECT concat_ws(' ', string_agg(table_name, ' & '), count(*), min(inserted) ) FROM new_table), ' 0 ');
553
-
554
- /* Loop through newly added tables */
555
- FOR trw IN
556
-
557
- SELECT DISTINCT table_name
558
- FROM new_table nt
559
-
560
- /* Table did not exist prior to this insert */
561
- WHERE NOT EXISTS (
562
- SELECT 1
563
- FROM prostgles.app_triggers t
564
- WHERE t.table_name = nt.table_name
565
- AND t.inserted < nt.inserted -- exclude current record (this is an after trigger). Turn into before trigger?
566
- )
567
-
568
- /* Table is valid */
569
- AND EXISTS (
570
- SELECT 1
571
- FROM information_schema.tables
572
- WHERE table_schema = 'public'
573
- AND table_name = nt.table_name
574
- )
575
- LOOP
576
-
577
- /*
578
- RAISE NOTICE ' CREATE DATA TRIGGER FOR: % TABLE EXISTS?', trw.table_name, SELECT EXISTS (
579
- SELECT 1
580
- FROM information_schema.tables
581
- WHERE table_schema = 'public'
582
- AND table_name = nt.table_name
583
- );
584
- */
585
-
586
- query := format(
587
- $q$
588
- DROP TRIGGER IF EXISTS %1$I ON %2$I;
589
- CREATE TRIGGER %1$I
590
- AFTER INSERT ON %2$I
591
- REFERENCING NEW TABLE AS new_table
592
- FOR EACH STATEMENT EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.data_watch_func}();
593
- COMMENT ON TRIGGER %1$I ON %2$I IS 'Prostgles internal trigger used to notify when data in the table changed';
594
- $q$,
595
- 'prostgles_triggers_' || trw.table_name || '_insert', trw.table_name
596
- ) || format(
597
- $q$
598
- DROP TRIGGER IF EXISTS %1$I ON %2$I;
599
- CREATE TRIGGER %1$I
600
- AFTER UPDATE ON %2$I
601
- REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
602
- FOR EACH STATEMENT EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.data_watch_func}();
603
- COMMENT ON TRIGGER %1$I ON %2$I IS 'Prostgles internal trigger used to notify when data in the table changed';
604
- $q$,
605
- 'prostgles_triggers_' || trw.table_name || '_update', trw.table_name
606
- ) || format(
607
- $q$
608
- DROP TRIGGER IF EXISTS %1$I ON %2$I;
609
- CREATE TRIGGER %1$I
610
- AFTER DELETE ON %2$I
611
- REFERENCING OLD TABLE AS old_table
612
- FOR EACH STATEMENT EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.data_watch_func}();
613
- COMMENT ON TRIGGER %1$I ON %2$I IS 'Prostgles internal trigger used to notify when data in the table changed';
614
- $q$,
615
- 'prostgles_triggers_' || trw.table_name || '_delete', trw.table_name
616
- );
617
-
618
- --RAISE NOTICE ' % ', query;
619
-
620
-
621
- query := format(
622
- $q$
623
- DO $e$
624
- BEGIN
625
-
626
- IF EXISTS (
627
- SELECT 1
628
- FROM information_schema.tables
629
- WHERE table_schema = 'public'
630
- AND table_name = %L
631
- ) THEN
632
-
633
- %s
634
-
635
- END IF;
636
-
637
- END $e$;
638
- $q$,
639
- trw.table_name,
640
- query
641
- ) ;
642
-
643
-
644
- EXECUTE query;
645
-
646
- END LOOP;
647
-
648
- END IF;
649
-
650
-
651
- RETURN NULL;
652
- END;
653
-
654
- $$ LANGUAGE plpgsql;
655
- COMMENT ON FUNCTION ${this.DB_OBJ_NAMES.trigger_add_remove_func} IS 'Used to add/remove table watch triggers concurrently ';
656
-
657
- DROP TRIGGER IF EXISTS prostgles_triggers_insert ON prostgles.app_triggers;
658
- CREATE TRIGGER prostgles_triggers_insert
659
- AFTER INSERT ON prostgles.app_triggers
660
- REFERENCING NEW TABLE AS new_table
661
- FOR EACH STATEMENT EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.trigger_add_remove_func}();
662
-
663
- DROP TRIGGER IF EXISTS prostgles_triggers_delete ON prostgles.app_triggers;
664
- CREATE TRIGGER prostgles_triggers_delete
665
- AFTER DELETE ON prostgles.app_triggers
666
- REFERENCING OLD TABLE AS old_table
667
- FOR EACH STATEMENT EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.trigger_add_remove_func}();
668
-
669
-
670
- CREATE OR REPLACE FUNCTION ${this.DB_OBJ_NAMES.schema_watch_func}() RETURNS event_trigger AS $$
671
-
672
- DECLARE curr_query TEXT := '';
673
- DECLARE arw RECORD;
674
-
675
- BEGIN
676
-
677
- --RAISE NOTICE 'SCHEMA_WATCH: %', tg_tag;
678
-
679
- /*
680
- This event trigger will outlive a prostgles app instance.
681
- Must ensure it only fires if an app instance is running
682
- */
683
- IF
684
- EXISTS (
685
- SELECT 1
686
- FROM information_schema.tables
687
- WHERE table_schema = 'prostgles'
688
- AND table_name = 'apps'
689
- )
690
- THEN
691
-
692
- SELECT LEFT(COALESCE(current_query(), ''), 5000)
693
- INTO curr_query;
694
-
695
- FOR arw IN
696
- SELECT * FROM prostgles.apps WHERE watching_schema IS TRUE
697
-
698
- LOOP
699
- PERFORM pg_notify(
700
- ${asValue(this.NOTIF_CHANNEL.preffix)} || arw.id,
701
- concat_ws(
702
- ${asValue(PubSubManager.DELIMITER)},
703
- ${asValue(this.NOTIF_TYPE.schema)}, tg_tag , TG_event, curr_query
704
- )
705
- );
706
- END LOOP;
707
-
708
- END IF;
709
-
710
- END;
711
- $$ LANGUAGE plpgsql;
712
- COMMENT ON FUNCTION ${this.DB_OBJ_NAMES.schema_watch_func} IS 'Prostgles internal function used to notify when schema has changed';
713
-
714
- END IF;
715
-
716
- END
717
- $do$;
718
-
719
-
720
- COMMIT;
721
- `;
722
-
723
- // const prgl_exists = await this.db.oneOrNone(`
724
- // DROP SCHEMA IF EXISTS prostgles CASCADE;
725
- // SELECT 1 FROM information_schema.schemata WHERE schema_name = 'prostgles'
726
- // `);
727
-
728
- // if(!prgl_exists){
729
- // await this.db.any(q);
730
- // }
731
- await this.db.any(q);
732
- if (!this.canContinue()) return;
733
-
734
-
735
- /* Prepare App id */
736
- if (!this.appID) {
737
- const raw = await this.db.one(
738
- "INSERT INTO prostgles.apps (check_frequency_ms, watching_schema, application_name) VALUES($1, $2, current_setting('application_name')) RETURNING *; "
739
- , [this.appCheckFrequencyMS, Boolean(this.onSchemaChange)]
740
- );
741
- this.appID = raw.id;
742
-
743
- if (!this.appCheck) {
744
-
745
- this.appCheck = setInterval(async () => {
746
- let appQ = "";
747
- try { // drop owned by api
748
-
749
- this.appChecking = true;
750
-
751
- let trgUpdateLastUsed = "",
752
- listeners = this.getActiveListeners();
753
-
754
- if (listeners.length) {
755
- trgUpdateLastUsed = `
756
- UPDATE prostgles.app_triggers
757
- SET last_used = CASE WHEN (table_name, condition) IN (
758
- ${listeners.map(l => ` ( ${asValue(l.table_name)}, ${asValue(l.condition)} ) `).join(", ")}
759
- ) THEN NOW() ELSE last_used END
760
- WHERE app_id = ${asValue(this.appID)};
761
- `;
762
- }
763
-
764
- appQ = `
765
-
766
- DO $$
767
- BEGIN
768
-
769
- /* ${REALTIME_TRIGGER_CHECK_QUERY} */
770
- /* prostgles schema must exist */
771
- IF
772
- EXISTS (
773
- SELECT 1
774
- FROM information_schema.tables
775
- WHERE table_schema = 'prostgles'
776
- AND table_name = 'apps'
777
- )
778
- THEN
779
-
780
-
781
- /* Concurrency control to avoid deadlock
782
- IF NOT EXISTS (
783
- SELECT 1 FROM prostgles.apps
784
- WHERE last_check < last_check_ended
785
- AND last_check_ended > NOW() - interval '5 minutes'
786
- ) THEN
787
- */
788
- UPDATE prostgles.apps
789
- SET last_check = NOW()
790
- WHERE id = ${asValue(this.appID)};
791
-
792
-
793
-
794
- /* Delete unused triggers. Might deadlock */
795
- IF EXISTS ( SELECT 1 FROM prostgles.app_triggers)
796
-
797
- /* If this is the latest app then proceed
798
- AND (
799
- SELECT id = ${asValue(this.appID)}
800
- FROM prostgles.apps
801
- ORDER BY last_check DESC
802
- LIMIT 1
803
- ) = TRUE
804
- */
805
-
806
- THEN
807
-
808
- /* TODO: Fixed deadlocks */
809
- --LOCK TABLE prostgles.app_triggers IN ACCESS EXCLUSIVE MODE;
810
-
811
- /* UPDATE currently used triggers */
812
- ${trgUpdateLastUsed}
813
-
814
- /* DELETE stale triggers for current app. Other triggers will be deleted on app startup */
815
- DELETE FROM prostgles.app_triggers
816
- WHERE app_id = ${asValue(this.appID)}
817
- AND last_used < NOW() - 4 * ${asValue(this.appCheckFrequencyMS)} * interval '1 millisecond'; -- 10 seconds at the moment
818
-
819
- END IF;
820
-
821
-
822
-
823
- UPDATE prostgles.apps
824
- SET last_check_ended = NOW()
825
- WHERE id = ${asValue(this.appID)};
826
-
827
- /*
828
- END IF;
829
- */
830
-
831
-
832
- END IF;
833
-
834
- -- must not commit without a lock
835
- --COMMIT;
836
- END $$;
837
- `
838
- await this.db.any(appQ);
839
- log("updated last_check");
840
- } catch (e: any) {
841
- /** In some cases a query idles and blocks everything else. Terminate all similar queries */
842
- this.db.any("SELECT state, pg_terminate_backend(pid) from pg_stat_activity WHERE query ilike ${qid} and pid <> pg_backend_pid();", { qid: "%" + REALTIME_TRIGGER_CHECK_QUERY + "%" });
843
-
844
- /** If this database was dropped then stop interval */
845
- if(e?.code === "3D000"){ // && e.message.includes(this.db.$cn.database)
846
- clearInterval(this.appCheck);
847
- }
848
- console.error("appCheck FAILED: \n", e, appQ);
849
- }
850
-
851
- this.appChecking = false;
852
- }, 0.8 * this.appCheckFrequencyMS);
853
- }
854
- }
855
-
856
- this.postgresNotifListenManager = new PostgresNotifListenManager(this.db, this.notifListener, this.NOTIF_CHANNEL.getFull());
857
-
858
- await this.prepareTriggers()
859
-
860
- return this;
861
-
862
- } catch (e) {
863
- console.error("PubSubManager init failed: ", e);
864
- }
865
- }
866
-
867
- DB_OBJ_NAMES = {
868
- trigger_add_remove_func: "prostgles.trigger_add_remove_func",
869
- data_watch_func: "prostgles.prostgles_trigger_function",
870
- schema_watch_func: "prostgles.schema_watch_func",
871
- schema_watch_trigger: "prostgles_schema_watch_trigger_new"
872
- }
873
-
874
- static SCHEMA_ALTERING_QUERIES = ['CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE VIEW', 'DROP VIEW', 'ALTER VIEW', 'CREATE TABLE AS', 'SELECT INTO'];
875
-
876
- static EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID = "prostgles internal query that should be excluded from "
877
- prepareTriggers = async () => {
878
- // SELECT * FROM pg_catalog.pg_event_trigger WHERE evtname
879
- if (!this.appID) throw "prepareTriggers failed: this.appID missing";
880
- if (this.dboBuilder.prostgles.opts.watchSchema && !(await isSuperUser(this.db))) {
881
- console.warn("prostgles watchSchema requires superuser db user. Will not watch using event triggers")
882
- }
883
-
884
- try {
885
-
886
- await this.db.any(`
887
- BEGIN;-- ISOLATION LEVEL SERIALIZABLE;
888
-
889
- /** ${PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID}
890
- * Drop stale triggers
891
- * */
892
- DO
893
- $do$
894
- DECLARE trg RECORD;
895
- q TEXT;
896
- ev_trg_needed BOOLEAN := FALSE;
897
- ev_trg_exists BOOLEAN := FALSE;
898
- is_super_user BOOLEAN := FALSE;
899
- BEGIN
900
- --SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
901
-
902
- LOCK TABLE prostgles.app_triggers IN ACCESS EXCLUSIVE MODE;
903
- EXECUTE format(
904
- $q$
905
-
906
- CREATE TEMP TABLE %1$I AS --ON COMMIT DROP AS
907
- SELECT * FROM prostgles.app_triggers;
908
-
909
- DELETE FROM prostgles.app_triggers;
910
-
911
- INSERT INTO prostgles.app_triggers
912
- SELECT * FROM %1$I;
913
-
914
- DROP TABLE IF EXISTS %1$I;
915
- $q$,
916
- ${asValue('triggers_' + this.appID)}
917
- );
918
-
919
- is_super_user := EXISTS (select 1 from pg_user where usename = CURRENT_USER AND usesuper IS TRUE);
920
- /**
921
- * Delete stale app records
922
- * */
923
- DELETE FROM prostgles.apps
924
- WHERE last_check < NOW() - 8 * check_frequency_ms * interval '1 millisecond';
925
-
926
- DELETE FROM prostgles.app_triggers
927
- WHERE app_id NOT IN (SELECT id FROM prostgles.apps);
928
-
929
- /* DROP the old buggy schema watch trigger */
930
- IF EXISTS (
931
- SELECT 1 FROM pg_catalog.pg_event_trigger
932
- WHERE evtname = 'prostgles_schema_watch_trigger'
933
- ) AND is_super_user IS TRUE
934
- THEN
935
- DROP EVENT TRIGGER IF EXISTS prostgles_schema_watch_trigger;
936
- END IF;
937
-
938
- ev_trg_needed := EXISTS (SELECT 1 FROM prostgles.apps WHERE watching_schema IS TRUE);
939
- ev_trg_exists := EXISTS (
940
- SELECT 1 FROM pg_catalog.pg_event_trigger
941
- WHERE evtname = ${asValue(this.DB_OBJ_NAMES.schema_watch_trigger)}
942
- );
943
-
944
- -- RAISE NOTICE ' ev_trg_needed %, ev_trg_exists %', ev_trg_needed, ev_trg_exists;
945
-
946
- /**
947
- * DROP stale event trigger
948
- * */
949
- IF is_super_user IS TRUE AND ev_trg_needed IS FALSE AND ev_trg_exists IS TRUE THEN
950
-
951
- SELECT format(
952
- $$ DROP EVENT TRIGGER IF EXISTS %I ; $$
953
- , ${asValue(this.DB_OBJ_NAMES.schema_watch_trigger)}
954
- )
955
- INTO q;
956
-
957
- --RAISE NOTICE ' DROP EVENT TRIGGER %', q;
958
-
959
- EXECUTE q;
960
-
961
- /**
962
- * CREATE event trigger
963
- * */
964
- ELSIF
965
- is_super_user IS TRUE
966
- AND ev_trg_needed IS TRUE
967
- AND ev_trg_exists IS FALSE
968
- THEN
969
-
970
- DROP EVENT TRIGGER IF EXISTS ${this.DB_OBJ_NAMES.schema_watch_trigger};
971
- CREATE EVENT TRIGGER ${this.DB_OBJ_NAMES.schema_watch_trigger} ON ddl_command_end
972
- WHEN TAG IN ('COMMENT', 'CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE VIEW', 'DROP VIEW', 'ALTER VIEW', 'CREATE TABLE AS', 'SELECT INTO')
973
- --WHEN TAG IN ('CREATE TABLE', 'ALTER TABLE', 'DROP TABLE', 'CREATE TRIGGER', 'DROP TRIGGER')
974
- EXECUTE PROCEDURE ${this.DB_OBJ_NAMES.schema_watch_func}();
975
-
976
- --RAISE NOTICE ' CREATED EVENT TRIGGER %', q;
977
- END IF;
978
-
979
-
980
- END
981
- $do$;
982
-
983
-
984
- COMMIT;
985
- `).catch(e => {
986
- console.error("prepareTriggers failed: ", e);
987
- throw e;
988
- });
989
-
990
- return true;
991
-
992
- } catch (e) {
993
- console.error("prepareTriggers failed: ", e);
994
- throw e;
995
- }
996
- }
997
-
998
- isReady() {
999
- if (!this.postgresNotifListenManager) throw "this.postgresNotifListenManager missing";
1000
- return this.postgresNotifListenManager.isListening();
1001
- }
1002
-
1003
- getSubs(table_name: string, condition: string): SubscriptionParams[] {
1004
- return this.subs?.[table_name]?.[condition]?.subs
1005
- }
1006
-
1007
- getSyncs(table_name: string, condition: string) {
1008
- return (this.syncs || [])
1009
- .filter((s: SyncParams) => s.table_name === table_name && s.condition === condition);
1010
- }
1011
-
1012
- /* Relay relevant data to relevant subscriptions */
1013
- notifListener = async (data: { payload: string }) => {
1014
- const str = data.payload;
1015
-
1016
- if (!str) {
1017
- console.error("Empty notif?")
1018
- return;
1019
- }
1020
- const dataArr = str.split(PubSubManager.DELIMITER),
1021
- notifType = dataArr[0];
1022
-
1023
- log(str);
1024
-
1025
- if (notifType === this.NOTIF_TYPE.schema) {
1026
- if (this.onSchemaChange) {
1027
- const command = dataArr[1],
1028
- event_type = dataArr[2],
1029
- query = dataArr[3];
1030
-
1031
- if (query) {
1032
- this.onSchemaChange({ command, query })
1033
- }
1034
- }
1035
-
1036
- return;
1037
- }
1038
-
1039
- if (notifType !== this.NOTIF_TYPE.data) {
1040
- console.error("Unexpected notif type: ", notifType);
1041
- return;
1042
- }
1043
-
1044
- const table_name = dataArr[1],
1045
- op_name = dataArr[2],
1046
- condition_ids_str = dataArr[3];
1047
-
1048
- // 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)]);
1049
- // const conditions: string[] = triggers.map(t => t.condition);
1050
-
1051
- log("PG Trigger ->", dataArr.join("__"))
1052
- if (
1053
- condition_ids_str && condition_ids_str.startsWith("error") &&
1054
- this._triggers && this._triggers[table_name] && this._triggers[table_name].length
1055
- ) {
1056
- const pref = "INTERNAL ERROR. Schema might have changed";
1057
- console.error(`${pref}: ${condition_ids_str}`)
1058
- this._triggers[table_name].map(c => {
1059
- const subs = this.getSubs(table_name, c);
1060
- subs.map(s => {
1061
- this.pushSubData(s, pref + ". Check server logs");
1062
- })
1063
- });
1064
- } else if (
1065
- condition_ids_str?.split(",").length &&
1066
- condition_ids_str?.split(",").every((c: string) => Number.isInteger(+c)) &&
1067
- this._triggers?.[table_name]?.length
1068
- ) {
1069
-
1070
-
1071
- const idxs = condition_ids_str.split(",").map(v => +v);
1072
- const conditions = this._triggers[table_name].filter((c, i) => idxs.includes(i))
1073
-
1074
- log("PG Trigger -> ", { table_name, op_name, condition_ids_str, conditions }, this._triggers[table_name]);
1075
-
1076
- conditions.map(condition => {
1077
-
1078
- const subs = this.getSubs(table_name, condition);
1079
- const syncs = this.getSyncs(table_name, condition);
1080
-
1081
-
1082
- syncs.map((s) => {
1083
- this.syncData(s, undefined, "trigger");
1084
- });
1085
-
1086
- if (!subs) {
1087
-
1088
- // console.error(`sub missing for ${table_name} ${condition}`, this.triggers);
1089
- // console.log(this.subs)
1090
- return;
1091
- }
1092
-
1093
- /* Throttle the subscriptions */
1094
- for (var i = 0; i < subs.length; i++) {
1095
- var sub = subs[i];
1096
- if (
1097
- this.dbo[sub.table_name] &&
1098
- sub.is_ready &&
1099
- (sub.socket_id && this.sockets[sub.socket_id]) || sub.func
1100
- ) {
1101
- const throttle = sub.throttle || 0;
1102
- if (sub.last_throttled <= Date.now() - throttle) {
1103
-
1104
- /* It is assumed the policy was checked before this point */
1105
- this.pushSubData(sub);
1106
- // sub.last_throttled = Date.now();
1107
- } else if (!sub.is_throttling) {
1108
-
1109
-
1110
- log("throttling sub")
1111
- sub.is_throttling = setTimeout(() => {
1112
- log("throttling finished. pushSubData...")
1113
- sub.is_throttling = null;
1114
- this.pushSubData(sub);
1115
- }, throttle);// sub.throttle);
1116
- }
1117
- }
1118
- }
1119
- });
1120
-
1121
- } else {
1122
-
1123
- // if(!this._triggers || !this._triggers[table_name] || !this._triggers[table_name].length){
1124
- // console.warn(190, "Trigger sub not found. DROPPING TRIGGER", table_name, condition_ids_str, this._triggers);
1125
- // this.dropTrigger(table_name);
1126
- // } else {
1127
- // }
1128
- console.warn(190, "Trigger sub issue: ", table_name, condition_ids_str, this._triggers);
1129
- }
1130
- }
1131
-
1132
-
1133
- pushSubData(sub: SubscriptionParams, err?: any) {
1134
- if (!sub) throw "pushSubData: invalid sub";
1135
- const { table_name, filter, params, table_rules, socket_id, channel_name, func } = sub; //, subOne = false
1136
-
1137
- sub.last_throttled = Date.now();
1138
-
1139
- if (err) {
1140
- if (socket_id) {
1141
- this.sockets[socket_id].emit(channel_name, { err });
1142
- }
1143
- return true;
1144
- }
1145
-
1146
- return new Promise(async (resolve, reject) => {
1147
- /* TODO: Retire subOne -> it's redundant */
1148
- // this.dbo[table_name][subOne? "findOne" : "find"](filter, params, null, table_rules)
1149
- if (!this.dbo?.[table_name]?.find) {
1150
- throw new Error(`1107 this.dbo.${table_name}.find`);
1151
- }
1152
-
1153
- this.dbo?.[table_name]?.find?.(filter, params, undefined, table_rules)
1154
- .then(data => {
1155
-
1156
- if (socket_id && this.sockets[socket_id]) {
1157
- log("Pushed " + data.length + " records to sub")
1158
- this.sockets[socket_id].emit(channel_name, { data }, () => {
1159
- resolve(data);
1160
- });
1161
- /* TO DO: confirm receiving data or server will unsubscribe
1162
- { data }, (cb)=> { console.log(cb) });
1163
- */
1164
- } else if (func) {
1165
- func(data);
1166
- resolve(data);
1167
- }
1168
- sub.last_throttled = Date.now();
1169
- }).catch(err => {
1170
- const errObj = { _err_msg: err.toString(), err };
1171
- if (socket_id && this.sockets[socket_id]) {
1172
- this.sockets[socket_id].emit(channel_name, { err: errObj });
1173
- } else if (func) {
1174
- func({ err: errObj });
1175
- }
1176
- reject(errObj)
1177
- });
1178
- });
1179
- }
1180
-
1181
- upsertSocket(socket: any, channel_name: string) {
1182
- if (socket && !this.sockets[socket.id]) {
1183
- this.sockets[socket.id] = socket;
1184
- socket.on("disconnect", () => this.onSocketDisconnected(socket));
1185
- }
1186
- }
1187
-
1188
- syncTimeout?: ReturnType<typeof setTimeout>;
1189
- async syncData(sync: SyncParams, clientData: ClientExpressData | undefined, source: "trigger" | "client") {
1190
- return await syncData(this, sync, clientData, source);
1191
- }
1192
-
1193
- /**
1194
- * Returns a sync channel
1195
- * A sync channel is unique per socket for each filter
1196
- */
1197
- async addSync(syncParams: AddSyncParams) {
1198
- const {
1199
- socket = null, table_info = null, table_rules, synced_field = null,
1200
- allow_delete = false, id_fields = [], filter = {},
1201
- params, condition = "", throttle = 0
1202
- } = syncParams || {};
1203
-
1204
- let conditionParsed = parseCondition(condition);
1205
- if (!socket || !table_info) throw "socket or table_info missing";
1206
-
1207
-
1208
- const { name: table_name } = table_info,
1209
- channel_name = `${this.socketChannelPreffix}.${table_name}.${JSON.stringify(filter)}.sync`;
1210
-
1211
- if (!synced_field) throw "synced_field missing from table_rules";
1212
-
1213
- this.upsertSocket(socket, channel_name);
1214
-
1215
- const upsertSync = () => {
1216
- let newSync = {
1217
- channel_name,
1218
- table_name,
1219
- filter,
1220
- condition: conditionParsed,
1221
- synced_field,
1222
- id_fields,
1223
- allow_delete,
1224
- table_rules,
1225
- throttle: Math.max(throttle || 0, table_rules?.sync?.throttle || 0),
1226
- batch_size: get(table_rules, "sync.batch_size") || DEFAULT_SYNC_BATCH_SIZE,
1227
- last_throttled: 0,
1228
- socket_id: socket.id,
1229
- is_sync: true,
1230
- last_synced: 0,
1231
- lr: undefined,
1232
- table_info,
1233
- is_syncing: false,
1234
- wal: undefined,
1235
- socket,
1236
- params
1237
- };
1238
-
1239
- /* Only a sync per socket per table per condition allowed */
1240
- this.syncs = this.syncs || [];
1241
- let existing = this.syncs.find(s => s.socket_id === socket.id && s.channel_name === channel_name);
1242
- if (!existing) {
1243
- this.syncs.push(newSync);
1244
- // console.log("Added SYNC");
1245
-
1246
- socket.removeAllListeners(channel_name + "unsync");
1247
- socket.once(channel_name + "unsync", (_data: any, cb: BasicCallback) => {
1248
- this.onSocketDisconnected(socket, channel_name);
1249
- cb(null, { res: "ok" })
1250
- });
1251
-
1252
- socket.removeAllListeners(channel_name);
1253
- socket.on(channel_name, (data: any, cb: BasicCallback) => {
1254
-
1255
- if (!data) {
1256
- cb({ err: "Unexpected request. Need data or onSyncRequest" });
1257
- return;
1258
- }
1259
-
1260
- /*
1261
- */
1262
-
1263
- /* Server will:
1264
- 1. Ask for last_synced emit(onSyncRequest)
1265
- 2. Ask for data >= server_synced emit(onPullRequest)
1266
- -> Upsert that data
1267
- 2. Push data >= last_synced emit(data.data)
1268
-
1269
- Client will:
1270
- 1. Send last_synced on(onSyncRequest)
1271
- 2. Send data >= server_synced on(onPullRequest)
1272
- 3. Send data on CRUD emit(data.data | data.deleted)
1273
- 4. Upsert data.data | deleted on(data.data | data.deleted)
1274
- */
1275
-
1276
- // if(data.data){
1277
- // console.error("THIS SHOUKD NEVER FIRE !! NEW DATA FROM SYNC");
1278
- // this.upsertClientData(newSync, data.data);
1279
- // } else
1280
- if (data.onSyncRequest) {
1281
- // console.log("syncData from socket")
1282
- this.syncData(newSync, data.onSyncRequest, "client");
1283
-
1284
- // console.log("onSyncRequest ", socket._user)
1285
- } else {
1286
- console.error("Unexpected sync request data from client: ", data)
1287
- }
1288
- });
1289
-
1290
- // socket.emit(channel_name, { onSyncRequest: true }, (response) => {
1291
- // console.log(response)
1292
- // });
1293
- } else {
1294
- console.error("UNCLOSED DUPLICATE SYNC FOUND");
1295
- }
1296
-
1297
- return newSync;
1298
- };
1299
-
1300
-
1301
- // const { min_id, max_id, count, max_synced } = params;
1302
-
1303
- let sync = upsertSync();
1304
-
1305
- await this.addTrigger({ table_name, condition: conditionParsed });
1306
-
1307
- return channel_name;
1308
- }
1309
-
1310
-
1311
- /* Must return a channel for socket */
1312
- /* The distinct list of channel names must have a corresponding trigger in the database */
1313
- async addSub(subscriptionParams: Omit<AddSubscriptionParams, "channel_name" | "parentSubParams">) {
1314
- const {
1315
- socket, func = null, table_info = null, table_rules, filter = {},
1316
- params = {}, condition = "", throttle = 0, //subOne = false,
1317
- relatedTableSubscriptions
1318
- } = subscriptionParams || {};
1319
-
1320
- let validated_throttle = subscriptionParams.throttle || 10;
1321
- if ((!socket && !func) || !table_info) throw "socket/func or table_info missing";
1322
-
1323
- const pubThrottle = get(table_rules, ["subscribe", "throttle"]) || 0;
1324
- if (pubThrottle && Number.isInteger(pubThrottle) && pubThrottle > 0) {
1325
- validated_throttle = pubThrottle;
1326
- }
1327
- if (throttle && Number.isInteger(throttle) && throttle >= pubThrottle) {
1328
- validated_throttle = throttle;
1329
- }
1330
-
1331
- const channel_name = `${this.socketChannelPreffix}.${table_info.name}.${JSON.stringify(filter)}.${JSON.stringify(params)}.${"m"}.sub`;
1332
-
1333
- this.upsertSocket(socket, channel_name);
1334
-
1335
- const upsertSub = (newSubData: { table_name: string; condition: string; is_ready: boolean; parentSubParams: SubscriptionParams["parentSubParams"] }) => {
1336
- const { table_name, condition: _cond, is_ready = false, parentSubParams } = newSubData,
1337
- condition = parseCondition(_cond),
1338
- newSub: SubscriptionParams = {
1339
- socket,
1340
- table_name: table_info.name,
1341
- table_info,
1342
- filter,
1343
- params,
1344
- table_rules,
1345
- channel_name,
1346
- parentSubParams,
1347
- func: func ?? undefined,
1348
- socket_id: socket?.id,
1349
- throttle: validated_throttle,
1350
- is_throttling: null,
1351
- last_throttled: 0,
1352
- is_ready,
1353
- };
1354
-
1355
- this.subs[table_name] = this.subs[table_name] ?? {};
1356
- this.subs[table_name][condition] = this.subs[table_name][condition] ?? { subs: [] };
1357
- this.subs[table_name][condition].subs = this.subs[table_name][condition].subs ?? [];
1358
-
1359
- // console.log("1034 upsertSub", this.subs)
1360
- const sub_idx = this.subs[table_name][condition].subs.findIndex(s =>
1361
- s.channel_name === channel_name &&
1362
- (
1363
- socket && s.socket_id === socket.id ||
1364
- func && s.func === func
1365
- )
1366
- );
1367
- if (sub_idx < 0) {
1368
- this.subs[table_name][condition].subs.push(newSub);
1369
- if (socket) {
1370
- const chnUnsub = channel_name + "unsubscribe";
1371
- socket.removeAllListeners(chnUnsub);
1372
- socket.once(chnUnsub, (_data: any, cb: BasicCallback) => {
1373
- const res = this.onSocketDisconnected(socket, channel_name);
1374
- cb(null, { res });
1375
- });
1376
- }
1377
- } else {
1378
- this.subs[table_name][condition].subs[sub_idx] = newSub;
1379
- }
1380
-
1381
- if (is_ready) {
1382
- this.pushSubData(newSub);
1383
- }
1384
- };
1385
-
1386
-
1387
- if (table_info.is_view) {
1388
- if (relatedTableSubscriptions?.length) {
1389
-
1390
- relatedTableSubscriptions.map(async relatedTable => {
1391
- const params: Omit<Parameters<typeof upsertSub>[0], "is_ready"> = {
1392
- table_name: relatedTable.tableName,
1393
- condition: relatedTable.condition,
1394
- parentSubParams: {
1395
- ...subscriptionParams,
1396
- channel_name
1397
- },
1398
- }
1399
-
1400
- upsertSub({
1401
- ...params,
1402
- is_ready: false
1403
- });
1404
-
1405
- await this.addTrigger(params);
1406
-
1407
- upsertSub({
1408
- ...params,
1409
- is_ready: true
1410
- });
1411
- });
1412
-
1413
- return channel_name
1414
- } else {
1415
- throw "PubSubManager: view parent_tables missing";
1416
- }
1417
- /* */
1418
- } else {
1419
- /* Just a table, add table + condition trigger */
1420
- // console.log(table_info, 202);
1421
-
1422
- upsertSub({
1423
- table_name: table_info.name,
1424
- condition: parseCondition(condition),
1425
- parentSubParams: undefined,
1426
- is_ready: false
1427
- });
1428
- await this.addTrigger({
1429
- table_name: table_info.name,
1430
- condition: parseCondition(condition),
1431
- });
1432
- upsertSub({
1433
- table_name: table_info.name,
1434
- condition: parseCondition(condition),
1435
- parentSubParams: undefined,
1436
- is_ready: true
1437
- });
1438
-
1439
- return channel_name
1440
- }
1441
- }
1442
-
1443
- removeLocalSub(table_name: string, condition: string, func: (items: object[]) => any) {
1444
- let cond = parseCondition(condition);
1445
- if (get(this.subs, [table_name, cond, "subs"])) {
1446
- this.subs[table_name][cond].subs.map((sub, i) => {
1447
- if (
1448
- sub.func && sub.func === func
1449
- ) {
1450
- this.subs[table_name][cond].subs.splice(i, 1);
1451
- }
1452
- });
1453
- } else {
1454
- console.error("Could not unsubscribe. Subscription might not have initialised yet")
1455
- }
1456
- }
1457
-
1458
- getActiveListeners = (): { table_name: string; condition: string }[] => {
1459
- let result: { table_name: string; condition: string }[] = [];
1460
- const upsert = (t: string, c: string) => {
1461
- if (!result.find(r => r.table_name === t && r.condition === c)) {
1462
- result.push({ table_name: t, condition: c });
1463
- }
1464
- }
1465
- (this.syncs || []).map(s => {
1466
- upsert(s.table_name, s.condition)
1467
- });
1468
- Object.keys(this.subs || {}).map(table_name => {
1469
- Object.keys(this.subs[table_name] || {}).map(condition => {
1470
- if (this.subs[table_name][condition].subs.length) {
1471
- upsert(table_name, condition);
1472
- }
1473
- });
1474
- });
1475
-
1476
- return result;
1477
- }
1478
-
1479
- onSocketDisconnected(socket?: PRGLIOSocket, channel_name?: string) {
1480
- // process.on('warning', e => {
1481
- // console.warn(e.stack)
1482
- // });
1483
- // console.log("onSocketDisconnected", channel_name, this.syncs)
1484
- if (this.subs) {
1485
- Object.keys(this.subs).map(table_name => {
1486
- Object.keys(this.subs[table_name]).map(condition => {
1487
- this.subs[table_name][condition].subs.map((sub, i) => {
1488
-
1489
- /**
1490
- * If a channel name is specified then delete triggers
1491
- */
1492
- if (
1493
- (socket && sub.socket_id === socket.id) &&
1494
- (!channel_name || sub.channel_name === channel_name)
1495
- ) {
1496
- this.subs[table_name][condition].subs.splice(i, 1);
1497
- if (!this.subs[table_name][condition].subs.length) {
1498
- delete this.subs[table_name][condition];
1499
-
1500
- if (isEmpty(this.subs[table_name])) {
1501
- delete this.subs[table_name];
1502
- }
1503
- }
1504
- }
1505
- });
1506
- })
1507
- });
1508
- }
1509
-
1510
- if (this.syncs) {
1511
- this.syncs = this.syncs.filter(s => {
1512
- const matchesSocket = Boolean(socket && s.socket_id !== socket.id)
1513
- if (channel_name) {
1514
- return matchesSocket || s.channel_name !== channel_name
1515
- }
1516
-
1517
- return matchesSocket;
1518
- });
1519
- }
1520
-
1521
- if (!socket) {
1522
-
1523
- } else if (!channel_name) {
1524
- delete this.sockets[socket.id];
1525
- } else {
1526
- socket.removeAllListeners(channel_name);
1527
- socket.removeAllListeners(channel_name + "unsync");
1528
- socket.removeAllListeners(channel_name + "unsubscribe");
1529
- }
1530
-
1531
- return "ok";
1532
- }
1533
-
1534
-
1535
- checkIfTimescaleBug = async (table_name: string) => {
1536
- const schema = "_timescaledb_catalog",
1537
- res = await this.db.oneOrNone("SELECT EXISTS( \
1538
- SELECT * \
1539
- FROM information_schema.tables \
1540
- WHERE 1 = 1 \
1541
- AND table_schema = ${schema} \
1542
- AND table_name = 'hypertable' \
1543
- );", { schema });
1544
- if (res.exists) {
1545
- let isHyperTable = await this.db.any("SELECT * FROM " + asName(schema) + ".hypertable WHERE table_name = ${table_name};", { table_name, schema });
1546
- if (isHyperTable && isHyperTable.length) {
1547
- throw "Triggers do not work on timescaledb hypertables due to bug:\nhttps://github.com/timescale/timescaledb/issues/1084"
1548
- }
1549
- }
1550
- return true;
1551
- }
1552
-
1553
- /*
1554
- A table will only have a trigger with all conditions (for different subs)
1555
- conditions = ["user_id = 1"]
1556
- fields = ["user_id"]
1557
- */
1558
-
1559
- getMyTriggerQuery = async () => {
1560
- return pgp.as.format(`
1561
- SELECT * --, ROW_NUMBER() OVER(PARTITION BY table_name ORDER BY table_name, condition ) - 1 as id
1562
- FROM prostgles.v_triggers
1563
- WHERE app_id = $1
1564
- ORDER BY table_name, condition
1565
- `, [this.appID]
1566
- )
1567
- }
1568
-
1569
- // waitingTriggers: { [key: string]: string[] } = undefined;
1570
- addingTrigger: any;
1571
- addTriggerPool?: Record<string, string[]> = undefined;
1572
- async addTrigger(params: { table_name: string; condition: string; }) {
1573
- try {
1574
-
1575
- let { table_name, condition } = { ...params }
1576
- if (!table_name) throw "MISSING table_name";
1577
- if (!this.appID) throw "MISSING appID";
1578
-
1579
- if (!condition || !condition.trim().length) {
1580
- condition = "TRUE";
1581
- }
1582
-
1583
- // console.log(1623, { app_id, addTrigger: { table_name, condition } });
1584
-
1585
- await this.checkIfTimescaleBug(table_name);
1586
-
1587
- const trgVals = {
1588
- tbl: asValue(table_name),
1589
- cond: asValue(condition),
1590
- };
1591
-
1592
- await this.db.any(`
1593
- BEGIN WORK;
1594
- LOCK TABLE prostgles.app_triggers IN ACCESS EXCLUSIVE MODE;
1595
-
1596
- INSERT INTO prostgles.app_triggers (table_name, condition, app_id)
1597
- VALUES (${trgVals.tbl}, ${trgVals.cond}, ${asValue(this.appID)})
1598
- ON CONFLICT DO NOTHING;
1599
-
1600
- COMMIT WORK;
1601
- `);
1602
-
1603
- log("addTrigger.. ", { table_name, condition });
1604
-
1605
- const triggers: {
1606
- table_name: string;
1607
- condition: string;
1608
- }[] = await this.db.any(await this.getMyTriggerQuery());
1609
-
1610
-
1611
- this._triggers = {};
1612
- triggers.map(t => {
1613
- this._triggers = this._triggers || {};
1614
- this._triggers[t.table_name] = this._triggers[t.table_name] || [];
1615
- if (!this._triggers[t.table_name].includes(t.condition)) {
1616
- this._triggers[t.table_name].push(t.condition)
1617
- }
1618
- });
1619
- log("trigger added.. ", { table_name, condition });
1620
-
1621
- return true;
1622
- // console.log("1612", JSON.stringify(triggers, null, 2))
1623
- // console.log("1613",JSON.stringify(this._triggers, null, 2))
1624
-
1625
-
1626
- } catch (e) {
1627
- console.trace("Failed adding trigger", e);
1628
- // throw e
1629
- }
1630
-
1631
- }
1632
- }
1633
-
1634
-
1635
- const parseCondition = (condition: string): string => Boolean(condition && condition.trim().length) ? condition : "TRUE"
1636
-
1637
- export function omitKeys<T extends AnyObject, Exclude extends keyof T>(obj: T, exclude: Exclude[]): Omit<T, Exclude> {
1638
- return pickKeys(obj, getKeys(obj).filter(k => !exclude.includes(k as any)))
1639
- }
1640
-
1641
- export function pickKeys<T extends AnyObject, Include extends keyof T>(obj: T, include: Include[] = []): Pick<T, Include> {
1642
- let keys = include;
1643
- if (!keys.length) {
1644
- return {} as any;
1645
- }
1646
- if (obj && keys.length) {
1647
- let res: AnyObject = {};
1648
- keys.forEach(k => {
1649
- res[k] = obj[k];
1650
- });
1651
- return res as any;
1652
- }
1653
-
1654
- return obj;
1655
- }