@restura/core 0.1.0-alpha.9 → 0.1.2

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.
package/dist/index.js CHANGED
@@ -65,13 +65,18 @@ var __decorateClass = (decorators, target, key, kind) => {
65
65
  };
66
66
 
67
67
  // src/index.ts
68
- var src_exports = {};
69
- __export(src_exports, {
68
+ var index_exports = {};
69
+ __export(index_exports, {
70
70
  HtmlStatusCodes: () => HtmlStatusCodes,
71
+ PsqlConnection: () => PsqlConnection,
72
+ PsqlEngine: () => PsqlEngine,
71
73
  PsqlPool: () => PsqlPool,
74
+ PsqlTransaction: () => PsqlTransaction,
72
75
  RsError: () => RsError,
73
76
  SQL: () => SQL,
74
77
  escapeColumnName: () => escapeColumnName,
78
+ eventManager: () => eventManager_default,
79
+ filterPsqlParser: () => filterPsqlParser_default,
75
80
  insertObjectQuery: () => insertObjectQuery,
76
81
  isValueNumber: () => isValueNumber2,
77
82
  logger: () => logger,
@@ -79,25 +84,18 @@ __export(src_exports, {
79
84
  restura: () => restura,
80
85
  updateObjectQuery: () => updateObjectQuery
81
86
  });
82
- module.exports = __toCommonJS(src_exports);
87
+ module.exports = __toCommonJS(index_exports);
83
88
 
84
89
  // src/logger/logger.ts
85
90
  var import_internal = require("@restura/internal");
86
91
  var import_winston = __toESM(require("winston"));
87
92
  var import_logform = require("logform");
88
93
 
89
- // src/config.schema.ts
94
+ // src/logger/loggerConfigSchema.ts
90
95
  var import_zod = require("zod");
91
96
  var loggerConfigSchema = import_zod.z.object({
92
97
  level: import_zod.z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
93
98
  });
94
- var resturaConfigSchema = import_zod.z.object({
95
- authToken: import_zod.z.string().min(1, "Missing Restura Auth Token"),
96
- sendErrorStackTrace: import_zod.z.boolean().default(false),
97
- schemaFilePath: import_zod.z.string().default(process.cwd() + "/restura.schema.json"),
98
- customApiFolderPath: import_zod.z.string().default(process.cwd() + "/dist/api"),
99
- generatedTypesPath: import_zod.z.string().default(process.cwd() + "/src/@types")
100
- });
101
99
 
102
100
  // src/logger/logger.ts
103
101
  var loggerConfig = import_internal.config.validate("logger", loggerConfigSchema);
@@ -134,7 +132,198 @@ var logger = import_winston.default.createLogger({
134
132
  ]
135
133
  });
136
134
 
137
- // src/restura/errors.ts
135
+ // src/restura/eventManager.ts
136
+ var import_bluebird = __toESM(require("bluebird"));
137
+ var EventManager = class {
138
+ constructor() {
139
+ this.actionHandlers = {
140
+ DATABASE_ROW_DELETE: [],
141
+ DATABASE_ROW_INSERT: [],
142
+ DATABASE_COLUMN_UPDATE: []
143
+ };
144
+ }
145
+ addRowInsertHandler(onInsert, filter) {
146
+ this.actionHandlers.DATABASE_ROW_INSERT.push({
147
+ callback: onInsert,
148
+ filter
149
+ });
150
+ }
151
+ addColumnChangeHandler(onUpdate, filter) {
152
+ this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
153
+ callback: onUpdate,
154
+ filter
155
+ });
156
+ }
157
+ addRowDeleteHandler(onDelete, filter) {
158
+ this.actionHandlers.DATABASE_ROW_DELETE.push({
159
+ callback: onDelete,
160
+ filter
161
+ });
162
+ }
163
+ async fireActionFromDbTrigger(sqlMutationData, result) {
164
+ if (sqlMutationData.mutationType === "INSERT") {
165
+ await this.fireInsertActions(sqlMutationData, result);
166
+ } else if (sqlMutationData.mutationType === "UPDATE") {
167
+ await this.fireUpdateActions(sqlMutationData, result);
168
+ } else if (sqlMutationData.mutationType === "DELETE") {
169
+ await this.fireDeleteActions(sqlMutationData, result);
170
+ }
171
+ }
172
+ async fireInsertActions(data, triggerResult) {
173
+ await import_bluebird.default.map(
174
+ this.actionHandlers.DATABASE_ROW_INSERT,
175
+ ({ callback, filter }) => {
176
+ if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
177
+ const insertData = {
178
+ tableName: triggerResult.table,
179
+ insertedId: triggerResult.insertedId || 0,
180
+ insertObject: triggerResult.record,
181
+ queryMetadata: data.queryMetadata
182
+ };
183
+ callback(insertData, data.queryMetadata);
184
+ },
185
+ { concurrency: 10 }
186
+ );
187
+ }
188
+ async fireDeleteActions(data, triggerResult) {
189
+ await import_bluebird.default.map(
190
+ this.actionHandlers.DATABASE_ROW_DELETE,
191
+ ({ callback, filter }) => {
192
+ if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
193
+ const deleteData = {
194
+ tableName: triggerResult.table,
195
+ deletedId: triggerResult.deletedId || 0,
196
+ deletedRow: triggerResult.previousRecord,
197
+ queryMetadata: data.queryMetadata
198
+ };
199
+ callback(deleteData, data.queryMetadata);
200
+ },
201
+ { concurrency: 10 }
202
+ );
203
+ }
204
+ async fireUpdateActions(data, triggerResult) {
205
+ await import_bluebird.default.map(
206
+ this.actionHandlers.DATABASE_COLUMN_UPDATE,
207
+ ({ callback, filter }) => {
208
+ if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
209
+ const columnChangeData = {
210
+ tableName: triggerResult.table,
211
+ changedId: triggerResult.changedId || 0,
212
+ newData: triggerResult.record,
213
+ oldData: triggerResult.previousRecord,
214
+ queryMetadata: data.queryMetadata
215
+ };
216
+ callback(columnChangeData, data.queryMetadata);
217
+ },
218
+ { concurrency: 10 }
219
+ );
220
+ }
221
+ hasHandlersForEventType(eventType, filter, triggerResult) {
222
+ if (filter) {
223
+ switch (eventType) {
224
+ case "DATABASE_ROW_INSERT":
225
+ case "DATABASE_ROW_DELETE":
226
+ if (filter.tableName && filter.tableName !== triggerResult.table) return false;
227
+ break;
228
+ case "DATABASE_COLUMN_UPDATE":
229
+ const filterColumnChange = filter;
230
+ if (filterColumnChange.tableName !== triggerResult.table) return false;
231
+ if (filterColumnChange.columns.length === 1) {
232
+ const firstColumn = filterColumnChange.columns[0];
233
+ if (firstColumn === "*") return true;
234
+ }
235
+ if (!filterColumnChange.columns.some((item) => {
236
+ const updatedColumns = Object.keys(
237
+ changedValues(triggerResult.record, triggerResult.previousRecord)
238
+ );
239
+ return updatedColumns.includes(item);
240
+ }))
241
+ return false;
242
+ break;
243
+ }
244
+ }
245
+ return true;
246
+ }
247
+ };
248
+ var eventManager = new EventManager();
249
+ var eventManager_default = eventManager;
250
+ function changedValues(record, previousRecord) {
251
+ const changed = {};
252
+ for (const i in previousRecord) {
253
+ if (previousRecord[i] !== record[i]) {
254
+ if (typeof previousRecord[i] === "object" && typeof record[i] === "object") {
255
+ const nestedChanged = changedValues(record[i], previousRecord[i]);
256
+ if (Object.keys(nestedChanged).length > 0) {
257
+ changed[i] = record[i];
258
+ }
259
+ } else {
260
+ changed[i] = record[i];
261
+ }
262
+ }
263
+ }
264
+ return changed;
265
+ }
266
+
267
+ // src/restura/restura.ts
268
+ var import_core_utils7 = require("@redskytech/core-utils");
269
+ var import_internal4 = require("@restura/internal");
270
+
271
+ // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
272
+ function _typeof(obj) {
273
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
274
+ _typeof = function _typeof2(obj2) {
275
+ return typeof obj2;
276
+ };
277
+ } else {
278
+ _typeof = function _typeof2(obj2) {
279
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
280
+ };
281
+ }
282
+ return _typeof(obj);
283
+ }
284
+ function boundMethod(target, key, descriptor) {
285
+ var fn = descriptor.value;
286
+ if (typeof fn !== "function") {
287
+ throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
288
+ }
289
+ var definingProperty = false;
290
+ return {
291
+ configurable: true,
292
+ get: function get() {
293
+ if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
294
+ return fn;
295
+ }
296
+ var boundFn = fn.bind(this);
297
+ definingProperty = true;
298
+ Object.defineProperty(this, key, {
299
+ configurable: true,
300
+ get: function get2() {
301
+ return boundFn;
302
+ },
303
+ set: function set(value) {
304
+ fn = value;
305
+ delete this[key];
306
+ }
307
+ });
308
+ definingProperty = false;
309
+ return boundFn;
310
+ },
311
+ set: function set(value) {
312
+ fn = value;
313
+ }
314
+ };
315
+ }
316
+
317
+ // src/restura/restura.ts
318
+ var import_body_parser = __toESM(require("body-parser"));
319
+ var import_compression = __toESM(require("compression"));
320
+ var import_cookie_parser = __toESM(require("cookie-parser"));
321
+ var express = __toESM(require("express"));
322
+ var import_fs4 = __toESM(require("fs"));
323
+ var import_path5 = __toESM(require("path"));
324
+ var prettier3 = __toESM(require("prettier"));
325
+
326
+ // src/restura/RsError.ts
138
327
  var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
139
328
  HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
140
329
  HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
