@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.mjs CHANGED
@@ -44,18 +44,11 @@ import { config } from "@restura/internal";
44
44
  import winston from "winston";
45
45
  import { format } from "logform";
46
46
 
47
- // src/config.schema.ts
47
+ // src/logger/loggerConfigSchema.ts
48
48
  import { z } from "zod";
49
49
  var loggerConfigSchema = z.object({
50
50
  level: z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
51
51
  });
52
- var resturaConfigSchema = z.object({
53
- authToken: z.string().min(1, "Missing Restura Auth Token"),
54
- sendErrorStackTrace: z.boolean().default(false),
55
- schemaFilePath: z.string().default(process.cwd() + "/restura.schema.json"),
56
- customApiFolderPath: z.string().default(process.cwd() + "/dist/api"),
57
- generatedTypesPath: z.string().default(process.cwd() + "/src/@types")
58
- });
59
52
 
60
53
  // src/logger/logger.ts
61
54
  var loggerConfig = config.validate("logger", loggerConfigSchema);
@@ -92,7 +85,198 @@ var logger = winston.createLogger({
92
85
  ]
93
86
  });
94
87
 
95
- // src/restura/errors.ts
88
+ // src/restura/eventManager.ts
89
+ import Bluebird from "bluebird";
90
+ var EventManager = class {
91
+ constructor() {
92
+ this.actionHandlers = {
93
+ DATABASE_ROW_DELETE: [],
94
+ DATABASE_ROW_INSERT: [],
95
+ DATABASE_COLUMN_UPDATE: []
96
+ };
97
+ }
98
+ addRowInsertHandler(onInsert, filter) {
99
+ this.actionHandlers.DATABASE_ROW_INSERT.push({
100
+ callback: onInsert,
101
+ filter
102
+ });
103
+ }
104
+ addColumnChangeHandler(onUpdate, filter) {
105
+ this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
106
+ callback: onUpdate,
107
+ filter
108
+ });
109
+ }
110
+ addRowDeleteHandler(onDelete, filter) {
111
+ this.actionHandlers.DATABASE_ROW_DELETE.push({
112
+ callback: onDelete,
113
+ filter
114
+ });
115
+ }
116
+ async fireActionFromDbTrigger(sqlMutationData, result) {
117
+ if (sqlMutationData.mutationType === "INSERT") {
118
+ await this.fireInsertActions(sqlMutationData, result);
119
+ } else if (sqlMutationData.mutationType === "UPDATE") {
120
+ await this.fireUpdateActions(sqlMutationData, result);
121
+ } else if (sqlMutationData.mutationType === "DELETE") {
122
+ await this.fireDeleteActions(sqlMutationData, result);
123
+ }
124
+ }
125
+ async fireInsertActions(data, triggerResult) {
126
+ await Bluebird.map(
127
+ this.actionHandlers.DATABASE_ROW_INSERT,
128
+ ({ callback, filter }) => {
129
+ if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
130
+ const insertData = {
131
+ tableName: triggerResult.table,
132
+ insertedId: triggerResult.insertedId || 0,
133
+ insertObject: triggerResult.record,
134
+ queryMetadata: data.queryMetadata
135
+ };
136
+ callback(insertData, data.queryMetadata);
137
+ },
138
+ { concurrency: 10 }
139
+ );
140
+ }
141
+ async fireDeleteActions(data, triggerResult) {
142
+ await Bluebird.map(
143
+ this.actionHandlers.DATABASE_ROW_DELETE,
144
+ ({ callback, filter }) => {
145
+ if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
146
+ const deleteData = {
147
+ tableName: triggerResult.table,
148
+ deletedId: triggerResult.deletedId || 0,
149
+ deletedRow: triggerResult.previousRecord,
150
+ queryMetadata: data.queryMetadata
151
+ };
152
+ callback(deleteData, data.queryMetadata);
153
+ },
154
+ { concurrency: 10 }
155
+ );
156
+ }
157
+ async fireUpdateActions(data, triggerResult) {
158
+ await Bluebird.map(
159
+ this.actionHandlers.DATABASE_COLUMN_UPDATE,
160
+ ({ callback, filter }) => {
161
+ if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
162
+ const columnChangeData = {
163
+ tableName: triggerResult.table,
164
+ changedId: triggerResult.changedId || 0,
165
+ newData: triggerResult.record,
166
+ oldData: triggerResult.previousRecord,
167
+ queryMetadata: data.queryMetadata
168
+ };
169
+ callback(columnChangeData, data.queryMetadata);
170
+ },
171
+ { concurrency: 10 }
172
+ );
173
+ }
174
+ hasHandlersForEventType(eventType, filter, triggerResult) {
175
+ if (filter) {
176
+ switch (eventType) {
177
+ case "DATABASE_ROW_INSERT":
178
+ case "DATABASE_ROW_DELETE":
179
+ if (filter.tableName && filter.tableName !== triggerResult.table) return false;
180
+ break;
181
+ case "DATABASE_COLUMN_UPDATE":
182
+ const filterColumnChange = filter;
183
+ if (filterColumnChange.tableName !== triggerResult.table) return false;
184
+ if (filterColumnChange.columns.length === 1) {
185
+ const firstColumn = filterColumnChange.columns[0];
186
+ if (firstColumn === "*") return true;
187
+ }
188
+ if (!filterColumnChange.columns.some((item) => {
189
+ const updatedColumns = Object.keys(
190
+ changedValues(triggerResult.record, triggerResult.previousRecord)
191
+ );
192
+ return updatedColumns.includes(item);
193
+ }))
194
+ return false;
195
+ break;
196
+ }
197
+ }
198
+ return true;
199
+ }
200
+ };
201
+ var eventManager = new EventManager();
202
+ var eventManager_default = eventManager;
203
+ function changedValues(record, previousRecord) {
204
+ const changed = {};
205
+ for (const i in previousRecord) {
206
+ if (previousRecord[i] !== record[i]) {
207
+ if (typeof previousRecord[i] === "object" && typeof record[i] === "object") {
208
+ const nestedChanged = changedValues(record[i], previousRecord[i]);
209
+ if (Object.keys(nestedChanged).length > 0) {
210
+ changed[i] = record[i];
211
+ }
212
+ } else {
213
+ changed[i] = record[i];
214
+ }
215
+ }
216
+ }
217
+ return changed;
218
+ }
219
+
220
+ // src/restura/restura.ts
221
+ import { ObjectUtils as ObjectUtils5, StringUtils as StringUtils3 } from "@redskytech/core-utils";
222
+ import { config as config2 } from "@restura/internal";
223
+
224
+ // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
225
+ function _typeof(obj) {
226
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
227
+ _typeof = function _typeof2(obj2) {
228
+ return typeof obj2;
229
+ };
230
+ } else {
231
+ _typeof = function _typeof2(obj2) {
232
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
233
+ };
234
+ }
235
+ return _typeof(obj);
236
+ }
237
+ function boundMethod(target, key, descriptor) {
238
+ var fn = descriptor.value;
239
+ if (typeof fn !== "function") {
240
+ throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
241
+ }
242
+ var definingProperty = false;
243
+ return {
244
+ configurable: true,
245
+ get: function get() {
246
+ if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
247
+ return fn;
248
+ }
249
+ var boundFn = fn.bind(this);
250
+ definingProperty = true;
251
+ Object.defineProperty(this, key, {
252
+ configurable: true,
253
+ get: function get2() {
254
+ return boundFn;
255
+ },
256
+ set: function set(value) {
257
+ fn = value;
258
+ delete this[key];
259
+ }
260
+ });
261
+ definingProperty = false;
262
+ return boundFn;
263
+ },
264
+ set: function set(value) {
265
+ fn = value;
266
+ }
267
+ };
268
+ }
269
+
270
+ // src/restura/restura.ts
271
+ import bodyParser from "body-parser";
272
+ import compression from "compression";
273
+ import cookieParser from "cookie-parser";
274
+ import * as express from "express";
275
+ import fs4 from "fs";
276
+ import path4 from "path";
277
+ import * as prettier3 from "prettier";
278
+
279
+ // src/restura/RsError.ts
96
280
  var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
97
281
  HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
98
282
  HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
