@restura/core 0.1.0-alpha.9 → 0.1.1

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
@@ -68,10 +68,15 @@ var __decorateClass = (decorators, target, key, kind) => {
68
68
  var src_exports = {};
69
69
  __export(src_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,
@@ -86,18 +91,11 @@ 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}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
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,33 @@ 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"));
1246
1588
 
1247
1589
  // src/restura/sql/PsqlUtils.ts
1248
1590
  var import_pg_format = __toESM(require("pg-format"));
@@ -1252,16 +1594,33 @@ function escapeColumnName(columnName) {
1252
1594
  }
1253
1595
  function questionMarksToOrderedParams(query) {
1254
1596
  let count = 1;
1255
- return query.replace(/'\?'|\?/g, () => `$${count++}`);
1597
+ let inSingleQuote = false;
1598
+ let inDoubleQuote = false;
1599
+ return query.replace(/('|"|\?)/g, (char) => {
1600
+ if (char === "'") {
1601
+ inSingleQuote = !inSingleQuote && !inDoubleQuote;
1602
+ return char;
1603
+ }
1604
+ if (char === '"') {
1605
+ inDoubleQuote = !inDoubleQuote && !inSingleQuote;
1606
+ return char;
1607
+ }
1608
+ if (char === "?" && !inSingleQuote && !inDoubleQuote) {
1609
+ return `$${count++}`;
1610
+ }
1611
+ return char;
1612
+ });
1256
1613
  }
1257
1614
  function insertObjectQuery(table, obj) {
1258
1615
  const keys = Object.keys(obj);
1259
1616
  const params = Object.values(obj);
1260
1617
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1261
1618
  const values = params.map((value) => SQL`${value}`).join(", ");
1262
- const query = `INSERT INTO "${table}" (${columns})
1619
+ let query = `
1620
+ INSERT INTO "${table}" (${columns})
1263
1621
  VALUES (${values})
1264
1622
  RETURNING *`;
1623
+ query = query.replace(/'(\?)'/, "?");
1265
1624
  return query;
1266
1625
  }
1267
1626
  function updateObjectQuery(table, obj, whereStatement) {
@@ -1269,25 +1628,113 @@ function updateObjectQuery(table, obj, whereStatement) {
1269
1628
  for (const i in obj) {
1270
1629
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1271
1630
  }
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;
1631
+ return `
1632
+ UPDATE ${escapeColumnName(table)}
1633
+ SET ${setArray.join(", ")} ${whereStatement}
1634
+ RETURNING *`;
1635
+ }
1636
+ function isValueNumber2(value) {
1637
+ return !isNaN(Number(value));
1638
+ }
1639
+ function SQL(strings, ...values) {
1640
+ let query = strings[0];
1641
+ values.forEach((value, index) => {
1642
+ if (typeof value === "boolean") {
1643
+ query += value;
1644
+ } else if (typeof value === "number") {
1645
+ query += value;
1646
+ } else if (Array.isArray(value)) {
1647
+ query += import_pg_format.default.literal(JSON.stringify(value)) + "::jsonb";
1648
+ } else {
1649
+ query += import_pg_format.default.literal(value);
1650
+ }
1651
+ query += strings[index + 1];
1652
+ });
1653
+ return query;
1654
+ }
1655
+
1656
+ // src/restura/sql/PsqlConnection.ts
1657
+ var PsqlConnection = class {
1658
+ constructor(instanceId) {
1659
+ this.instanceId = instanceId || import_crypto.default.randomUUID();
1660
+ }
1661
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1662
+ async queryOne(query, options, requesterDetails) {
1663
+ const formattedQuery = questionMarksToOrderedParams(query);
1664
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1665
+ this.logSqlStatement(formattedQuery, options, meta);
1666
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1667
+ `;
1668
+ try {
1669
+ const response = await this.query(queryMetadata + formattedQuery, options);
1670
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1671
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1672
+ return response.rows[0];
1673
+ } catch (error) {
1674
+ if (RsError.isRsError(error)) throw error;
1675
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1676
+ throw new RsError("DUPLICATE", error.message);
1677
+ }
1678
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1679
+ }
1680
+ }
1681
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1682
+ async runQuery(query, options, requesterDetails) {
1683
+ const formattedQuery = questionMarksToOrderedParams(query);
1684
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1685
+ this.logSqlStatement(formattedQuery, options, meta);
1686
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1687
+ `;
1688
+ try {
1689
+ const response = await this.query(queryMetadata + formattedQuery, options);
1690
+ return response.rows;
1691
+ } catch (error) {
1692
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1693
+ throw new RsError("DUPLICATE", error.message);
1694
+ }
1695
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1696
+ }
1697
+ }
1698
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1699
+ if (logger.level !== "silly") return;
1700
+ let sqlStatement = "";
1701
+ if (options.length === 0) {
1702
+ sqlStatement = query;
1284
1703
  } else {
1285
- query += import_pg_format.default.literal(value);
1704
+ let stringIndex = 0;
1705
+ sqlStatement = query.replace(/\$\d+/g, () => {
1706
+ const value = options[stringIndex++];
1707
+ if (typeof value === "number") return value.toString();
1708
+ return import_pg_format2.default.literal(value);
1709
+ });
1286
1710
  }
1287
- query += strings[index + 1];
1288
- });
1289
- return query;
1290
- }
1711
+ let initiator = "Anonymous";
1712
+ if ("userId" in queryMetadata && queryMetadata.userId)
1713
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1714
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1715
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1716
+ ${sqlStatement}`);
1717
+ }
1718
+ };
1719
+
1720
+ // src/restura/sql/PsqlPool.ts
1721
+ var { Pool } = import_pg.default;
1722
+ var PsqlPool = class extends PsqlConnection {
1723
+ constructor(poolConfig) {
1724
+ super();
1725
+ this.poolConfig = poolConfig;
1726
+ this.pool = new Pool(poolConfig);
1727
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1728
+ logger.info("Connected to PostgreSQL database");
1729
+ }).catch((error) => {
1730
+ logger.error("Error connecting to database", error);
1731
+ process.exit(1);
1732
+ });
1733
+ }
1734
+ async query(query, values) {
1735
+ return this.pool.query(query, values);
1736
+ }
1737
+ };
1291
1738
 
