prostgles-server 2.0.185 → 2.0.188

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/DboBuilder/insert.d.ts +5 -0
  2. package/dist/DboBuilder/insert.d.ts.map +1 -0
  3. package/dist/DboBuilder/insert.js +129 -0
  4. package/dist/DboBuilder/insert.js.map +1 -0
  5. package/dist/DboBuilder/insertDataParse.d.ts +11 -0
  6. package/dist/DboBuilder/insertDataParse.d.ts.map +1 -0
  7. package/dist/DboBuilder/insertDataParse.js +283 -0
  8. package/dist/DboBuilder/insertDataParse.js.map +1 -0
  9. package/dist/DboBuilder.d.ts +10 -5
  10. package/dist/DboBuilder.d.ts.map +1 -1
  11. package/dist/DboBuilder.js +121 -374
  12. package/dist/DboBuilder.js.map +1 -1
  13. package/dist/FileManager.d.ts.map +1 -1
  14. package/dist/FileManager.js +17 -12
  15. package/dist/FileManager.js.map +1 -1
  16. package/dist/Prostgles.d.ts +16 -14
  17. package/dist/Prostgles.d.ts.map +1 -1
  18. package/dist/Prostgles.js +7 -7
  19. package/dist/Prostgles.js.map +1 -1
  20. package/dist/PubSubManager.d.ts +1 -0
  21. package/dist/PubSubManager.d.ts.map +1 -1
  22. package/dist/PubSubManager.js +2 -1
  23. package/dist/PubSubManager.js.map +1 -1
  24. package/dist/QueryBuilder.d.ts +7 -3
  25. package/dist/QueryBuilder.d.ts.map +1 -1
  26. package/dist/QueryBuilder.js +7 -2
  27. package/dist/QueryBuilder.js.map +1 -1
  28. package/dist/TableConfig.d.ts +1 -4
  29. package/dist/TableConfig.d.ts.map +1 -1
  30. package/dist/TableConfig.js +16 -1
  31. package/dist/TableConfig.js.map +1 -1
  32. package/lib/DboBuilder/insert.d.ts +5 -0
  33. package/lib/DboBuilder/insert.d.ts.map +1 -0
  34. package/lib/DboBuilder/insert.js +128 -0
  35. package/lib/DboBuilder/insert.ts +138 -0
  36. package/lib/DboBuilder/insertDataParse.d.ts +11 -0
  37. package/lib/DboBuilder/insertDataParse.d.ts.map +1 -0
  38. package/lib/DboBuilder/insertDataParse.js +282 -0
  39. package/lib/DboBuilder/insertDataParse.ts +355 -0
  40. package/lib/DboBuilder.d.ts +10 -5
  41. package/lib/DboBuilder.d.ts.map +1 -1
  42. package/lib/DboBuilder.js +121 -374
  43. package/lib/DboBuilder.ts +138 -453
  44. package/lib/FileManager.d.ts.map +1 -1
  45. package/lib/FileManager.js +17 -12
  46. package/lib/FileManager.ts +18 -13
  47. package/lib/Prostgles.d.ts +16 -14
  48. package/lib/Prostgles.d.ts.map +1 -1
  49. package/lib/Prostgles.js +7 -7
  50. package/lib/Prostgles.ts +664 -650
  51. package/lib/PubSubManager.d.ts +1 -0
  52. package/lib/PubSubManager.d.ts.map +1 -1
  53. package/lib/PubSubManager.js +2 -1
  54. package/lib/PubSubManager.ts +3 -1
  55. package/lib/QueryBuilder.d.ts.map +1 -1
  56. package/lib/QueryBuilder.js +7 -2
  57. package/lib/QueryBuilder.ts +12 -7
  58. package/lib/SchemaWatchManager.ts +72 -0
  59. package/lib/TableConfig.d.ts +1 -4
  60. package/lib/TableConfig.d.ts.map +1 -1
  61. package/lib/TableConfig.js +16 -1
  62. package/lib/TableConfig.ts +21 -8
  63. package/package.json +3 -3
  64. package/tests/client/PID.txt +1 -1
  65. package/tests/client/package-lock.json +15 -15
  66. package/tests/client/package.json +1 -1
  67. package/tests/client/tsconfig.json +1 -1
  68. package/tests/client_only_queries.d.ts +1 -1
  69. package/tests/client_only_queries.d.ts.map +1 -1
  70. package/tests/client_only_queries.ts +1 -1
  71. package/tests/isomorphic_queries.d.ts +1 -1
  72. package/tests/isomorphic_queries.d.ts.map +1 -1
  73. package/tests/isomorphic_queries.js +49 -1
  74. package/tests/isomorphic_queries.ts +66 -4
  75. package/tests/manual_test/DBoGenerated.d.ts +398 -0
  76. package/tests/manual_test/index.d.ts +2 -0
  77. package/tests/manual_test/index.d.ts.map +1 -0
  78. package/tests/{config_test2 → manual_test}/index.html +14 -23
  79. package/tests/{config_test2 → manual_test}/index.js +21 -15
  80. package/tests/{config_test2 → manual_test}/index.ts +22 -17
  81. package/tests/{config_test2 → manual_test}/init.sql +36 -5
  82. package/tests/manual_test/package-lock.json +2483 -0
  83. package/tests/{config_test2 → manual_test}/package.json +6 -7
  84. package/tests/manual_test/tsconfig.json +21 -0
  85. package/tests/server/DBoGenerated.d.ts +70 -0
  86. package/tests/server/index.js +29 -2
  87. package/tests/server/index.ts +30 -4
  88. package/tests/server/init.sql +25 -0
  89. package/tests/server/package-lock.json +5 -5
  90. package/tests/config_test2/DBoGenerated.d.ts +0 -135
  91. package/tests/config_test2/tsconfig.json +0 -21
package/lib/Prostgles.ts CHANGED
@@ -30,70 +30,70 @@ type DbConnection = string | pg.IConnectionParameters<pg.IClient>;
30
30
  type DbConnectionOpts = pg.IDefaults;
31
31
  export const TABLE_METHODS = ["update", "find", "findOne", "insert", "delete", "upsert"] as const;
32
32
  function getDbConnection(dbConnection: DbConnection, options: DbConnectionOpts | undefined, debugQueries = false, onNotice: ProstglesInitOptions["onNotice"]): { db: DB, pgp: PGP } {
33
- let pgp: PGP = pgPromise({
34
-
35
- promiseLib: promise,
36
- ...(debugQueries? {
37
- query: function (e) {
38
- console.log({psql: e.query, params: e.params});
39
- },
40
- } : {}),
41
- ...((onNotice || debugQueries)? {
42
- connect: function (client, dc, isFresh) {
43
- if (isFresh && !client.listeners('notice').length) {
44
- client.on('notice', function (msg) {
45
- if(onNotice){
46
- onNotice(msg, get(msg, "message"));
47
- } else {
48
- console.log("notice: %j", get(msg, "message"));
49
- }
50
- });
51
- }
52
- if (isFresh && !client.listeners('error').length) {
53
- client.on('error', function (msg) {
54
- if(onNotice){
55
- onNotice(msg, get(msg, "message"));
56
- } else {
57
- console.log("error: %j", get(msg, "message"));
58
- }
59
- });
60
- }
61
- },
62
- } : {})
63
- });
64
- pgp.pg.defaults.max = 70;
33
+ let pgp: PGP = pgPromise({
34
+
35
+ promiseLib: promise,
36
+ ...(debugQueries ? {
37
+ query: function (e) {
38
+ console.log({ psql: e.query, params: e.params });
39
+ },
40
+ } : {}),
41
+ ...((onNotice || debugQueries) ? {
42
+ connect: function (client, dc, isFresh) {
43
+ if (isFresh && !client.listeners('notice').length) {
44
+ client.on('notice', function (msg) {
45
+ if (onNotice) {
46
+ onNotice(msg, get(msg, "message"));
47
+ } else {
48
+ console.log("notice: %j", get(msg, "message"));
49
+ }
50
+ });
51
+ }
52
+ if (isFresh && !client.listeners('error').length) {
53
+ client.on('error', function (msg) {
54
+ if (onNotice) {
55
+ onNotice(msg, get(msg, "message"));
56
+ } else {
57
+ console.log("error: %j", get(msg, "message"));
58
+ }
59
+ });
60
+ }
61
+ },
62
+ } : {})
63
+ });
64
+ pgp.pg.defaults.max = 70;
65
65
 
66
- // /* Casts count/sum/max to bigint. Needs rework to remove casting "+count" and other issues; */
67
- // pgp.pg.types.setTypeParser(20, BigInt);
66
+ // /* Casts count/sum/max to bigint. Needs rework to remove casting "+count" and other issues; */
67
+ // pgp.pg.types.setTypeParser(20, BigInt);
68
68
 
69
- if(options){
70
- Object.assign(pgp.pg.defaults, options);
71
- }
72
-
73
- return {
74
- db: pgp(dbConnection),
75
- pgp
76
- };
69
+ if (options) {
70
+ Object.assign(pgp.pg.defaults, options);
71
+ }
72
+
73
+ return {
74
+ db: pgp(dbConnection),
75
+ pgp
76
+ };
77
77
  }