@@ -158,7 +347,6 @@ var RsError = class _RsError {
158
347
  static htmlStatus(code) {
159
348
  return htmlStatusMap[code];
160
349
  }
161
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
350
  static isRsError(error) {
163
351
  return error instanceof _RsError;
164
352
  }
@@ -200,73 +388,171 @@ var htmlStatusMap = {
200
388
  SCHEMA_ERROR: 500 /* SERVER_ERROR */
201
389
  };
202
390
 
203
- // src/restura/restura.ts
204
- var import_core_utils6 = require("@redskytech/core-utils");
205
- var import_internal2 = require("@restura/internal");
206
-
207
- // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
208
- function _typeof(obj) {
209
- if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
210
- _typeof = function _typeof2(obj2) {
211
- return typeof obj2;
212
- };
213
- } else {
214
- _typeof = function _typeof2(obj2) {
215
- return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
391
+ // src/restura/compareSchema.ts
392
+ var import_lodash = __toESM(require("lodash.clonedeep"));
393
+ var CompareSchema = class {
394
+ async diffSchema(newSchema, latestSchema, psqlEngine) {
395
+ const endPoints = this.diffEndPoints(newSchema.endpoints[0].routes, latestSchema.endpoints[0].routes);
396
+ const globalParams = this.diffStringArray(newSchema.globalParams, latestSchema.globalParams);
397
+ const roles = this.diffStringArray(newSchema.roles, latestSchema.roles);
398
+ let commands = "";
399
+ if (JSON.stringify(newSchema.database) !== JSON.stringify(latestSchema.database))
400
+ commands = await psqlEngine.diffDatabaseToSchema(newSchema);
401
+ const hasCustomTypesChanged = JSON.stringify(newSchema.customTypes) !== JSON.stringify(latestSchema.customTypes);
402
+ const schemaPreview = {
403
+ endPoints,
404
+ globalParams,
405
+ roles,
406
+ commands,
407
+ customTypes: hasCustomTypesChanged
216
408
  };
217
- }
218
- return _typeof(obj);
219
- }
220
- function boundMethod(target, key, descriptor) {
221
- var fn = descriptor.value;
222
- if (typeof fn !== "function") {
223
- throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
224
- }
225
- var definingProperty = false;
226
- return {
227
- configurable: true,
228
- get: function get() {
229
- if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
230
- return fn;
409
+ return schemaPreview;
410
+ }
411
+ diffStringArray(newArray, originalArray) {
412
+ const stringsDiff = [];
413
+ const originalClone = new Set(originalArray);
414
+ newArray.forEach((item) => {
415
+ const originalIndex = originalClone.has(item);
416
+ if (!originalIndex) {
417
+ stringsDiff.push({
418
+ name: item,
419
+ changeType: "NEW"
420
+ });
421
+ } else {
422
+ originalClone.delete(item);
231
423
  }
232
- var boundFn = fn.bind(this);
233
- definingProperty = true;
234
- Object.defineProperty(this, key, {
235
- configurable: true,
236
- get: function get2() {
237
- return boundFn;
238
- },
239
- set: function set(value) {
240
- fn = value;
241
- delete this[key];
424
+ });
425
+ originalClone.forEach((item) => {
426
+ stringsDiff.push({
427
+ name: item,
428
+ changeType: "DELETED"
429
+ });
430
+ });
431
+ return stringsDiff;
432
+ }
433
+ diffEndPoints(newEndPoints, originalEndpoints) {
434
+ const originalClone = (0, import_lodash.default)(originalEndpoints);
435
+ const diffObj = [];
436
+ newEndPoints.forEach((endPoint) => {
437
+ const { path: path5, method } = endPoint;
438
+ const endPointIndex = originalClone.findIndex((original) => {
439
+ return original.path === endPoint.path && original.method === endPoint.method;
440
+ });
441
+ if (endPointIndex === -1) {
442
+ diffObj.push({
443
+ name: `${method} ${path5}`,
444
+ changeType: "NEW"
445
+ });
446
+ } else {
447
+ const original = originalClone.findIndex((original2) => {
448
+ return this.compareEndPoints(endPoint, original2);
449
+ });
450
+ if (original === -1) {
451
+ diffObj.push({
452
+ name: `${method} ${path5}`,
453
+ changeType: "MODIFIED"
454
+ });
242
455
  }
456
+ originalClone.splice(endPointIndex, 1);
457
+ }
458
+ });
459
+ originalClone.forEach((original) => {
460
+ const { path: path5, method } = original;
461
+ diffObj.push({
462
+ name: `${method} ${path5}`,
463
+ changeType: "DELETED"
243
464
  });
244
- definingProperty = false;
245
- return boundFn;
246
- },
247
- set: function set(value) {
248
- fn = value;
465
+ });
466
+ return diffObj;
467
+ }
468
+ compareEndPoints(endPoint1, endPoint2) {
469
+ return JSON.stringify(endPoint1) === JSON.stringify(endPoint2);
470
+ }
471
+ };
472
+ __decorateClass([
473
+ boundMethod
474
+ ], CompareSchema.prototype, "diffSchema", 1);
475
+ __decorateClass([
476
+ boundMethod
477
+ ], CompareSchema.prototype, "diffStringArray", 1);
478
+ __decorateClass([
479
+ boundMethod
480
+ ], CompareSchema.prototype, "diffEndPoints", 1);
481
+ __decorateClass([
482
+ boundMethod
483
+ ], CompareSchema.prototype, "compareEndPoints", 1);
484
+ var compareSchema = new CompareSchema();
485
+ var compareSchema_default = compareSchema;
486
+
487
+ // src/restura/customApiFactory.ts
488
+ var import_bluebird2 = __toESM(require("bluebird"));
489
+ var import_fs = __toESM(require("fs"));
490
+ var import_path = __toESM(require("path"));
491
+ var import_internal2 = require("@restura/internal");
492
+ var CustomApiFactory = class {
493
+ constructor() {
494
+ this.customApis = {};
495
+ }
496
+ async loadApiFiles(baseFolderPath) {
497
+ const apiVersions = ["v1"];
498
+ for (const apiVersion of apiVersions) {
499
+ const apiVersionFolderPath = import_path.default.join(baseFolderPath, apiVersion);
500
+ const directoryExists = await import_internal2.FileUtils.existDir(apiVersionFolderPath);
501
+ if (!directoryExists) continue;
502
+ await this.addDirectory(apiVersionFolderPath, apiVersion);
249
503
  }
250
- };
251
- }
504
+ }
505
+ getCustomApi(customApiName) {
506
+ return this.customApis[customApiName];
507
+ }
508
+ async addDirectory(directoryPath, apiVersion) {
509
+ var _a2;
510
+ const entries = await import_fs.default.promises.readdir(directoryPath, {
511
+ withFileTypes: true
512
+ });
513
+ const isTsx2 = (_a2 = process.argv[1]) == null ? void 0 : _a2.endsWith(".ts");
514
+ const isTsNode2 = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
515
+ const extension = isTsx2 || isTsNode2 ? "ts" : "js";
516
+ const shouldEndWith = `.api.${apiVersion}.${extension}`;
517
+ await import_bluebird2.default.map(entries, async (entry) => {
518
+ if (entry.isFile()) {
519
+ if (entry.name.endsWith(shouldEndWith) === false) return;
520
+ try {
521
+ const importPath = `${import_path.default.join(directoryPath, entry.name)}`;
522
+ const ApiImport = await import(importPath);
523
+ const customApiClass = new ApiImport.default();
524
+ logger.info(`Registering custom API: ${ApiImport.default.name}`);
525
+ this.bindMethodsToInstance(customApiClass);
526
+ this.customApis[ApiImport.default.name] = customApiClass;
527
+ } catch (e) {
528
+ logger.error(e);
529
+ }
530
+ }
531
+ });
532
+ }
533
+ bindMethodsToInstance(instance) {
534
+ const proto = Object.getPrototypeOf(instance);
535
+ Object.getOwnPropertyNames(proto).forEach((key) => {
536
+ const property = instance[key];
537
+ if (typeof property === "function" && key !== "constructor") {
538
+ instance[key] = property.bind(instance);
539
+ }
540
+ });
541
+ }
542
+ };
543
+ var customApiFactory = new CustomApiFactory();
544
+ var customApiFactory_default = customApiFactory;
252
545
 
253
- // src/restura/restura.ts
254
- var import_body_parser = __toESM(require("body-parser"));
255
- var import_compression = __toESM(require("compression"));
256
- var import_cookie_parser = __toESM(require("cookie-parser"));
257
- var import_crypto = require("crypto");
258
- var express = __toESM(require("express"));
259
- var import_fs3 = __toESM(require("fs"));
260
- var import_path3 = __toESM(require("path"));
261
- var import_pg2 = __toESM(require("pg"));
262
- var prettier3 = __toESM(require("prettier"));
546
+ // src/restura/generators/apiGenerator.ts
547
+ var import_core_utils = require("@redskytech/core-utils");
548
+ var import_prettier = __toESM(require("prettier"));
263
549
 
264
550
  // src/restura/sql/SqlUtils.ts
265
551
  var SqlUtils = class _SqlUtils {
266
552
  static convertDatabaseTypeToTypescript(type, value) {
267
553
  type = type.toLocaleLowerCase();
268
554
  if (type.startsWith("tinyint") || type.startsWith("boolean")) return "boolean";
269
- if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float"))
555
+ if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float") || type.indexOf("serial") > -1 || type.startsWith("decimal") || type.startsWith("real") || type.startsWith("double precision") || type.startsWith("numeric"))
270
556
  return "number";
271
557
  if (type === "json") {
272
558
  if (!value) return "object";
@@ -287,7 +573,7 @@ var SqlUtils = class _SqlUtils {
287
573
  }
288
574
  };
289
575
 
290
- // src/restura/ResponseValidator.ts
576
+ // src/restura/validators/ResponseValidator.ts
291
577
  var ResponseValidator = class _ResponseValidator {
292
578
  constructor(schema) {
293
579
  this.database = schema.database;
@@ -352,9 +638,9 @@ var ResponseValidator = class _ResponseValidator {
352
638
  return { validator: "any" };
353
639
  }
354
640
  getTypeFromTable(selector, name) {
355
- const path4 = selector.split(".");
356
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { validator: "any", isOptional: false };
357
- const tableName = path4.length == 2 ? path4[0] : name, columnName = path4.length == 2 ? path4[1] : path4[0];
641
+ const path5 = selector.split(".");
642
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { validator: "any", isOptional: false };
643
+ const tableName = path5.length == 2 ? path5[0] : name, columnName = path5.length == 2 ? path5[1] : path5[0];
358
644
  const table = this.database.find((t) => t.name == tableName);
359
645
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
360
646
  if (!table || !column) return { validator: "any", isOptional: false };
@@ -436,9 +722,7 @@ var ResponseValidator = class _ResponseValidator {
436
722
  }
437
723
  };
438
724
 
439
- // src/restura/apiGenerator.ts
440
- var import_core_utils = require("@redskytech/core-utils");
441
- var import_prettier = __toESM(require("prettier"));
725
+ // src/restura/generators/apiGenerator.ts
442
726
  var ApiTree = class _ApiTree {
443
727
  constructor(namespace, database) {
444
728
  this.database = database;
@@ -536,7 +820,7 @@ var ApiTree = class _ApiTree {
536
820
  break;
537
821
  }
538
822
  }
539
- return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
823
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
540
824
  }).join(";\n")}${import_core_utils.ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
541
825
  `;
542
826
  modelString += `}`;
@@ -550,30 +834,37 @@ var ApiTree = class _ApiTree {
550
834
  return `export type Res = CustomTypes.${route.responseType}[]`;
551
835
  else return `export type Res = CustomTypes.${route.responseType}`;
552
836
  }
553
- return `export interface Res ${this.getFields(route.response)}`;
837
+ return `export interface Res ${this.getFields(route.response, route.table, route.joins)}`;
554
838
  }
555
- getFields(fields) {
556
- const nameFields = fields.map((f) => this.getNameAndType(f));
839
+ getFields(fields, routeBaseTable, joins) {
840
+ const nameFields = fields.map((f) => this.getNameAndType(f, routeBaseTable, joins));
557
841
  const nested = `{
558
842
  ${nameFields.join(";\n ")}${import_core_utils.ObjectUtils.isArrayWithData(nameFields) ? ";" : ""}
559
843
  }`;
560
844
  return nested;
561
845
  }
562
- getNameAndType(p) {
563
- let responseType = "any", optional = false, array = false;
846
+ getNameAndType(p, routeBaseTable, joins) {
847
+ let responseType = "any", isNullable = false, array = false;
564
848
  if (p.selector) {
565
- ({ responseType, optional } = this.getTypeFromTable(p.selector, p.name));
849
+ ({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
850
+ const selectorKey = p.selector.split(".")[0];
851
+ if (selectorKey !== routeBaseTable) {
852
+ const join = joins.find((j) => j.alias === selectorKey);
853
+ if (join && join.type !== "INNER") {
854
+ isNullable = true;
855
+ }
856
+ }
566
857
  } else if (p.subquery) {
567
- responseType = this.getFields(p.subquery.properties);
858
+ responseType = this.getFields(p.subquery.properties, p.subquery.table, p.subquery.joins);
568
859
  array = true;
569
860
  }
570
- return `${p.name}${optional ? "?" : ""}:${responseType}${array ? "[]" : ""}`;
861
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
571
862
  }
572
863
  getTypeFromTable(selector, name) {
573
- const path4 = selector.split(".");
574
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", optional: false };
575
- let tableName = path4.length == 2 ? path4[0] : name;
576
- const columnName = path4.length == 2 ? path4[1] : path4[0];
864
+ const path5 = selector.split(".");
865
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { responseType: "any", isNullable: false };
866
+ let tableName = path5.length == 2 ? path5[0] : name;
867
+ const columnName = path5.length == 2 ? path5[1] : path5[0];
577
868
  let table = this.database.find((t) => t.name == tableName);
578
869
  if (!table && tableName.includes("_")) {
579
870
  const tableAliasSplit = tableName.split("_");
@@ -581,18 +872,19 @@ var ApiTree = class _ApiTree {
581
872
  table = this.database.find((t) => t.name == tableName);
582
873
  }
583
874
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
584
- if (!table || !column) return { responseType: "any", optional: false };
875
+ if (!table || !column) return { responseType: "any", isNullable: false };
585
876
  return {
586
877
  responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
587
- optional: column.roles.length > 0 || column.isNullable
878
+ isNullable: column.roles.length > 0 || column.isNullable
588
879
  };
589
880
  }
590
881
  };
591
- function pathToNamespaces(path4) {
592
- return path4.split("/").map((e) => import_core_utils.StringUtils.toPascalCasing(e)).filter((e) => e);
882
+ function pathToNamespaces(path5) {
883
+ return path5.split("/").map((e) => import_core_utils.StringUtils.toPascalCasing(e)).filter((e) => e);
593
884
  }
594
- function apiGenerator(schema, schemaHash) {
595
- let apiString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/`;
885
+ function apiGenerator(schema) {
886
+ let apiString = `/** Auto generated file. DO NOT MODIFY **/
887
+ `;
596
888
  const rootNamespace = ApiTree.createRootNode(schema.database);
597
889
  for (const endpoint of schema.endpoints) {
598
890
  const endpointNamespaces = pathToNamespaces(endpoint.baseUrl);
@@ -607,7 +899,7 @@ function apiGenerator(schema, schemaHash) {
607
899
  apiString += `
608
900
 
609
901
  declare namespace CustomTypes {
610
- ${schema.customTypes}
902
+ ${schema.customTypes.join("\n")}
611
903
  }`;
612
904
  }
613
905
  return import_prettier.default.format(apiString, __spreadValues({
@@ -622,79 +914,32 @@ function apiGenerator(schema, schemaHash) {
622
914
  }));
623
915
  }
624
916
 
625
- // src/restura/customApiFactory.ts
626
- var import_fs = __toESM(require("fs"));
627
- var import_path = __toESM(require("path"));
628
- var CustomApiFactory = class {
629
- constructor() {
630
- this.customApis = {};
631
- }
632
- async loadApiFiles(baseFolderPath) {
633
- const apiVersions = ["v1"];
634
- for (const apiVersion of apiVersions) {
635
- const apiVersionFolderPath = import_path.default.join(baseFolderPath, apiVersion);
636
- if (!import_fs.default.existsSync(apiVersionFolderPath)) continue;
637
- await this.addDirectory(apiVersionFolderPath, apiVersion);
638
- }
639
- }
640
- getCustomApi(customApiName) {
641
- return this.customApis[customApiName];
642
- }
643
- async addDirectory(directoryPath, apiVersion) {
644
- const entries = import_fs.default.readdirSync(directoryPath, {
645
- withFileTypes: true
646
- });
647
- for (const entry of entries) {
648
- if (entry.isFile()) {
649
- if (entry.name.endsWith(`.api.${apiVersion}.js`) === false) continue;
650
- try {
651
- const importPath = `${import_path.default.join(directoryPath, entry.name)}`;
652
- const ApiImport = await import(importPath);
653
- const customApiClass = new ApiImport.default();
654
- logger.info(`Registering custom API: ${ApiImport.default.name}`);
655
- this.bindMethodsToInstance(customApiClass);
656
- this.customApis[ApiImport.default.name] = customApiClass;
657
- } catch (e) {
658
- console.error(e);
659
- }
660
- }
661
- }
662
- }
663
- bindMethodsToInstance(instance) {
664
- const proto = Object.getPrototypeOf(instance);
665
- Object.getOwnPropertyNames(proto).forEach((key) => {
666
- const property = instance[key];
667
- if (typeof property === "function" && key !== "constructor") {
668
- instance[key] = property.bind(instance);
669
- }
670
- });
671
- }
672
- };
673
- var customApiFactory = new CustomApiFactory();
674
- var customApiFactory_default = customApiFactory;
675
-
676
- // src/restura/customTypeValidationGenerator.ts
917
+ // src/restura/generators/customTypeValidationGenerator.ts
677
918
  var import_fs2 = __toESM(require("fs"));
678
- var TJS = __toESM(require("typescript-json-schema"));
679
919
  var import_path2 = __toESM(require("path"));
680
920
  var import_tmp = __toESM(require("tmp"));
681
- var process2 = __toESM(require("process"));
921
+ var TJS = __toESM(require("typescript-json-schema"));
682
922
  function customTypeValidationGenerator(currentSchema) {
683
923
  const schemaObject = {};
684
- const customInterfaceNames = currentSchema.customTypes.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
924
+ const customInterfaceNames = currentSchema.customTypes.map((customType) => {
925
+ const matches = customType.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
926
+ if (matches && matches.length > 0) return matches[0];
927
+ return "";
928
+ }).filter(Boolean);
685
929
  if (!customInterfaceNames) return {};
686
930
  const temporaryFile = import_tmp.default.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
687
- import_fs2.default.writeFileSync(temporaryFile.name, currentSchema.customTypes);
931
+ import_fs2.default.writeFileSync(temporaryFile.name, currentSchema.customTypes.join("\n"));
688
932
  const compilerOptions = {
689
933
  strictNullChecks: true,
690
934
  skipLibCheck: true
935
+ // Needed if we are processing ES modules
691
936
  };
692
937
  const program = TJS.getProgramFromFiles(
693
938
  [
694
939
  (0, import_path2.resolve)(temporaryFile.name),
695
- // find a way to remove
696
- import_path2.default.join(process2.cwd(), "src/@types/models.d.ts"),
697
- import_path2.default.join(process2.cwd(), "src/@types/api.d.ts")
940
+ import_path2.default.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"),
941
+ import_path2.default.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"),
942
+ import_path2.default.join(restura.resturaConfig.generatedTypesPath, "api.d.ts")
698
943
  ],
699
944
  compilerOptions
700
945
  );
@@ -708,6 +953,61 @@ function customTypeValidationGenerator(currentSchema) {
708
953
  return schemaObject;
709
954
  }
710
955
 
956
+ // src/restura/generators/modelGenerator.ts
957
+ var import_core_utils2 = require("@redskytech/core-utils");
958
+ var import_prettier2 = __toESM(require("prettier"));
959
+ function modelGenerator(schema) {
960
+ let modelString = `/** Auto generated file. DO NOT MODIFY **/
961
+
962
+ `;
963
+ modelString += `declare namespace Model {
964
+ `;
965
+ for (const table of schema.database) {
966
+ modelString += convertTable(table);
967
+ }
968
+ modelString += `}`;
969
+ return import_prettier2.default.format(modelString, __spreadValues({
970
+ parser: "typescript"
971
+ }, {
972
+ trailingComma: "none",
973
+ tabWidth: 4,
974
+ useTabs: true,
975
+ endOfLine: "lf",
976
+ printWidth: 120,
977
+ singleQuote: true
978
+ }));
979
+ }
980
+ function convertTable(table) {
981
+ let modelString = ` export interface ${import_core_utils2.StringUtils.capitalizeFirst(table.name)} {
982
+ `;
983
+ for (const column of table.columns) {
984
+ modelString += ` ${column.name}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)}${column.isNullable ? " | null" : ""};
985
+ `;
986
+ }
987
+ modelString += ` }
988
+ `;
989
+ return modelString;
990
+ }
991
+
992
+ // src/restura/generators/resturaGlobalTypesGenerator.ts
993
+ function resturaGlobalTypesGenerator() {
994
+ return `/** Auto generated file. DO NOT MODIFY **/
995
+ /** This file contains types that may be used in the CustomTypes of Restura **/
996
+ /** For example export interface MyPagedQuery extends Restura.PageQuery { } **/
997
+
998
+ declare namespace Restura {
999
+ export type StandardOrderTypes = 'ASC' | 'DESC' | 'RAND' | 'NONE';
1000
+ export interface PageQuery {
1001
+ page?: number;
1002
+ perPage?: number;
1003
+ sortBy?: string;
1004
+ sortOrder?: StandardOrderTypes;
1005
+ filter?: string;
1006
+ }
1007
+ }
1008
+ `;
1009
+ }
1010
+
711
1011
  // src/restura/middleware/addApiResponseFunctions.ts
712
1012
  function addApiResponseFunctions(req, res, next) {
713
1013
  res.sendData = function(data, statusCode = 200) {
@@ -740,16 +1040,41 @@ function addApiResponseFunctions(req, res, next) {
740
1040
  function authenticateUser(applicationAuthenticateHandler) {
741
1041
  return (req, res, next) => {
742
1042
  applicationAuthenticateHandler(req, res, (userDetails) => {
743
- req.requesterDetails = __spreadValues(__spreadValues({}, req.requesterDetails), userDetails);
1043
+ req.requesterDetails = __spreadValues({ host: req.hostname, ipAddress: req.ip || "" }, userDetails);
744
1044
  next();
745
1045
  });
746
1046
  };
747
1047
  }
748
1048
 
749
- // src/restura/restura.schema.ts
1049
+ // src/restura/middleware/getMulterUpload.ts
1050
+ var import_multer = __toESM(require("multer"));
1051
+ var os = __toESM(require("os"));
1052
+ var import_path3 = require("path");
1053
+ var OneHundredMB = 100 * 1024 * 1024;
1054
+ var commonUpload = null;
1055
+ var getMulterUpload = (directory) => {
1056
+ if (commonUpload) return commonUpload;
1057
+ const storage = import_multer.default.diskStorage({
1058
+ destination: directory || os.tmpdir(),
1059
+ filename: function(request, file, cb) {
1060
+ const extension = (0, import_path3.extname)(file.originalname);
1061
+ const uniqueName = Date.now() + "-" + Math.round(Math.random() * 1e3);
1062
+ cb(null, `${uniqueName}${extension}`);
1063
+ }
1064
+ });
1065
+ commonUpload = (0, import_multer.default)({
1066
+ storage,
1067
+ limits: {
1068
+ fileSize: OneHundredMB
1069
+ }
1070
+ });
1071
+ return commonUpload;
1072
+ };
1073
+
1074
+ // src/restura/schemas/resturaSchema.ts
750
1075
  var import_zod3 = require("zod");
751
1076
 
752
- // src/restura/types/validation.types.ts
1077
+ // src/restura/schemas/validatorDataSchema.ts
753
1078
  var import_zod2 = require("zod");
754
1079
  var validatorDataSchemeValue = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.number(), import_zod2.z.array(import_zod2.z.number())]);
755
1080
  var validatorDataSchema = import_zod2.z.object({
@@ -757,7 +1082,7 @@ var validatorDataSchema = import_zod2.z.object({
757
1082
  value: validatorDataSchemeValue
758
1083
  }).strict();
759
1084
 
760
- // src/restura/restura.schema.ts
1085
+ // src/restura/schemas/resturaSchema.ts
761
1086
  var orderBySchema = import_zod3.z.object({
762
1087
  columnName: import_zod3.z.string(),
763
1088
  order: import_zod3.z.enum(["ASC", "DESC"]),
@@ -770,7 +1095,7 @@ var groupBySchema = import_zod3.z.object({
770
1095
  var whereDataSchema = import_zod3.z.object({
771
1096
  tableName: import_zod3.z.string().optional(),
772
1097
  columnName: import_zod3.z.string().optional(),
773
- operator: import_zod3.z.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH"]).optional(),
1098
+ operator: import_zod3.z.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH", "IS", "IS NOT"]).optional(),
774
1099
  value: import_zod3.z.string().or(import_zod3.z.number()).optional(),
775
1100
  custom: import_zod3.z.string().optional(),
776
1101
  conjunction: import_zod3.z.enum(["AND", "OR"]).optional()
@@ -790,6 +1115,7 @@ var joinDataSchema = import_zod3.z.object({
790
1115
  var requestDataSchema = import_zod3.z.object({
791
1116
  name: import_zod3.z.string(),
792
1117
  required: import_zod3.z.boolean(),
1118
+ isNullable: import_zod3.z.boolean().optional(),
793
1119
  validator: import_zod3.z.array(validatorDataSchema)
794
1120
  }).strict();
795
1121
  var responseDataSchema = import_zod3.z.object({
@@ -875,6 +1201,12 @@ var postgresColumnDateTypesSchema = import_zod3.z.enum([
875
1201
  "INTERVAL"
876
1202
  // time span
877
1203
  ]);
1204
+ var postgresColumnJsonTypesSchema = import_zod3.z.enum([
1205
+ "JSON",
1206
+ // stores JSON data as raw text
1207
+ "JSONB"
1208
+ // stores JSON data in a binary format, optimized for query performance
1209
+ ]);
878
1210
  var mariaDbColumnNumericTypesSchema = import_zod3.z.enum([
879
1211
  "BOOLEAN",
880
1212
  // 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
@@ -937,6 +1269,7 @@ var columnDataSchema = import_zod3.z.object({
937
1269
  postgresColumnNumericTypesSchema,
938
1270
  postgresColumnStringTypesSchema,
939
1271
  postgresColumnDateTypesSchema,
1272
+ postgresColumnJsonTypesSchema,
940
1273
  mariaDbColumnNumericTypesSchema,
941
1274
  mariaDbColumnStringTypesSchema,
942
1275
  mariaDbColumnDateTypesSchema
@@ -988,7 +1321,8 @@ var tableDataSchema = import_zod3.z.object({
988
1321
  indexes: import_zod3.z.array(indexDataSchema),
989
1322
  foreignKeys: import_zod3.z.array(foreignKeyDataSchema),
990
1323
  checkConstraints: import_zod3.z.array(checkConstraintDataSchema),
991
- roles: import_zod3.z.array(import_zod3.z.string())
1324
+ roles: import_zod3.z.array(import_zod3.z.string()),
1325
+ notify: import_zod3.z.union([import_zod3.z.literal("ALL"), import_zod3.z.array(import_zod3.z.string())]).optional()
992
1326
  }).strict();
993
1327
  var endpointDataSchema = import_zod3.z.object({
994
1328
  name: import_zod3.z.string(),
@@ -996,16 +1330,16 @@ var endpointDataSchema = import_zod3.z.object({
996
1330
  baseUrl: import_zod3.z.string(),
997
1331
  routes: import_zod3.z.array(import_zod3.z.union([standardRouteSchema, customRouteSchema]))
998
1332
  }).strict();
999
- var resturaZodSchema = import_zod3.z.object({
1333
+ var resturaSchema = import_zod3.z.object({
1000
1334
  database: import_zod3.z.array(tableDataSchema),
1001
1335
  endpoints: import_zod3.z.array(endpointDataSchema),
1002
1336
  globalParams: import_zod3.z.array(import_zod3.z.string()),
1003
1337
  roles: import_zod3.z.array(import_zod3.z.string()),
1004
- customTypes: import_zod3.z.string()
1338
+ customTypes: import_zod3.z.array(import_zod3.z.string())
1005
1339
  }).strict();
1006
1340
  async function isSchemaValid(schemaToCheck) {
1007
1341
  try {
1008
- resturaZodSchema.parse(schemaToCheck);
1342
+ resturaSchema.parse(schemaToCheck);
1009
1343
  return true;
1010
1344
  } catch (error) {
1011
1345
  logger.error(error);
@@ -1013,12 +1347,12 @@ async function isSchemaValid(schemaToCheck) {
1013
1347
  }
1014
1348
  }
1015
1349
 
1016
- // src/restura/validateRequestParams.ts
1017
- var import_core_utils2 = require("@redskytech/core-utils");
1350
+ // src/restura/validators/requestValidator.ts
1351
+ var import_core_utils3 = require("@redskytech/core-utils");
1018
1352
  var import_jsonschema = __toESM(require("jsonschema"));
1019
1353
  var import_zod4 = require("zod");
1020
1354
 
1021
- // src/restura/utils/addQuotesToStrings.ts
1355
+ // src/restura/utils/utils.ts
1022
1356
  function addQuotesToStrings(variable) {
1023
1357
  if (typeof variable === "string") {
1024
1358
  return `'${variable}'`;
@@ -1029,9 +1363,20 @@ function addQuotesToStrings(variable) {
1029
1363
  return variable;
1030
1364
  }
1031
1365
  }
1366
+ function sortObjectKeysAlphabetically(obj) {
1367
+ if (Array.isArray(obj)) {
1368
+ return obj.map(sortObjectKeysAlphabetically);
1369
+ } else if (obj !== null && typeof obj === "object") {
1370
+ return Object.keys(obj).sort().reduce((sorted, key) => {
1371
+ sorted[key] = sortObjectKeysAlphabetically(obj[key]);
1372
+ return sorted;
1373
+ }, {});
1374
+ }
1375
+ return obj;
1376
+ }
1032
1377
 
1033
- // src/restura/validateRequestParams.ts
1034
- function validateRequestParams(req, routeData, validationSchema) {
1378
+ // src/restura/validators/requestValidator.ts
1379
+ function requestValidator(req, routeData, validationSchema) {
1035
1380
  const requestData = getRequestData(req);
1036
1381
  req.data = requestData;
1037
1382
  if (routeData.request === void 0) {
@@ -1065,6 +1410,7 @@ function validateRequestParams(req, routeData, validationSchema) {
1065
1410
  });
1066
1411
  }
1067
1412
  function validateRequestSingleParam(requestValue, requestParam) {
1413
+ if (requestParam.isNullable && requestValue === null) return;
1068
1414
  requestParam.validator.forEach((validator) => {
1069
1415
  switch (validator.type) {
1070
1416
  case "TYPE_CHECK":
@@ -1152,7 +1498,7 @@ function performMaxCheck(requestValue, validator, requestParamName) {
1152
1498
  );
1153
1499
  }
1154
1500
  function performOneOfCheck(requestValue, validator, requestParamName) {
1155
- if (!import_core_utils2.ObjectUtils.isArrayWithData(validator.value))
1501
+ if (!import_core_utils3.ObjectUtils.isArrayWithData(validator.value))
1156
1502
  throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not of type array`);
1157
1503
  if (typeof requestValue === "object")
1158
1504
  throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not of type string or number`);
@@ -1181,13 +1527,19 @@ function getRequestData(req) {
1181
1527
  if (isNaN(Number(value))) continue;
1182
1528
  attrList.push(Number(value));
1183
1529
  }
1184
- if (import_core_utils2.ObjectUtils.isArrayWithData(attrList)) {
1530
+ if (import_core_utils3.ObjectUtils.isArrayWithData(attrList)) {
1185
1531
  bodyData[attr] = attrList;
1186
1532
  }
1187
1533
  } else {
1188
- bodyData[attr] = import_core_utils2.ObjectUtils.safeParse(bodyData[attr]);
1189
- if (isNaN(Number(bodyData[attr]))) continue;
1190
- bodyData[attr] = Number(bodyData[attr]);
1534
+ if (bodyData[attr] === "true") {
1535
+ bodyData[attr] = true;
1536
+ } else if (bodyData[attr] === "false") {
1537
+ bodyData[attr] = false;
1538
+ } else {
1539
+ bodyData[attr] = import_core_utils3.ObjectUtils.safeParse(bodyData[attr]);
1540
+ if (isNaN(Number(bodyData[attr]))) continue;
1541
+ bodyData[attr] = Number(bodyData[attr]);
1542
+ }
1191
1543
  }
1192
1544
  }
1193
1545
  }
@@ -1198,7 +1550,7 @@ function getRequestData(req) {
1198
1550
  async function schemaValidation(req, res, next) {
1199
1551
  req.data = getRequestData(req);
1200
1552
  try {
1201
- resturaZodSchema.parse(req.data);
1553
+ resturaSchema.parse(req.data);
1202
1554
  next();
1203
1555
  } catch (error) {
1204
1556
  logger.error(error);
@@ -1206,43 +1558,34 @@ async function schemaValidation(req, res, next) {
1206
1558
  }
1207
1559
  }
1208
1560
 
1209
- // src/restura/modelGenerator.ts
1210
- var import_core_utils3 = require("@redskytech/core-utils");
1211
- var import_prettier2 = __toESM(require("prettier"));
1212
- function modelGenerator(schema, schemaHash) {
1213
- let modelString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/
1214
- `;
1215
- modelString += `declare namespace Model {
1216
- `;
1217
- for (const table of schema.database) {
1218
- modelString += convertTable(table);
1219
- }
1220
- modelString += `}`;
1221
- return import_prettier2.default.format(modelString, __spreadValues({
1222
- parser: "typescript"
1223
- }, {
1224
- trailingComma: "none",
1225
- tabWidth: 4,
1226
- useTabs: true,
1227
- endOfLine: "lf",
1228
- printWidth: 120,
1229
- singleQuote: true
1230
- }));
1231
- }
1232
- function convertTable(table) {
1233
- let modelString = ` export interface ${import_core_utils3.StringUtils.capitalizeFirst(table.name)} {
1234
- `;
1235
- for (const column of table.columns) {
1236
- modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
1237
- `;
1238
- }
1239
- modelString += ` }
1240
- `;
1241
- return modelString;
1242
- }
1561
+ // src/restura/schemas/resturaConfigSchema.ts
1562
+ var import_zod5 = require("zod");
1563
+ var _a;
1564
+ var isTsx = (_a = process.argv[1]) == null ? void 0 : _a.endsWith(".ts");
1565
+ var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
1566
+ var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
1567
+ var resturaConfigSchema = import_zod5.z.object({
1568
+ authToken: import_zod5.z.string().min(1, "Missing Restura Auth Token"),
1569
+ sendErrorStackTrace: import_zod5.z.boolean().default(false),
1570
+ schemaFilePath: import_zod5.z.string().default(process.cwd() + "/restura.schema.json"),
1571
+ customApiFolderPath: import_zod5.z.string().default(process.cwd() + customApiFolderPath),
1572
+ generatedTypesPath: import_zod5.z.string().default(process.cwd() + "/src/@types"),
1573
+ fileTempCachePath: import_zod5.z.string().optional()
1574
+ });
1243
1575
 
1244
1576
  // src/restura/sql/PsqlEngine.ts
1245
1577
  var import_core_utils5 = require("@redskytech/core-utils");
1578
+ var import_pg_diff_sync = __toESM(require("@wmfs/pg-diff-sync"));
1579
+ var import_pg_info = __toESM(require("@wmfs/pg-info"));
1580
+ var import_pg2 = __toESM(require("pg"));
1581
+
1582
+ // src/restura/sql/PsqlPool.ts
1583
+ var import_pg = __toESM(require("pg"));
1584
+
1585
+ // src/restura/sql/PsqlConnection.ts
1586
+ var import_crypto = __toESM(require("crypto"));
1587
+ var import_pg_format2 = __toESM(require("pg-format"));
1588
+ var import_sql_formatter = require("sql-formatter");
1246
1589
 
1247
1590
  // src/restura/sql/PsqlUtils.ts
1248
1591
  var import_pg_format = __toESM(require("pg-format"));
@@ -1252,16 +1595,33 @@ function escapeColumnName(columnName) {
1252
1595
  }
1253
1596
  function questionMarksToOrderedParams(query) {
1254
1597
  let count = 1;
1255
- return query.replace(/'\?'|\?/g, () => `$${count++}`);
1598
+ let inSingleQuote = false;
1599
+ let inDoubleQuote = false;
1600
+ return query.replace(/('|"|\?)/g, (char) => {
1601
+ if (char === "'") {
1602
+ inSingleQuote = !inSingleQuote && !inDoubleQuote;
1603
+ return char;
1604
+ }
1605
+ if (char === '"') {
1606
+ inDoubleQuote = !inDoubleQuote && !inSingleQuote;
1607
+ return char;
1608
+ }
1609
+ if (char === "?" && !inSingleQuote && !inDoubleQuote) {
1610
+ return `$${count++}`;
1611
+ }
1612
+ return char;
1613
+ });
1256
1614
  }
1257
1615
  function insertObjectQuery(table, obj) {
1258
1616
  const keys = Object.keys(obj);
1259
1617
  const params = Object.values(obj);
1260
1618
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1261
1619
  const values = params.map((value) => SQL`${value}`).join(", ");
1262
- const query = `INSERT INTO "${table}" (${columns})
1620
+ let query = `
1621
+ INSERT INTO "${table}" (${columns})
1263
1622
  VALUES (${values})
1264
1623
  RETURNING *`;
1624
+ query = query.replace(/'(\?)'/, "?");
1265
1625
  return query;
1266
1626
  }
1267
1627
  function updateObjectQuery(table, obj, whereStatement) {
@@ -1269,25 +1629,132 @@ function updateObjectQuery(table, obj, whereStatement) {
1269
1629
  for (const i in obj) {
1270
1630
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1271
1631
  }
1272
- return `UPDATE ${escapeColumnName(table)}
1273
- SET ${setArray.join(", ")} ${whereStatement}
1274
- RETURNING *`;
1275
- }
1276
- function isValueNumber2(value) {
1277
- return !isNaN(Number(value));
1278
- }
1279
- function SQL(strings, ...values) {
1280
- let query = strings[0];
1281
- values.forEach((value, index) => {
1282
- if (isValueNumber2(value)) {
1283
- query += value;
1632
+ return `
1633
+ UPDATE ${escapeColumnName(table)}
1634
+ SET ${setArray.join(", ")} ${whereStatement}
1635
+ RETURNING *`;
1636
+ }
1637
+ function isValueNumber2(value) {
1638
+ return !isNaN(Number(value));
1639
+ }
1640
+ function SQL(strings, ...values) {
1641
+ let query = strings[0];
1642
+ values.forEach((value, index) => {
1643
+ if (typeof value === "boolean") {
1644
+ query += value;
1645
+ } else if (typeof value === "number") {
1646
+ query += value;
1647
+ } else if (Array.isArray(value)) {
1648
+ query += import_pg_format.default.literal(JSON.stringify(value)) + "::jsonb";
1649
+ } else {
1650
+ query += import_pg_format.default.literal(value);
1651
+ }
1652
+ query += strings[index + 1];
1653
+ });
1654
+ return query;
1655
+ }
1656
+
1657
+ // src/restura/sql/PsqlConnection.ts
1658
+ var PsqlConnection = class {
1659
+ constructor(instanceId) {
1660
+ this.instanceId = instanceId || import_crypto.default.randomUUID();
1661
+ }
1662
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1663
+ async queryOne(query, options, requesterDetails) {
1664
+ const formattedQuery = questionMarksToOrderedParams(query);
1665
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1666
+ this.logSqlStatement(formattedQuery, options, meta);
1667
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1668
+ `;
1669
+ const startTime = process.hrtime();
1670
+ try {
1671
+ const response = await this.query(queryMetadata + formattedQuery, options);
1672
+ this.logQueryDuration(startTime);
1673
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1674
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1675
+ return response.rows[0];
1676
+ } catch (error) {
1677
+ if (RsError.isRsError(error)) throw error;
1678
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1679
+ throw new RsError("DUPLICATE", error.message);
1680
+ }
1681
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1682
+ }
1683
+ }
1684
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1685
+ async runQuery(query, options, requesterDetails) {
1686
+ const formattedQuery = questionMarksToOrderedParams(query);
1687
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1688
+ this.logSqlStatement(formattedQuery, options, meta);
1689
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1690
+ `;
1691
+ const startTime = process.hrtime();
1692
+ try {
1693
+ const response = await this.query(queryMetadata + formattedQuery, options);
1694
+ this.logQueryDuration(startTime);
1695
+ return response.rows;
1696
+ } catch (error) {
1697
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1698
+ throw new RsError("DUPLICATE", error.message);
1699
+ }
1700
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1701
+ }
1702
+ }
1703
+ logQueryDuration(startTime) {
1704
+ if (logger.level === "silly") {
1705
+ const [seconds, nanoseconds] = process.hrtime(startTime);
1706
+ const duration = seconds * 1e3 + nanoseconds / 1e6;
1707
+ logger.silly(`Query duration: ${duration.toFixed(2)}ms`);
1708
+ }
1709
+ }
1710
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1711
+ if (logger.level !== "silly") return;
1712
+ let sqlStatement = "";
1713
+ if (options.length === 0) {
1714
+ sqlStatement = query;
1284
1715
  } else {
1285
- query += import_pg_format.default.literal(value);
1716
+ let stringIndex = 0;
1717
+ sqlStatement = query.replace(/\$\d+/g, () => {
1718
+ const value = options[stringIndex++];
1719
+ if (typeof value === "number") return value.toString();
1720
+ return import_pg_format2.default.literal(value);
1721
+ });
1286
1722
  }
1287
- query += strings[index + 1];
1288
- });
1289
- return query;
1290
- }
1723
+ const formattedSql = (0, import_sql_formatter.format)(sqlStatement, {
1724
+ language: "postgresql",
1725
+ linesBetweenQueries: 2,
1726
+ indentStyle: "standard",
1727
+ keywordCase: "upper",
1728
+ useTabs: true,
1729
+ tabWidth: 4
1730
+ });
1731
+ let initiator = "Anonymous";
1732
+ if ("userId" in queryMetadata && queryMetadata.userId)
1733
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1734
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1735
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1736
+ ${formattedSql}`);
1737
+ }
1738
+ };
1739
+
1740
+ // src/restura/sql/PsqlPool.ts
1741
+ var { Pool } = import_pg.default;
1742
+ var PsqlPool = class extends PsqlConnection {
1743
+ constructor(poolConfig) {
1744
+ super();
1745
+ this.poolConfig = poolConfig;
1746
+ this.pool = new Pool(poolConfig);
1747
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1748
+ logger.info("Connected to PostgreSQL database");
1749
+ }).catch((error) => {
1750
+ logger.error("Error connecting to database", error);
1751
+ process.exit(1);
1752
+ });
1753
+ }
1754
+ async query(query, values) {
1755
+ return this.pool.query(query, values);
1756
+ }
1757
+ };
1291
1758
 
1292
1759
  // src/restura/sql/SqlEngine.ts
1293
1760
  var import_core_utils4 = require("@redskytech/core-utils");
@@ -1355,11 +1822,11 @@ var SqlEngine = class {
1355
1822
  return returnValue;
1356
1823
  }
1357
1824
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1358
- var _a;
1825
+ var _a2;
1359
1826
  if (!routeData.request) return value;
1360
1827
  const data = req.data;
1361
1828
  if (typeof value === "string") {
1362
- (_a = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1829
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1363
1830
  const requestParam = routeData.request.find((item) => {
1364
1831
  return item.name === param.replace("$", "");
1365
1832
  });
@@ -1372,9 +1839,9 @@ var SqlEngine = class {
1372
1839
  return value;
1373
1840
  }
1374
1841
  replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1375
- var _a;
1842
+ var _a2;
1376
1843
  if (typeof value === "string") {
1377
- (_a = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1844
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1378
1845
  param = param.replace("#", "");
1379
1846
  const globalParamValue = req.requesterDetails[param];
1380
1847
  if (!globalParamValue)
@@ -1393,32 +1860,86 @@ var SqlEngine = class {
1393
1860
  // src/restura/sql/filterPsqlParser.ts
1394
1861
  var import_pegjs = __toESM(require("pegjs"));
1395
1862
  var filterSqlGrammar = `
1863
+ {
1864
+ // ported from pg-format but intentionally will add double quotes to every column
1865
+ function quoteSqlIdentity(value) {
1866
+ if (value === undefined || value === null) {
1867
+ throw new Error('SQL identifier cannot be null or undefined');
1868
+ } else if (value === false) {
1869
+ return '"f"';
1870
+ } else if (value === true) {
1871
+ return '"t"';
1872
+ } else if (value instanceof Date) {
1873
+ // return '"' + formatDate(value.toISOString()) + '"';
1874
+ } else if (value instanceof Buffer) {
1875
+ throw new Error('SQL identifier cannot be a buffer');
1876
+ } else if (Array.isArray(value) === true) {
1877
+ var temp = [];
1878
+ for (var i = 0; i < value.length; i++) {
1879
+ if (Array.isArray(value[i]) === true) {
1880
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1881
+ } else {
1882
+ // temp.push(quoteIdent(value[i]));
1883
+ }
1884
+ }
1885
+ return temp.toString();
1886
+ } else if (value === Object(value)) {
1887
+ throw new Error('SQL identifier cannot be an object');
1888
+ }
1889
+
1890
+ var ident = value.toString().slice(0); // create copy
1891
+
1892
+ // do not quote a valid, unquoted identifier
1893
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1894
+ // return ident;
1895
+ // }
1896
+
1897
+ var quoted = '"';
1898
+
1899
+ for (var i = 0; i < ident.length; i++) {
1900
+ var c = ident[i];
1901
+ if (c === '"') {
1902
+ quoted += c + c;
1903
+ } else {
1904
+ quoted += c;
1905
+ }
1906
+ }
1907
+
1908
+ quoted += '"';
1909
+
1910
+ return quoted;
1911
+ };
1912
+ }
1913
+
1396
1914
  start = expressionList
1397
1915
 
1916
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1917
+
1398
1918
  expressionList =
1399
- leftExpression:expression operator:operator rightExpression:expressionList
1919
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1400
1920
  { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1401
1921
  / expression
1402
1922
 
1403
1923
  expression =
1404
- negate:negate?"(" "column:" column:column ","? value:value? ","? type:type? ")"
1405
- {return \`\${negate? "!" : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1924
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1925
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1406
1926
  /
1407
- negate:negate?"("expression:expressionList")" { return \`\${negate? "!" : ""}(\${expression})\`; }
1927
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1408
1928
 
1409
1929
  negate = "!"
1410
1930
 
1411
1931
  operator = "and"i / "or"i
1412
1932
 
1413
1933
 
1414
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1934
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1415
1935
  /
1416
- text:text { return format.ident(text); }
1936
+ text:text { return quoteSqlIdentity(text); }
1417
1937
 
1418
1938
 
1419
- text = text:[a-z0-9-_:@]i+ { return text.join("");}
1939
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
1420
1940
 
1421
- type = "type:" type:typeString { return type; }
1941
+
1942
+ type = "type" _ ":" _ type:typeString { return type; }
1422
1943
  typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1423
1944
  text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1424
1945
  text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
@@ -1428,8 +1949,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
1428
1949
  text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1429
1950
  text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1430
1951
  text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1431
-
1432
- value = "value:" value:text { return value; }
1952
+
1953
+ value = "value" _ ":" value:text { return value; }
1954
+
1433
1955
 
1434
1956
  `;
1435
1957
  var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
@@ -1439,18 +1961,224 @@ var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
1439
1961
  var filterPsqlParser_default = filterPsqlParser;
1440
1962
 
1441
1963
  // src/restura/sql/PsqlEngine.ts
1964
+ var { Client, types } = import_pg2.default;
1965
+ var systemUser = {
1966
+ role: "",
1967
+ host: "",
1968
+ ipAddress: "",
1969
+ isSystemUser: true
1970
+ };
1442
1971
  var PsqlEngine = class extends SqlEngine {
1443
- constructor(psqlConnectionPool) {
1972
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1444
1973
  super();
1445
1974
  this.psqlConnectionPool = psqlConnectionPool;
1975
+ this.setupPgReturnTypes();
1976
+ if (shouldListenForDbTriggers) {
1977
+ this.setupTriggerListeners = this.listenForDbTriggers();
1978
+ }
1446
1979
  }
1447
- async diffDatabaseToSchema(schema) {
1448
- console.log(schema);
1449
- return Promise.resolve("");
1980
+ async close() {
1981
+ if (this.triggerClient) {
1982
+ await this.triggerClient.end();
1983
+ }
1984
+ }
1985
+ setupPgReturnTypes() {
1986
+ const TIMESTAMPTZ_OID = 1184;
1987
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1988
+ return val === null ? null : new Date(val).toISOString();
1989
+ });
1990
+ const BIGINT_OID = 20;
1991
+ types.setTypeParser(BIGINT_OID, (val) => {
1992
+ return val === null ? null : Number(val);
1993
+ });
1994
+ }
1995
+ async listenForDbTriggers() {
1996
+ this.triggerClient = new Client({
1997
+ user: this.psqlConnectionPool.poolConfig.user,
1998
+ host: this.psqlConnectionPool.poolConfig.host,
1999
+ database: this.psqlConnectionPool.poolConfig.database,
2000
+ password: this.psqlConnectionPool.poolConfig.password,
2001
+ port: this.psqlConnectionPool.poolConfig.port,
2002
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2003
+ });
2004
+ await this.triggerClient.connect();
2005
+ const promises = [];
2006
+ promises.push(this.triggerClient.query("LISTEN insert"));
2007
+ promises.push(this.triggerClient.query("LISTEN update"));
2008
+ promises.push(this.triggerClient.query("LISTEN delete"));
2009
+ await Promise.all(promises);
2010
+ this.triggerClient.on("notification", async (msg) => {
2011
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
2012
+ const payload = import_core_utils5.ObjectUtils.safeParse(msg.payload);
2013
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
2014
+ }
2015
+ });
2016
+ }
2017
+ async handleTrigger(payload, mutationType) {
2018
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
2019
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
2020
+ }
2021
+ }
2022
+ async createDatabaseFromSchema(schema, connection) {
2023
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
2024
+ await connection.runQuery(sqlFullStatement, [], systemUser);
2025
+ return sqlFullStatement;
1450
2026
  }
1451
2027
  generateDatabaseSchemaFromSchema(schema) {
1452
- console.log(schema);
1453
- return "";
2028
+ const sqlStatements = [];
2029
+ const indexes = [];
2030
+ const triggers = [];
2031
+ for (const table of schema.database) {
2032
+ if (table.notify) {
2033
+ triggers.push(this.createInsertTriggers(table.name, table.notify));
2034
+ triggers.push(this.createUpdateTrigger(table.name, table.notify));
2035
+ triggers.push(this.createDeleteTrigger(table.name, table.notify));
2036
+ }
2037
+ let sql = `CREATE TABLE "${table.name}"
2038
+ ( `;
2039
+ const tableColumns = [];
2040
+ for (const column of table.columns) {
2041
+ let columnSql = "";
2042
+ columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
2043
+ let value = column.value;
2044
+ if (column.type === "JSON") value = "";
2045
+ if (column.type === "JSONB") value = "";
2046
+ if (column.type === "DECIMAL" && value) {
2047
+ value = value.replace("-", ",").replace(/['"]/g, "");
2048
+ }
2049
+ if (value && column.type !== "ENUM") {
2050
+ columnSql += `(${value})`;
2051
+ } else if (column.length) columnSql += `(${column.length})`;
2052
+ if (column.isPrimary) {
2053
+ columnSql += " PRIMARY KEY ";
2054
+ }
2055
+ if (column.isUnique) {
2056
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
2057
+ }
2058
+ if (column.isNullable) columnSql += " NULL";
2059
+ else columnSql += " NOT NULL";
2060
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
2061
+ if (value && column.type === "ENUM") {
2062
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
2063
+ }
2064
+ tableColumns.push(columnSql);
2065
+ }
2066
+ sql += tableColumns.join(", \n");
2067
+ for (const index of table.indexes) {
2068
+ if (!index.isPrimaryKey) {
2069
+ let unique = " ";
2070
+ if (index.isUnique) unique = "UNIQUE ";
2071
+ indexes.push(
2072
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
2073
+ return `"${item}" ${index.order}`;
2074
+ }).join(", ")});`
2075
+ );
2076
+ }
2077
+ }
2078
+ sql += "\n);";
2079
+ sqlStatements.push(sql);
2080
+ }
2081
+ for (const table of schema.database) {
2082
+ if (!table.foreignKeys.length) continue;
2083
+ const sql = `ALTER TABLE "${table.name}" `;
2084
+ const constraints = [];
2085
+ for (const foreignKey of table.foreignKeys) {
2086
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2087
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2088
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2089
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2090
+ constraints.push(constraint);
2091
+ }
2092
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2093
+ }
2094
+ for (const table of schema.database) {
2095
+ if (!table.checkConstraints.length) continue;
2096
+ const sql = `ALTER TABLE "${table.name}" `;
2097
+ const constraints = [];
2098
+ for (const check of table.checkConstraints) {
2099
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2100
+ constraints.push(constraint);
2101
+ }
2102
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2103
+ }
2104
+ sqlStatements.push(indexes.join("\n"));
2105
+ sqlStatements.push(triggers.join("\n"));
2106
+ return sqlStatements.join("\n\n");
2107
+ }
2108
+ async getScratchPool() {
2109
+ var _a2, _b;
2110
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
2111
+ `SELECT *
2112
+ FROM pg_database
2113
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
2114
+ [],
2115
+ systemUser
2116
+ );
2117
+ if (scratchDbExists.length === 0) {
2118
+ await this.psqlConnectionPool.runQuery(
2119
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
2120
+ [],
2121
+ systemUser
2122
+ );
2123
+ }
2124
+ const scratchPool = new PsqlPool({
2125
+ host: this.psqlConnectionPool.poolConfig.host,
2126
+ port: this.psqlConnectionPool.poolConfig.port,
2127
+ user: this.psqlConnectionPool.poolConfig.user,
2128
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2129
+ password: this.psqlConnectionPool.poolConfig.password,
2130
+ max: this.psqlConnectionPool.poolConfig.max,
2131
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2132
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2133
+ });
2134
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2135
+ await scratchPool.runQuery(
2136
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2137
+ [],
2138
+ systemUser
2139
+ );
2140
+ const schemaComment = await this.psqlConnectionPool.runQuery(
2141
+ `SELECT pg_description.description
2142
+ FROM pg_description
2143
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2144
+ WHERE pg_namespace.nspname = 'public';`,
2145
+ [],
2146
+ systemUser
2147
+ );
2148
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2149
+ await scratchPool.runQuery(
2150
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
2151
+ [],
2152
+ systemUser
2153
+ );
2154
+ }
2155
+ return scratchPool;
2156
+ }
2157
+ async diffDatabaseToSchema(schema) {
2158
+ const scratchPool = await this.getScratchPool();
2159
+ await this.createDatabaseFromSchema(schema, scratchPool);
2160
+ const originalClient = new Client({
2161
+ database: this.psqlConnectionPool.poolConfig.database,
2162
+ user: this.psqlConnectionPool.poolConfig.user,
2163
+ password: this.psqlConnectionPool.poolConfig.password,
2164
+ host: this.psqlConnectionPool.poolConfig.host,
2165
+ port: this.psqlConnectionPool.poolConfig.port
2166
+ });
2167
+ const scratchClient = new Client({
2168
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2169
+ user: this.psqlConnectionPool.poolConfig.user,
2170
+ password: this.psqlConnectionPool.poolConfig.password,
2171
+ host: this.psqlConnectionPool.poolConfig.host,
2172
+ port: this.psqlConnectionPool.poolConfig.port
2173
+ });
2174
+ const promises = [originalClient.connect(), scratchClient.connect()];
2175
+ await Promise.all(promises);
2176
+ const infoPromises = [(0, import_pg_info.default)({ client: originalClient }), (0, import_pg_info.default)({ client: scratchClient })];
2177
+ const [info1, info2] = await Promise.all(infoPromises);
2178
+ const diff = (0, import_pg_diff_sync.default)(info1, info2);
2179
+ const endPromises = [originalClient.end(), scratchClient.end()];
2180
+ await Promise.all(endPromises);
2181
+ return diff.join("\n");
1454
2182
  }
1455
2183
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1456
2184
  if (!item.subquery) return "";
@@ -1464,8 +2192,7 @@ var PsqlEngine = class extends SqlEngine {
1464
2192
  )) {
1465
2193
  return "'[]'";
1466
2194
  }
1467
- return `COALESCE((
1468
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
2195
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1469
2196
  ${item.subquery.properties.map((nestedItem) => {
1470
2197
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1471
2198
  ...routeData.joins,
@@ -1474,7 +2201,7 @@ var PsqlEngine = class extends SqlEngine {
1474
2201
  return;
1475
2202
  }
1476
2203
  if (nestedItem.subquery) {
1477
- return `"${nestedItem.name}", ${this.createNestedSelect(
2204
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1478
2205
  // recursion
1479
2206
  req,
1480
2207
  schema,
@@ -1485,7 +2212,7 @@ var PsqlEngine = class extends SqlEngine {
1485
2212
  )}`;
1486
2213
  }
1487
2214
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1488
- }).filter(Boolean).join(",")}
2215
+ }).filter(Boolean).join(", ")}
1489
2216
  ))
1490
2217
  FROM
1491
2218
  "${item.subquery.table}"
@@ -1500,16 +2227,19 @@ var PsqlEngine = class extends SqlEngine {
1500
2227
  parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
1501
2228
  });
1502
2229
  const query = insertObjectQuery(routeData.table, __spreadValues(__spreadValues({}, req.data), parameterObj));
1503
- const createdItem = await this.psqlConnectionPool.queryOne(query, sqlParams, req.requesterDetails);
1504
- const insertId = createdItem == null ? void 0 : createdItem.id;
1505
- const whereData = [
1506
- {
1507
- tableName: routeData.table,
1508
- value: insertId,
1509
- columnName: "id",
1510
- operator: "="
1511
- }
1512
- ];
2230
+ const createdItem = await this.psqlConnectionPool.queryOne(
2231
+ query,
2232
+ sqlParams,
2233
+ req.requesterDetails
2234
+ );
2235
+ const insertId = createdItem.id;
2236
+ const whereId = {
2237
+ tableName: routeData.table,
2238
+ value: insertId,
2239
+ columnName: "id",
2240
+ operator: "="
2241
+ };
2242
+ const whereData = [whereId];
1513
2243
  req.data = { id: insertId };
1514
2244
  return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
1515
2245
  }
@@ -1528,7 +2258,9 @@ var PsqlEngine = class extends SqlEngine {
1528
2258
  let selectStatement = "SELECT \n";
1529
2259
  selectStatement += ` ${selectColumns.map((item) => {
1530
2260
  if (item.subquery) {
1531
- return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${item.name}`;
2261
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2262
+ item.name
2263
+ )}`;
1532
2264
  }
1533
2265
  return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
1534
2266
  }).join(",\n ")}
@@ -1561,29 +2293,31 @@ var PsqlEngine = class extends SqlEngine {
1561
2293
  );
1562
2294
  } else if (routeData.type === "PAGED") {
1563
2295
  const data = req.data;
1564
- const pageResults = await this.psqlConnectionPool.runQuery(
1565
- `${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT ? OFFSET ?;SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
1566
- ${sqlStatement};`,
1567
- [
1568
- ...sqlParams,
1569
- data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER,
1570
- (data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER,
1571
- ...sqlParams
1572
- ],
2296
+ const pagePromise = this.psqlConnectionPool.runQuery(
2297
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2298
+ sqlParams,
1573
2299
  req.requesterDetails
1574
2300
  );
2301
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2302
+ ${sqlStatement};`;
2303
+ const totalPromise = this.psqlConnectionPool.runQuery(
2304
+ totalQuery,
2305
+ sqlParams,
2306
+ req.requesterDetails
2307
+ );
2308
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1575
2309
  let total = 0;
1576
- if (import_core_utils5.ObjectUtils.isArrayWithData(pageResults)) {
1577
- total = pageResults[1][0].total;
2310
+ if (import_core_utils5.ObjectUtils.isArrayWithData(totalResponse)) {
2311
+ total = totalResponse[0].total;
1578
2312
  }
1579
- return { data: pageResults[0], total };
2313
+ return { data: pageResults, total };
1580
2314
  } else {
1581
2315
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1582
2316
  }
1583
2317
  }
1584
2318
  async executeUpdateRequest(req, routeData, schema) {
1585
2319
  const sqlParams = [];
1586
- const _a = req.body, { id } = _a, bodyNoId = __objRest(_a, ["id"]);
2320
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
1587
2321
  const table = schema.database.find((item) => {
1588
2322
  return item.name === routeData.table;
1589
2323
  });
@@ -1594,10 +2328,10 @@ ${sqlStatement};`,
1594
2328
  for (const assignment of routeData.assignments) {
1595
2329
  const column = table.columns.find((column2) => column2.name === assignment.name);
1596
2330
  if (!column) continue;
1597
- const assignmentWithPrefix = escapeColumnName(`${routeData.table}.${assignment.name}`);
2331
+ const assignmentEscaped = escapeColumnName(assignment.name);
1598
2332
  if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
1599
- bodyNoId[assignmentWithPrefix] = Number(assignment.value);
1600
- else bodyNoId[assignmentWithPrefix] = assignment.value;
2333
+ bodyNoId[assignmentEscaped] = Number(assignment.value);
2334
+ else bodyNoId[assignmentEscaped] = assignment.value;
1601
2335
  }
1602
2336
  const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1603
2337
  const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
@@ -1615,10 +2349,12 @@ ${sqlStatement};`,
1615
2349
  req.requesterDetails.role,
1616
2350
  sqlParams
1617
2351
  );
1618
- let deleteStatement = `DELETE
1619
- FROM "${routeData.table}" ${joinStatement}`;
1620
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1621
- deleteStatement += ";";
2352
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2353
+ if (whereClause.replace(/\s/g, "") === "") {
2354
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2355
+ }
2356
+ const deleteStatement = `
2357
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1622
2358
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1623
2359
  return true;
1624
2360
  }
@@ -1629,7 +2365,7 @@ ${sqlStatement};`,
1629
2365
  throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
1630
2366
  if (item.custom) {
1631
2367
  const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
1632
- joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)} ON ${customReplaced}
2368
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON ${customReplaced}
1633
2369
  `;
1634
2370
  } else {
1635
2371
  joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON "${baseTable}"."${item.localColumnName}" = ${escapeColumnName(item.alias ? item.alias : item.table)}.${escapeColumnName(
@@ -1681,38 +2417,37 @@ ${sqlStatement};`,
1681
2417
  );
1682
2418
  let operator = item.operator;
1683
2419
  if (operator === "LIKE") {
1684
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2420
+ item.value = `'%${item.value}%'`;
1685
2421
  } else if (operator === "STARTS WITH") {
1686
2422
  operator = "LIKE";
1687
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2423
+ item.value = `'${item.value}%'`;
1688
2424
  } else if (operator === "ENDS WITH") {
1689
2425
  operator = "LIKE";
1690
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2426
+ item.value = `'%${item.value}'`;
1691
2427
  }
1692
2428
  const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
1693
- const escapedValue = SQL`${replacedValue}`;
1694
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
2429
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
1695
2430
  `;
1696
2431
  });
1697
2432
  const data = req.data;
1698
2433
  if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
1699
2434
  let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1700
- var _a;
2435
+ var _a2;
1701
2436
  const requestParam = routeData.request.find((item) => {
1702
2437
  return item.name === value.replace("$", "");
1703
2438
  });
1704
2439
  if (!requestParam)
1705
2440
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1706
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2441
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1707
2442
  });
1708
2443
  statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1709
- var _a;
2444
+ var _a2;
1710
2445
  const requestParam = routeData.request.find((item) => {
1711
2446
  return item.name === value.replace("#", "");
1712
2447
  });
1713
2448
  if (!requestParam)
1714
2449
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1715
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2450
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1716
2451
  });
1717
2452
  statement = filterPsqlParser_default.parse(statement);
1718
2453
  if (whereClause.startsWith("WHERE")) {
@@ -1725,82 +2460,259 @@ ${sqlStatement};`,
1725
2460
  }
1726
2461
  return whereClause;
1727
2462
  }
1728
- };
2463
+ createUpdateTrigger(tableName, notify) {
2464
+ if (!notify) return "";
2465
+ if (notify === "ALL") {
2466
+ return `
2467
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2468
+ RETURNS TRIGGER AS $$
2469
+ DECLARE
2470
+ query_metadata JSON;
2471
+ BEGIN
2472
+ SELECT INTO query_metadata
2473
+ (regexp_match(
2474
+ current_query(),
2475
+ '^--QUERY_METADATA\\(({.*})', 'n'
2476
+ ))[1]::json;
1729
2477
 
1730
- // src/restura/sql/PsqlPool.ts
1731
- var import_pg = __toESM(require("pg"));
1732
- var import_pg_format2 = __toESM(require("pg-format"));
1733
- var { Pool } = import_pg.default;
1734
- var PsqlPool = class {
1735
- constructor(poolConfig) {
1736
- this.poolConfig = poolConfig;
1737
- this.pool = new Pool(poolConfig);
1738
- this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1739
- logger.info("Connected to PostgreSQL database");
1740
- }).catch((error) => {
1741
- logger.error("Error connecting to database", error);
1742
- process.exit(1);
1743
- });
1744
- }
1745
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1746
- async queryOne(query, options, requesterDetails) {
1747
- const formattedQuery = questionMarksToOrderedParams(query);
1748
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1749
- try {
1750
- const response = await this.pool.query(formattedQuery, options);
1751
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1752
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1753
- return response.rows[0];
1754
- } catch (error) {
1755
- console.error(error, query, options);
1756
- if (RsError.isRsError(error)) throw error;
1757
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1758
- throw new RsError("DUPLICATE", error.message);
1759
- }
1760
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2478
+ PERFORM pg_notify(
2479
+ 'update',
2480
+ json_build_object(
2481
+ 'table', '${tableName}',
2482
+ 'queryMetadata', query_metadata,
2483
+ 'changedId', NEW.id,
2484
+ 'record', NEW,
2485
+ 'previousRecord', OLD
2486
+ )::text
2487
+ );
2488
+ RETURN NEW;
2489
+ END;
2490
+ $$ LANGUAGE plpgsql;
2491
+
2492
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2493
+ AFTER UPDATE ON "${tableName}"
2494
+ FOR EACH ROW
2495
+ EXECUTE FUNCTION notify_${tableName}_update();
2496
+ `;
1761
2497
  }
1762
- }
1763
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1764
- async runQuery(query, options, requesterDetails) {
1765
- const formattedQuery = questionMarksToOrderedParams(query);
1766
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1767
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1768
- console.log(queryUpdated, options);
1769
- try {
1770
- const response = await this.pool.query(formattedQuery, options);
1771
- return response.rows;
1772
- } catch (error) {
1773
- console.error(error, query, options);
1774
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1775
- throw new RsError("DUPLICATE", error.message);
1776
- }
1777
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2498
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2499
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2500
+ return `
2501
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2502
+ RETURNS TRIGGER AS $$
2503
+ DECLARE
2504
+ query_metadata JSON;
2505
+ BEGIN
2506
+ SELECT INTO query_metadata
2507
+ (regexp_match(
2508
+ current_query(),
2509
+ '^--QUERY_METADATA\\(({.*})', 'n'
2510
+ ))[1]::json;
2511
+
2512
+ PERFORM pg_notify(
2513
+ 'update',
2514
+ json_build_object(
2515
+ 'table', '${tableName}',
2516
+ 'queryMetadata', query_metadata,
2517
+ 'changedId', NEW.id,
2518
+ 'record', json_build_object(
2519
+ ${notifyColumnNewBuildString}
2520
+ ),
2521
+ 'previousRecord', json_build_object(
2522
+ ${notifyColumnOldBuildString}
2523
+ )
2524
+ )::text
2525
+ );
2526
+ RETURN NEW;
2527
+ END;
2528
+ $$ LANGUAGE plpgsql;
2529
+
2530
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2531
+ AFTER UPDATE ON "${tableName}"
2532
+ FOR EACH ROW
2533
+ EXECUTE FUNCTION notify_${tableName}_update();
2534
+ `;
2535
+ }
2536
+ createDeleteTrigger(tableName, notify) {
2537
+ if (!notify) return "";
2538
+ if (notify === "ALL") {
2539
+ return `
2540
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2541
+ RETURNS TRIGGER AS $$
2542
+ DECLARE
2543
+ query_metadata JSON;
2544
+ BEGIN
2545
+ SELECT INTO query_metadata
2546
+ (regexp_match(
2547
+ current_query(),
2548
+ '^--QUERY_METADATA\\(({.*})', 'n'
2549
+ ))[1]::json;
2550
+
2551
+ PERFORM pg_notify(
2552
+ 'delete',
2553
+ json_build_object(
2554
+ 'table', '${tableName}',
2555
+ 'queryMetadata', query_metadata,
2556
+ 'deletedId', OLD.id,
2557
+ 'previousRecord', OLD
2558
+ )::text
2559
+ );
2560
+ RETURN NEW;
2561
+ END;
2562
+ $$ LANGUAGE plpgsql;
2563
+
2564
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2565
+ AFTER DELETE ON "${tableName}"
2566
+ FOR EACH ROW
2567
+ EXECUTE FUNCTION notify_${tableName}_delete();
2568
+ `;
1778
2569
  }
1779
- }
1780
- logSqlStatement(query, options, requesterDetails, prefix = "") {
1781
- if (logger.level !== "silly") return;
1782
- let sqlStatement = "";
1783
- if (options.length === 0) {
1784
- sqlStatement = query;
1785
- } else {
1786
- let stringIndex = 0;
1787
- sqlStatement = query.replace(/\$\d+/g, () => {
1788
- const value = options[stringIndex++];
1789
- if (typeof value === "number") return value.toString();
1790
- return import_pg_format2.default.literal(value);
1791
- });
2570
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2571
+ return `
2572
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2573
+ RETURNS TRIGGER AS $$
2574
+ DECLARE
2575
+ query_metadata JSON;
2576
+ BEGIN
2577
+ SELECT INTO query_metadata
2578
+ (regexp_match(
2579
+ current_query(),
2580
+ '^--QUERY_METADATA\\(({.*})', 'n'
2581
+ ))[1]::json;
2582
+
2583
+ PERFORM pg_notify(
2584
+ 'delete',
2585
+ json_build_object(
2586
+ 'table', '${tableName}',
2587
+ 'queryMetadata', query_metadata,
2588
+ 'deletedId', OLD.id,
2589
+ 'previousRecord', json_build_object(
2590
+ ${notifyColumnOldBuildString}
2591
+ )
2592
+ )::text
2593
+ );
2594
+ RETURN NEW;
2595
+ END;
2596
+ $$ LANGUAGE plpgsql;
2597
+
2598
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2599
+ AFTER DELETE ON "${tableName}"
2600
+ FOR EACH ROW
2601
+ EXECUTE FUNCTION notify_${tableName}_delete();
2602
+ `;
2603
+ }
2604
+ createInsertTriggers(tableName, notify) {
2605
+ if (!notify) return "";
2606
+ if (notify === "ALL") {
2607
+ return `
2608
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2609
+ RETURNS TRIGGER AS $$
2610
+ DECLARE
2611
+ query_metadata JSON;
2612
+ BEGIN
2613
+ SELECT INTO query_metadata
2614
+ (regexp_match(
2615
+ current_query(),
2616
+ '^--QUERY_METADATA\\(({.*})', 'n'
2617
+ ))[1]::json;
2618
+
2619
+ PERFORM pg_notify(
2620
+ 'insert',
2621
+ json_build_object(
2622
+ 'table', '${tableName}',
2623
+ 'queryMetadata', query_metadata,
2624
+ 'insertedId', NEW.id,
2625
+ 'record', NEW
2626
+ )::text
2627
+ );
2628
+
2629
+ RETURN NEW;
2630
+ END;
2631
+ $$ LANGUAGE plpgsql;
2632
+
2633
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2634
+ AFTER INSERT ON "${tableName}"
2635
+ FOR EACH ROW
2636
+ EXECUTE FUNCTION notify_${tableName}_insert();
2637
+ `;
1792
2638
  }
1793
- let initiator = "Anonymous";
1794
- if ("userId" in requesterDetails && requesterDetails.userId)
1795
- initiator = `User Id (${requesterDetails.userId.toString()})`;
1796
- if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1797
- logger.silly(`${prefix}query by ${initiator}, Query ->
1798
- ${sqlStatement}`);
2639
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2640
+ return `
2641
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2642
+ RETURNS TRIGGER AS $$
2643
+ DECLARE
2644
+ query_metadata JSON;
2645
+ BEGIN
2646
+ SELECT INTO query_metadata
2647
+ (regexp_match(
2648
+ current_query(),
2649
+ '^--QUERY_METADATA\\(({.*})', 'n'
2650
+ ))[1]::json;
2651
+
2652
+ PERFORM pg_notify(
2653
+ 'insert',
2654
+ json_build_object(
2655
+ 'table', '${tableName}',
2656
+ 'queryMetadata', query_metadata,
2657
+ 'insertedId', NEW.id,
2658
+ 'record', json_build_object(
2659
+ ${notifyColumnNewBuildString}
2660
+ )
2661
+ )::text
2662
+ );
2663
+
2664
+ RETURN NEW;
2665
+ END;
2666
+ $$ LANGUAGE plpgsql;
2667
+
2668
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2669
+ AFTER INSERT ON "${tableName}"
2670
+ FOR EACH ROW
2671
+ EXECUTE FUNCTION notify_${tableName}_insert();
2672
+ `;
2673
+ }
2674
+ schemaToPsqlType(column) {
2675
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2676
+ if (column.type === "ENUM") return `TEXT`;
2677
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2678
+ if (column.type === "MEDIUMINT") return "INT";
2679
+ return column.type;
2680
+ }
2681
+ };
2682
+
2683
+ // src/restura/utils/TempCache.ts
2684
+ var import_fs3 = __toESM(require("fs"));
2685
+ var import_path4 = __toESM(require("path"));
2686
+ var import_core_utils6 = require("@redskytech/core-utils");
2687
+ var import_internal3 = require("@restura/internal");
2688
+ var import_bluebird3 = __toESM(require("bluebird"));
2689
+ var os2 = __toESM(require("os"));
2690
+ var TempCache = class {
2691
+ constructor(location) {
2692
+ this.maxDurationDays = 7;
2693
+ this.location = location || os2.tmpdir();
2694
+ import_internal3.FileUtils.ensureDir(this.location).catch((e) => {
2695
+ throw e;
2696
+ });
2697
+ }
2698
+ async cleanup() {
2699
+ const fileList = await import_fs3.default.promises.readdir(this.location);
2700
+ await import_bluebird3.default.map(
2701
+ fileList,
2702
+ async (file) => {
2703
+ const fullFilePath = import_path4.default.join(this.location, file);
2704
+ const fileStats = await import_fs3.default.promises.stat(fullFilePath);
2705
+ if (import_core_utils6.DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2706
+ logger.info(`Deleting old temp file: ${file}`);
2707
+ await import_fs3.default.promises.unlink(fullFilePath);
2708
+ }
2709
+ },
2710
+ { concurrency: 10 }
2711
+ );
1799
2712
  }
1800
2713
  };
1801
2714
 
1802
2715
  // src/restura/restura.ts
1803
- var { types } = import_pg2.default;
1804
2716
  var ResturaEngine = class {
1805
2717
  constructor() {
1806
2718
  this.publicEndpoints = {
@@ -1818,10 +2730,11 @@ var ResturaEngine = class {
1818
2730
  * @returns A promise that resolves when the initialization is complete.
1819
2731
  */
1820
2732
  async init(app, authenticationHandler, psqlConnectionPool) {
1821
- this.resturaConfig = import_internal2.config.validate("restura", resturaConfigSchema);
2733
+ this.resturaConfig = import_internal4.config.validate("restura", resturaConfigSchema);
2734
+ this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
2735
+ new TempCache(this.resturaConfig.fileTempCachePath);
1822
2736
  this.psqlConnectionPool = psqlConnectionPool;
1823
- this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
1824
- setupPgReturnTypes();
2737
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
1825
2738
  await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
1826
2739
  this.authenticationHandler = authenticationHandler;
1827
2740
  app.use((0, import_compression.default)());
@@ -1885,10 +2798,7 @@ var ResturaEngine = class {
1885
2798
  * @returns A promise that resolves when the API has been successfully generated and written to the output file.
1886
2799
  */
1887
2800
  async generateApiFromSchema(outputFile, providedSchema) {
1888
- import_fs3.default.writeFileSync(
1889
- outputFile,
1890
- await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1891
- );
2801
+ import_fs4.default.writeFileSync(outputFile, await apiGenerator(providedSchema));
1892
2802
  }
1893
2803
  /**
1894
2804
  * Generates a model from the provided schema and writes it to the specified output file.
@@ -1898,10 +2808,15 @@ var ResturaEngine = class {
1898
2808
  * @returns A promise that resolves when the model has been successfully written to the output file.
1899
2809
  */
1900
2810
  async generateModelFromSchema(outputFile, providedSchema) {
1901
- import_fs3.default.writeFileSync(
1902
- outputFile,
1903
- await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1904
- );
2811
+ import_fs4.default.writeFileSync(outputFile, await modelGenerator(providedSchema));
2812
+ }
2813
+ /**
2814
+ * Generates the ambient module declaration for Restura global types and writes it to the specified output file.
2815
+ * These types are used sometimes in the CustomTypes
2816
+ * @param outputFile
2817
+ */
2818
+ generateResturaGlobalTypes(outputFile) {
2819
+ import_fs4.default.writeFileSync(outputFile, resturaGlobalTypesGenerator());
1905
2820
  }
1906
2821
  /**
1907
2822
  * Retrieves the latest file system schema for Restura.
@@ -1910,12 +2825,12 @@ var ResturaEngine = class {
1910
2825
  * @throws {Error} If the schema file is missing or the schema is not valid.
1911
2826
  */
1912
2827
  async getLatestFileSystemSchema() {
1913
- if (!import_fs3.default.existsSync(this.resturaConfig.schemaFilePath)) {
2828
+ if (!import_fs4.default.existsSync(this.resturaConfig.schemaFilePath)) {
1914
2829
  logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
1915
2830
  throw new Error("Missing restura schema file");
1916
2831
  }
1917
- const schemaFileData = import_fs3.default.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
1918
- const schema = import_core_utils6.ObjectUtils.safeParse(schemaFileData);
2832
+ const schemaFileData = import_fs4.default.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2833
+ const schema = import_core_utils7.ObjectUtils.safeParse(schemaFileData);
1919
2834
  const isValid = await isSchemaValid(schema);
1920
2835
  if (!isValid) {
1921
2836
  logger.error("Schema is not valid");
@@ -1923,28 +2838,6 @@ var ResturaEngine = class {
1923
2838
  }
1924
2839
  return schema;
1925
2840
  }
1926
- /**
1927
- * Asynchronously generates and retrieves hashes for the provided schema and related generated files.
1928
- *
1929
- * @param providedSchema - The schema for which hashes need to be generated.
1930
- * @returns A promise that resolves to an object containing:
1931
- * - `schemaHash`: The hash of the provided schema.
1932
- * - `apiCreatedSchemaHash`: The hash extracted from the generated `api.d.ts` file.
1933
- * - `modelCreatedSchemaHash`: The hash extracted from the generated `models.d.ts` file.
1934
- */
1935
- async getHashes(providedSchema) {
1936
- var _a, _b, _c, _d;
1937
- const schemaHash = await this.generateHashForSchema(providedSchema);
1938
- const apiFile = import_fs3.default.readFileSync(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1939
- const apiCreatedSchemaHash = (_b = (_a = apiFile.toString().match(/\((.*)\)/)) == null ? void 0 : _a[1]) != null ? _b : "";
1940
- const modelFile = import_fs3.default.readFileSync(import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1941
- const modelCreatedSchemaHash = (_d = (_c = modelFile.toString().match(/\((.*)\)/)) == null ? void 0 : _c[1]) != null ? _d : "";
1942
- return {
1943
- schemaHash,
1944
- apiCreatedSchemaHash,
1945
- modelCreatedSchemaHash
1946
- };
1947
- }
1948
2841
  async reloadEndpoints() {
1949
2842
  this.schema = await this.getLatestFileSystemSchema();
1950
2843
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
@@ -1973,30 +2866,10 @@ var ResturaEngine = class {
1973
2866
  logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
1974
2867
  }
1975
2868
  async validateGeneratedTypesFolder() {
1976
- if (!import_fs3.default.existsSync(this.resturaConfig.generatedTypesPath)) {
1977
- import_fs3.default.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1978
- }
1979
- const hasApiFile = import_fs3.default.existsSync(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1980
- const hasModelsFile = import_fs3.default.existsSync(import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1981
- if (!hasApiFile) {
1982
- await this.generateApiFromSchema(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1983
- }
1984
- if (!hasModelsFile) {
1985
- await this.generateModelFromSchema(
1986
- import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1987
- this.schema
1988
- );
1989
- }
1990
- const hashes = await this.getHashes(this.schema);
1991
- if (hashes.schemaHash !== hashes.apiCreatedSchemaHash) {
1992
- await this.generateApiFromSchema(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1993
- }
1994
- if (hashes.schemaHash !== hashes.modelCreatedSchemaHash) {
1995
- await this.generateModelFromSchema(
1996
- import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1997
- this.schema
1998
- );
2869
+ if (!import_fs4.default.existsSync(this.resturaConfig.generatedTypesPath)) {
2870
+ import_fs4.default.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1999
2871
  }
2872
+ this.updateTypes();
2000
2873
  }
2001
2874
  resturaAuthentication(req, res, next) {
2002
2875
  if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
@@ -2004,7 +2877,7 @@ var ResturaEngine = class {
2004
2877
  }
2005
2878
  async previewCreateSchema(req, res) {
2006
2879
  try {
2007
- const schemaDiff = { commands: "", endPoints: [], globalParams: [], roles: [], customTypes: false };
2880
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
2008
2881
  res.send({ data: schemaDiff });
2009
2882
  } catch (err) {
2010
2883
  res.status(400).send(err);
@@ -2012,7 +2885,7 @@ var ResturaEngine = class {
2012
2885
  }
2013
2886
  async updateSchema(req, res) {
2014
2887
  try {
2015
- this.schema = req.data;
2888
+ this.schema = sortObjectKeysAlphabetically(req.data);
2016
2889
  await this.storeFileSystemSchema();
2017
2890
  await this.reloadEndpoints();
2018
2891
  await this.updateTypes();
@@ -2023,11 +2896,12 @@ var ResturaEngine = class {
2023
2896
  }
2024
2897
  }
2025
2898
  async updateTypes() {
2026
- await this.generateApiFromSchema(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2899
+ await this.generateApiFromSchema(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2027
2900
  await this.generateModelFromSchema(
2028
- import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2901
+ import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2029
2902
  this.schema
2030
2903
  );
2904
+ this.generateResturaGlobalTypes(import_path5.default.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
2031
2905
  }
2032
2906
  async getSchema(req, res) {
2033
2907
  res.send({ data: this.schema });
@@ -2035,19 +2909,38 @@ var ResturaEngine = class {
2035
2909
  async getSchemaAndTypes(req, res) {
2036
2910
  try {
2037
2911
  const schema = await this.getLatestFileSystemSchema();
2038
- const schemaHash = await this.generateHashForSchema(schema);
2039
- const apiText = await apiGenerator(schema, schemaHash);
2040
- const modelsText = await modelGenerator(schema, schemaHash);
2912
+ const apiText = await apiGenerator(schema);
2913
+ const modelsText = await modelGenerator(schema);
2041
2914
  res.send({ schema, api: apiText, models: modelsText });
2042
2915
  } catch (err) {
2043
2916
  res.status(400).send({ error: err });
2044
2917
  }
2045
2918
  }
2919
+ async getMulterFilesIfAny(req, res, routeData) {
2920
+ var _a2;
2921
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2922
+ if (!this.isCustomRoute(routeData)) return;
2923
+ if (!routeData.fileUploadType) {
2924
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2925
+ }
2926
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2927
+ return new Promise((resolve2, reject) => {
2928
+ multerFileUploadFunction(req, res, (err) => {
2929
+ if (err) {
2930
+ logger.warn("Multer error: " + err);
2931
+ reject(err);
2932
+ }
2933
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2934
+ resolve2();
2935
+ });
2936
+ });
2937
+ }
2046
2938
  async executeRouteLogic(req, res, next) {
2047
2939
  try {
2048
2940
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
2049
2941
  this.validateAuthorization(req, routeData);
2050
- validateRequestParams(req, routeData, this.customTypeValidation);
2942
+ await this.getMulterFilesIfAny(req, res, routeData);
2943
+ requestValidator(req, routeData, this.customTypeValidation);
2051
2944
  if (this.isCustomRoute(routeData)) {
2052
2945
  await this.runCustomRouteLogic(req, res, routeData);
2053
2946
  return;
@@ -2072,33 +2965,21 @@ var ResturaEngine = class {
2072
2965
  let domain = routeData.path.split("/")[1];
2073
2966
  domain = domain.split("-").reduce((acc, value, index) => {
2074
2967
  if (index === 0) acc = value;
2075
- else acc += import_core_utils6.StringUtils.capitalizeFirst(value);
2968
+ else acc += import_core_utils7.StringUtils.capitalizeFirst(value);
2076
2969
  return acc;
2077
2970
  }, "");
2078
- const customApiName = `${import_core_utils6.StringUtils.capitalizeFirst(domain)}Api${import_core_utils6.StringUtils.capitalizeFirst(version)}`;
2971
+ const customApiName = `${import_core_utils7.StringUtils.capitalizeFirst(domain)}Api${import_core_utils7.StringUtils.capitalizeFirst(version)}`;
2079
2972
  const customApi = customApiFactory_default.getCustomApi(customApiName);
2080
2973
  if (!customApi) throw new RsError("NOT_FOUND", `API domain ${domain}-${version} not found`);
2081
2974
  const functionName = `${routeData.method.toLowerCase()}${routeData.path.replace(new RegExp("-", "g"), "/").split("/").reduce((acc, cur) => {
2082
2975
  if (cur === "") return acc;
2083
- return acc + import_core_utils6.StringUtils.capitalizeFirst(cur);
2976
+ return acc + import_core_utils7.StringUtils.capitalizeFirst(cur);
2084
2977
  }, "")}`;
2085
2978
  const customFunction = customApi[functionName];
2086
- if (!customFunction) throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented`);
2979
+ if (!customFunction)
2980
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2087
2981
  await customFunction(req, res, routeData);
2088
2982
  }
2089
- async generateHashForSchema(providedSchema) {
2090
- const schemaPrettyStr = await prettier3.format(JSON.stringify(providedSchema), __spreadValues({
2091
- parser: "json"
2092
- }, {
2093
- trailingComma: "none",
2094
- tabWidth: 4,
2095
- useTabs: true,
2096
- endOfLine: "lf",
2097
- printWidth: 120,
2098
- singleQuote: true
2099
- }));
2100
- return (0, import_crypto.createHash)("sha256").update(schemaPrettyStr).digest("hex");
2101
- }
2102
2983
  async storeFileSystemSchema() {
2103
2984
  const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
2104
2985
  parser: "json"
@@ -2110,7 +2991,7 @@ var ResturaEngine = class {
2110
2991
  printWidth: 120,
2111
2992
  singleQuote: true
2112
2993
  }));
2113
- import_fs3.default.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2994
+ import_fs4.default.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2114
2995
  }
2115
2996
  resetPublicEndpoints() {
2116
2997
  this.publicEndpoints = {
@@ -2127,13 +3008,13 @@ var ResturaEngine = class {
2127
3008
  if (!routeData.roles.includes(role))
2128
3009
  throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
2129
3010
  }
2130
- getRouteData(method, baseUrl, path4) {
3011
+ getRouteData(method, baseUrl, path5) {
2131
3012
  const endpoint = this.schema.endpoints.find((item) => {
2132
3013
  return item.baseUrl === baseUrl;
2133
3014
  });
2134
3015
  if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
2135
3016
  const route = endpoint.routes.find((item) => {
2136
- return item.method === method && item.path === path4;
3017
+ return item.method === method && item.path === path5;
2137
3018
  });
2138
3019
  if (!route) throw new RsError("NOT_FOUND", "Route not found");
2139
3020
  return route;
@@ -2154,6 +3035,9 @@ __decorateClass([
2154
3035
  __decorateClass([
2155
3036
  boundMethod
2156
3037
  ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
3038
+ __decorateClass([
3039
+ boundMethod
3040
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
2157
3041
  __decorateClass([
2158
3042
  boundMethod
2159
3043
  ], ResturaEngine.prototype, "executeRouteLogic", 1);
@@ -2163,25 +3047,55 @@ __decorateClass([
2163
3047
  __decorateClass([
2164
3048
  boundMethod
2165
3049
  ], ResturaEngine.prototype, "runCustomRouteLogic", 1);
2166
- var setupPgReturnTypes = () => {
2167
- const TIMESTAMPTZ_OID = 1184;
2168
- types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
2169
- return val === null ? null : new Date(val).toISOString();
2170
- });
2171
- const BIGINT_OID = 20;
2172
- types.setTypeParser(BIGINT_OID, (val) => {
2173
- return val === null ? null : Number(val);
2174
- });
2175
- };
2176
- setupPgReturnTypes();
2177
3050
  var restura = new ResturaEngine();
3051
+
3052
+ // src/restura/sql/PsqlTransaction.ts
3053
+ var import_pg3 = __toESM(require("pg"));
3054
+ var { Client: Client2 } = import_pg3.default;
3055
+ var PsqlTransaction = class extends PsqlConnection {
3056
+ constructor(clientConfig, instanceId) {
3057
+ super(instanceId);
3058
+ this.clientConfig = clientConfig;
3059
+ this.client = new Client2(clientConfig);
3060
+ this.connectPromise = this.client.connect();
3061
+ this.beginTransactionPromise = this.beginTransaction();
3062
+ }
3063
+ async close() {
3064
+ if (this.client) {
3065
+ await this.client.end();
3066
+ }
3067
+ }
3068
+ async beginTransaction() {
3069
+ await this.connectPromise;
3070
+ return this.client.query("BEGIN");
3071
+ }
3072
+ async rollback() {
3073
+ return this.query("ROLLBACK");
3074
+ }
3075
+ async commit() {
3076
+ return this.query("COMMIT");
3077
+ }
3078
+ async release() {
3079
+ return this.client.end();
3080
+ }
3081
+ async query(query, values) {
3082
+ await this.connectPromise;
3083
+ await this.beginTransactionPromise;
3084
+ return this.client.query(query, values);
3085
+ }
3086
+ };
2178
3087
  // Annotate the CommonJS export names for ESM import in node:
2179
3088
  0 && (module.exports = {
2180
3089
  HtmlStatusCodes,
3090
+ PsqlConnection,
3091
+ PsqlEngine,
2181
3092
  PsqlPool,
3093
+ PsqlTransaction,
2182
3094
  RsError,
2183
3095
  SQL,
2184
3096
  escapeColumnName,
3097
+ eventManager,
3098
+ filterPsqlParser,
2185
3099
  insertObjectQuery,
2186
3100
  isValueNumber,
2187
3101
  logger,