1292
1739
  // src/restura/sql/SqlEngine.ts
1293
1740
  var import_core_utils4 = require("@redskytech/core-utils");
@@ -1355,11 +1802,11 @@ var SqlEngine = class {
1355
1802
  return returnValue;
1356
1803
  }
1357
1804
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1358
- var _a;
1805
+ var _a2;
1359
1806
  if (!routeData.request) return value;
1360
1807
  const data = req.data;
1361
1808
  if (typeof value === "string") {
1362
- (_a = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1809
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1363
1810
  const requestParam = routeData.request.find((item) => {
1364
1811
  return item.name === param.replace("$", "");
1365
1812
  });
@@ -1372,9 +1819,9 @@ var SqlEngine = class {
1372
1819
  return value;
1373
1820
  }
1374
1821
  replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1375
- var _a;
1822
+ var _a2;
1376
1823
  if (typeof value === "string") {
1377
- (_a = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1824
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1378
1825
  param = param.replace("#", "");
1379
1826
  const globalParamValue = req.requesterDetails[param];
1380
1827
  if (!globalParamValue)
@@ -1393,32 +1840,86 @@ var SqlEngine = class {
1393
1840
  // src/restura/sql/filterPsqlParser.ts
1394
1841
  var import_pegjs = __toESM(require("pegjs"));
1395
1842
  var filterSqlGrammar = `
1843
+ {
1844
+ // ported from pg-format but intentionally will add double quotes to every column
1845
+ function quoteSqlIdentity(value) {
1846
+ if (value === undefined || value === null) {
1847
+ throw new Error('SQL identifier cannot be null or undefined');
1848
+ } else if (value === false) {
1849
+ return '"f"';
1850
+ } else if (value === true) {
1851
+ return '"t"';
1852
+ } else if (value instanceof Date) {
1853
+ // return '"' + formatDate(value.toISOString()) + '"';
1854
+ } else if (value instanceof Buffer) {
1855
+ throw new Error('SQL identifier cannot be a buffer');
1856
+ } else if (Array.isArray(value) === true) {
1857
+ var temp = [];
1858
+ for (var i = 0; i < value.length; i++) {
1859
+ if (Array.isArray(value[i]) === true) {
1860
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1861
+ } else {
1862
+ // temp.push(quoteIdent(value[i]));
1863
+ }
1864
+ }
1865
+ return temp.toString();
1866
+ } else if (value === Object(value)) {
1867
+ throw new Error('SQL identifier cannot be an object');
1868
+ }
1869
+
1870
+ var ident = value.toString().slice(0); // create copy
1871
+
1872
+ // do not quote a valid, unquoted identifier
1873
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1874
+ // return ident;
1875
+ // }
1876
+
1877
+ var quoted = '"';
1878
+
1879
+ for (var i = 0; i < ident.length; i++) {
1880
+ var c = ident[i];
1881
+ if (c === '"') {
1882
+ quoted += c + c;
1883
+ } else {
1884
+ quoted += c;
1885
+ }
1886
+ }
1887
+
1888
+ quoted += '"';
1889
+
1890
+ return quoted;
1891
+ };
1892
+ }
1893
+
1396
1894
  start = expressionList
1397
1895
 
1896
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1897
+
1398
1898
  expressionList =
1399
- leftExpression:expression operator:operator rightExpression:expressionList
1899
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1400
1900
  { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1401
1901
  / expression
1402
1902
 
1403
1903
  expression =
1404
- negate:negate?"(" "column:" column:column ","? value:value? ","? type:type? ")"
1405
- {return \`\${negate? "!" : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1904
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1905
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1406
1906
  /
1407
- negate:negate?"("expression:expressionList")" { return \`\${negate? "!" : ""}(\${expression})\`; }
1907
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1408
1908
 
1409
1909
  negate = "!"
1410
1910
 
1411
1911
  operator = "and"i / "or"i
1412
1912
 
1413
1913
 
1414
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1914
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1415
1915
  /
1416
- text:text { return format.ident(text); }
1916
+ text:text { return quoteSqlIdentity(text); }
1417
1917
 
1418
1918
 
1419
- text = text:[a-z0-9-_:@]i+ { return text.join("");}
1919
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
1920
+
1420
1921
 
1421
- type = "type:" type:typeString { return type; }
1922
+ type = "type" _ ":" _ type:typeString { return type; }
1422
1923
  typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1423
1924
  text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1424
1925
  text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
@@ -1428,8 +1929,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
1428
1929
  text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1429
1930
  text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1430
1931
  text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1431
-
1432
- value = "value:" value:text { return value; }
1932
+
1933
+ value = "value" _ ":" value:text { return value; }
1934
+
1433
1935
 
1434
1936
  `;
1435
1937
  var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
@@ -1439,18 +1941,224 @@ var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
1439
1941
  var filterPsqlParser_default = filterPsqlParser;
1440
1942
 
1441
1943
  // src/restura/sql/PsqlEngine.ts
1944
+ var { Client, types } = import_pg2.default;
1945
+ var systemUser = {
1946
+ role: "",
1947
+ host: "",
1948
+ ipAddress: "",
1949
+ isSystemUser: true
1950
+ };
1442
1951
  var PsqlEngine = class extends SqlEngine {
1443
- constructor(psqlConnectionPool) {
1952
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1444
1953
  super();
1445
1954
  this.psqlConnectionPool = psqlConnectionPool;
1955
+ this.setupPgReturnTypes();
1956
+ if (shouldListenForDbTriggers) {
1957
+ this.setupTriggerListeners = this.listenForDbTriggers();
1958
+ }
1446
1959
  }
1447
- async diffDatabaseToSchema(schema) {
1448
- console.log(schema);
1449
- return Promise.resolve("");
1960
+ async close() {
1961
+ if (this.triggerClient) {
1962
+ await this.triggerClient.end();
1963
+ }
1964
+ }
1965
+ setupPgReturnTypes() {
1966
+ const TIMESTAMPTZ_OID = 1184;
1967
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1968
+ return val === null ? null : new Date(val).toISOString();
1969
+ });
1970
+ const BIGINT_OID = 20;
1971
+ types.setTypeParser(BIGINT_OID, (val) => {
1972
+ return val === null ? null : Number(val);
1973
+ });
1974
+ }
1975
+ async listenForDbTriggers() {
1976
+ this.triggerClient = new Client({
1977
+ user: this.psqlConnectionPool.poolConfig.user,
1978
+ host: this.psqlConnectionPool.poolConfig.host,
1979
+ database: this.psqlConnectionPool.poolConfig.database,
1980
+ password: this.psqlConnectionPool.poolConfig.password,
1981
+ port: this.psqlConnectionPool.poolConfig.port,
1982
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1983
+ });
1984
+ await this.triggerClient.connect();
1985
+ const promises = [];
1986
+ promises.push(this.triggerClient.query("LISTEN insert"));
1987
+ promises.push(this.triggerClient.query("LISTEN update"));
1988
+ promises.push(this.triggerClient.query("LISTEN delete"));
1989
+ await Promise.all(promises);
1990
+ this.triggerClient.on("notification", async (msg) => {
1991
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1992
+ const payload = import_core_utils5.ObjectUtils.safeParse(msg.payload);
1993
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1994
+ }
1995
+ });
1996
+ }
1997
+ async handleTrigger(payload, mutationType) {
1998
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
1999
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
2000
+ }
2001
+ }
2002
+ async createDatabaseFromSchema(schema, connection) {
2003
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
2004
+ await connection.runQuery(sqlFullStatement, [], systemUser);
2005
+ return sqlFullStatement;
1450
2006
  }
1451
2007
  generateDatabaseSchemaFromSchema(schema) {
1452
- console.log(schema);
1453
- return "";
2008
+ const sqlStatements = [];
2009
+ const indexes = [];
2010
+ const triggers = [];
2011
+ for (const table of schema.database) {
2012
+ if (table.notify) {
2013
+ triggers.push(this.createInsertTriggers(table.name, table.notify));
2014
+ triggers.push(this.createUpdateTrigger(table.name, table.notify));
2015
+ triggers.push(this.createDeleteTrigger(table.name, table.notify));
2016
+ }
2017
+ let sql = `CREATE TABLE "${table.name}"
2018
+ ( `;
2019
+ const tableColumns = [];
2020
+ for (const column of table.columns) {
2021
+ let columnSql = "";
2022
+ columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
2023
+ let value = column.value;
2024
+ if (column.type === "JSON") value = "";
2025
+ if (column.type === "JSONB") value = "";
2026
+ if (column.type === "DECIMAL" && value) {
2027
+ value = value.replace("-", ",").replace(/['"]/g, "");
2028
+ }
2029
+ if (value && column.type !== "ENUM") {
2030
+ columnSql += `(${value})`;
2031
+ } else if (column.length) columnSql += `(${column.length})`;
2032
+ if (column.isPrimary) {
2033
+ columnSql += " PRIMARY KEY ";
2034
+ }
2035
+ if (column.isUnique) {
2036
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
2037
+ }
2038
+ if (column.isNullable) columnSql += " NULL";
2039
+ else columnSql += " NOT NULL";
2040
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
2041
+ if (value && column.type === "ENUM") {
2042
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
2043
+ }
2044
+ tableColumns.push(columnSql);
2045
+ }
2046
+ sql += tableColumns.join(", \n");
2047
+ for (const index of table.indexes) {
2048
+ if (!index.isPrimaryKey) {
2049
+ let unique = " ";
2050
+ if (index.isUnique) unique = "UNIQUE ";
2051
+ indexes.push(
2052
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
2053
+ return `"${item}" ${index.order}`;
2054
+ }).join(", ")});`
2055
+ );
2056
+ }
2057
+ }
2058
+ sql += "\n);";
2059
+ sqlStatements.push(sql);
2060
+ }
2061
+ for (const table of schema.database) {
2062
+ if (!table.foreignKeys.length) continue;
2063
+ const sql = `ALTER TABLE "${table.name}" `;
2064
+ const constraints = [];
2065
+ for (const foreignKey of table.foreignKeys) {
2066
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2067
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2068
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2069
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2070
+ constraints.push(constraint);
2071
+ }
2072
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2073
+ }
2074
+ for (const table of schema.database) {
2075
+ if (!table.checkConstraints.length) continue;
2076
+ const sql = `ALTER TABLE "${table.name}" `;
2077
+ const constraints = [];
2078
+ for (const check of table.checkConstraints) {
2079
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2080
+ constraints.push(constraint);
2081
+ }
2082
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2083
+ }
2084
+ sqlStatements.push(indexes.join("\n"));
2085
+ sqlStatements.push(triggers.join("\n"));
2086
+ return sqlStatements.join("\n\n");
2087
+ }
2088
+ async getScratchPool() {
2089
+ var _a2, _b;
2090
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
2091
+ `SELECT *
2092
+ FROM pg_database
2093
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
2094
+ [],
2095
+ systemUser
2096
+ );
2097
+ if (scratchDbExists.length === 0) {
2098
+ await this.psqlConnectionPool.runQuery(
2099
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
2100
+ [],
2101
+ systemUser
2102
+ );
2103
+ }
2104
+ const scratchPool = new PsqlPool({
2105
+ host: this.psqlConnectionPool.poolConfig.host,
2106
+ port: this.psqlConnectionPool.poolConfig.port,
2107
+ user: this.psqlConnectionPool.poolConfig.user,
2108
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2109
+ password: this.psqlConnectionPool.poolConfig.password,
2110
+ max: this.psqlConnectionPool.poolConfig.max,
2111
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2112
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2113
+ });
2114
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2115
+ await scratchPool.runQuery(
2116
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2117
+ [],
2118
+ systemUser
2119
+ );
2120
+ const schemaComment = await this.psqlConnectionPool.runQuery(
2121
+ `SELECT pg_description.description
2122
+ FROM pg_description
2123
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2124
+ WHERE pg_namespace.nspname = 'public';`,
2125
+ [],
2126
+ systemUser
2127
+ );
2128
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2129
+ await scratchPool.runQuery(
2130
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
2131
+ [],
2132
+ systemUser
2133
+ );
2134
+ }
2135
+ return scratchPool;
2136
+ }
2137
+ async diffDatabaseToSchema(schema) {
2138
+ const scratchPool = await this.getScratchPool();
2139
+ await this.createDatabaseFromSchema(schema, scratchPool);
2140
+ const originalClient = new Client({
2141
+ database: this.psqlConnectionPool.poolConfig.database,
2142
+ user: this.psqlConnectionPool.poolConfig.user,
2143
+ password: this.psqlConnectionPool.poolConfig.password,
2144
+ host: this.psqlConnectionPool.poolConfig.host,
2145
+ port: this.psqlConnectionPool.poolConfig.port
2146
+ });
2147
+ const scratchClient = new Client({
2148
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2149
+ user: this.psqlConnectionPool.poolConfig.user,
2150
+ password: this.psqlConnectionPool.poolConfig.password,
2151
+ host: this.psqlConnectionPool.poolConfig.host,
2152
+ port: this.psqlConnectionPool.poolConfig.port
2153
+ });
2154
+ const promises = [originalClient.connect(), scratchClient.connect()];
2155
+ await Promise.all(promises);
2156
+ const infoPromises = [(0, import_pg_info.default)({ client: originalClient }), (0, import_pg_info.default)({ client: scratchClient })];
2157
+ const [info1, info2] = await Promise.all(infoPromises);
2158
+ const diff = (0, import_pg_diff_sync.default)(info1, info2);
2159
+ const endPromises = [originalClient.end(), scratchClient.end()];
2160
+ await Promise.all(endPromises);
2161
+ return diff.join("\n");
1454
2162
  }
1455
2163
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1456
2164
  if (!item.subquery) return "";
@@ -1464,8 +2172,7 @@ var PsqlEngine = class extends SqlEngine {
1464
2172
  )) {
1465
2173
  return "'[]'";
1466
2174
  }
1467
- return `COALESCE((
1468
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
2175
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1469
2176
  ${item.subquery.properties.map((nestedItem) => {
1470
2177
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1471
2178
  ...routeData.joins,
@@ -1474,7 +2181,7 @@ var PsqlEngine = class extends SqlEngine {
1474
2181
  return;
1475
2182
  }
1476
2183
  if (nestedItem.subquery) {
1477
- return `"${nestedItem.name}", ${this.createNestedSelect(
2184
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1478
2185
  // recursion
1479
2186
  req,
1480
2187
  schema,
@@ -1485,7 +2192,7 @@ var PsqlEngine = class extends SqlEngine {
1485
2192
  )}`;
1486
2193
  }
1487
2194
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1488
- }).filter(Boolean).join(",")}
2195
+ }).filter(Boolean).join(", ")}
1489
2196
  ))
1490
2197
  FROM
1491
2198
  "${item.subquery.table}"
@@ -1500,16 +2207,19 @@ var PsqlEngine = class extends SqlEngine {
1500
2207
  parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
1501
2208
  });
1502
2209
  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
- ];
2210
+ const createdItem = await this.psqlConnectionPool.queryOne(
2211
+ query,
2212
+ sqlParams,
2213
+ req.requesterDetails
2214
+ );
2215
+ const insertId = createdItem.id;
2216
+ const whereId = {
2217
+ tableName: routeData.table,
2218
+ value: insertId,
2219
+ columnName: "id",
2220
+ operator: "="
2221
+ };
2222
+ const whereData = [whereId];
1513
2223
  req.data = { id: insertId };
1514
2224
  return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
1515
2225
  }
@@ -1528,7 +2238,9 @@ var PsqlEngine = class extends SqlEngine {
1528
2238
  let selectStatement = "SELECT \n";
1529
2239
  selectStatement += ` ${selectColumns.map((item) => {
1530
2240
  if (item.subquery) {
1531
- return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${item.name}`;
2241
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2242
+ item.name
2243
+ )}`;
1532
2244
  }
1533
2245
  return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
1534
2246
  }).join(",\n ")}
@@ -1561,29 +2273,31 @@ var PsqlEngine = class extends SqlEngine {
1561
2273
  );
1562
2274
  } else if (routeData.type === "PAGED") {
1563
2275
  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
- ],
2276
+ const pagePromise = this.psqlConnectionPool.runQuery(
2277
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2278
+ sqlParams,
2279
+ req.requesterDetails
2280
+ );
2281
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2282
+ ${sqlStatement};`;
2283
+ const totalPromise = this.psqlConnectionPool.runQuery(
2284
+ totalQuery,
2285
+ sqlParams,
1573
2286
  req.requesterDetails
1574
2287
  );
2288
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1575
2289
  let total = 0;
1576
- if (import_core_utils5.ObjectUtils.isArrayWithData(pageResults)) {
1577
- total = pageResults[1][0].total;
2290
+ if (import_core_utils5.ObjectUtils.isArrayWithData(totalResponse)) {
2291
+ total = totalResponse[0].total;
1578
2292
  }
1579
- return { data: pageResults[0], total };
2293
+ return { data: pageResults, total };
1580
2294
  } else {
1581
2295
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1582
2296
  }
1583
2297
  }
1584
2298
  async executeUpdateRequest(req, routeData, schema) {
1585
2299
  const sqlParams = [];
1586
- const _a = req.body, { id } = _a, bodyNoId = __objRest(_a, ["id"]);
2300
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
1587
2301
  const table = schema.database.find((item) => {
1588
2302
  return item.name === routeData.table;
1589
2303
  });
@@ -1615,10 +2329,12 @@ ${sqlStatement};`,
1615
2329
  req.requesterDetails.role,
1616
2330
  sqlParams
1617
2331
  );
1618
- let deleteStatement = `DELETE
1619
- FROM "${routeData.table}" ${joinStatement}`;
1620
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1621
- deleteStatement += ";";
2332
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2333
+ if (whereClause.replace(/\s/g, "") === "") {
2334
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2335
+ }
2336
+ const deleteStatement = `
2337
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1622
2338
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1623
2339
  return true;
1624
2340
  }
@@ -1681,38 +2397,37 @@ ${sqlStatement};`,
1681
2397
  );