78
78
 
79
79
  export const JOIN_TYPES = ["one-many", "many-one", "one-one", "many-many"] as const;
80
80
  export type Join = {
81
- tables: [string, string];
82
- on: { [key: string]: string };
83
- type: typeof JOIN_TYPES[number];
81
+ tables: [string, string];
82
+ on: { [key: string]: string }[]; // Allow multi references to table
83
+ type: typeof JOIN_TYPES[number];
84
84
  };
85
85
  export type Joins = Join[] | "inferred";
86
86
 
87
87
 
88
88
 
89
89
  type Keywords = {
90
- $and: string;
91
- $or: string;
92
- $not: string;
90
+ $and: string;
91
+ $or: string;
92
+ $not: string;
93
93
  };
94
94
 
95
95
  export type DeepPartial<T> = {
96
- [P in keyof T]?: DeepPartial<T[P]>;
96
+ [P in keyof T]?: DeepPartial<T[P]>;
97
97
  };
98
98
  // export type I18N_CONFIG<LANG_IDS = { en: 1, fr: 1 }> = {
99
99
  // fallbackLang: keyof LANG_IDS;
@@ -107,23 +107,23 @@ export type DeepPartial<T> = {
107
107
  // }
108
108
 
109
109
  type ExpressApp = {
110
- get: (
111
- routePath: string,
112
- cb: (
113
- req: {
114
- params: { name: string },
115
- cookies: { sid: string }
116
- },
117
- res: {
118
- redirect: (redirectUrl: string) => any;
119
- contentType: (type: string) => void;
120
- sendFile: (fileName: string, opts?: { root: string }) => any;
121
- status: (code: number) => {
122
- json: (response: AnyObject) => any;
123
- }
124
- }
125
- ) => any
110
+ get: (
111
+ routePath: string,
112
+ cb: (
113
+ req: {
114
+ params: { name: string },
115
+ cookies: { sid: string }
116
+ },
117
+ res: {
118
+ redirect: (redirectUrl: string) => any;
119
+ contentType: (type: string) => void;
120
+ sendFile: (fileName: string, opts?: { root: string }) => any;
121
+ status: (code: number) => {
122
+ json: (response: AnyObject) => any;
123
+ }
124
+ }
126
125
  ) => any
126
+ ) => any
127
127
  };
128
128
 
129
129
  /**
@@ -143,85 +143,99 @@ type ExpressApp = {
143
143
  * 2. create a lookup table lookup_media_{referencedTable} that joins referencedTable to the media table
144
144
  */
145
145
  export type FileTableConfig = {
146
- tableName?: string; /* defaults to 'media' */
147
-
148
- /**
149
- * GET path used in serving media. defaults to /${tableName}
150
- */
151
- fileServeRoute?: string;
152
-
153
- awsS3Config?: S3Config;
154
- localConfig?: LocalConfig;
155
- // {
156
- // region: string;
157
- // bucket: string;
158
- // accessKeyId: string;
159
- // secretAccessKey: string;
160
- // },
161
- expressApp: ExpressApp;
162
- referencedTables?: {
163
- [tableName: string]: "one" | "many"
164
- },
165
- imageOptions?: ImageOptions
146
+ tableName?: string; /* defaults to 'media' */
147
+
148
+ /**
149
+ * GET path used in serving media. defaults to /${tableName}
150
+ */
151
+ fileServeRoute?: string;
152
+
153
+ awsS3Config?: S3Config;
154
+ localConfig?: LocalConfig;
155
+ // {
156
+ // region: string;
157
+ // bucket: string;
158
+ // accessKeyId: string;
159
+ // secretAccessKey: string;
160
+ // },
161
+ expressApp: ExpressApp;
162
+ referencedTables?: { // minFiles: number; maxFiles: number;
163
+ [tableName: string]:
164
+ | "one" | "many"
165
+
166
+ /**
167
+ * If defined then will try to create (if necessary) a column in the files table which will reference this table's primary key (must have one)
168
+ * */
169
+ // | { type: "lookup_table"; minFilesPerRow: number; maxFilesPerRow: number; maxFileSizeMB: number; }
170
+
171
+ /**
172
+ * If defined then will try to create (if necessary) this column which will reference files_table(id)
173
+ * Prostgles UI will use these hints (obtained through tableHandler.getInfo())
174
+ * */
175
+ // | { type: "column", referenceColumns: Record<string, { maxFileSizeMB: number; }> }
176
+ },
177
+ imageOptions?: ImageOptions
166
178
  };
167
179
 
168
180
  export type ProstglesInitOptions<S = void> = {
169
- dbConnection: DbConnection;
170
- dbOptions?: DbConnectionOpts;
171
- tsGeneratedTypesDir?: string;
172
- io?: any;
173
- publish?: Publish<S>;
174
- publishMethods?: PublishMethods<S>;
175
- publishRawSQL?(params: PublishParams<S>): ( (boolean | "*") | Promise<(boolean | "*")>);
176
- joins?: Joins;
177
- schema?: string;
178
- sqlFilePath?: string;
179
- onReady(dbo: DBOFullyTyped<S>, db: DB): void;
180
- transactions?: string | boolean;
181
- wsChannelNamePrefix?: string;
182
- onSocketConnect?(socket: PRGLIOSocket, dbo: DBOFullyTyped<S>, db?: DB): any;
183
- onSocketDisconnect?(socket: PRGLIOSocket, dbo: DBOFullyTyped<S>, db?: DB): any;
184
- auth?: Auth<S>;
185
- DEBUG_MODE?: boolean;
186
- watchSchemaType?:
187
-
188
- /**
189
- * Will check client queries for schema changes
190
- * Default
191
- */
192
- | "events"
193
-
194
- /**
195
- * Will set database event trigger for schema changes. Requires superuser
196
- */
197
- | "queries";
198
-
199
- watchSchema?:
200
-
201
- /**
202
- * If true then DBoGenerated.d.ts will be updated and "onReady" will be called with new schema on both client and server
203
- */
204
- | boolean
205
-
206
- /**
207
- * "hotReloadMode" will only rewrite the DBoGenerated.d.ts found in tsGeneratedTypesDir
208
- * This is meant to be used in development when server restarts on file change
209
- */
210
- | "hotReloadMode"
211
-
212
- /**
213
- * Function called when schema changes. Nothing else triggered
214
- */
215
- | ((event: { command: string; query: string }) => void)
216
-
217
- /**
218
- * Schema checked for changes every 'checkIntervalMillis" milliseconds
219
- */
220
- | { checkIntervalMillis: number };
221
- keywords?: Keywords;
222
- onNotice?: (notice: AnyObject, message?: string) => void;
223
- fileTable?: FileTableConfig;
224
- tableConfig?: TableConfig;
181
+ dbConnection: DbConnection;
182
+ dbOptions?: DbConnectionOpts;
183
+ tsGeneratedTypesDir?: string;
184
+ io?: any;
185
+ publish?: Publish<S>;
186
+ publishMethods?: PublishMethods<S>;
187
+ publishRawSQL?(params: PublishParams<S>): ((boolean | "*") | Promise<(boolean | "*")>);
188
+ joins?: Joins;
189
+ schema?: string;
190
+ sqlFilePath?: string;
191
+ onReady(dbo: DBOFullyTyped<S>, db: DB): void;
192
+ transactions?: string | boolean;
193
+ wsChannelNamePrefix?: string;
194
+ onSocketConnect?(socket: PRGLIOSocket, dbo: DBOFullyTyped<S>, db?: DB): any;
195
+ onSocketDisconnect?(socket: PRGLIOSocket, dbo: DBOFullyTyped<S>, db?: DB): any;
196
+ auth?: Auth<S>;
197
+ DEBUG_MODE?: boolean;
198
+ watchSchemaType?:
199
+
200
+ /**
201
+ * Will set database event trigger for schema changes. Requires superuser
202
+ * Default
203
+ */
204
+ | "DDL_trigger"
205
+
206
+ /**
207
+ * Will check client queries for schema changes
208
+ * fallback if DDL not possible
209
+ */
210
+ | "prostgles_queries"
211
+
212
+ /**
213
+ * Schema checked for changes every 'checkIntervalMillis" milliseconds
214
+ */
215
+ | { checkIntervalMillis: number };
216
+
217
+ watchSchema?:
218
+
219
+ /**
220
+ * If true then DBoGenerated.d.ts will be updated and "onReady" will be called with new schema on both client and server
221
+ */
222
+ | boolean
223
+
224
+ /**
225
+ * Will only rewrite the DBoGenerated.d.ts found in tsGeneratedTypesDir
226
+ * This is meant to be used in development when server restarts on file change
227
+ */
228
+ | "hotReloadMode"
229
+
230
+ /**
231
+ * Function called when schema changes. Nothing else triggered
232
+ */
233
+ | ((event: { command: string; query: string }) => void)
234
+
235
+ keywords?: Keywords;
236
+ onNotice?: (notice: AnyObject, message?: string) => void;
237
+ fileTable?: FileTableConfig;
238
+ tableConfig?: TableConfig;
225
239
  }
226
240
 
227
241
  /*
@@ -233,572 +247,572 @@ export type ProstglesInitOptions<S = void> = {
233
247
  */
234
248
 
235
249
  export type OnReady = {
236
- dbo: DBHandlerServer;
237
- db: DB;
250
+ dbo: DBHandlerServer;
251
+ db: DB;
238
252
  }
239
253
 
240
254
  const DEFAULT_KEYWORDS = {
241
- $filter: "$filter",
242
- $and: "$and",
243
- $or: "$or",
244
- $not: "$not"
255
+ $filter: "$filter",
256
+ $and: "$and",
257
+ $or: "$or",
258
+ $not: "$not"
245
259
  };
246
260
 
247
261
  import * as fs from 'fs';
248
262
  import { DBOFullyTyped } from "./DBSchemaBuilder";
249
263
  export class Prostgles {
250
264
 
251
- opts: ProstglesInitOptions = {
252
- DEBUG_MODE: false,
253
- dbConnection: {
254
- host: "localhost",
255
- port: 5432,
256
- application_name: "prostgles_app"
257
- },
258
- onReady: () => {},
259
- schema: "public",
260
- watchSchema: false,
261
- watchSchemaType: "queries",
262
- };
263
-
264
- // dbConnection: DbConnection = {
265
- // host: "localhost",
266
- // port: 5432,
267
- // application_name: "prostgles_app"
268
- // };
269
- // dbOptions: DbConnectionOpts;
270
- db?: DB;
271
- pgp?: PGP;
272
- dbo?: DBHandlerServer;
273
- _dboBuilder?: DboBuilder;
274
- get dboBuilder(): DboBuilder {
275
- if(!this._dboBuilder) throw "get dboBuilder: it's undefined"
276
- return this._dboBuilder;
265
+ opts: ProstglesInitOptions = {
266
+ DEBUG_MODE: false,
267
+ dbConnection: {
268
+ host: "localhost",
269
+ port: 5432,
270
+ application_name: "prostgles_app"
271
+ },
272
+ onReady: () => { },
273
+ schema: "public",
274
+ watchSchema: false,
275
+ watchSchemaType: "DDL_trigger",
276
+ };
277
+
278
+ // dbConnection: DbConnection = {
279
+ // host: "localhost",
280
+ // port: 5432,
281
+ // application_name: "prostgles_app"
282
+ // };
283
+ // dbOptions: DbConnectionOpts;
284
+ db?: DB;
285
+ pgp?: PGP;
286
+ dbo?: DBHandlerServer;
287
+ _dboBuilder?: DboBuilder;
288
+ get dboBuilder(): DboBuilder {
289
+ if (!this._dboBuilder) throw "get dboBuilder: it's undefined"
290
+ return this._dboBuilder;
291
+ }
292
+ set dboBuilder(d: DboBuilder) {
293
+ this._dboBuilder = d;
294
+ }
295
+ publishParser?: PublishParser;
296
+
297
+ authHandler?: AuthHandler;
298
+
299
+
300
+ keywords = DEFAULT_KEYWORDS;
301
+ private loaded = false;
302
+
303
+ dbEventsManager?: DBEventsManager;
304
+
305
+
306
+ fileManager?: FileManager;
307
+
308
+ tableConfigurator?: TableConfigurator;
309
+
310
+ isMedia(tableName: string) {
311
+ return this.opts?.fileTable?.tableName === tableName;
312
+ }
313
+
314
+ constructor(params: ProstglesInitOptions) {
315
+ if (!params) throw "ProstglesInitOptions missing";
316
+ if (!params.io) console.warn("io missing. WebSockets will not be set up");
317
+
318
+ // TODO: find an exact keyof T<->arr TS matching method
319
+ let config: Array<keyof ProstglesInitOptions> = [
320
+ "transactions", "joins", "tsGeneratedTypesDir",
321
+ "onReady", "dbConnection", "dbOptions", "publishMethods", "io",
322
+ "publish", "schema", "publishRawSQL", "wsChannelNamePrefix", "onSocketConnect",
323
+ "onSocketDisconnect", "sqlFilePath", "auth", "DEBUG_MODE", "watchSchema", "watchSchemaType",
324
+ "fileTable", "tableConfig"
325
+ ];
326
+ const unknownParams = Object.keys(params).filter((key: string) => !(config as string[]).includes(key))
327
+ if (unknownParams.length) {
328
+ console.error(`Unrecognised ProstglesInitOptions params: ${unknownParams.join()}`);
277
329
  }
278
- set dboBuilder(d: DboBuilder) {
279
- this._dboBuilder = d;
330
+
331
+ Object.assign(this.opts, params);
332
+
333
+ /* set defaults */
334
+ if (this.opts?.fileTable) {
335
+ this.opts.fileTable.tableName = this.opts?.fileTable?.tableName || "media";
280
336
  }
281
- publishParser?: PublishParser;
337
+ this.opts.schema = this.opts.schema || "public";
282
338
 
283
- authHandler?: AuthHandler;
339
+ this.keywords = {
340
+ ...DEFAULT_KEYWORDS,
341
+ ...params.keywords,
342
+ }
343
+ }
344
+
345
+ destroyed = false;
346
+
347
+ async onSchemaChange(event: { command: string; query: string }) {
348
+ const { watchSchema, watchSchemaType, onReady, tsGeneratedTypesDir } = this.opts;
349
+ if (watchSchema && this.loaded) {
350
+ console.log("Schema changed");
351
+ const { query } = event;
352
+ if (typeof query === "string" && query.includes(PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID)) {
353
+ console.log("Schema change event excluded from triggers due to EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID");
354
+ return;
355
+ }
356
+
357
+ if (typeof watchSchema === "function") {
358
+ /* Only call the provided func */
359
+ watchSchema(event);
360
+
361
+ } else if (watchSchema === "hotReloadMode") {
362
+ if (tsGeneratedTypesDir) {
363
+ /* Hot reload integration. Will only touch tsGeneratedTypesDir */
364
+ console.log("watchSchema: Re-writing TS schema");
365
+
366
+ await this.refreshDBO();
367
+ this.writeDBSchema(true);
368
+ }
284
369
 
370
+ } else if (watchSchema === true || isPlainObject(watchSchemaType) && "checkIntervalMillis" in watchSchemaType) {
371
+ /* Full re-init. Sockets must reconnect */
372
+ console.log("watchSchema: Full re-initialisation")
373
+ this.init(onReady);
374
+ }
375
+ }
376
+ }
377
+
378
+ checkDb() {
379
+ if (!this.db || !this.db.connect) throw "something went wrong getting a db connection";
380
+ }
381
+
382
+ getTSFileName() {
383
+ const fileName = "DBoGenerated.d.ts" //`dbo_${this.schema}_types.ts`;
384
+ const fullPath = (this.opts.tsGeneratedTypesDir || "") + fileName;
385
+ return { fileName, fullPath }
386
+ }
387
+
388
+ private getFileText(fullPath: string, format = "utf8"): Promise<string> {
389
+ return new Promise((resolve, reject) => {
390
+ fs.readFile(fullPath, 'utf8', function (err, data) {
391
+ if (err) reject(err);
392
+ else resolve(data);
393
+ });
394
+ })
395
+ }
396
+
397
+ writeDBSchema(force = false) {
398
+
399
+ if (this.opts.tsGeneratedTypesDir) {
400
+ const { fullPath, fileName } = this.getTSFileName();
401
+ const header = `/* This file was generated by Prostgles \n` +
402
+ // `* ${(new Date).toUTCString()} \n`
403
+ `*/ \n\n `;
404
+ const fileContent = header + this.dboBuilder.tsTypesDefinition;
405
+ fs.readFile(fullPath, 'utf8', function (err, data) {
406
+ if (err || (force || data !== fileContent)) {
407
+ fs.writeFileSync(fullPath, fileContent);
408
+ console.log("Prostgles: Created typescript schema definition file: \n " + fileName)
409
+ }
410
+ });
411
+ } else if (force) {
412
+ console.error("Schema changed. tsGeneratedTypesDir needs to be set to reload server")
413
+ }
414
+ }
415
+
416
+ refreshDBO = async () => {
417
+ if (this._dboBuilder) this._dboBuilder.destroy();
418
+ this.dboBuilder = await DboBuilder.create(this as any) as any;
419
+ if (!this.dboBuilder) throw "this.dboBuilder"
420
+ this.dbo = this.dboBuilder.dbo as any;
421
+ return this.dbo;
422
+ }
423
+
424
+ isSuperUser = false;
425
+ schema_checkIntervalMillis?: NodeJS.Timeout;
426
+ async init(onReady: (dbo: DBOFullyTyped, db: DB) => any): Promise<{
427
+ db: DBOFullyTyped;
428
+ _db: DB;
429
+ pgp: PGP;
430
+ io?: any;
431
+ destroy: () => Promise<boolean>;
432
+ }> {
433
+ this.loaded = false;
434
+
435
+
436
+ if (this.opts.watchSchema === "hotReloadMode" && !this.opts.tsGeneratedTypesDir) {
437
+ throw "tsGeneratedTypesDir option is needed for watchSchema: hotReloadMode to work ";
438
+ } else if (
439
+ this.opts.watchSchema &&
440
+ typeof this.opts.watchSchemaType === "object" &&
441
+ "checkIntervalMillis" in this.opts.watchSchemaType &&
442
+ typeof this.opts.watchSchemaType.checkIntervalMillis === "number"
443
+ ) {
444
+
445
+ if (this.schema_checkIntervalMillis) {
446
+ clearInterval(this.schema_checkIntervalMillis);
447
+ this.schema_checkIntervalMillis = setInterval(async () => {
448
+ const dbuilder = await DboBuilder.create(this as any);
449
+ if (dbuilder.tsTypesDefinition !== this.dboBuilder.tsTypesDefinition) {
450
+ this.refreshDBO();
451
+ this.init(onReady);
452
+ }
453
+ }, this.opts.watchSchemaType.checkIntervalMillis)
454
+ }
455
+ }
285
456
 
286
- keywords = DEFAULT_KEYWORDS;
287
- private loaded = false;
457
+ /* 1. Connect to db */
458
+ if (!this.db) {
459
+ const { db, pgp } = getDbConnection(this.opts.dbConnection, this.opts.dbOptions, this.opts.DEBUG_MODE,
460
+ notice => {
461
+ if (this.opts.onNotice) this.opts.onNotice(notice);
462
+ if (this.dbEventsManager) {
463
+ this.dbEventsManager.onNotice(notice)
464
+ }
465
+ }
466
+ );
467
+ this.db = db;
468
+ this.pgp = pgp;
469
+ this.isSuperUser = await isSuperUser(db);
470
+ }
471
+ this.checkDb();
472
+ const db = this.db!;
473
+ const pgp = this.pgp!;
288
474
 
289
- dbEventsManager?: DBEventsManager;
475
+ /* 2. Execute any SQL file if provided */
476
+ if (this.opts.sqlFilePath) {
477
+ await this.runSQLFile(this.opts.sqlFilePath);
478
+ }
290
479
 
480
+ try {
291
481
 
292
- fileManager?: FileManager;
482
+ await this.refreshDBO();
483
+ if (this.opts.tableConfig) {
484
+ this.tableConfigurator = new TableConfigurator(this as any);
485
+ try {
486
+ await this.tableConfigurator.init();
487
+ } catch (e) {
488
+ console.error("TableConfigurator: ", e);
489
+ throw e;
490
+ }
491
+ }
293
492
 
294
- tableConfigurator?: TableConfigurator;
493
+ /* 3. Make DBO object from all tables and views */
494
+ await this.refreshDBO();
295
495
 
296
- isMedia(tableName: string){
297
- return this.opts?.fileTable?.tableName === tableName;
298
- }
496
+ /* Create media table if required */
497
+ if (this.opts.fileTable) {
498
+ const { awsS3Config, localConfig, imageOptions } = this.opts.fileTable;
499
+ await this.refreshDBO();
500
+ if (!awsS3Config && !localConfig) throw "fileTable missing param: Must provide awsS3Config OR localConfig";
501
+ //@ts-ignore
502
+ this.fileManager = new FileManager(awsS3Config || localConfig, imageOptions);
299
503
 
300
- constructor(params: ProstglesInitOptions){
301
- if(!params) throw "ProstglesInitOptions missing";
302
- if(!params.io) console.warn("io missing. WebSockets will not be set up");
303
-
304
- // TODO: find an exact keyof T<->arr TS matching method
305
- let config: Array<keyof ProstglesInitOptions> = [
306
- "transactions", "joins", "tsGeneratedTypesDir",
307
- "onReady", "dbConnection", "dbOptions", "publishMethods", "io",
308
- "publish", "schema", "publishRawSQL", "wsChannelNamePrefix", "onSocketConnect",
309
- "onSocketDisconnect", "sqlFilePath", "auth", "DEBUG_MODE", "watchSchema", "watchSchemaType",
310
- "fileTable", "tableConfig"
311
- ];
312
- const unknownParams = Object.keys(params).filter((key: string) => !(config as string[]).includes(key))
313
- if(unknownParams.length){
314
- console.error(`Unrecognised ProstglesInitOptions params: ${unknownParams.join()}`);
504
+ try {
505
+ await this.fileManager.init(this as any);
506
+ } catch (e) {
507
+ console.error("FileManager: ", e);
508
+ throw e;
315
509
  }
316
-
317
- Object.assign(this.opts, params);
510
+ }
511
+ await this.refreshDBO();
318
512
 
319
- /* set defaults */
320
- if(this.opts?.fileTable){
321
- this.opts.fileTable.tableName = this.opts?.fileTable?.tableName || "media";
322
- }
323
- this.opts.schema = this.opts.schema || "public";
324
513
 
325
- this.keywords = {
326
- ...DEFAULT_KEYWORDS,
327
- ...params.keywords,
328
- }
329
- }
514
+ if (this.opts.publish) {
330
515
 
331
- destroyed = false;
516
+ if (!this.opts.io) console.warn("IO missing. Publish has no effect without io");
332
517
 
333
- async onSchemaChange(event: { command: string; query: string }){
334
- const { watchSchema, onReady, tsGeneratedTypesDir } = this.opts;
335
- if(watchSchema && this.loaded){
336
- console.log("Schema changed");
337
- const { query } = event;
338
- if(typeof query === "string" && query.includes(PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID)){
339
- console.log("Schema change event excluded from triggers due to EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID");
340
- return;
341
- }
342
-
343
- if(typeof watchSchema === "function"){
344
- /* Only call the provided func */
345
- watchSchema(event);
346
-
347
- } else if(watchSchema === "hotReloadMode") {
348
- if(tsGeneratedTypesDir) {
349
- /* Hot reload integration. Will only touch tsGeneratedTypesDir */
350
- console.log("watchSchema: Re-writing TS schema");
351
-
352
- await this.refreshDBO();
353
- this.writeDBSchema(true);
354
- }
355
-
356
- } else if(watchSchema === true || "checkIntervalMillis" in watchSchema){
357
- /* Full re-init. Sockets must reconnect */
358
- console.log("watchSchema: Full re-initialisation")
359
- this.init(onReady);
360
- }
361
- }
362
- }
518
+ /* 3.9 Check auth config */
519
+ this.authHandler = new AuthHandler(this as any);
520
+ await this.authHandler.init();
363
521
 
364
- checkDb(){
365
- if(!this.db || !this.db.connect) throw "something went wrong getting a db connection";
366
- }
522
+ this.publishParser = new PublishParser(this.opts.publish, this.opts.publishMethods, this.opts.publishRawSQL, this.dbo!, this.db, this as any);
523
+ this.dboBuilder.publishParser = this.publishParser;
367
524
 
368
- getTSFileName(){
369
- const fileName = "DBoGenerated.d.ts" //`dbo_${this.schema}_types.ts`;
370
- const fullPath = (this.opts.tsGeneratedTypesDir || "") + fileName;
371
- return { fileName, fullPath }
372
- }
525
+ /* 4. Set publish and auth listeners */
526
+ await this.setSocketEvents();
373
527
 
374
- private getFileText(fullPath: string, format = "utf8"): Promise<string>{
375
- return new Promise((resolve, reject) => {
376
- fs.readFile(fullPath, 'utf8', function(err, data) {
377
- if(err) reject(err);
378
- else resolve(data);
379
- });
380
- })
381
- }
528
+ } else if (this.opts.auth) throw "Auth config does not work without publish";
382
529
 
383
- writeDBSchema(force = false){
384
-
385
- if(this.opts.tsGeneratedTypesDir){
386
- const { fullPath, fileName } = this.getTSFileName();
387
- const header = `/* This file was generated by Prostgles \n` +
388
- // `* ${(new Date).toUTCString()} \n`
389
- `*/ \n\n `;
390
- const fileContent = header + this.dboBuilder.tsTypesDefinition;
391
- fs.readFile(fullPath, 'utf8', function(err, data) {
392
- if (err || (force || data !== fileContent)) {
393
- fs.writeFileSync(fullPath, fileContent);
394
- console.log("Prostgles: Created typescript schema definition file: \n " + fileName)
395
- }
396
- });
397
- } else if(force) {
398
- console.error("Schema changed. tsGeneratedTypesDir needs to be set to reload server")
399
- }
400
- }
530
+ // if(this.watchSchema){
531
+ // if(!(await isSuperUser(db))) throw "Cannot watchSchema without a super user schema. Set watchSchema=false or provide a super user";
532
+ // }
533
+
534
+ this.dbEventsManager = new DBEventsManager(db, pgp);
401
535
 
402
- refreshDBO = async () => {
403
- if(this._dboBuilder) this._dboBuilder.destroy();
404
- this.dboBuilder = await DboBuilder.create(this as any) as any;
405
- if(!this.dboBuilder) throw "this.dboBuilder"
406
- this.dbo = this.dboBuilder.dbo as any;
407
- return this.dbo;
408
- }
409
536
 
410
- isSuperUser = false;
411
- schema_checkIntervalMillis: any;
412
- async init(onReady: (dbo: DBOFullyTyped, db: DB) => any): Promise<{
413
- db: DBOFullyTyped;
414
- _db: DB;
415
- pgp: PGP;
416
- io?: any;
417
- destroy: () => Promise<boolean>;
418
- }> {
419
- this.loaded = false;
420
-
421
-
422
- if(this.opts.watchSchema === "hotReloadMode" && !this.opts.tsGeneratedTypesDir) {
423
- throw "tsGeneratedTypesDir option is needed for watchSchema: hotReloadMode to work ";
424
- } else if(
425
- this.opts.watchSchema &&
426
- typeof this.opts.watchSchema === "object" &&
427
- "checkIntervalMillis" in this.opts.watchSchema &&
428
- typeof this.opts.watchSchema.checkIntervalMillis === "number"
429
- ){
430
-
431
- if(this.schema_checkIntervalMillis){
432
- clearInterval(this.schema_checkIntervalMillis);
433
- this.schema_checkIntervalMillis = setInterval(async () => {
434
- const dbuilder = await DboBuilder.create(this as any);
435
- if(dbuilder.tsTypesDefinition !== this.dboBuilder.tsTypesDefinition){
436
- this.refreshDBO();
437
- this.init(onReady);
438
- }
439
- }, this.opts.watchSchema.checkIntervalMillis)
537
+ this.writeDBSchema();
538
+
539
+ /* 5. Finish init and provide DBO object */
540
+ try {
541
+ if (this.destroyed) {
542
+ console.trace(1)
543
+ }
544
+ onReady(this.dbo as any, this.db);
545
+ } catch (err) {
546
+ console.error("Prostgles: Error within onReady: \n", err)
547
+ }
548
+
549
+ this.loaded = true;
550
+ return {
551
+ db: this.dbo! as any,
552
+ _db: db,
553
+ pgp,
554
+ io: this.opts.io,
555
+ destroy: async () => {
556
+ console.log("destroying prgl instance")
557
+ this.destroyed = true;
558
+ if (this.opts.io) {
559
+ this.opts.io.on("connection", () => {
560
+ console.log("Socket connected to destroyed instance")
561
+ });
562
+ if (typeof this.opts.io.close === "function") {
563
+ this.opts.io.close();
564
+ console.log("this.io.close")
440
565
  }
566
+ }
567
+
568
+ this.dboBuilder?.destroy();
569
+ this.dbo = undefined;
570
+ this.db = undefined;
571
+ await db.$pool.end();
572
+ await sleep(1000);
573
+ return true;
441
574
  }
575
+ };
576
+ } catch (e) {
577
+ console.trace(e)
578
+ // @ts-ignore
579
+ throw "init issues: " + e.toString();
580
+ }
581
+ }
582
+
583
+ async runSQLFile(filePath: string) {
584
+
585
+ const fileContent = await this.getFileText(filePath);//.then(console.log);
586
+
587
+ return this.db?.multi(fileContent).then((data) => {
588
+ console.log("Prostgles: SQL file executed successfuly \n -> " + filePath);
589
+ return data
590
+ }).catch((err) => {
591
+ const { position, length } = err,
592
+ lines = fileContent.split("\n");
593
+ let errMsg = filePath + " error: ";
594
+
595
+ if (position && length && fileContent) {
596
+ const startLine = Math.max(0, fileContent.substring(0, position).split("\n").length - 2),
597
+ endLine = startLine + 3;
598
+
599
+ errMsg += "\n\n";
600
+ errMsg += lines.slice(startLine, endLine).map((txt, i) => `${startLine + i + 1} ${i === 1 ? "->" : " "} ${txt}`).join("\n");
601
+ errMsg += "\n\n";
602
+ }
603
+ console.error(errMsg, err);
604
+ throw err;
605
+ });
606
+ }
442
607
 
443
- /* 1. Connect to db */
444
- if(!this.db){
445
- const { db, pgp } = getDbConnection(this.opts.dbConnection, this.opts.dbOptions, this.opts.DEBUG_MODE,
446
- notice => {
447
- if(this.opts.onNotice) this.opts.onNotice(notice);
448
- if(this.dbEventsManager){
449
- this.dbEventsManager.onNotice(notice)
450
- }
451
- }
452
- );
453
- this.db = db;
454
- this.pgp = pgp;
455
- this.isSuperUser = await isSuperUser(db);
456
- }
457
- this.checkDb();
458
- const db = this.db!;
459
- const pgp = this.pgp!;
460
608
 
461
- /* 2. Execute any SQL file if provided */
462
- if(this.opts.sqlFilePath){
463
- await this.runSQLFile(this.opts.sqlFilePath);
464
- }
609
+ connectedSockets: any[] = [];
610
+ async setSocketEvents() {
611
+ this.checkDb();
465
612
 
466
- try {
613
+ if (!this.dbo) throw "dbo missing";
467
614
 
468
- await this.refreshDBO();
469
- if(this.opts.tableConfig){
470
- this.tableConfigurator = new TableConfigurator(this as any);
471
- try {
472
- await this.tableConfigurator.init();
473
- } catch(e){
474
- console.error("TableConfigurator: ",e);
475
- throw e;
476
- }
477
- }
615
+ let publishParser = new PublishParser(this.opts.publish, this.opts.publishMethods, this.opts.publishRawSQL, this.dbo, this.db!, this as any);
616
+ this.publishParser = publishParser;
478
617
 
479
- /* 3. Make DBO object from all tables and views */
480
- await this.refreshDBO();
481
-
482
- /* Create media table if required */
483
- if(this.opts.fileTable){
484
- const { awsS3Config, localConfig, imageOptions } = this.opts.fileTable;
485
- await this.refreshDBO();
486
- if(!awsS3Config && !localConfig) throw "fileTable missing param: Must provide awsS3Config OR localConfig";
487
- //@ts-ignore
488
- this.fileManager = new FileManager(awsS3Config || localConfig, imageOptions);
489
-
490
- try {
491
- await this.fileManager.init(this as any);
492
- } catch(e){
493
- console.error("FileManager: ",e);
494
- throw e;
495
- }
496
- }
497
- await this.refreshDBO();
498
-
499
-
500
- if(this.opts.publish){
501
-
502
- if(!this.opts.io) console.warn("IO missing. Publish has no effect without io");
503
-
504
- /* 3.9 Check auth config */
505
- this.authHandler = new AuthHandler(this as any);
506
- await this.authHandler.init();
507
-
508
- this.publishParser = new PublishParser(this.opts.publish, this.opts.publishMethods, this.opts.publishRawSQL, this.dbo!, this.db, this as any);
509
- this.dboBuilder.publishParser = this.publishParser;
510
-
511
- /* 4. Set publish and auth listeners */
512
- await this.setSocketEvents();
513
-
514
- } else if(this.opts.auth) throw "Auth config does not work without publish";
515
-
516
- // if(this.watchSchema){
517
- // if(!(await isSuperUser(db))) throw "Cannot watchSchema without a super user schema. Set watchSchema=false or provide a super user";
518
- // }
519
-
520
- this.dbEventsManager = new DBEventsManager(db, pgp);
521
-
522
-
523
- this.writeDBSchema();
524
-
525
- /* 5. Finish init and provide DBO object */
526
- try {
527
- if(this.destroyed) {
528
- console.trace(1)
529
- }
530
- onReady(this.dbo as any, this.db);
531
- } catch(err){
532
- console.error("Prostgles: Error within onReady: \n", err)
533
- }
618
+ if (!this.opts.io) return;
534
619
 
535
- this.loaded = true;
536
- return {
537
- db: this.dbo! as any,
538
- _db: db,
539
- pgp,
540
- io: this.opts.io,
541
- destroy: async () => {
542
- console.log("destroying prgl instance")
543
- this.destroyed = true;
544
- if(this.opts.io){
545
- this.opts.io.on("connection", () => {
546
- console.log("Socket connected to destroyed instance")
547
- });
548
- if(typeof this.opts.io.close === "function"){
549
- this.opts.io.close();
550
- console.log("this.io.close")
551
- }
552
- }
553
-
554
- this.dboBuilder?.destroy();
555
- this.dbo = undefined;
556
- this.db = undefined;
557
- await db.$pool.end();
558
- await sleep(1000);
559
- return true;
560
- }
561
- };
562
- } catch (e) {
563
- console.trace(e)
564
- // @ts-ignore
565
- throw "init issues: " + e.toString();
566
- }
620
+ /* Already initialised. Only reconnect sockets */
621
+ if (this.connectedSockets.length) {
622
+ this.connectedSockets.forEach((s: any) => {
623
+ s.emit(CHANNELS.SCHEMA_CHANGED);
624
+ this.pushSocketSchema(s);
625
+ });
626
+ return;
567
627
  }
568
628
 
569
- async runSQLFile(filePath: string){
570
-
571
- const fileContent = await this.getFileText(filePath);//.then(console.log);
572
-
573
- return this.db?.multi(fileContent).then((data)=>{
574
- console.log("Prostgles: SQL file executed successfuly \n -> " + filePath);
575
- return data
576
- }).catch((err) => {
577
- const { position, length } = err,
578
- lines = fileContent.split("\n");
579
- let errMsg = filePath + " error: ";
580
-
581
- if(position && length && fileContent){
582
- const startLine = Math.max(0, fileContent.substring(0, position).split("\n").length - 2),
583
- endLine = startLine + 3;
584
-
585
- errMsg += "\n\n";
586
- errMsg += lines.slice(startLine, endLine).map((txt, i) => `${startLine + i + 1} ${i === 1? "->" : " "} ${txt}`).join("\n");
587
- errMsg += "\n\n";
588
- }
589
- console.error(errMsg, err);
590
- throw err;
591
- });
592
- }
629
+ /* Initialise */
630
+ this.opts.io.on('connection', async (socket: PRGLIOSocket) => {
631
+ if (this.destroyed) {
632
+ console.log("Socket connected to destroyed instance");
633
+ socket.disconnect();
634
+ return
635
+ }
636
+ this.connectedSockets.push(socket);
637
+
638
+ if (!this.db || !this.dbo) throw "db/dbo missing";
639
+ let { dbo, db, pgp } = this;
593
640
 
641
+ try {
642
+ if (this.opts.onSocketConnect) await this.opts.onSocketConnect(socket, dbo as any, db);
594
643
 
595
- connectedSockets: any[] = [];
596
- async setSocketEvents(){
597
- this.checkDb();
598
644
 
599
- if(!this.dbo) throw "dbo missing";
645
+ /* RUN Client request from Publish.
646
+ Checks request against publish and if OK run it with relevant publish functions. Local (server) requests do not check the policy
647
+ */
648
+ socket.removeAllListeners(CHANNELS.DEFAULT)
649
+ socket.on(CHANNELS.DEFAULT, async ({ tableName, command, param1, param2, param3 }: SocketRequestParams, cb = (...callback: any[]) => { }) => {
600
650
 
601
- let publishParser = new PublishParser(this.opts.publish, this.opts.publishMethods, this.opts.publishRawSQL, this.dbo, this.db!, this as any);
602
- this.publishParser = publishParser;
651
+ try { /* Channel name will only include client-sent params so we ignore table_rules enforced params */
652
+ if (!socket || !this.authHandler || !this.publishParser || !this.dbo) {
653
+ console.error("socket or authhandler missing??!!")
654
+ throw "socket or authhandler missing??!!";
655
+ }
603
656
 
604
- if(!this.opts.io) return;
657
+ const clientInfo = await this.authHandler.getClientInfo({ socket });
658
+ let valid_table_command_rules = await this.publishParser.getValidatedRequestRule({ tableName, command, localParams: { socket } }, clientInfo);
659
+ if (valid_table_command_rules) {
660
+ //@ts-ignore
661
+ let res = await this.dbo[tableName][command]!(param1, param2, param3, valid_table_command_rules, { socket, has_rules: true });
662
+ cb(null, res);
663
+ } else throw `Invalid OR disallowed request: ${tableName}.${command} `;
664
+
665
+ } catch (err) {
666
+ // const _err_msg = err.toString();
667
+ // cb({ msg: _err_msg, err });
668
+ console.trace(err);
669
+ cb(err)
670
+ // console.warn("runPublishedRequest ERROR: ", err, socket._user);
671
+ }
672
+ });
605
673
 
606
- /* Already initialised. Only reconnect sockets */
607
- if(this.connectedSockets.length){
608
- this.connectedSockets.forEach((s: any) => {
609
- s.emit(CHANNELS.SCHEMA_CHANGED);
610
- this.pushSocketSchema(s);
611
- });
612
- return;
613
- }
614
-
615
- /* Initialise */
616
- this.opts.io.on('connection', async (socket: PRGLIOSocket) => {
617
- if(this.destroyed){
618
- console.log("Socket connected to destroyed instance");
619
- socket.disconnect();
620
- return
674
+ socket.on("disconnect", () => {
675
+ this.dbEventsManager?.removeNotice(socket);
676
+ this.dbEventsManager?.removeNotify(undefined, socket);
677
+ this.connectedSockets = this.connectedSockets.filter(s => s.id !== socket.id);
678
+ // subscriptions = subscriptions.filter(sub => sub.socket.id !== socket.id);
679
+ if (this.opts.onSocketDisconnect) {
680
+ this.opts.onSocketDisconnect(socket, dbo as any);
681
+ };
682
+ });
683
+
684
+ socket.removeAllListeners(CHANNELS.METHOD)
685
+ socket.on(CHANNELS.METHOD, async ({ method, params }: SocketMethodRequest, cb = (...callback: any) => { }) => {
686
+ try {
687
+ const methods = await this.publishParser?.getMethods(socket) as any;
688
+
689
+ if (!methods || !methods[method]) {
690
+ cb("Disallowed/missing method " + JSON.stringify(method));
691
+ } else {
692
+ try {
693
+ const res = await methods[method](...params);
694
+ cb(null, res);
695
+ } catch (err) {
696
+ makeSocketError(cb, err);
697
+ }
621
698
  }
622
- this.connectedSockets.push(socket);
623
-
624
- if(!this.db || !this.dbo) throw "db/dbo missing";
625
- let { dbo, db, pgp } = this;
626
-
627
- try {
628
- if(this.opts.onSocketConnect) await this.opts.onSocketConnect(socket, dbo as any, db);
629
-
630
-
631
- /* RUN Client request from Publish.
632
- Checks request against publish and if OK run it with relevant publish functions. Local (server) requests do not check the policy
633
- */
634
- socket.removeAllListeners(CHANNELS.DEFAULT)
635
- socket.on(CHANNELS.DEFAULT, async ({ tableName, command, param1, param2, param3 }: SocketRequestParams, cb = (...callback: any[]) => {} ) => {
636
-
637
- try { /* Channel name will only include client-sent params so we ignore table_rules enforced params */
638
- if(!socket || !this.authHandler || !this.publishParser || !this.dbo) {
639
- console.error("socket or authhandler missing??!!")
640
- throw "socket or authhandler missing??!!";
641
- }
642
-
643
- const clientInfo = await this.authHandler.getClientInfo({ socket });
644
- let valid_table_command_rules = await this.publishParser.getValidatedRequestRule({ tableName, command, localParams: { socket } }, clientInfo);
645
- if(valid_table_command_rules){
646
- //@ts-ignore
647
- let res = await this.dbo[tableName][command]!(param1, param2, param3, valid_table_command_rules, { socket, has_rules: true });
648
- cb(null, res);
649
- } else throw `Invalid OR disallowed request: ${tableName}.${command} `;
650
-
651
- } catch(err) {
652
- // const _err_msg = err.toString();
653
- // cb({ msg: _err_msg, err });
654
- console.trace(err);
655
- cb(err)
656
- // console.warn("runPublishedRequest ERROR: ", err, socket._user);
657
- }
658
- });
659
-
660
- socket.on("disconnect", () => {
661
- this.dbEventsManager?.removeNotice(socket);
662
- this.dbEventsManager?.removeNotify(undefined, socket);
663
- this.connectedSockets = this.connectedSockets.filter(s => s.id !== socket.id);
664
- // subscriptions = subscriptions.filter(sub => sub.socket.id !== socket.id);
665
- if(this.opts.onSocketDisconnect){
666
- this.opts.onSocketDisconnect(socket, dbo as any);
667
- };
668
- });
669
-
670
- socket.removeAllListeners(CHANNELS.METHOD)
671
- socket.on(CHANNELS.METHOD, async ({ method, params }: SocketMethodRequest, cb = (...callback: any) => {} ) => {
672
- try {
673
- const methods = await this.publishParser?.getMethods(socket) as any;
674
-
675
- if(!methods || !methods[method]){
676
- cb("Disallowed/missing method " + JSON.stringify(method));
677
- } else {
678
- try {
679
- const res = await methods[method](...params);
680
- cb(null, res);
681
- } catch(err){
682
- makeSocketError(cb, err);
683
- }
684
- }
685
- } catch(err) {
686
- makeSocketError(cb, err);
687
- console.warn("method ERROR: ", err, socket._user);
688
- }
689
- });
690
-
691
- this.pushSocketSchema(socket);
692
- } catch(e) {
693
- console.trace("setSocketEvents: ", e)
694
- }
699
+ } catch (err) {
700
+ makeSocketError(cb, err);
701
+ console.warn("method ERROR: ", err, socket._user);
702
+ }
703
+ });
704
+
705
+ this.pushSocketSchema(socket);
706
+ } catch (e) {
707
+ console.trace("setSocketEvents: ", e)
708
+ }
709
+ });
710
+ }
711
+
712
+ pushSocketSchema = async (socket: any) => {
713
+
714
+ let auth: any = await this.authHandler?.makeSocketAuth(socket) || {};
715
+
716
+ // let needType = this.publishRawSQL && typeof this.publishRawSQL === "function";
717
+ // let DATA_TYPES = !needType? [] : await this.db.any("SELECT oid, typname FROM pg_type");
718
+ // let USER_TABLES = !needType? [] : await this.db.any("SELECT relid, relname FROM pg_catalog.pg_statio_user_tables");
719
+
720
+ const { dbo, db, pgp, publishParser } = this;
721
+ let fullSchema: {
722
+ schema: TableSchemaForClient;
723
+ tables: DBSchemaTable[];
724
+ } | undefined;
725
+ let publishValidationError;
726
+ let rawSQL = false;
727
+
728
+ try {
729
+ if (!publishParser) throw "publishParser undefined";
730
+ fullSchema = await publishParser.getSchemaFromPublish(socket);
731
+ } catch (e) {
732
+ publishValidationError = "Server Error: PUBLISH VALIDATION ERROR";
733
+ console.error(`\nProstgles PUBLISH VALIDATION ERROR (after socket connected):\n ->`, e);
734
+ }
735
+ socket.prostgles = socket.prostgles || {};
736
+ socket.prostgles.schema = fullSchema?.schema;
737
+ /* RUN Raw sql from client IF PUBLISHED
738
+ */
739
+
740
+ if (this.opts.publishRawSQL && typeof this.opts.publishRawSQL === "function") {
741
+ const canRunSQL = async () => {
742
+ const publishParams = await this.publishParser?.getPublishParams({ socket })
743
+ let res = await this.opts.publishRawSQL?.(publishParams as any);
744
+ return Boolean(res && typeof res === "boolean" || res === "*");
745
+ }
746
+
747
+ if (await canRunSQL()) {
748
+ socket.removeAllListeners(CHANNELS.SQL)
749
+ socket.on(CHANNELS.SQL, async ({ query, params, options }: SQLRequest, cb = (...callback: any) => { }) => {
750
+
751
+ if (!this.dbo?.sql) throw "Internal error: sql handler missing";
752
+
753
+ this.dbo.sql(query, params, options, { socket }).then(res => {
754
+ cb(null, res)
755
+ }).catch(err => {
756
+ makeSocketError(cb, err);
757
+ })
695
758
  });
759
+ if (db) {
760
+ // let allTablesViews = await db.any(STEP2_GET_ALL_TABLES_AND_COLUMNS);
761
+ // fullSchema = allTablesViews;
762
+ rawSQL = true;
763
+ } else console.error("db missing");
764
+ }
696
765
  }
697
766
 
698
- pushSocketSchema = async (socket: any) => {
699
-
700
- let auth: any = await this.authHandler?.makeSocketAuth(socket) || {};
701
-
702
- // let needType = this.publishRawSQL && typeof this.publishRawSQL === "function";
703
- // let DATA_TYPES = !needType? [] : await this.db.any("SELECT oid, typname FROM pg_type");
704
- // let USER_TABLES = !needType? [] : await this.db.any("SELECT relid, relname FROM pg_catalog.pg_statio_user_tables");
705
-
706
- const { dbo, db, pgp, publishParser } = this;
707
- let fullSchema: {
708
- schema: TableSchemaForClient;
709
- tables: DBSchemaTable[];
710
- } | undefined;
711
- let publishValidationError;
712
- let rawSQL = false;
713
-
714
- try {
715
- if(!publishParser) throw "publishParser undefined";
716
- fullSchema = await publishParser.getSchemaFromPublish(socket);
717
- } catch(e){
718
- publishValidationError = "Server Error: PUBLISH VALIDATION ERROR";
719
- console.error(`\nProstgles PUBLISH VALIDATION ERROR (after socket connected):\n ->`, e);
720
- }
721
- socket.prostgles = socket.prostgles || {};
722
- socket.prostgles.schema = fullSchema?.schema;
723
- /* RUN Raw sql from client IF PUBLISHED
724
- */
725
-
726
- if(this.opts.publishRawSQL && typeof this.opts.publishRawSQL === "function"){
727
- const canRunSQL = async () => {
728
- const publishParams = await this.publishParser?.getPublishParams({ socket })
729
- let res = await this.opts.publishRawSQL?.(publishParams as any);
730
- return Boolean(res && typeof res === "boolean" || res === "*");
731
- }
732
-
733
- if(await canRunSQL()){
734
- socket.removeAllListeners(CHANNELS.SQL)
735
- socket.on(CHANNELS.SQL, async ({ query, params, options }: SQLRequest, cb = (...callback: any) => {}) => {
736
-
737
- if(!this.dbo?.sql) throw "Internal error: sql handler missing";
738
-
739
- this.dbo.sql(query, params, options, { socket }).then(res => {
740
- cb(null, res)
741
- }).catch(err => {
742
- makeSocketError(cb, err);
743
- })
744
- });
745
- if(db){
746
- // let allTablesViews = await db.any(STEP2_GET_ALL_TABLES_AND_COLUMNS);
747
- // fullSchema = allTablesViews;
748
- rawSQL = true;
749
- } else console.error("db missing");
750
- }
767
+ const { schema, tables } = fullSchema ?? { schema: {}, tables: [] };
768
+ let joinTables2: string[][] = [];
769
+ if (this.opts.joins) {
770
+ // joinTables = Array.from(new Set(flat(this.dboBuilder.getJoins().map(j => j.tables)).filter(t => schema[t])));
771
+ let _joinTables2 = this.dboBuilder.getJoinPaths()
772
+ .filter(jp =>
773
+ ![jp.t1, jp.t2].find(t => !schema[t] || !schema[t].findOne)
774
+ ).map(jp => [jp.t1, jp.t2].sort());
775
+ _joinTables2.map(jt => {
776
+ if (!joinTables2.find(_jt => _jt.join() === jt.join())) {
777
+ joinTables2.push(jt);
751
778
  }
779
+ });
780
+ }
752
781
 
753
- const { schema, tables } = fullSchema ?? { schema: {}, tables: [] };
754
- let joinTables2: string[][] = [];
755
- if(this.opts.joins){
756
- // joinTables = Array.from(new Set(flat(this.dboBuilder.getJoins().map(j => j.tables)).filter(t => schema[t])));
757
- let _joinTables2 = this.dboBuilder.getJoinPaths()
758
- .filter(jp =>
759
- ![jp.t1, jp.t2].find(t => !schema[t] || !schema[t].findOne)
760
- ).map(jp => [jp.t1, jp.t2].sort());
761
- _joinTables2.map(jt => {
762
- if(!joinTables2.find(_jt => _jt.join() === jt.join())){
763
- joinTables2.push(jt);
764
- }
765
- });
766
- }
767
-
768
- const methods = await publishParser?.getMethods(socket);
769
- const clientSchema: ClientSchema = {
770
- schema,
771
- methods: getKeys(methods),
772
- tableSchema: tables,
773
- rawSQL,
774
- joinTables: joinTables2,
775
- auth,
776
- version,
777
- err: publishValidationError
778
- }
779
- socket.emit(CHANNELS.SCHEMA, clientSchema);
782
+ const methods = await publishParser?.getMethods(socket);
783
+ const clientSchema: ClientSchema = {
784
+ schema,
785
+ methods: getKeys(methods),
786
+ tableSchema: tables,
787
+ rawSQL,
788
+ joinTables: joinTables2,
789
+ auth,
790
+ version,
791
+ err: publishValidationError
780
792
  }
793
+ socket.emit(CHANNELS.SCHEMA, clientSchema);
794
+ }
781
795
  }
782
- function makeSocketError(cb: Function, err: any){
783
- const err_msg = (err instanceof Error)?
784
- err.toString() :
785
- isPlainObject(err)?
786
- JSON.stringify(err, null, 2) :
787
- (err as any).toString(),
788
- e = { err_msg, err };
789
- cb(e);
796
+ function makeSocketError(cb: Function, err: any) {
797
+ const err_msg = (err instanceof Error) ?
798
+ err.toString() :
799
+ isPlainObject(err) ?
800
+ JSON.stringify(err, null, 2) :
801
+ (err as any).toString(),
802
+ e = { err_msg, err };
803
+ cb(e);
790
804
  }
791
805
 
792
806
  type SocketRequestParams = {
793
- tableName: string;
794
- command: typeof TABLE_METHODS[number];
795
- param1: any;
796
- param2: any;
797
- param3: any;
807
+ tableName: string;
808
+ command: typeof TABLE_METHODS[number];
809
+ param1: any;
810
+ param2: any;
811
+ param3: any;
798
812
  }
799
813
  type SocketMethodRequest = {
800
- method: string;
801
- params: any;
814
+ method: string;
815
+ params: any;
802
816
  }
803
817
 
804
818
  // const ALL_PUBLISH_METHODS = ["update", "upsert", "delete", "insert", "find", "findOne", "subscribe", "unsubscribe", "sync", "unsync", "remove"];
@@ -809,14 +823,14 @@ type SocketMethodRequest = {
809
823
  // let res = arr.reduce(function (farr, toFlatten) {
810
824
  // return farr.concat(Array.isArray(toFlatten) ? flat(toFlatten) : toFlatten);
811
825
  // }, []);
812
-
826
+
813
827
  // return res;
814
828
  // }
815
829
 
816
830
 
817
831
 
818
- export async function isSuperUser(db: DB): Promise<boolean>{
819
- return db.oneOrNone("select usesuper from pg_user where usename = CURRENT_USER;").then(r => r.usesuper);
832
+ export async function isSuperUser(db: DB): Promise<boolean> {
833
+ return db.oneOrNone("select usesuper from pg_user where usename = CURRENT_USER;").then(r => r.usesuper);
820
834
  }
821
835
 
822
836