@@ -116,7 +300,6 @@ var RsError = class _RsError {
116
300
  static htmlStatus(code) {
117
301
  return htmlStatusMap[code];
118
302
  }
119
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
303
  static isRsError(error) {
121
304
  return error instanceof _RsError;
122
305
  }
@@ -158,73 +341,171 @@ var htmlStatusMap = {
158
341
  SCHEMA_ERROR: 500 /* SERVER_ERROR */
159
342
  };
160
343
 
161
- // src/restura/restura.ts
162
- import { ObjectUtils as ObjectUtils5, StringUtils as StringUtils3 } from "@redskytech/core-utils";
163
- import { config as config2 } from "@restura/internal";
164
-
165
- // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
166
- function _typeof(obj) {
167
- if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
168
- _typeof = function _typeof2(obj2) {
169
- return typeof obj2;
170
- };
171
- } else {
172
- _typeof = function _typeof2(obj2) {
173
- return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
344
+ // src/restura/compareSchema.ts
345
+ import cloneDeep from "lodash.clonedeep";
346
+ var CompareSchema = class {
347
+ async diffSchema(newSchema, latestSchema, psqlEngine) {
348
+ const endPoints = this.diffEndPoints(newSchema.endpoints[0].routes, latestSchema.endpoints[0].routes);
349
+ const globalParams = this.diffStringArray(newSchema.globalParams, latestSchema.globalParams);
350
+ const roles = this.diffStringArray(newSchema.roles, latestSchema.roles);
351
+ let commands = "";
352
+ if (JSON.stringify(newSchema.database) !== JSON.stringify(latestSchema.database))
353
+ commands = await psqlEngine.diffDatabaseToSchema(newSchema);
354
+ const hasCustomTypesChanged = JSON.stringify(newSchema.customTypes) !== JSON.stringify(latestSchema.customTypes);
355
+ const schemaPreview = {
356
+ endPoints,
357
+ globalParams,
358
+ roles,
359
+ commands,
360
+ customTypes: hasCustomTypesChanged
174
361
  };
175
- }
176
- return _typeof(obj);
177
- }
178
- function boundMethod(target, key, descriptor) {
179
- var fn = descriptor.value;
180
- if (typeof fn !== "function") {
181
- throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
182
- }
183
- var definingProperty = false;
184
- return {
185
- configurable: true,
186
- get: function get() {
187
- if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
188
- return fn;
362
+ return schemaPreview;
363
+ }
364
+ diffStringArray(newArray, originalArray) {
365
+ const stringsDiff = [];
366
+ const originalClone = new Set(originalArray);
367
+ newArray.forEach((item) => {
368
+ const originalIndex = originalClone.has(item);
369
+ if (!originalIndex) {
370
+ stringsDiff.push({
371
+ name: item,
372
+ changeType: "NEW"
373
+ });
374
+ } else {
375
+ originalClone.delete(item);
189
376
  }
190
- var boundFn = fn.bind(this);
191
- definingProperty = true;
192
- Object.defineProperty(this, key, {
193
- configurable: true,
194
- get: function get2() {
195
- return boundFn;
196
- },
197
- set: function set(value) {
198
- fn = value;
199
- delete this[key];
377
+ });
378
+ originalClone.forEach((item) => {
379
+ stringsDiff.push({
380
+ name: item,
381
+ changeType: "DELETED"
382
+ });
383
+ });
384
+ return stringsDiff;
385
+ }
386
+ diffEndPoints(newEndPoints, originalEndpoints) {
387
+ const originalClone = cloneDeep(originalEndpoints);
388
+ const diffObj = [];
389
+ newEndPoints.forEach((endPoint) => {
390
+ const { path: path5, method } = endPoint;
391
+ const endPointIndex = originalClone.findIndex((original) => {
392
+ return original.path === endPoint.path && original.method === endPoint.method;
393
+ });
394
+ if (endPointIndex === -1) {
395
+ diffObj.push({
396
+ name: `${method} ${path5}`,
397
+ changeType: "NEW"
398
+ });
399
+ } else {
400
+ const original = originalClone.findIndex((original2) => {
401
+ return this.compareEndPoints(endPoint, original2);
402
+ });
403
+ if (original === -1) {
404
+ diffObj.push({
405
+ name: `${method} ${path5}`,
406
+ changeType: "MODIFIED"
407
+ });
200
408
  }
409
+ originalClone.splice(endPointIndex, 1);
410
+ }
411
+ });
412
+ originalClone.forEach((original) => {
413
+ const { path: path5, method } = original;
414
+ diffObj.push({
415
+ name: `${method} ${path5}`,
416
+ changeType: "DELETED"
201
417
  });
202
- definingProperty = false;
203
- return boundFn;
204
- },
205
- set: function set(value) {
206
- fn = value;
418
+ });
419
+ return diffObj;
420
+ }
421
+ compareEndPoints(endPoint1, endPoint2) {
422
+ return JSON.stringify(endPoint1) === JSON.stringify(endPoint2);
423
+ }
424
+ };
425
+ __decorateClass([
426
+ boundMethod
427
+ ], CompareSchema.prototype, "diffSchema", 1);
428
+ __decorateClass([
429
+ boundMethod
430
+ ], CompareSchema.prototype, "diffStringArray", 1);
431
+ __decorateClass([
432
+ boundMethod
433
+ ], CompareSchema.prototype, "diffEndPoints", 1);
434
+ __decorateClass([
435
+ boundMethod
436
+ ], CompareSchema.prototype, "compareEndPoints", 1);
437
+ var compareSchema = new CompareSchema();
438
+ var compareSchema_default = compareSchema;
439
+
440
+ // src/restura/customApiFactory.ts
441
+ import Bluebird2 from "bluebird";
442
+ import fs from "fs";
443
+ import path from "path";
444
+ import { FileUtils } from "@restura/internal";
445
+ var CustomApiFactory = class {
446
+ constructor() {
447
+ this.customApis = {};
448
+ }
449
+ async loadApiFiles(baseFolderPath) {
450
+ const apiVersions = ["v1"];
451
+ for (const apiVersion of apiVersions) {
452
+ const apiVersionFolderPath = path.join(baseFolderPath, apiVersion);
453
+ const directoryExists = await FileUtils.existDir(apiVersionFolderPath);
454
+ if (!directoryExists) continue;
455
+ await this.addDirectory(apiVersionFolderPath, apiVersion);
207
456
  }
208
- };
209
- }
457
+ }
458
+ getCustomApi(customApiName) {
459
+ return this.customApis[customApiName];
460
+ }
461
+ async addDirectory(directoryPath, apiVersion) {
462
+ var _a2;
463
+ const entries = await fs.promises.readdir(directoryPath, {
464
+ withFileTypes: true
465
+ });
466
+ const isTsx2 = (_a2 = process.argv[1]) == null ? void 0 : _a2.endsWith(".ts");
467
+ const isTsNode2 = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
468
+ const extension = isTsx2 || isTsNode2 ? "ts" : "js";
469
+ const shouldEndWith = `.api.${apiVersion}.${extension}`;
470
+ await Bluebird2.map(entries, async (entry) => {
471
+ if (entry.isFile()) {
472
+ if (entry.name.endsWith(shouldEndWith) === false) return;
473
+ try {
474
+ const importPath = `${path.join(directoryPath, entry.name)}`;
475
+ const ApiImport = await import(importPath);
476
+ const customApiClass = new ApiImport.default();
477
+ logger.info(`Registering custom API: ${ApiImport.default.name}`);
478
+ this.bindMethodsToInstance(customApiClass);
479
+ this.customApis[ApiImport.default.name] = customApiClass;
480
+ } catch (e) {
481
+ logger.error(e);
482
+ }
483
+ }
484
+ });
485
+ }
486
+ bindMethodsToInstance(instance) {
487
+ const proto = Object.getPrototypeOf(instance);
488
+ Object.getOwnPropertyNames(proto).forEach((key) => {
489
+ const property = instance[key];
490
+ if (typeof property === "function" && key !== "constructor") {
491
+ instance[key] = property.bind(instance);
492
+ }
493
+ });
494
+ }
495
+ };
496
+ var customApiFactory = new CustomApiFactory();
497
+ var customApiFactory_default = customApiFactory;
210
498
 
211
- // src/restura/restura.ts
212
- import bodyParser from "body-parser";
213
- import compression from "compression";
214
- import cookieParser from "cookie-parser";
215
- import { createHash } from "crypto";
216
- import * as express from "express";
217
- import fs3 from "fs";
218
- import path3 from "path";
219
- import pg2 from "pg";
220
- import * as prettier3 from "prettier";
499
+ // src/restura/generators/apiGenerator.ts
500
+ import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
501
+ import prettier from "prettier";
221
502
 
222
503
  // src/restura/sql/SqlUtils.ts
223
504
  var SqlUtils = class _SqlUtils {
224
505
  static convertDatabaseTypeToTypescript(type, value) {
225
506
  type = type.toLocaleLowerCase();
226
507
  if (type.startsWith("tinyint") || type.startsWith("boolean")) return "boolean";
227
- if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float"))
508
+ 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"))
228
509
  return "number";
229
510
  if (type === "json") {
230
511
  if (!value) return "object";
@@ -245,7 +526,7 @@ var SqlUtils = class _SqlUtils {
245
526
  }
246
527
  };
247
528
 
248
- // src/restura/ResponseValidator.ts
529
+ // src/restura/validators/ResponseValidator.ts
249
530
  var ResponseValidator = class _ResponseValidator {
250
531
  constructor(schema) {
251
532
  this.database = schema.database;
@@ -310,9 +591,9 @@ var ResponseValidator = class _ResponseValidator {
310
591
  return { validator: "any" };
311
592
  }
312
593
  getTypeFromTable(selector, name) {
313
- const path4 = selector.split(".");
314
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { validator: "any", isOptional: false };
315
- const tableName = path4.length == 2 ? path4[0] : name, columnName = path4.length == 2 ? path4[1] : path4[0];
594
+ const path5 = selector.split(".");
595
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { validator: "any", isOptional: false };
596
+ const tableName = path5.length == 2 ? path5[0] : name, columnName = path5.length == 2 ? path5[1] : path5[0];
316
597
  const table = this.database.find((t) => t.name == tableName);
317
598
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
318
599
  if (!table || !column) return { validator: "any", isOptional: false };
@@ -394,9 +675,7 @@ var ResponseValidator = class _ResponseValidator {
394
675
  }
395
676
  };
396
677
 
397
- // src/restura/apiGenerator.ts
398
- import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
399
- import prettier from "prettier";
678
+ // src/restura/generators/apiGenerator.ts
400
679
  var ApiTree = class _ApiTree {
401
680
  constructor(namespace, database) {
402
681
  this.database = database;
@@ -494,7 +773,7 @@ var ApiTree = class _ApiTree {
494
773
  break;
495
774
  }
496
775
  }
497
- return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
776
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
498
777
  }).join(";\n")}${ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
499
778
  `;
500
779
  modelString += `}`;
@@ -508,30 +787,37 @@ var ApiTree = class _ApiTree {
508
787
  return `export type Res = CustomTypes.${route.responseType}[]`;
509
788
  else return `export type Res = CustomTypes.${route.responseType}`;
510
789
  }
511
- return `export interface Res ${this.getFields(route.response)}`;
790
+ return `export interface Res ${this.getFields(route.response, route.table, route.joins)}`;
512
791
  }
513
- getFields(fields) {
514
- const nameFields = fields.map((f) => this.getNameAndType(f));
792
+ getFields(fields, routeBaseTable, joins) {
793
+ const nameFields = fields.map((f) => this.getNameAndType(f, routeBaseTable, joins));
515
794
  const nested = `{
516
795
  ${nameFields.join(";\n ")}${ObjectUtils.isArrayWithData(nameFields) ? ";" : ""}
517
796
  }`;
518
797
  return nested;
519
798
  }
520
- getNameAndType(p) {
521
- let responseType = "any", optional = false, array = false;
799
+ getNameAndType(p, routeBaseTable, joins) {
800
+ let responseType = "any", isNullable = false, array = false;
522
801
  if (p.selector) {
523
- ({ responseType, optional } = this.getTypeFromTable(p.selector, p.name));
802
+ ({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
803
+ const selectorKey = p.selector.split(".")[0];
804
+ if (selectorKey !== routeBaseTable) {
805
+ const join = joins.find((j) => j.alias === selectorKey);
806
+ if (join && join.type !== "INNER") {
807
+ isNullable = true;
808
+ }
809
+ }
524
810
  } else if (p.subquery) {
525
- responseType = this.getFields(p.subquery.properties);
811
+ responseType = this.getFields(p.subquery.properties, p.subquery.table, p.subquery.joins);
526
812
  array = true;
527
813
  }
528
- return `${p.name}${optional ? "?" : ""}:${responseType}${array ? "[]" : ""}`;
814
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
529
815
  }
530
816
  getTypeFromTable(selector, name) {
531
- const path4 = selector.split(".");
532
- if (path4.length === 0 || path4.length > 2 || path4[0] === "") return { responseType: "any", optional: false };
533
- let tableName = path4.length == 2 ? path4[0] : name;
534
- const columnName = path4.length == 2 ? path4[1] : path4[0];
817
+ const path5 = selector.split(".");
818
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { responseType: "any", isNullable: false };
819
+ let tableName = path5.length == 2 ? path5[0] : name;
820
+ const columnName = path5.length == 2 ? path5[1] : path5[0];
535
821
  let table = this.database.find((t) => t.name == tableName);
536
822
  if (!table && tableName.includes("_")) {
537
823
  const tableAliasSplit = tableName.split("_");
@@ -539,18 +825,19 @@ var ApiTree = class _ApiTree {
539
825
  table = this.database.find((t) => t.name == tableName);
540
826
  }
541
827
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
542
- if (!table || !column) return { responseType: "any", optional: false };
828
+ if (!table || !column) return { responseType: "any", isNullable: false };
543
829
  return {
544
830
  responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
545
- optional: column.roles.length > 0 || column.isNullable
831
+ isNullable: column.roles.length > 0 || column.isNullable
546
832
  };
547
833
  }
548
834
  };
549
- function pathToNamespaces(path4) {
550
- return path4.split("/").map((e) => StringUtils.toPascalCasing(e)).filter((e) => e);
835
+ function pathToNamespaces(path5) {
836
+ return path5.split("/").map((e) => StringUtils.toPascalCasing(e)).filter((e) => e);
551
837
  }
552
- function apiGenerator(schema, schemaHash) {
553
- let apiString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/`;
838
+ function apiGenerator(schema) {
839
+ let apiString = `/** Auto generated file. DO NOT MODIFY **/
840
+ `;
554
841
  const rootNamespace = ApiTree.createRootNode(schema.database);
555
842
  for (const endpoint of schema.endpoints) {
556
843
  const endpointNamespaces = pathToNamespaces(endpoint.baseUrl);
@@ -565,7 +852,7 @@ function apiGenerator(schema, schemaHash) {
565
852
  apiString += `
566
853
 
567
854
  declare namespace CustomTypes {
568
- ${schema.customTypes}
855
+ ${schema.customTypes.join("\n")}
569
856
  }`;
570
857
  }
571
858
  return prettier.format(apiString, __spreadValues({
@@ -580,79 +867,32 @@ function apiGenerator(schema, schemaHash) {
580
867
  }));
581
868
  }
582
869
 
583
- // src/restura/customApiFactory.ts
584
- import fs from "fs";
585
- import path from "path";
586
- var CustomApiFactory = class {
587
- constructor() {
588
- this.customApis = {};
589
- }
590
- async loadApiFiles(baseFolderPath) {
591
- const apiVersions = ["v1"];
592
- for (const apiVersion of apiVersions) {
593
- const apiVersionFolderPath = path.join(baseFolderPath, apiVersion);
594
- if (!fs.existsSync(apiVersionFolderPath)) continue;
595
- await this.addDirectory(apiVersionFolderPath, apiVersion);
596
- }
597
- }
598
- getCustomApi(customApiName) {
599
- return this.customApis[customApiName];
600
- }
601
- async addDirectory(directoryPath, apiVersion) {
602
- const entries = fs.readdirSync(directoryPath, {
603
- withFileTypes: true
604
- });
605
- for (const entry of entries) {
606
- if (entry.isFile()) {
607
- if (entry.name.endsWith(`.api.${apiVersion}.js`) === false) continue;
608
- try {
609
- const importPath = `${path.join(directoryPath, entry.name)}`;
610
- const ApiImport = await import(importPath);
611
- const customApiClass = new ApiImport.default();
612
- logger.info(`Registering custom API: ${ApiImport.default.name}`);
613
- this.bindMethodsToInstance(customApiClass);
614
- this.customApis[ApiImport.default.name] = customApiClass;
615
- } catch (e) {
616
- console.error(e);
617
- }
618
- }
619
- }
620
- }
621
- bindMethodsToInstance(instance) {
622
- const proto = Object.getPrototypeOf(instance);
623
- Object.getOwnPropertyNames(proto).forEach((key) => {
624
- const property = instance[key];
625
- if (typeof property === "function" && key !== "constructor") {
626
- instance[key] = property.bind(instance);
627
- }
628
- });
629
- }
630
- };
631
- var customApiFactory = new CustomApiFactory();
632
- var customApiFactory_default = customApiFactory;
633
-
634
- // src/restura/customTypeValidationGenerator.ts
870
+ // src/restura/generators/customTypeValidationGenerator.ts
635
871
  import fs2 from "fs";
636
- import * as TJS from "typescript-json-schema";
637
872
  import path2, { resolve } from "path";
638
873
  import tmp from "tmp";
639
- import * as process2 from "process";
874
+ import * as TJS from "typescript-json-schema";
640
875
  function customTypeValidationGenerator(currentSchema) {
641
876
  const schemaObject = {};
642
- const customInterfaceNames = currentSchema.customTypes.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
877
+ const customInterfaceNames = currentSchema.customTypes.map((customType) => {
878
+ const matches = customType.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
879
+ if (matches && matches.length > 0) return matches[0];
880
+ return "";
881
+ }).filter(Boolean);
643
882
  if (!customInterfaceNames) return {};
644
883
  const temporaryFile = tmp.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
645
- fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes);
884
+ fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes.join("\n"));
646
885
  const compilerOptions = {
647
886
  strictNullChecks: true,
648
887
  skipLibCheck: true
888
+ // Needed if we are processing ES modules
649
889
  };
650
890
  const program = TJS.getProgramFromFiles(
651
891
  [
652
892
  resolve(temporaryFile.name),
653
- // find a way to remove
654
- path2.join(process2.cwd(), "src/@types/models.d.ts"),
655
- path2.join(process2.cwd(), "src/@types/api.d.ts")
893
+ path2.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"),
894
+ path2.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"),
895
+ path2.join(restura.resturaConfig.generatedTypesPath, "api.d.ts")
656
896
  ],
657
897
  compilerOptions
658
898
  );
@@ -666,6 +906,61 @@ function customTypeValidationGenerator(currentSchema) {
666
906
  return schemaObject;
667
907
  }
668
908
 
909
+ // src/restura/generators/modelGenerator.ts
910
+ import { StringUtils as StringUtils2 } from "@redskytech/core-utils";
911
+ import prettier2 from "prettier";
912
+ function modelGenerator(schema) {
913
+ let modelString = `/** Auto generated file. DO NOT MODIFY **/
914
+
915
+ `;
916
+ modelString += `declare namespace Model {
917
+ `;
918
+ for (const table of schema.database) {
919
+ modelString += convertTable(table);
920
+ }
921
+ modelString += `}`;
922
+ return prettier2.format(modelString, __spreadValues({
923
+ parser: "typescript"
924
+ }, {
925
+ trailingComma: "none",
926
+ tabWidth: 4,
927
+ useTabs: true,
928
+ endOfLine: "lf",
929
+ printWidth: 120,
930
+ singleQuote: true
931
+ }));
932
+ }
933
+ function convertTable(table) {
934
+ let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
935
+ `;
936
+ for (const column of table.columns) {
937
+ modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
938
+ `;
939
+ }
940
+ modelString += ` }
941
+ `;
942
+ return modelString;
943
+ }
944
+
945
+ // src/restura/generators/resturaGlobalTypesGenerator.ts
946
+ function resturaGlobalTypesGenerator() {
947
+ return `/** Auto generated file. DO NOT MODIFY **/
948
+ /** This file contains types that may be used in the CustomTypes of Restura **/
949
+ /** For example export interface MyPagedQuery extends Restura.PageQuery { } **/
950
+
951
+ declare namespace Restura {
952
+ export type StandardOrderTypes = 'ASC' | 'DESC' | 'RAND' | 'NONE';
953
+ export interface PageQuery {
954
+ page?: number;
955
+ perPage?: number;
956
+ sortBy?: string;
957
+ sortOrder?: StandardOrderTypes;
958
+ filter?: string;
959
+ }
960
+ }
961
+ `;
962
+ }
963
+
669
964
  // src/restura/middleware/addApiResponseFunctions.ts
670
965
  function addApiResponseFunctions(req, res, next) {
671
966
  res.sendData = function(data, statusCode = 200) {
@@ -698,16 +993,41 @@ function addApiResponseFunctions(req, res, next) {
698
993
  function authenticateUser(applicationAuthenticateHandler) {
699
994
  return (req, res, next) => {
700
995
  applicationAuthenticateHandler(req, res, (userDetails) => {
701
- req.requesterDetails = __spreadValues(__spreadValues({}, req.requesterDetails), userDetails);
996
+ req.requesterDetails = __spreadValues({ host: req.hostname, ipAddress: req.ip || "" }, userDetails);
702
997
  next();
703
998
  });
704
999
  };
705
1000
  }
706
1001
 
707
- // src/restura/restura.schema.ts
1002
+ // src/restura/middleware/getMulterUpload.ts
1003
+ import multer from "multer";
1004
+ import * as os from "os";
1005
+ import { extname } from "path";
1006
+ var OneHundredMB = 100 * 1024 * 1024;
1007
+ var commonUpload = null;
1008
+ var getMulterUpload = (directory) => {
1009
+ if (commonUpload) return commonUpload;
1010
+ const storage = multer.diskStorage({
1011
+ destination: directory || os.tmpdir(),
1012
+ filename: function(request, file, cb) {
1013
+ const extension = extname(file.originalname);
1014
+ const uniqueName = Date.now() + "-" + Math.round(Math.random() * 1e3);
1015
+ cb(null, `${uniqueName}${extension}`);
1016
+ }
1017
+ });
1018
+ commonUpload = multer({
1019
+ storage,
1020
+ limits: {
1021
+ fileSize: OneHundredMB
1022
+ }
1023
+ });
1024
+ return commonUpload;
1025
+ };
1026
+
1027
+ // src/restura/schemas/resturaSchema.ts
708
1028
  import { z as z3 } from "zod";
709
1029
 
710
- // src/restura/types/validation.types.ts
1030
+ // src/restura/schemas/validatorDataSchema.ts
711
1031
  import { z as z2 } from "zod";
712
1032
  var validatorDataSchemeValue = z2.union([z2.string(), z2.array(z2.string()), z2.number(), z2.array(z2.number())]);
713
1033
  var validatorDataSchema = z2.object({
@@ -715,7 +1035,7 @@ var validatorDataSchema = z2.object({
715
1035
  value: validatorDataSchemeValue
716
1036
  }).strict();
717
1037
 
718
- // src/restura/restura.schema.ts
1038
+ // src/restura/schemas/resturaSchema.ts
719
1039
  var orderBySchema = z3.object({
720
1040
  columnName: z3.string(),
721
1041
  order: z3.enum(["ASC", "DESC"]),
@@ -728,7 +1048,7 @@ var groupBySchema = z3.object({
728
1048
  var whereDataSchema = z3.object({
729
1049
  tableName: z3.string().optional(),
730
1050
  columnName: z3.string().optional(),
731
- operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH"]).optional(),
1051
+ operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH", "IS", "IS NOT"]).optional(),
732
1052
  value: z3.string().or(z3.number()).optional(),
733
1053
  custom: z3.string().optional(),
734
1054
  conjunction: z3.enum(["AND", "OR"]).optional()
@@ -748,6 +1068,7 @@ var joinDataSchema = z3.object({
748
1068
  var requestDataSchema = z3.object({
749
1069
  name: z3.string(),
750
1070
  required: z3.boolean(),
1071
+ isNullable: z3.boolean().optional(),
751
1072
  validator: z3.array(validatorDataSchema)
752
1073
  }).strict();
753
1074
  var responseDataSchema = z3.object({
@@ -833,6 +1154,12 @@ var postgresColumnDateTypesSchema = z3.enum([
833
1154
  "INTERVAL"
834
1155
  // time span
835
1156
  ]);
1157
+ var postgresColumnJsonTypesSchema = z3.enum([
1158
+ "JSON",
1159
+ // stores JSON data as raw text
1160
+ "JSONB"
1161
+ // stores JSON data in a binary format, optimized for query performance
1162
+ ]);
836
1163
  var mariaDbColumnNumericTypesSchema = z3.enum([
837
1164
  "BOOLEAN",
838
1165
  // 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
@@ -895,6 +1222,7 @@ var columnDataSchema = z3.object({
895
1222
  postgresColumnNumericTypesSchema,
896
1223
  postgresColumnStringTypesSchema,
897
1224
  postgresColumnDateTypesSchema,
1225
+ postgresColumnJsonTypesSchema,
898
1226
  mariaDbColumnNumericTypesSchema,
899
1227
  mariaDbColumnStringTypesSchema,
900
1228
  mariaDbColumnDateTypesSchema
@@ -946,7 +1274,8 @@ var tableDataSchema = z3.object({
946
1274
  indexes: z3.array(indexDataSchema),
947
1275
  foreignKeys: z3.array(foreignKeyDataSchema),
948
1276
  checkConstraints: z3.array(checkConstraintDataSchema),
949
- roles: z3.array(z3.string())
1277
+ roles: z3.array(z3.string()),
1278
+ notify: z3.union([z3.literal("ALL"), z3.array(z3.string())]).optional()
950
1279
  }).strict();
951
1280
  var endpointDataSchema = z3.object({
952
1281
  name: z3.string(),
@@ -954,16 +1283,16 @@ var endpointDataSchema = z3.object({
954
1283
  baseUrl: z3.string(),
955
1284
  routes: z3.array(z3.union([standardRouteSchema, customRouteSchema]))
956
1285
  }).strict();
957
- var resturaZodSchema = z3.object({
1286
+ var resturaSchema = z3.object({
958
1287
  database: z3.array(tableDataSchema),
959
1288
  endpoints: z3.array(endpointDataSchema),
960
1289
  globalParams: z3.array(z3.string()),
961
1290
  roles: z3.array(z3.string()),
962
- customTypes: z3.string()
1291
+ customTypes: z3.array(z3.string())
963
1292
  }).strict();
964
1293
  async function isSchemaValid(schemaToCheck) {
965
1294
  try {
966
- resturaZodSchema.parse(schemaToCheck);
1295
+ resturaSchema.parse(schemaToCheck);
967
1296
  return true;
968
1297
  } catch (error) {
969
1298
  logger.error(error);
@@ -971,12 +1300,12 @@ async function isSchemaValid(schemaToCheck) {
971
1300
  }
972
1301
  }
973
1302
 
974
- // src/restura/validateRequestParams.ts
1303
+ // src/restura/validators/requestValidator.ts
975
1304
  import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
976
1305
  import jsonschema from "jsonschema";
977
1306
  import { z as z4 } from "zod";
978
1307
 
979
- // src/restura/utils/addQuotesToStrings.ts
1308
+ // src/restura/utils/utils.ts
980
1309
  function addQuotesToStrings(variable) {
981
1310
  if (typeof variable === "string") {
982
1311
  return `'${variable}'`;
@@ -987,9 +1316,20 @@ function addQuotesToStrings(variable) {
987
1316
  return variable;
988
1317
  }
989
1318
  }
1319
+ function sortObjectKeysAlphabetically(obj) {
1320
+ if (Array.isArray(obj)) {
1321
+ return obj.map(sortObjectKeysAlphabetically);
1322
+ } else if (obj !== null && typeof obj === "object") {
1323
+ return Object.keys(obj).sort().reduce((sorted, key) => {
1324
+ sorted[key] = sortObjectKeysAlphabetically(obj[key]);
1325
+ return sorted;
1326
+ }, {});
1327
+ }
1328
+ return obj;
1329
+ }
990
1330
 
991
- // src/restura/validateRequestParams.ts
992
- function validateRequestParams(req, routeData, validationSchema) {
1331
+ // src/restura/validators/requestValidator.ts
1332
+ function requestValidator(req, routeData, validationSchema) {
993
1333
  const requestData = getRequestData(req);
994
1334
  req.data = requestData;
995
1335
  if (routeData.request === void 0) {
@@ -1023,6 +1363,7 @@ function validateRequestParams(req, routeData, validationSchema) {
1023
1363
  });
1024
1364
  }
1025
1365
  function validateRequestSingleParam(requestValue, requestParam) {
1366
+ if (requestParam.isNullable && requestValue === null) return;
1026
1367
  requestParam.validator.forEach((validator) => {
1027
1368
  switch (validator.type) {
1028
1369
  case "TYPE_CHECK":
@@ -1143,9 +1484,15 @@ function getRequestData(req) {
1143
1484
  bodyData[attr] = attrList;
1144
1485
  }
1145
1486
  } else {
1146
- bodyData[attr] = ObjectUtils2.safeParse(bodyData[attr]);
1147
- if (isNaN(Number(bodyData[attr]))) continue;
1148
- bodyData[attr] = Number(bodyData[attr]);
1487
+ if (bodyData[attr] === "true") {
1488
+ bodyData[attr] = true;
1489
+ } else if (bodyData[attr] === "false") {
1490
+ bodyData[attr] = false;
1491
+ } else {
1492
+ bodyData[attr] = ObjectUtils2.safeParse(bodyData[attr]);
1493
+ if (isNaN(Number(bodyData[attr]))) continue;
1494
+ bodyData[attr] = Number(bodyData[attr]);
1495
+ }
1149
1496
  }
1150
1497
  }
1151
1498
  }
@@ -1156,7 +1503,7 @@ function getRequestData(req) {
1156
1503
  async function schemaValidation(req, res, next) {
1157
1504
  req.data = getRequestData(req);
1158
1505
  try {
1159
- resturaZodSchema.parse(req.data);
1506
+ resturaSchema.parse(req.data);
1160
1507
  next();
1161
1508
  } catch (error) {
1162
1509
  logger.error(error);
@@ -1164,43 +1511,33 @@ async function schemaValidation(req, res, next) {
1164
1511
  }
1165
1512
  }
1166
1513
 
1167
- // src/restura/modelGenerator.ts
1168
- import { StringUtils as StringUtils2 } from "@redskytech/core-utils";
1169
- import prettier2 from "prettier";
1170
- function modelGenerator(schema, schemaHash) {
1171
- let modelString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/
1172
- `;
1173
- modelString += `declare namespace Model {
1174
- `;
1175
- for (const table of schema.database) {
1176
- modelString += convertTable(table);
1177
- }
1178
- modelString += `}`;
1179
- return prettier2.format(modelString, __spreadValues({
1180
- parser: "typescript"
1181
- }, {
1182
- trailingComma: "none",
1183
- tabWidth: 4,
1184
- useTabs: true,
1185
- endOfLine: "lf",
1186
- printWidth: 120,
1187
- singleQuote: true
1188
- }));
1189
- }
1190
- function convertTable(table) {
1191
- let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
1192
- `;
1193
- for (const column of table.columns) {
1194
- modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
1195
- `;
1196
- }
1197
- modelString += ` }
1198
- `;
1199
- return modelString;
1200
- }
1514
+ // src/restura/schemas/resturaConfigSchema.ts
1515
+ import { z as z5 } from "zod";
1516
+ var _a;
1517
+ var isTsx = (_a = process.argv[1]) == null ? void 0 : _a.endsWith(".ts");
1518
+ var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
1519
+ var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
1520
+ var resturaConfigSchema = z5.object({
1521
+ authToken: z5.string().min(1, "Missing Restura Auth Token"),
1522
+ sendErrorStackTrace: z5.boolean().default(false),
1523
+ schemaFilePath: z5.string().default(process.cwd() + "/restura.schema.json"),
1524
+ customApiFolderPath: z5.string().default(process.cwd() + customApiFolderPath),
1525
+ generatedTypesPath: z5.string().default(process.cwd() + "/src/@types"),
1526
+ fileTempCachePath: z5.string().optional()
1527
+ });
1201
1528
 
1202
1529
  // src/restura/sql/PsqlEngine.ts
1203
1530
  import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
1531
+ import getDiff from "@wmfs/pg-diff-sync";
1532
+ import pgInfo from "@wmfs/pg-info";
1533
+ import pg2 from "pg";
1534
+
1535
+ // src/restura/sql/PsqlPool.ts
1536
+ import pg from "pg";
1537
+
1538
+ // src/restura/sql/PsqlConnection.ts
1539
+ import crypto from "crypto";
1540
+ import format3 from "pg-format";
1204
1541
 
1205
1542
  // src/restura/sql/PsqlUtils.ts
1206
1543
  import format2 from "pg-format";
@@ -1210,16 +1547,33 @@ function escapeColumnName(columnName) {
1210
1547
  }
1211
1548
  function questionMarksToOrderedParams(query) {
1212
1549
  let count = 1;
1213
- return query.replace(/'\?'|\?/g, () => `$${count++}`);
1550
+ let inSingleQuote = false;
1551
+ let inDoubleQuote = false;
1552
+ return query.replace(/('|"|\?)/g, (char) => {
1553
+ if (char === "'") {
1554
+ inSingleQuote = !inSingleQuote && !inDoubleQuote;
1555
+ return char;
1556
+ }
1557
+ if (char === '"') {
1558
+ inDoubleQuote = !inDoubleQuote && !inSingleQuote;
1559
+ return char;
1560
+ }
1561
+ if (char === "?" && !inSingleQuote && !inDoubleQuote) {
1562
+ return `$${count++}`;
1563
+ }
1564
+ return char;
1565
+ });
1214
1566
  }
1215
1567
  function insertObjectQuery(table, obj) {
1216
1568
  const keys = Object.keys(obj);
1217
1569
  const params = Object.values(obj);
1218
1570
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1219
1571
  const values = params.map((value) => SQL`${value}`).join(", ");
1220
- const query = `INSERT INTO "${table}" (${columns})
1572
+ let query = `
1573
+ INSERT INTO "${table}" (${columns})
1221
1574
  VALUES (${values})
1222
1575
  RETURNING *`;
1576
+ query = query.replace(/'(\?)'/, "?");
1223
1577
  return query;
1224
1578
  }
1225
1579
  function updateObjectQuery(table, obj, whereStatement) {
@@ -1227,25 +1581,113 @@ function updateObjectQuery(table, obj, whereStatement) {
1227
1581
  for (const i in obj) {
1228
1582
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1229
1583
  }
1230
- return `UPDATE ${escapeColumnName(table)}
1231
- SET ${setArray.join(", ")} ${whereStatement}
1232
- RETURNING *`;
1233
- }
1234
- function isValueNumber2(value) {
1235
- return !isNaN(Number(value));
1236
- }
1237
- function SQL(strings, ...values) {
1238
- let query = strings[0];
1239
- values.forEach((value, index) => {
1240
- if (isValueNumber2(value)) {
1241
- query += value;
1584
+ return `
1585
+ UPDATE ${escapeColumnName(table)}
1586
+ SET ${setArray.join(", ")} ${whereStatement}
1587
+ RETURNING *`;
1588
+ }
1589
+ function isValueNumber2(value) {
1590
+ return !isNaN(Number(value));
1591
+ }
1592
+ function SQL(strings, ...values) {
1593
+ let query = strings[0];
1594
+ values.forEach((value, index) => {
1595
+ if (typeof value === "boolean") {
1596
+ query += value;
1597
+ } else if (typeof value === "number") {
1598
+ query += value;
1599
+ } else if (Array.isArray(value)) {
1600
+ query += format2.literal(JSON.stringify(value)) + "::jsonb";
1601
+ } else {
1602
+ query += format2.literal(value);
1603
+ }
1604
+ query += strings[index + 1];
1605
+ });
1606
+ return query;
1607
+ }
1608
+
1609
+ // src/restura/sql/PsqlConnection.ts
1610
+ var PsqlConnection = class {
1611
+ constructor(instanceId) {
1612
+ this.instanceId = instanceId || crypto.randomUUID();
1613
+ }
1614
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1615
+ async queryOne(query, options, requesterDetails) {
1616
+ const formattedQuery = questionMarksToOrderedParams(query);
1617
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1618
+ this.logSqlStatement(formattedQuery, options, meta);
1619
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1620
+ `;
1621
+ try {
1622
+ const response = await this.query(queryMetadata + formattedQuery, options);
1623
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1624
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1625
+ return response.rows[0];
1626
+ } catch (error) {
1627
+ if (RsError.isRsError(error)) throw error;
1628
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1629
+ throw new RsError("DUPLICATE", error.message);
1630
+ }
1631
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1632
+ }
1633
+ }
1634
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1635
+ async runQuery(query, options, requesterDetails) {
1636
+ const formattedQuery = questionMarksToOrderedParams(query);
1637
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1638
+ this.logSqlStatement(formattedQuery, options, meta);
1639
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1640
+ `;
1641
+ try {
1642
+ const response = await this.query(queryMetadata + formattedQuery, options);
1643
+ return response.rows;
1644
+ } catch (error) {
1645
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1646
+ throw new RsError("DUPLICATE", error.message);
1647
+ }
1648
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1649
+ }
1650
+ }
1651
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1652
+ if (logger.level !== "silly") return;
1653
+ let sqlStatement = "";
1654
+ if (options.length === 0) {
1655
+ sqlStatement = query;
1242
1656
  } else {
1243
- query += format2.literal(value);
1657
+ let stringIndex = 0;
1658
+ sqlStatement = query.replace(/\$\d+/g, () => {
1659
+ const value = options[stringIndex++];
1660
+ if (typeof value === "number") return value.toString();
1661
+ return format3.literal(value);
1662
+ });
1244
1663
  }
1245
- query += strings[index + 1];
1246
- });
1247
- return query;
1248
- }
1664
+ let initiator = "Anonymous";
1665
+ if ("userId" in queryMetadata && queryMetadata.userId)
1666
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1667
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1668
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1669
+ ${sqlStatement}`);
1670
+ }
1671
+ };
1672
+
1673
+ // src/restura/sql/PsqlPool.ts
1674
+ var { Pool } = pg;
1675
+ var PsqlPool = class extends PsqlConnection {
1676
+ constructor(poolConfig) {
1677
+ super();
1678
+ this.poolConfig = poolConfig;
1679
+ this.pool = new Pool(poolConfig);
1680
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1681
+ logger.info("Connected to PostgreSQL database");
1682
+ }).catch((error) => {
1683
+ logger.error("Error connecting to database", error);
1684
+ process.exit(1);
1685
+ });
1686
+ }
1687
+ async query(query, values) {
1688
+ return this.pool.query(query, values);
1689
+ }
1690
+ };
1249
1691
 
1250
1692
  // src/restura/sql/SqlEngine.ts
1251
1693
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
@@ -1313,11 +1755,11 @@ var SqlEngine = class {
1313
1755
  return returnValue;
1314
1756
  }
1315
1757
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1316
- var _a;
1758
+ var _a2;
1317
1759
  if (!routeData.request) return value;
1318
1760
  const data = req.data;
1319
1761
  if (typeof value === "string") {
1320
- (_a = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1762
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1321
1763
  const requestParam = routeData.request.find((item) => {
1322
1764
  return item.name === param.replace("$", "");
1323
1765
  });
@@ -1330,9 +1772,9 @@ var SqlEngine = class {
1330
1772
  return value;
1331
1773
  }
1332
1774
  replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1333
- var _a;
1775
+ var _a2;
1334
1776
  if (typeof value === "string") {
1335
- (_a = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1777
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1336
1778
  param = param.replace("#", "");
1337
1779
  const globalParamValue = req.requesterDetails[param];
1338
1780
  if (!globalParamValue)
@@ -1351,32 +1793,86 @@ var SqlEngine = class {
1351
1793
  // src/restura/sql/filterPsqlParser.ts
1352
1794
  import peg from "pegjs";
1353
1795
  var filterSqlGrammar = `
1796
+ {
1797
+ // ported from pg-format but intentionally will add double quotes to every column
1798
+ function quoteSqlIdentity(value) {
1799
+ if (value === undefined || value === null) {
1800
+ throw new Error('SQL identifier cannot be null or undefined');
1801
+ } else if (value === false) {
1802
+ return '"f"';
1803
+ } else if (value === true) {
1804
+ return '"t"';
1805
+ } else if (value instanceof Date) {
1806
+ // return '"' + formatDate(value.toISOString()) + '"';
1807
+ } else if (value instanceof Buffer) {
1808
+ throw new Error('SQL identifier cannot be a buffer');
1809
+ } else if (Array.isArray(value) === true) {
1810
+ var temp = [];
1811
+ for (var i = 0; i < value.length; i++) {
1812
+ if (Array.isArray(value[i]) === true) {
1813
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1814
+ } else {
1815
+ // temp.push(quoteIdent(value[i]));
1816
+ }
1817
+ }
1818
+ return temp.toString();
1819
+ } else if (value === Object(value)) {
1820
+ throw new Error('SQL identifier cannot be an object');
1821
+ }
1822
+
1823
+ var ident = value.toString().slice(0); // create copy
1824
+
1825
+ // do not quote a valid, unquoted identifier
1826
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1827
+ // return ident;
1828
+ // }
1829
+
1830
+ var quoted = '"';
1831
+
1832
+ for (var i = 0; i < ident.length; i++) {
1833
+ var c = ident[i];
1834
+ if (c === '"') {
1835
+ quoted += c + c;
1836
+ } else {
1837
+ quoted += c;
1838
+ }
1839
+ }
1840
+
1841
+ quoted += '"';
1842
+
1843
+ return quoted;
1844
+ };
1845
+ }
1846
+
1354
1847
  start = expressionList
1355
1848
 
1849
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1850
+
1356
1851
  expressionList =
1357
- leftExpression:expression operator:operator rightExpression:expressionList
1852
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1358
1853
  { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1359
1854
  / expression
1360
1855
 
1361
1856
  expression =
1362
- negate:negate?"(" "column:" column:column ","? value:value? ","? type:type? ")"
1363
- {return \`\${negate? "!" : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1857
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1858
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1364
1859
  /
1365
- negate:negate?"("expression:expressionList")" { return \`\${negate? "!" : ""}(\${expression})\`; }
1860
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1366
1861
 
1367
1862
  negate = "!"
1368
1863
 
1369
1864
  operator = "and"i / "or"i
1370
1865
 
1371
1866
 
1372
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1867
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1373
1868
  /
1374
- text:text { return format.ident(text); }
1869
+ text:text { return quoteSqlIdentity(text); }
1375
1870
 
1376
1871
 
1377
- text = text:[a-z0-9-_:@]i+ { return text.join("");}
1872
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
1873
+
1378
1874
 
1379
- type = "type:" type:typeString { return type; }
1875
+ type = "type" _ ":" _ type:typeString { return type; }
1380
1876
  typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1381
1877
  text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1382
1878
  text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
@@ -1386,8 +1882,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
1386
1882
  text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1387
1883
  text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1388
1884
  text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1389
-
1390
- value = "value:" value:text { return value; }
1885
+
1886
+ value = "value" _ ":" value:text { return value; }
1887
+
1391
1888
 
1392
1889
  `;
1393
1890
  var filterPsqlParser = peg.generate(filterSqlGrammar, {
@@ -1397,18 +1894,224 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1397
1894
  var filterPsqlParser_default = filterPsqlParser;
1398
1895
 
1399
1896
  // src/restura/sql/PsqlEngine.ts
1897
+ var { Client, types } = pg2;
1898
+ var systemUser = {
1899
+ role: "",
1900
+ host: "",
1901
+ ipAddress: "",
1902
+ isSystemUser: true
1903
+ };
1400
1904
  var PsqlEngine = class extends SqlEngine {
1401
- constructor(psqlConnectionPool) {
1905
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1402
1906
  super();
1403
1907
  this.psqlConnectionPool = psqlConnectionPool;
1908
+ this.setupPgReturnTypes();
1909
+ if (shouldListenForDbTriggers) {
1910
+ this.setupTriggerListeners = this.listenForDbTriggers();
1911
+ }
1404
1912
  }
1405
- async diffDatabaseToSchema(schema) {
1406
- console.log(schema);
1407
- return Promise.resolve("");
1913
+ async close() {
1914
+ if (this.triggerClient) {
1915
+ await this.triggerClient.end();
1916
+ }
1917
+ }
1918
+ setupPgReturnTypes() {
1919
+ const TIMESTAMPTZ_OID = 1184;
1920
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1921
+ return val === null ? null : new Date(val).toISOString();
1922
+ });
1923
+ const BIGINT_OID = 20;
1924
+ types.setTypeParser(BIGINT_OID, (val) => {
1925
+ return val === null ? null : Number(val);
1926
+ });
1927
+ }
1928
+ async listenForDbTriggers() {
1929
+ this.triggerClient = new Client({
1930
+ user: this.psqlConnectionPool.poolConfig.user,
1931
+ host: this.psqlConnectionPool.poolConfig.host,
1932
+ database: this.psqlConnectionPool.poolConfig.database,
1933
+ password: this.psqlConnectionPool.poolConfig.password,
1934
+ port: this.psqlConnectionPool.poolConfig.port,
1935
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1936
+ });
1937
+ await this.triggerClient.connect();
1938
+ const promises = [];
1939
+ promises.push(this.triggerClient.query("LISTEN insert"));
1940
+ promises.push(this.triggerClient.query("LISTEN update"));
1941
+ promises.push(this.triggerClient.query("LISTEN delete"));
1942
+ await Promise.all(promises);
1943
+ this.triggerClient.on("notification", async (msg) => {
1944
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1945
+ const payload = ObjectUtils4.safeParse(msg.payload);
1946
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1947
+ }
1948
+ });
1949
+ }
1950
+ async handleTrigger(payload, mutationType) {
1951
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
1952
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
1953
+ }
1954
+ }
1955
+ async createDatabaseFromSchema(schema, connection) {
1956
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1957
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1958
+ return sqlFullStatement;
1408
1959
  }
1409
1960
  generateDatabaseSchemaFromSchema(schema) {
1410
- console.log(schema);
1411
- return "";
1961
+ const sqlStatements = [];
1962
+ const indexes = [];
1963
+ const triggers = [];
1964
+ for (const table of schema.database) {
1965
+ if (table.notify) {
1966
+ triggers.push(this.createInsertTriggers(table.name, table.notify));
1967
+ triggers.push(this.createUpdateTrigger(table.name, table.notify));
1968
+ triggers.push(this.createDeleteTrigger(table.name, table.notify));
1969
+ }
1970
+ let sql = `CREATE TABLE "${table.name}"
1971
+ ( `;
1972
+ const tableColumns = [];
1973
+ for (const column of table.columns) {
1974
+ let columnSql = "";
1975
+ columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
1976
+ let value = column.value;
1977
+ if (column.type === "JSON") value = "";
1978
+ if (column.type === "JSONB") value = "";
1979
+ if (column.type === "DECIMAL" && value) {
1980
+ value = value.replace("-", ",").replace(/['"]/g, "");
1981
+ }
1982
+ if (value && column.type !== "ENUM") {
1983
+ columnSql += `(${value})`;
1984
+ } else if (column.length) columnSql += `(${column.length})`;
1985
+ if (column.isPrimary) {
1986
+ columnSql += " PRIMARY KEY ";
1987
+ }
1988
+ if (column.isUnique) {
1989
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
1990
+ }
1991
+ if (column.isNullable) columnSql += " NULL";
1992
+ else columnSql += " NOT NULL";
1993
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
1994
+ if (value && column.type === "ENUM") {
1995
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
1996
+ }
1997
+ tableColumns.push(columnSql);
1998
+ }
1999
+ sql += tableColumns.join(", \n");
2000
+ for (const index of table.indexes) {
2001
+ if (!index.isPrimaryKey) {
2002
+ let unique = " ";
2003
+ if (index.isUnique) unique = "UNIQUE ";
2004
+ indexes.push(
2005
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
2006
+ return `"${item}" ${index.order}`;
2007
+ }).join(", ")});`
2008
+ );
2009
+ }
2010
+ }
2011
+ sql += "\n);";
2012
+ sqlStatements.push(sql);
2013
+ }
2014
+ for (const table of schema.database) {
2015
+ if (!table.foreignKeys.length) continue;
2016
+ const sql = `ALTER TABLE "${table.name}" `;
2017
+ const constraints = [];
2018
+ for (const foreignKey of table.foreignKeys) {
2019
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2020
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2021
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2022
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2023
+ constraints.push(constraint);
2024
+ }
2025
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2026
+ }
2027
+ for (const table of schema.database) {
2028
+ if (!table.checkConstraints.length) continue;
2029
+ const sql = `ALTER TABLE "${table.name}" `;
2030
+ const constraints = [];
2031
+ for (const check of table.checkConstraints) {
2032
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2033
+ constraints.push(constraint);
2034
+ }
2035
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2036
+ }
2037
+ sqlStatements.push(indexes.join("\n"));
2038
+ sqlStatements.push(triggers.join("\n"));
2039
+ return sqlStatements.join("\n\n");
2040
+ }
2041
+ async getScratchPool() {
2042
+ var _a2, _b;
2043
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
2044
+ `SELECT *
2045
+ FROM pg_database
2046
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
2047
+ [],
2048
+ systemUser
2049
+ );
2050
+ if (scratchDbExists.length === 0) {
2051
+ await this.psqlConnectionPool.runQuery(
2052
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
2053
+ [],
2054
+ systemUser
2055
+ );
2056
+ }
2057
+ const scratchPool = new PsqlPool({
2058
+ host: this.psqlConnectionPool.poolConfig.host,
2059
+ port: this.psqlConnectionPool.poolConfig.port,
2060
+ user: this.psqlConnectionPool.poolConfig.user,
2061
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2062
+ password: this.psqlConnectionPool.poolConfig.password,
2063
+ max: this.psqlConnectionPool.poolConfig.max,
2064
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2065
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2066
+ });
2067
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2068
+ await scratchPool.runQuery(
2069
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2070
+ [],
2071
+ systemUser
2072
+ );
2073
+ const schemaComment = await this.psqlConnectionPool.runQuery(
2074
+ `SELECT pg_description.description
2075
+ FROM pg_description
2076
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2077
+ WHERE pg_namespace.nspname = 'public';`,
2078
+ [],
2079
+ systemUser
2080
+ );
2081
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2082
+ await scratchPool.runQuery(
2083
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
2084
+ [],
2085
+ systemUser
2086
+ );
2087
+ }
2088
+ return scratchPool;
2089
+ }
2090
+ async diffDatabaseToSchema(schema) {
2091
+ const scratchPool = await this.getScratchPool();
2092
+ await this.createDatabaseFromSchema(schema, scratchPool);
2093
+ const originalClient = new Client({
2094
+ database: this.psqlConnectionPool.poolConfig.database,
2095
+ user: this.psqlConnectionPool.poolConfig.user,
2096
+ password: this.psqlConnectionPool.poolConfig.password,
2097
+ host: this.psqlConnectionPool.poolConfig.host,
2098
+ port: this.psqlConnectionPool.poolConfig.port
2099
+ });
2100
+ const scratchClient = new Client({
2101
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2102
+ user: this.psqlConnectionPool.poolConfig.user,
2103
+ password: this.psqlConnectionPool.poolConfig.password,
2104
+ host: this.psqlConnectionPool.poolConfig.host,
2105
+ port: this.psqlConnectionPool.poolConfig.port
2106
+ });
2107
+ const promises = [originalClient.connect(), scratchClient.connect()];
2108
+ await Promise.all(promises);
2109
+ const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
2110
+ const [info1, info2] = await Promise.all(infoPromises);
2111
+ const diff = getDiff(info1, info2);
2112
+ const endPromises = [originalClient.end(), scratchClient.end()];
2113
+ await Promise.all(endPromises);
2114
+ return diff.join("\n");
1412
2115
  }
1413
2116
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1414
2117
  if (!item.subquery) return "";
@@ -1422,8 +2125,7 @@ var PsqlEngine = class extends SqlEngine {
1422
2125
  )) {
1423
2126
  return "'[]'";
1424
2127
  }
1425
- return `COALESCE((
1426
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
2128
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1427
2129
  ${item.subquery.properties.map((nestedItem) => {
1428
2130
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1429
2131
  ...routeData.joins,
@@ -1432,7 +2134,7 @@ var PsqlEngine = class extends SqlEngine {
1432
2134
  return;
1433
2135
  }
1434
2136
  if (nestedItem.subquery) {
1435
- return `"${nestedItem.name}", ${this.createNestedSelect(
2137
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1436
2138
  // recursion
1437
2139
  req,
1438
2140
  schema,
@@ -1443,7 +2145,7 @@ var PsqlEngine = class extends SqlEngine {
1443
2145
  )}`;
1444
2146
  }
1445
2147
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1446
- }).filter(Boolean).join(",")}
2148
+ }).filter(Boolean).join(", ")}
1447
2149
  ))
1448
2150
  FROM
1449
2151
  "${item.subquery.table}"
@@ -1458,16 +2160,19 @@ var PsqlEngine = class extends SqlEngine {
1458
2160
  parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
1459
2161
  });
1460
2162
  const query = insertObjectQuery(routeData.table, __spreadValues(__spreadValues({}, req.data), parameterObj));
1461
- const createdItem = await this.psqlConnectionPool.queryOne(query, sqlParams, req.requesterDetails);
1462
- const insertId = createdItem == null ? void 0 : createdItem.id;
1463
- const whereData = [
1464
- {
1465
- tableName: routeData.table,
1466
- value: insertId,
1467
- columnName: "id",
1468
- operator: "="
1469
- }
1470
- ];
2163
+ const createdItem = await this.psqlConnectionPool.queryOne(
2164
+ query,
2165
+ sqlParams,
2166
+ req.requesterDetails
2167
+ );
2168
+ const insertId = createdItem.id;
2169
+ const whereId = {
2170
+ tableName: routeData.table,
2171
+ value: insertId,
2172
+ columnName: "id",
2173
+ operator: "="
2174
+ };
2175
+ const whereData = [whereId];
1471
2176
  req.data = { id: insertId };
1472
2177
  return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
1473
2178
  }
@@ -1486,7 +2191,9 @@ var PsqlEngine = class extends SqlEngine {
1486
2191
  let selectStatement = "SELECT \n";
1487
2192
  selectStatement += ` ${selectColumns.map((item) => {
1488
2193
  if (item.subquery) {
1489
- return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${item.name}`;
2194
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2195
+ item.name
2196
+ )}`;
1490
2197
  }
1491
2198
  return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
1492
2199
  }).join(",\n ")}
@@ -1519,29 +2226,31 @@ var PsqlEngine = class extends SqlEngine {
1519
2226
  );
1520
2227
  } else if (routeData.type === "PAGED") {
1521
2228
  const data = req.data;
1522
- const pageResults = await this.psqlConnectionPool.runQuery(
1523
- `${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT ? OFFSET ?;SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
1524
- ${sqlStatement};`,
1525
- [
1526
- ...sqlParams,
1527
- data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER,
1528
- (data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER,
1529
- ...sqlParams
1530
- ],
2229
+ const pagePromise = this.psqlConnectionPool.runQuery(
2230
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2231
+ sqlParams,
2232
+ req.requesterDetails
2233
+ );
2234
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2235
+ ${sqlStatement};`;
2236
+ const totalPromise = this.psqlConnectionPool.runQuery(
2237
+ totalQuery,
2238
+ sqlParams,
1531
2239
  req.requesterDetails
1532
2240
  );
2241
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1533
2242
  let total = 0;
1534
- if (ObjectUtils4.isArrayWithData(pageResults)) {
1535
- total = pageResults[1][0].total;
2243
+ if (ObjectUtils4.isArrayWithData(totalResponse)) {
2244
+ total = totalResponse[0].total;
1536
2245
  }
1537
- return { data: pageResults[0], total };
2246
+ return { data: pageResults, total };
1538
2247
  } else {
1539
2248
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1540
2249
  }
1541
2250
  }
1542
2251
  async executeUpdateRequest(req, routeData, schema) {
1543
2252
  const sqlParams = [];
1544
- const _a = req.body, { id } = _a, bodyNoId = __objRest(_a, ["id"]);
2253
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
1545
2254
  const table = schema.database.find((item) => {
1546
2255
  return item.name === routeData.table;
1547
2256
  });
@@ -1573,10 +2282,12 @@ ${sqlStatement};`,
1573
2282
  req.requesterDetails.role,
1574
2283
  sqlParams
1575
2284
  );
1576
- let deleteStatement = `DELETE
1577
- FROM "${routeData.table}" ${joinStatement}`;
1578
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1579
- deleteStatement += ";";
2285
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2286
+ if (whereClause.replace(/\s/g, "") === "") {
2287
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2288
+ }
2289
+ const deleteStatement = `
2290
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1580
2291
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1581
2292
  return true;
1582
2293
  }
@@ -1639,38 +2350,37 @@ ${sqlStatement};`,
1639
2350
  );
1640
2351
  let operator = item.operator;
1641
2352
  if (operator === "LIKE") {
1642
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2353
+ item.value = `'%${item.value}%'`;
1643
2354
  } else if (operator === "STARTS WITH") {
1644
2355
  operator = "LIKE";
1645
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2356
+ item.value = `'${item.value}%'`;
1646
2357
  } else if (operator === "ENDS WITH") {
1647
2358
  operator = "LIKE";
1648
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2359
+ item.value = `'%${item.value}'`;
1649
2360
  }
1650
2361
  const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
1651
- const escapedValue = SQL`${replacedValue}`;
1652
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
2362
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
1653
2363
  `;
1654
2364
  });
1655
2365
  const data = req.data;
1656
2366
  if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
1657
2367
  let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1658
- var _a;
2368
+ var _a2;
1659
2369
  const requestParam = routeData.request.find((item) => {
1660
2370
  return item.name === value.replace("$", "");
1661
2371
  });
1662
2372
  if (!requestParam)
1663
2373
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1664
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2374
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1665
2375
  });
1666
2376
  statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1667
- var _a;
2377
+ var _a2;
1668
2378
  const requestParam = routeData.request.find((item) => {
1669
2379
  return item.name === value.replace("#", "");
1670
2380
  });
1671
2381
  if (!requestParam)
1672
2382
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1673
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2383
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1674
2384
  });
1675
2385
  statement = filterPsqlParser_default.parse(statement);
1676
2386
  if (whereClause.startsWith("WHERE")) {
@@ -1683,82 +2393,259 @@ ${sqlStatement};`,
1683
2393
  }
1684
2394
  return whereClause;
1685
2395
  }
1686
- };
2396
+ createUpdateTrigger(tableName, notify) {
2397
+ if (!notify) return "";
2398
+ if (notify === "ALL") {
2399
+ return `
2400
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2401
+ RETURNS TRIGGER AS $$
2402
+ DECLARE
2403
+ query_metadata JSON;
2404
+ BEGIN
2405
+ SELECT INTO query_metadata
2406
+ (regexp_match(
2407
+ current_query(),
2408
+ '^--QUERY_METADATA\\(({.*})', 'n'
2409
+ ))[1]::json;
1687
2410
 
1688
- // src/restura/sql/PsqlPool.ts
1689
- import pg from "pg";
1690
- import format3 from "pg-format";
1691
- var { Pool } = pg;
1692
- var PsqlPool = class {
1693
- constructor(poolConfig) {
1694
- this.poolConfig = poolConfig;
1695
- this.pool = new Pool(poolConfig);
1696
- this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1697
- logger.info("Connected to PostgreSQL database");
1698
- }).catch((error) => {
1699
- logger.error("Error connecting to database", error);
1700
- process.exit(1);
1701
- });
1702
- }
1703
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1704
- async queryOne(query, options, requesterDetails) {
1705
- const formattedQuery = questionMarksToOrderedParams(query);
1706
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1707
- try {
1708
- const response = await this.pool.query(formattedQuery, options);
1709
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1710
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1711
- return response.rows[0];
1712
- } catch (error) {
1713
- console.error(error, query, options);
1714
- if (RsError.isRsError(error)) throw error;
1715
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1716
- throw new RsError("DUPLICATE", error.message);
1717
- }
1718
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2411
+ PERFORM pg_notify(
2412
+ 'update',
2413
+ json_build_object(
2414
+ 'table', '${tableName}',
2415
+ 'queryMetadata', query_metadata,
2416
+ 'changedId', NEW.id,
2417
+ 'record', NEW,
2418
+ 'previousRecord', OLD
2419
+ )::text
2420
+ );
2421
+ RETURN NEW;
2422
+ END;
2423
+ $$ LANGUAGE plpgsql;
2424
+
2425
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2426
+ AFTER UPDATE ON "${tableName}"
2427
+ FOR EACH ROW
2428
+ EXECUTE FUNCTION notify_${tableName}_update();
2429
+ `;
1719
2430
  }
1720
- }
1721
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1722
- async runQuery(query, options, requesterDetails) {
1723
- const formattedQuery = questionMarksToOrderedParams(query);
1724
- this.logSqlStatement(formattedQuery, options, requesterDetails);
1725
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1726
- console.log(queryUpdated, options);
1727
- try {
1728
- const response = await this.pool.query(formattedQuery, options);
1729
- return response.rows;
1730
- } catch (error) {
1731
- console.error(error, query, options);
1732
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1733
- throw new RsError("DUPLICATE", error.message);
1734
- }
1735
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2431
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2432
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2433
+ return `
2434
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2435
+ RETURNS TRIGGER AS $$
2436
+ DECLARE
2437
+ query_metadata JSON;
2438
+ BEGIN
2439
+ SELECT INTO query_metadata
2440
+ (regexp_match(
2441
+ current_query(),
2442
+ '^--QUERY_METADATA\\(({.*})', 'n'
2443
+ ))[1]::json;
2444
+
2445
+ PERFORM pg_notify(
2446
+ 'update',
2447
+ json_build_object(
2448
+ 'table', '${tableName}',
2449
+ 'queryMetadata', query_metadata,
2450
+ 'changedId', NEW.id,
2451
+ 'record', json_build_object(
2452
+ ${notifyColumnNewBuildString}
2453
+ ),
2454
+ 'previousRecord', json_build_object(
2455
+ ${notifyColumnOldBuildString}
2456
+ )
2457
+ )::text
2458
+ );
2459
+ RETURN NEW;
2460
+ END;
2461
+ $$ LANGUAGE plpgsql;
2462
+
2463
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2464
+ AFTER UPDATE ON "${tableName}"
2465
+ FOR EACH ROW
2466
+ EXECUTE FUNCTION notify_${tableName}_update();
2467
+ `;
2468
+ }
2469
+ createDeleteTrigger(tableName, notify) {
2470
+ if (!notify) return "";
2471
+ if (notify === "ALL") {
2472
+ return `
2473
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2474
+ RETURNS TRIGGER AS $$
2475
+ DECLARE
2476
+ query_metadata JSON;
2477
+ BEGIN
2478
+ SELECT INTO query_metadata
2479
+ (regexp_match(
2480
+ current_query(),
2481
+ '^--QUERY_METADATA\\(({.*})', 'n'
2482
+ ))[1]::json;
2483
+
2484
+ PERFORM pg_notify(
2485
+ 'delete',
2486
+ json_build_object(
2487
+ 'table', '${tableName}',
2488
+ 'queryMetadata', query_metadata,
2489
+ 'deletedId', OLD.id,
2490
+ 'previousRecord', OLD
2491
+ )::text
2492
+ );
2493
+ RETURN NEW;
2494
+ END;
2495
+ $$ LANGUAGE plpgsql;
2496
+
2497
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2498
+ AFTER DELETE ON "${tableName}"
2499
+ FOR EACH ROW
2500
+ EXECUTE FUNCTION notify_${tableName}_delete();
2501
+ `;
1736
2502
  }
1737
- }
1738
- logSqlStatement(query, options, requesterDetails, prefix = "") {
1739
- if (logger.level !== "silly") return;
1740
- let sqlStatement = "";
1741
- if (options.length === 0) {
1742
- sqlStatement = query;
1743
- } else {
1744
- let stringIndex = 0;
1745
- sqlStatement = query.replace(/\$\d+/g, () => {
1746
- const value = options[stringIndex++];
1747
- if (typeof value === "number") return value.toString();
1748
- return format3.literal(value);
1749
- });
2503
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2504
+ return `
2505
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2506
+ RETURNS TRIGGER AS $$
2507
+ DECLARE
2508
+ query_metadata JSON;
2509
+ BEGIN
2510
+ SELECT INTO query_metadata
2511
+ (regexp_match(
2512
+ current_query(),
2513
+ '^--QUERY_METADATA\\(({.*})', 'n'
2514
+ ))[1]::json;
2515
+
2516
+ PERFORM pg_notify(
2517
+ 'delete',
2518
+ json_build_object(
2519
+ 'table', '${tableName}',
2520
+ 'queryMetadata', query_metadata,
2521
+ 'deletedId', OLD.id,
2522
+ 'previousRecord', json_build_object(
2523
+ ${notifyColumnOldBuildString}
2524
+ )
2525
+ )::text
2526
+ );
2527
+ RETURN NEW;
2528
+ END;
2529
+ $$ LANGUAGE plpgsql;
2530
+
2531
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2532
+ AFTER DELETE ON "${tableName}"
2533
+ FOR EACH ROW
2534
+ EXECUTE FUNCTION notify_${tableName}_delete();
2535
+ `;
2536
+ }
2537
+ createInsertTriggers(tableName, notify) {
2538
+ if (!notify) return "";
2539
+ if (notify === "ALL") {
2540
+ return `
2541
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2542
+ RETURNS TRIGGER AS $$
2543
+ DECLARE
2544
+ query_metadata JSON;
2545
+ BEGIN
2546
+ SELECT INTO query_metadata
2547
+ (regexp_match(
2548
+ current_query(),
2549
+ '^--QUERY_METADATA\\(({.*})', 'n'
2550
+ ))[1]::json;
2551
+
2552
+ PERFORM pg_notify(
2553
+ 'insert',
2554
+ json_build_object(
2555
+ 'table', '${tableName}',
2556
+ 'queryMetadata', query_metadata,
2557
+ 'insertedId', NEW.id,
2558
+ 'record', NEW
2559
+ )::text
2560
+ );
2561
+
2562
+ RETURN NEW;
2563
+ END;
2564
+ $$ LANGUAGE plpgsql;
2565
+
2566
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2567
+ AFTER INSERT ON "${tableName}"
2568
+ FOR EACH ROW
2569
+ EXECUTE FUNCTION notify_${tableName}_insert();
2570
+ `;
1750
2571
  }
1751
- let initiator = "Anonymous";
1752
- if ("userId" in requesterDetails && requesterDetails.userId)
1753
- initiator = `User Id (${requesterDetails.userId.toString()})`;
1754
- if ("isSystemUser" in requesterDetails && requesterDetails.isSystemUser) initiator = "SYSTEM";
1755
- logger.silly(`${prefix}query by ${initiator}, Query ->
1756
- ${sqlStatement}`);
2572
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2573
+ return `
2574
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2575
+ RETURNS TRIGGER AS $$
2576
+ DECLARE
2577
+ query_metadata JSON;
2578
+ BEGIN
2579
+ SELECT INTO query_metadata
2580
+ (regexp_match(
2581
+ current_query(),
2582
+ '^--QUERY_METADATA\\(({.*})', 'n'
2583
+ ))[1]::json;
2584
+
2585
+ PERFORM pg_notify(
2586
+ 'insert',
2587
+ json_build_object(
2588
+ 'table', '${tableName}',
2589
+ 'queryMetadata', query_metadata,
2590
+ 'insertedId', NEW.id,
2591
+ 'record', json_build_object(
2592
+ ${notifyColumnNewBuildString}
2593
+ )
2594
+ )::text
2595
+ );
2596
+
2597
+ RETURN NEW;
2598
+ END;
2599
+ $$ LANGUAGE plpgsql;
2600
+
2601
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2602
+ AFTER INSERT ON "${tableName}"
2603
+ FOR EACH ROW
2604
+ EXECUTE FUNCTION notify_${tableName}_insert();
2605
+ `;
2606
+ }
2607
+ schemaToPsqlType(column) {
2608
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2609
+ if (column.type === "ENUM") return `TEXT`;
2610
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2611
+ if (column.type === "MEDIUMINT") return "INT";
2612
+ return column.type;
2613
+ }
2614
+ };
2615
+
2616
+ // src/restura/utils/TempCache.ts
2617
+ import fs3 from "fs";
2618
+ import path3 from "path";
2619
+ import { DateUtils } from "@redskytech/core-utils";
2620
+ import { FileUtils as FileUtils2 } from "@restura/internal";
2621
+ import Bluebird3 from "bluebird";
2622
+ import * as os2 from "os";
2623
+ var TempCache = class {
2624
+ constructor(location) {
2625
+ this.maxDurationDays = 7;
2626
+ this.location = location || os2.tmpdir();
2627
+ FileUtils2.ensureDir(this.location).catch((e) => {
2628
+ throw e;
2629
+ });
2630
+ }
2631
+ async cleanup() {
2632
+ const fileList = await fs3.promises.readdir(this.location);
2633
+ await Bluebird3.map(
2634
+ fileList,
2635
+ async (file) => {
2636
+ const fullFilePath = path3.join(this.location, file);
2637
+ const fileStats = await fs3.promises.stat(fullFilePath);
2638
+ if (DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2639
+ logger.info(`Deleting old temp file: ${file}`);
2640
+ await fs3.promises.unlink(fullFilePath);
2641
+ }
2642
+ },
2643
+ { concurrency: 10 }
2644
+ );
1757
2645
  }
1758
2646
  };
1759
2647
 
1760
2648
  // src/restura/restura.ts
1761
- var { types } = pg2;
1762
2649
  var ResturaEngine = class {
1763
2650
  constructor() {
1764
2651
  this.publicEndpoints = {
@@ -1777,9 +2664,10 @@ var ResturaEngine = class {
1777
2664
  */
1778
2665
  async init(app, authenticationHandler, psqlConnectionPool) {
1779
2666
  this.resturaConfig = config2.validate("restura", resturaConfigSchema);
2667
+ this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
2668
+ new TempCache(this.resturaConfig.fileTempCachePath);
1780
2669
  this.psqlConnectionPool = psqlConnectionPool;
1781
- this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
1782
- setupPgReturnTypes();
2670
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
1783
2671
  await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
1784
2672
  this.authenticationHandler = authenticationHandler;
1785
2673
  app.use(compression());
@@ -1843,10 +2731,7 @@ var ResturaEngine = class {
1843
2731
  * @returns A promise that resolves when the API has been successfully generated and written to the output file.
1844
2732
  */
1845
2733
  async generateApiFromSchema(outputFile, providedSchema) {
1846
- fs3.writeFileSync(
1847
- outputFile,
1848
- await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1849
- );
2734
+ fs4.writeFileSync(outputFile, await apiGenerator(providedSchema));
1850
2735
  }
1851
2736
  /**
1852
2737
  * Generates a model from the provided schema and writes it to the specified output file.
@@ -1856,10 +2741,15 @@ var ResturaEngine = class {
1856
2741
  * @returns A promise that resolves when the model has been successfully written to the output file.
1857
2742
  */
1858
2743
  async generateModelFromSchema(outputFile, providedSchema) {
1859
- fs3.writeFileSync(
1860
- outputFile,
1861
- await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1862
- );
2744
+ fs4.writeFileSync(outputFile, await modelGenerator(providedSchema));
2745
+ }
2746
+ /**
2747
+ * Generates the ambient module declaration for Restura global types and writes it to the specified output file.
2748
+ * These types are used sometimes in the CustomTypes
2749
+ * @param outputFile
2750
+ */
2751
+ generateResturaGlobalTypes(outputFile) {
2752
+ fs4.writeFileSync(outputFile, resturaGlobalTypesGenerator());
1863
2753
  }
1864
2754
  /**
1865
2755
  * Retrieves the latest file system schema for Restura.
@@ -1868,11 +2758,11 @@ var ResturaEngine = class {
1868
2758
  * @throws {Error} If the schema file is missing or the schema is not valid.
1869
2759
  */
1870
2760
  async getLatestFileSystemSchema() {
1871
- if (!fs3.existsSync(this.resturaConfig.schemaFilePath)) {
2761
+ if (!fs4.existsSync(this.resturaConfig.schemaFilePath)) {
1872
2762
  logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
1873
2763
  throw new Error("Missing restura schema file");
1874
2764
  }
1875
- const schemaFileData = fs3.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2765
+ const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
1876
2766
  const schema = ObjectUtils5.safeParse(schemaFileData);
1877
2767
  const isValid = await isSchemaValid(schema);
1878
2768
  if (!isValid) {
@@ -1881,28 +2771,6 @@ var ResturaEngine = class {
1881
2771
  }
1882
2772
  return schema;
1883
2773
  }
1884
- /**
1885
- * Asynchronously generates and retrieves hashes for the provided schema and related generated files.
1886
- *
1887
- * @param providedSchema - The schema for which hashes need to be generated.
1888
- * @returns A promise that resolves to an object containing:
1889
- * - `schemaHash`: The hash of the provided schema.
1890
- * - `apiCreatedSchemaHash`: The hash extracted from the generated `api.d.ts` file.
1891
- * - `modelCreatedSchemaHash`: The hash extracted from the generated `models.d.ts` file.
1892
- */
1893
- async getHashes(providedSchema) {
1894
- var _a, _b, _c, _d;
1895
- const schemaHash = await this.generateHashForSchema(providedSchema);
1896
- const apiFile = fs3.readFileSync(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1897
- const apiCreatedSchemaHash = (_b = (_a = apiFile.toString().match(/\((.*)\)/)) == null ? void 0 : _a[1]) != null ? _b : "";
1898
- const modelFile = fs3.readFileSync(path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1899
- const modelCreatedSchemaHash = (_d = (_c = modelFile.toString().match(/\((.*)\)/)) == null ? void 0 : _c[1]) != null ? _d : "";
1900
- return {
1901
- schemaHash,
1902
- apiCreatedSchemaHash,
1903
- modelCreatedSchemaHash
1904
- };
1905
- }
1906
2774
  async reloadEndpoints() {
1907
2775
  this.schema = await this.getLatestFileSystemSchema();
1908
2776
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
@@ -1931,30 +2799,10 @@ var ResturaEngine = class {
1931
2799
  logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
1932
2800
  }
1933
2801
  async validateGeneratedTypesFolder() {
1934
- if (!fs3.existsSync(this.resturaConfig.generatedTypesPath)) {
1935
- fs3.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1936
- }
1937
- const hasApiFile = fs3.existsSync(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1938
- const hasModelsFile = fs3.existsSync(path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1939
- if (!hasApiFile) {
1940
- await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1941
- }
1942
- if (!hasModelsFile) {
1943
- await this.generateModelFromSchema(
1944
- path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1945
- this.schema
1946
- );
1947
- }
1948
- const hashes = await this.getHashes(this.schema);
1949
- if (hashes.schemaHash !== hashes.apiCreatedSchemaHash) {
1950
- await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1951
- }
1952
- if (hashes.schemaHash !== hashes.modelCreatedSchemaHash) {
1953
- await this.generateModelFromSchema(
1954
- path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1955
- this.schema
1956
- );
2802
+ if (!fs4.existsSync(this.resturaConfig.generatedTypesPath)) {
2803
+ fs4.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1957
2804
  }
2805
+ this.updateTypes();
1958
2806
  }
1959
2807
  resturaAuthentication(req, res, next) {
1960
2808
  if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
@@ -1962,7 +2810,7 @@ var ResturaEngine = class {
1962
2810
  }
1963
2811
  async previewCreateSchema(req, res) {
1964
2812
  try {
1965
- const schemaDiff = { commands: "", endPoints: [], globalParams: [], roles: [], customTypes: false };
2813
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
1966
2814
  res.send({ data: schemaDiff });
1967
2815
  } catch (err) {
1968
2816
  res.status(400).send(err);
@@ -1970,7 +2818,7 @@ var ResturaEngine = class {
1970
2818
  }
1971
2819
  async updateSchema(req, res) {
1972
2820
  try {
1973
- this.schema = req.data;
2821
+ this.schema = sortObjectKeysAlphabetically(req.data);
1974
2822
  await this.storeFileSystemSchema();
1975
2823
  await this.reloadEndpoints();
1976
2824
  await this.updateTypes();
@@ -1981,11 +2829,12 @@ var ResturaEngine = class {
1981
2829
  }
1982
2830
  }
1983
2831
  async updateTypes() {
1984
- await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2832
+ await this.generateApiFromSchema(path4.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1985
2833
  await this.generateModelFromSchema(
1986
- path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2834
+ path4.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1987
2835
  this.schema
1988
2836
  );
2837
+ this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
1989
2838
  }
1990
2839
  async getSchema(req, res) {
1991
2840
  res.send({ data: this.schema });
@@ -1993,19 +2842,38 @@ var ResturaEngine = class {
1993
2842
  async getSchemaAndTypes(req, res) {
1994
2843
  try {
1995
2844
  const schema = await this.getLatestFileSystemSchema();
1996
- const schemaHash = await this.generateHashForSchema(schema);
1997
- const apiText = await apiGenerator(schema, schemaHash);
1998
- const modelsText = await modelGenerator(schema, schemaHash);
2845
+ const apiText = await apiGenerator(schema);
2846
+ const modelsText = await modelGenerator(schema);
1999
2847
  res.send({ schema, api: apiText, models: modelsText });
2000
2848
  } catch (err) {
2001
2849
  res.status(400).send({ error: err });
2002
2850
  }
2003
2851
  }
2852
+ async getMulterFilesIfAny(req, res, routeData) {
2853
+ var _a2;
2854
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2855
+ if (!this.isCustomRoute(routeData)) return;
2856
+ if (!routeData.fileUploadType) {
2857
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2858
+ }
2859
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2860
+ return new Promise((resolve2, reject) => {
2861
+ multerFileUploadFunction(req, res, (err) => {
2862
+ if (err) {
2863
+ logger.warn("Multer error: " + err);
2864
+ reject(err);
2865
+ }
2866
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2867
+ resolve2();
2868
+ });
2869
+ });
2870
+ }
2004
2871
  async executeRouteLogic(req, res, next) {
2005
2872
  try {
2006
2873
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
2007
2874
  this.validateAuthorization(req, routeData);
2008
- validateRequestParams(req, routeData, this.customTypeValidation);
2875
+ await this.getMulterFilesIfAny(req, res, routeData);
2876
+ requestValidator(req, routeData, this.customTypeValidation);
2009
2877
  if (this.isCustomRoute(routeData)) {
2010
2878
  await this.runCustomRouteLogic(req, res, routeData);
2011
2879
  return;
@@ -2041,22 +2909,10 @@ var ResturaEngine = class {
2041
2909
  return acc + StringUtils3.capitalizeFirst(cur);
2042
2910
  }, "")}`;
2043
2911
  const customFunction = customApi[functionName];
2044
- if (!customFunction) throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented`);
2912
+ if (!customFunction)
2913
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2045
2914
  await customFunction(req, res, routeData);
2046
2915
  }
2047
- async generateHashForSchema(providedSchema) {
2048
- const schemaPrettyStr = await prettier3.format(JSON.stringify(providedSchema), __spreadValues({
2049
- parser: "json"
2050
- }, {
2051
- trailingComma: "none",
2052
- tabWidth: 4,
2053
- useTabs: true,
2054
- endOfLine: "lf",
2055
- printWidth: 120,
2056
- singleQuote: true
2057
- }));
2058
- return createHash("sha256").update(schemaPrettyStr).digest("hex");
2059
- }
2060
2916
  async storeFileSystemSchema() {
2061
2917
  const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
2062
2918
  parser: "json"
@@ -2068,7 +2924,7 @@ var ResturaEngine = class {
2068
2924
  printWidth: 120,
2069
2925
  singleQuote: true
2070
2926
  }));
2071
- fs3.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2927
+ fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2072
2928
  }
2073
2929
  resetPublicEndpoints() {
2074
2930
  this.publicEndpoints = {
@@ -2085,13 +2941,13 @@ var ResturaEngine = class {
2085
2941
  if (!routeData.roles.includes(role))
2086
2942
  throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
2087
2943
  }
2088
- getRouteData(method, baseUrl, path4) {
2944
+ getRouteData(method, baseUrl, path5) {
2089
2945
  const endpoint = this.schema.endpoints.find((item) => {
2090
2946
  return item.baseUrl === baseUrl;
2091
2947
  });
2092
2948
  if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
2093
2949
  const route = endpoint.routes.find((item) => {
2094
- return item.method === method && item.path === path4;
2950
+ return item.method === method && item.path === path5;
2095
2951
  });
2096
2952
  if (!route) throw new RsError("NOT_FOUND", "Route not found");
2097
2953
  return route;
@@ -2112,6 +2968,9 @@ __decorateClass([
2112
2968
  __decorateClass([
2113
2969
  boundMethod
2114
2970
  ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
2971
+ __decorateClass([
2972
+ boundMethod
2973
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
2115
2974
  __decorateClass([
2116
2975
  boundMethod
2117
2976
  ], ResturaEngine.prototype, "executeRouteLogic", 1);
@@ -2121,24 +2980,54 @@ __decorateClass([
2121
2980
  __decorateClass([
2122
2981
  boundMethod
2123
2982
  ], ResturaEngine.prototype, "runCustomRouteLogic", 1);
2124
- var setupPgReturnTypes = () => {
2125
- const TIMESTAMPTZ_OID = 1184;
2126
- types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
2127
- return val === null ? null : new Date(val).toISOString();
2128
- });
2129
- const BIGINT_OID = 20;
2130
- types.setTypeParser(BIGINT_OID, (val) => {
2131
- return val === null ? null : Number(val);
2132
- });
2133
- };
2134
- setupPgReturnTypes();
2135
2983
  var restura = new ResturaEngine();
2984
+
2985
+ // src/restura/sql/PsqlTransaction.ts
2986
+ import pg3 from "pg";
2987
+ var { Client: Client2 } = pg3;
2988
+ var PsqlTransaction = class extends PsqlConnection {
2989
+ constructor(clientConfig, instanceId) {
2990
+ super(instanceId);
2991
+ this.clientConfig = clientConfig;
2992
+ this.client = new Client2(clientConfig);
2993
+ this.connectPromise = this.client.connect();
2994
+ this.beginTransactionPromise = this.beginTransaction();
2995
+ }
2996
+ async close() {
2997
+ if (this.client) {
2998
+ await this.client.end();
2999
+ }
3000
+ }
3001
+ async beginTransaction() {
3002
+ await this.connectPromise;
3003
+ return this.client.query("BEGIN");
3004
+ }
3005
+ async rollback() {
3006
+ return this.query("ROLLBACK");
3007
+ }
3008
+ async commit() {
3009
+ return this.query("COMMIT");
3010
+ }
3011
+ async release() {
3012
+ return this.client.end();
3013
+ }
3014
+ async query(query, values) {
3015
+ await this.connectPromise;
3016
+ await this.beginTransactionPromise;
3017
+ return this.client.query(query, values);
3018
+ }
3019
+ };
2136
3020
  export {
2137
3021
  HtmlStatusCodes,
3022
+ PsqlConnection,
3023
+ PsqlEngine,
2138
3024
  PsqlPool,
3025
+ PsqlTransaction,
2139
3026
  RsError,
2140
3027
  SQL,
2141
3028
  escapeColumnName,
3029
+ eventManager_default as eventManager,
3030
+ filterPsqlParser_default as filterPsqlParser,
2142
3031
  insertObjectQuery,
2143
3032
  isValueNumber2 as isValueNumber,
2144
3033
  logger,