1682
2398
  let operator = item.operator;
1683
2399
  if (operator === "LIKE") {
1684
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2400
+ item.value = `'%${item.value}%'`;
1685
2401
  } else if (operator === "STARTS WITH") {
1686
2402
  operator = "LIKE";
1687
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2403
+ item.value = `'${item.value}%'`;
1688
2404
  } else if (operator === "ENDS WITH") {
1689
2405
  operator = "LIKE";
1690
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2406
+ item.value = `'%${item.value}'`;
1691
2407
  }
1692
2408
  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}
2409
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
1695
2410
  `;
1696
2411
  });
1697
2412
  const data = req.data;
1698
2413
  if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
1699
2414
  let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1700
- var _a;
2415
+ var _a2;
1701
2416
  const requestParam = routeData.request.find((item) => {
1702
2417
  return item.name === value.replace("$", "");
1703
2418
  });
1704
2419
  if (!requestParam)
1705
2420
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1706
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2421
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1707
2422
  });
1708
2423
  statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1709
- var _a;
2424
+ var _a2;
1710
2425
  const requestParam = routeData.request.find((item) => {
1711
2426
  return item.name === value.replace("#", "");
1712
2427
  });
1713
2428
  if (!requestParam)
1714
2429
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1715
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2430
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1716
2431
  });
1717
2432
  statement = filterPsqlParser_default.parse(statement);
1718
2433
  if (whereClause.startsWith("WHERE")) {
@@ -1725,82 +2440,259 @@ ${sqlStatement};`,
1725
2440
  }
1726
2441
  return whereClause;
1727
2442
  }
1728
- };
2443
+ createUpdateTrigger(tableName, notify) {
2444
+ if (!notify) return "";
2445
+ if (notify === "ALL") {
2446
+ return `
2447
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2448
+ RETURNS TRIGGER AS $$
2449
+ DECLARE
2450
+ query_metadata JSON;
2451
+ BEGIN
2452
+ SELECT INTO query_metadata
2453
+ (regexp_match(
2454
+ current_query(),
2455
+ '^--QUERY_METADATA\\(({.*})', 'n'
2456
+ ))[1]::json;
1729
2457
 
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}`);
2458
+ PERFORM pg_notify(
2459
+ 'update',
2460
+ json_build_object(
2461
+ 'table', '${tableName}',
2462
+ 'queryMetadata', query_metadata,
2463
+ 'changedId', NEW.id,
2464
+ 'record', NEW,
2465
+ 'previousRecord', OLD
2466
+ )::text
2467
+ );
2468
+ RETURN NEW;
2469
+ END;
2470
+ $$ LANGUAGE plpgsql;
2471
+
2472
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2473
+ AFTER UPDATE ON "${tableName}"
2474
+ FOR EACH ROW
2475
+ EXECUTE FUNCTION notify_${tableName}_update();
2476
+ `;
1761
2477
  }
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}`);
2478
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2479
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2480
+ return `
2481
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2482
+ RETURNS TRIGGER AS $$
2483
+ DECLARE
2484
+ query_metadata JSON;
2485
+ BEGIN
2486
+ SELECT INTO query_metadata
2487
+ (regexp_match(
2488
+ current_query(),
2489
+ '^--QUERY_METADATA\\(({.*})', 'n'
2490
+ ))[1]::json;
2491
+
2492
+ PERFORM pg_notify(
2493
+ 'update',
2494
+ json_build_object(
2495
+ 'table', '${tableName}',
2496
+ 'queryMetadata', query_metadata,
2497
+ 'changedId', NEW.id,
2498
+ 'record', json_build_object(
2499
+ ${notifyColumnNewBuildString}
2500
+ ),
2501
+ 'previousRecord', json_build_object(
2502
+ ${notifyColumnOldBuildString}
2503
+ )
2504
+ )::text
2505
+ );
2506
+ RETURN NEW;
2507
+ END;
2508
+ $$ LANGUAGE plpgsql;
2509
+
2510
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2511
+ AFTER UPDATE ON "${tableName}"
2512
+ FOR EACH ROW
2513
+ EXECUTE FUNCTION notify_${tableName}_update();
2514
+ `;
2515
+ }
2516
+ createDeleteTrigger(tableName, notify) {
2517
+ if (!notify) return "";
2518
+ if (notify === "ALL") {
2519
+ return `
2520
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2521
+ RETURNS TRIGGER AS $$
2522
+ DECLARE
2523
+ query_metadata JSON;
2524
+ BEGIN
2525
+ SELECT INTO query_metadata
2526
+ (regexp_match(
2527
+ current_query(),
2528
+ '^--QUERY_METADATA\\(({.*})', 'n'
2529
+ ))[1]::json;
2530
+
2531
+ PERFORM pg_notify(
2532
+ 'delete',
2533
+ json_build_object(
2534
+ 'table', '${tableName}',
2535
+ 'queryMetadata', query_metadata,
2536
+ 'deletedId', OLD.id,
2537
+ 'previousRecord', OLD
2538
+ )::text
2539
+ );
2540
+ RETURN NEW;
2541
+ END;
2542
+ $$ LANGUAGE plpgsql;
2543
+
2544
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2545
+ AFTER DELETE ON "${tableName}"
2546
+ FOR EACH ROW
2547
+ EXECUTE FUNCTION notify_${tableName}_delete();
2548
+ `;
1778
2549
  }
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
- });
2550
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2551
+ return `
2552
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2553
+ RETURNS TRIGGER AS $$
2554
+ DECLARE
2555
+ query_metadata JSON;
2556
+ BEGIN
2557
+ SELECT INTO query_metadata
2558
+ (regexp_match(
2559
+ current_query(),
2560
+ '^--QUERY_METADATA\\(({.*})', 'n'
2561
+ ))[1]::json;
2562
+
2563
+ PERFORM pg_notify(
2564
+ 'delete',
2565
+ json_build_object(
2566
+ 'table', '${tableName}',
2567
+ 'queryMetadata', query_metadata,
2568
+ 'deletedId', OLD.id,
2569
+ 'previousRecord', json_build_object(
2570
+ ${notifyColumnOldBuildString}
2571
+ )
2572
+ )::text
2573
+ );
2574
+ RETURN NEW;
2575
+ END;
2576
+ $$ LANGUAGE plpgsql;
2577
+
2578
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2579
+ AFTER DELETE ON "${tableName}"
2580
+ FOR EACH ROW
2581
+ EXECUTE FUNCTION notify_${tableName}_delete();
2582
+ `;
2583
+ }
2584
+ createInsertTriggers(tableName, notify) {
2585
+ if (!notify) return "";
2586
+ if (notify === "ALL") {
2587
+ return `
2588
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2589
+ RETURNS TRIGGER AS $$
2590
+ DECLARE
2591
+ query_metadata JSON;
2592
+ BEGIN
2593
+ SELECT INTO query_metadata
2594
+ (regexp_match(
2595
+ current_query(),
2596
+ '^--QUERY_METADATA\\(({.*})', 'n'
2597
+ ))[1]::json;
2598
+
2599
+ PERFORM pg_notify(
2600
+ 'insert',
2601
+ json_build_object(
2602
+ 'table', '${tableName}',
2603
+ 'queryMetadata', query_metadata,
2604
+ 'insertedId', NEW.id,
2605
+ 'record', NEW
2606
+ )::text
2607
+ );
2608
+
2609
+ RETURN NEW;
2610
+ END;
2611
+ $$ LANGUAGE plpgsql;
2612
+
2613
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2614
+ AFTER INSERT ON "${tableName}"
2615
+ FOR EACH ROW
2616
+ EXECUTE FUNCTION notify_${tableName}_insert();
2617
+ `;
1792
2618
  }
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}`);
2619
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2620
+ return `
2621
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2622
+ RETURNS TRIGGER AS $$
2623
+ DECLARE
2624
+ query_metadata JSON;
2625
+ BEGIN
2626
+ SELECT INTO query_metadata
2627
+ (regexp_match(
2628
+ current_query(),
2629
+ '^--QUERY_METADATA\\(({.*})', 'n'
2630
+ ))[1]::json;
2631
+
2632
+ PERFORM pg_notify(
2633
+ 'insert',
2634
+ json_build_object(
2635
+ 'table', '${tableName}',
2636
+ 'queryMetadata', query_metadata,
2637
+ 'insertedId', NEW.id,
2638
+ 'record', json_build_object(
2639
+ ${notifyColumnNewBuildString}
2640
+ )
2641
+ )::text
2642
+ );
2643
+
2644
+ RETURN NEW;
2645
+ END;
2646
+ $$ LANGUAGE plpgsql;
2647
+
2648
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2649
+ AFTER INSERT ON "${tableName}"
2650
+ FOR EACH ROW
2651
+ EXECUTE FUNCTION notify_${tableName}_insert();
2652
+ `;
2653
+ }
2654
+ schemaToPsqlType(column) {
2655
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2656
+ if (column.type === "ENUM") return `TEXT`;
2657
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2658
+ if (column.type === "MEDIUMINT") return "INT";
2659
+ return column.type;
2660
+ }
2661
+ };
2662
+
2663
+ // src/restura/utils/TempCache.ts
2664
+ var import_fs3 = __toESM(require("fs"));
2665
+ var import_path4 = __toESM(require("path"));
2666
+ var import_core_utils6 = require("@redskytech/core-utils");
2667
+ var import_internal3 = require("@restura/internal");
2668
+ var import_bluebird3 = __toESM(require("bluebird"));
2669
+ var os2 = __toESM(require("os"));
2670
+ var TempCache = class {
2671
+ constructor(location) {
2672
+ this.maxDurationDays = 7;
2673
+ this.location = location || os2.tmpdir();
2674
+ import_internal3.FileUtils.ensureDir(this.location).catch((e) => {
2675
+ throw e;
2676
+ });
2677
+ }
2678
+ async cleanup() {
2679
+ const fileList = await import_fs3.default.promises.readdir(this.location);
2680
+ await import_bluebird3.default.map(
2681
+ fileList,
2682
+ async (file) => {
2683
+ const fullFilePath = import_path4.default.join(this.location, file);
2684
+ const fileStats = await import_fs3.default.promises.stat(fullFilePath);
2685
+ if (import_core_utils6.DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2686
+ logger.info(`Deleting old temp file: ${file}`);
2687
+ await import_fs3.default.promises.unlink(fullFilePath);
2688
+ }
2689
+ },
2690
+ { concurrency: 10 }
2691
+ );
1799
2692
  }
1800
2693
  };
1801
2694
 
1802
2695
  // src/restura/restura.ts
1803
- var { types } = import_pg2.default;
1804
2696
  var ResturaEngine = class {
1805
2697
  constructor() {
1806
2698
  this.publicEndpoints = {
@@ -1818,10 +2710,11 @@ var ResturaEngine = class {
1818
2710
  * @returns A promise that resolves when the initialization is complete.
1819
2711
  */
1820
2712
  async init(app, authenticationHandler, psqlConnectionPool) {
1821
- this.resturaConfig = import_internal2.config.validate("restura", resturaConfigSchema);
2713
+ this.resturaConfig = import_internal4.config.validate("restura", resturaConfigSchema);
2714
+ this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
2715
+ new TempCache(this.resturaConfig.fileTempCachePath);
1822
2716
  this.psqlConnectionPool = psqlConnectionPool;
1823
- this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
1824
- setupPgReturnTypes();
2717
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
1825
2718
  await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
1826
2719
  this.authenticationHandler = authenticationHandler;
1827
2720
  app.use((0, import_compression.default)());
@@ -1885,10 +2778,7 @@ var ResturaEngine = class {
1885
2778
  * @returns A promise that resolves when the API has been successfully generated and written to the output file.
1886
2779
  */
1887
2780
  async generateApiFromSchema(outputFile, providedSchema) {
1888
- import_fs3.default.writeFileSync(
1889
- outputFile,
1890
- await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1891
- );
2781
+ import_fs4.default.writeFileSync(outputFile, await apiGenerator(providedSchema));
1892
2782
  }
1893
2783
  /**
1894
2784
  * Generates a model from the provided schema and writes it to the specified output file.
@@ -1898,10 +2788,15 @@ var ResturaEngine = class {
1898
2788
  * @returns A promise that resolves when the model has been successfully written to the output file.
1899
2789
  */
1900
2790
  async generateModelFromSchema(outputFile, providedSchema) {
1901
- import_fs3.default.writeFileSync(
1902
- outputFile,
1903
- await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1904
- );
2791
+ import_fs4.default.writeFileSync(outputFile, await modelGenerator(providedSchema));
2792
+ }
2793
+ /**
2794
+ * Generates the ambient module declaration for Restura global types and writes it to the specified output file.
2795
+ * These types are used sometimes in the CustomTypes
2796
+ * @param outputFile
2797
+ */
2798
+ generateResturaGlobalTypes(outputFile) {
2799
+ import_fs4.default.writeFileSync(outputFile, resturaGlobalTypesGenerator());
1905
2800
  }
1906
2801
  /**
1907
2802
  * Retrieves the latest file system schema for Restura.
@@ -1910,12 +2805,12 @@ var ResturaEngine = class {
1910
2805
  * @throws {Error} If the schema file is missing or the schema is not valid.
1911
2806
  */
1912
2807
  async getLatestFileSystemSchema() {
1913
- if (!import_fs3.default.existsSync(this.resturaConfig.schemaFilePath)) {
2808
+ if (!import_fs4.default.existsSync(this.resturaConfig.schemaFilePath)) {
1914
2809
  logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
1915
2810
  throw new Error("Missing restura schema file");
1916
2811
  }
1917
- const schemaFileData = import_fs3.default.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
1918
- const schema = import_core_utils6.ObjectUtils.safeParse(schemaFileData);
2812
+ const schemaFileData = import_fs4.default.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2813
+ const schema = import_core_utils7.ObjectUtils.safeParse(schemaFileData);
1919
2814
  const isValid = await isSchemaValid(schema);
1920
2815
  if (!isValid) {
1921
2816
  logger.error("Schema is not valid");
@@ -1923,28 +2818,6 @@ var ResturaEngine = class {
1923
2818
  }
1924
2819
  return schema;
1925
2820
  }
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
2821
  async reloadEndpoints() {
1949
2822
  this.schema = await this.getLatestFileSystemSchema();
1950
2823
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
@@ -1973,30 +2846,10 @@ var ResturaEngine = class {
1973
2846
  logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
1974
2847
  }
1975
2848
  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
- );
2849
+ if (!import_fs4.default.existsSync(this.resturaConfig.generatedTypesPath)) {
2850
+ import_fs4.default.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1999
2851
  }
2852
+ this.updateTypes();
2000
2853
  }
2001
2854
  resturaAuthentication(req, res, next) {
2002
2855
  if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
@@ -2004,7 +2857,7 @@ var ResturaEngine = class {
2004
2857
  }
2005
2858
  async previewCreateSchema(req, res) {
2006
2859
  try {
2007
- const schemaDiff = { commands: "", endPoints: [], globalParams: [], roles: [], customTypes: false };
2860
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
2008
2861
  res.send({ data: schemaDiff });
2009
2862
  } catch (err) {
2010
2863
  res.status(400).send(err);
@@ -2012,7 +2865,7 @@ var ResturaEngine = class {
2012
2865
  }
2013
2866
  async updateSchema(req, res) {
2014
2867
  try {
2015
- this.schema = req.data;
2868
+ this.schema = sortObjectKeysAlphabetically(req.data);
2016
2869
  await this.storeFileSystemSchema();
2017
2870
  await this.reloadEndpoints();
2018
2871
  await this.updateTypes();
@@ -2023,11 +2876,12 @@ var ResturaEngine = class {
2023
2876
  }
2024
2877
  }
2025
2878
  async updateTypes() {
2026
- await this.generateApiFromSchema(import_path3.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2879
+ await this.generateApiFromSchema(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2027
2880
  await this.generateModelFromSchema(
2028
- import_path3.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2881
+ import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2029
2882
  this.schema
2030
2883
  );
2884
+ this.generateResturaGlobalTypes(import_path5.default.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
2031
2885
  }
2032
2886
  async getSchema(req, res) {
2033
2887
  res.send({ data: this.schema });
@@ -2035,19 +2889,38 @@ var ResturaEngine = class {
2035
2889
  async getSchemaAndTypes(req, res) {
2036
2890
  try {
2037
2891
  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);
2892
+ const apiText = await apiGenerator(schema);
2893
+ const modelsText = await modelGenerator(schema);
2041
2894
  res.send({ schema, api: apiText, models: modelsText });
2042
2895
  } catch (err) {
2043
2896
  res.status(400).send({ error: err });
2044
2897
  }
2045
2898
  }
2899
+ async getMulterFilesIfAny(req, res, routeData) {
2900
+ var _a2;
2901
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2902
+ if (!this.isCustomRoute(routeData)) return;
2903
+ if (!routeData.fileUploadType) {
2904
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2905
+ }
2906
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2907
+ return new Promise((resolve2, reject) => {
2908
+ multerFileUploadFunction(req, res, (err) => {
2909
+ if (err) {
2910
+ logger.warn("Multer error: " + err);
2911
+ reject(err);
2912
+ }
2913
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2914
+ resolve2();
2915
+ });
2916
+ });
2917
+ }
2046
2918
  async executeRouteLogic(req, res, next) {
2047
2919
  try {
2048
2920
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
2049
2921
  this.validateAuthorization(req, routeData);
2050
- validateRequestParams(req, routeData, this.customTypeValidation);
2922
+ await this.getMulterFilesIfAny(req, res, routeData);
2923
+ requestValidator(req, routeData, this.customTypeValidation);
2051
2924
  if (this.isCustomRoute(routeData)) {
2052
2925
  await this.runCustomRouteLogic(req, res, routeData);
2053
2926
  return;
@@ -2072,33 +2945,21 @@ var ResturaEngine = class {
2072
2945
  let domain = routeData.path.split("/")[1];
2073
2946
  domain = domain.split("-").reduce((acc, value, index) => {
2074
2947
  if (index === 0) acc = value;
2075
- else acc += import_core_utils6.StringUtils.capitalizeFirst(value);
2948
+ else acc += import_core_utils7.StringUtils.capitalizeFirst(value);
2076
2949
  return acc;
2077
2950
  }, "");
2078
- const customApiName = `${import_core_utils6.StringUtils.capitalizeFirst(domain)}Api${import_core_utils6.StringUtils.capitalizeFirst(version)}`;
2951
+ const customApiName = `${import_core_utils7.StringUtils.capitalizeFirst(domain)}Api${import_core_utils7.StringUtils.capitalizeFirst(version)}`;
2079
2952
  const customApi = customApiFactory_default.getCustomApi(customApiName);
2080
2953
  if (!customApi) throw new RsError("NOT_FOUND", `API domain ${domain}-${version} not found`);
2081
2954
  const functionName = `${routeData.method.toLowerCase()}${routeData.path.replace(new RegExp("-", "g"), "/").split("/").reduce((acc, cur) => {
2082
2955
  if (cur === "") return acc;
2083
- return acc + import_core_utils6.StringUtils.capitalizeFirst(cur);
2956
+ return acc + import_core_utils7.StringUtils.capitalizeFirst(cur);
2084
2957
  }, "")}`;
2085
2958
  const customFunction = customApi[functionName];
2086
- if (!customFunction) throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented`);
2959
+ if (!customFunction)
2960
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2087
2961
  await customFunction(req, res, routeData);
2088
2962
  }
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
2963
  async storeFileSystemSchema() {
2103
2964
  const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
2104
2965
  parser: "json"
@@ -2110,7 +2971,7 @@ var ResturaEngine = class {
2110
2971
  printWidth: 120,
2111
2972
  singleQuote: true
2112
2973
  }));
2113
- import_fs3.default.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2974
+ import_fs4.default.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2114
2975
  }
2115
2976
  resetPublicEndpoints() {
2116
2977
  this.publicEndpoints = {
@@ -2127,13 +2988,13 @@ var ResturaEngine = class {
2127
2988
  if (!routeData.roles.includes(role))
2128
2989
  throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
2129
2990
  }
2130
- getRouteData(method, baseUrl, path4) {
2991
+ getRouteData(method, baseUrl, path5) {
2131
2992
  const endpoint = this.schema.endpoints.find((item) => {
2132
2993
  return item.baseUrl === baseUrl;
2133
2994
  });
2134
2995
  if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
2135
2996
  const route = endpoint.routes.find((item) => {
2136
- return item.method === method && item.path === path4;
2997
+ return item.method === method && item.path === path5;
2137
2998
  });
2138
2999
  if (!route) throw new RsError("NOT_FOUND", "Route not found");
2139
3000
  return route;
@@ -2154,6 +3015,9 @@ __decorateClass([
2154
3015
  __decorateClass([
2155
3016
  boundMethod
2156
3017
  ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
3018
+ __decorateClass([
3019
+ boundMethod
3020
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
2157
3021
  __decorateClass([
2158
3022
  boundMethod
2159
3023
  ], ResturaEngine.prototype, "executeRouteLogic", 1);
@@ -2163,25 +3027,55 @@ __decorateClass([
2163
3027
  __decorateClass([
2164
3028
  boundMethod
2165
3029
  ], 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
3030
  var restura = new ResturaEngine();
3031
+
3032
+ // src/restura/sql/PsqlTransaction.ts
3033
+ var import_pg3 = __toESM(require("pg"));
3034
+ var { Client: Client2 } = import_pg3.default;
3035
+ var PsqlTransaction = class extends PsqlConnection {
3036
+ constructor(clientConfig, instanceId) {
3037
+ super(instanceId);
3038
+ this.clientConfig = clientConfig;
3039
+ this.client = new Client2(clientConfig);
3040
+ this.connectPromise = this.client.connect();
3041
+ this.beginTransactionPromise = this.beginTransaction();
3042
+ }
3043
+ async close() {
3044
+ if (this.client) {
3045
+ await this.client.end();
3046
+ }
3047
+ }
3048
+ async beginTransaction() {
3049
+ await this.connectPromise;
3050
+ return this.client.query("BEGIN");
3051
+ }
3052
+ async rollback() {
3053
+ return this.query("ROLLBACK");
3054
+ }
3055
+ async commit() {
3056
+ return this.query("COMMIT");
3057
+ }
3058
+ async release() {
3059
+ return this.client.end();
3060
+ }
3061
+ async query(query, values) {
3062
+ await this.connectPromise;
3063
+ await this.beginTransactionPromise;
3064
+ return this.client.query(query, values);
3065
+ }
3066
+ };
2178
3067
  // Annotate the CommonJS export names for ESM import in node:
2179
3068
  0 && (module.exports = {
2180
3069
  HtmlStatusCodes,
3070
+ PsqlConnection,
3071
+ PsqlEngine,
2181
3072
  PsqlPool,
3073
+ PsqlTransaction,
2182
3074
  RsError,
2183
3075
  SQL,
2184
3076
  escapeColumnName,
3077
+ eventManager,
3078
+ filterPsqlParser,
2185
3079
  insertObjectQuery,
2186
3080
  isValueNumber,
2187
3081
  logger,