@restura/core 0.1.0-alpha.9 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)}${column.isNullable ? " | null" : ""};
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,34 @@ 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";
1541
+ import { format as sqlFormat } from "sql-formatter";
1204
1542
 
1205
1543
  // src/restura/sql/PsqlUtils.ts
1206
1544
  import format2 from "pg-format";
@@ -1210,16 +1548,33 @@ function escapeColumnName(columnName) {
1210
1548
  }
1211
1549
  function questionMarksToOrderedParams(query) {
1212
1550
  let count = 1;
1213
- return query.replace(/'\?'|\?/g, () => `$${count++}`);
1551
+ let inSingleQuote = false;
1552
+ let inDoubleQuote = false;
1553
+ return query.replace(/('|"|\?)/g, (char) => {
1554
+ if (char === "'") {
1555
+ inSingleQuote = !inSingleQuote && !inDoubleQuote;
1556
+ return char;
1557
+ }
1558
+ if (char === '"') {
1559
+ inDoubleQuote = !inDoubleQuote && !inSingleQuote;
1560
+ return char;
1561
+ }
1562
+ if (char === "?" && !inSingleQuote && !inDoubleQuote) {
1563
+ return `$${count++}`;
1564
+ }
1565
+ return char;
1566
+ });
1214
1567
  }
1215
1568
  function insertObjectQuery(table, obj) {
1216
1569
  const keys = Object.keys(obj);
1217
1570
  const params = Object.values(obj);
1218
1571
  const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1219
1572
  const values = params.map((value) => SQL`${value}`).join(", ");
1220
- const query = `INSERT INTO "${table}" (${columns})
1573
+ let query = `
1574
+ INSERT INTO "${table}" (${columns})
1221
1575
  VALUES (${values})
1222
1576
  RETURNING *`;
1577
+ query = query.replace(/'(\?)'/, "?");
1223
1578
  return query;
1224
1579
  }
1225
1580
  function updateObjectQuery(table, obj, whereStatement) {
@@ -1227,25 +1582,132 @@ function updateObjectQuery(table, obj, whereStatement) {
1227
1582
  for (const i in obj) {
1228
1583
  setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1229
1584
  }
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;
1585
+ return `
1586
+ UPDATE ${escapeColumnName(table)}
1587
+ SET ${setArray.join(", ")} ${whereStatement}
1588
+ RETURNING *`;
1589
+ }
1590
+ function isValueNumber2(value) {
1591
+ return !isNaN(Number(value));
1592
+ }
1593
+ function SQL(strings, ...values) {
1594
+ let query = strings[0];
1595
+ values.forEach((value, index) => {
1596
+ if (typeof value === "boolean") {
1597
+ query += value;
1598
+ } else if (typeof value === "number") {
1599
+ query += value;
1600
+ } else if (Array.isArray(value)) {
1601
+ query += format2.literal(JSON.stringify(value)) + "::jsonb";
1602
+ } else {
1603
+ query += format2.literal(value);
1604
+ }
1605
+ query += strings[index + 1];
1606
+ });
1607
+ return query;
1608
+ }
1609
+
1610
+ // src/restura/sql/PsqlConnection.ts
1611
+ var PsqlConnection = class {
1612
+ constructor(instanceId) {
1613
+ this.instanceId = instanceId || crypto.randomUUID();
1614
+ }
1615
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1616
+ async queryOne(query, options, requesterDetails) {
1617
+ const formattedQuery = questionMarksToOrderedParams(query);
1618
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1619
+ this.logSqlStatement(formattedQuery, options, meta);
1620
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1621
+ `;
1622
+ const startTime = process.hrtime();
1623
+ try {
1624
+ const response = await this.query(queryMetadata + formattedQuery, options);
1625
+ this.logQueryDuration(startTime);
1626
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1627
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1628
+ return response.rows[0];
1629
+ } catch (error) {
1630
+ if (RsError.isRsError(error)) throw error;
1631
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1632
+ throw new RsError("DUPLICATE", error.message);
1633
+ }
1634
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1635
+ }
1636
+ }
1637
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1638
+ async runQuery(query, options, requesterDetails) {
1639
+ const formattedQuery = questionMarksToOrderedParams(query);
1640
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1641
+ this.logSqlStatement(formattedQuery, options, meta);
1642
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1643
+ `;
1644
+ const startTime = process.hrtime();
1645
+ try {
1646
+ const response = await this.query(queryMetadata + formattedQuery, options);
1647
+ this.logQueryDuration(startTime);
1648
+ return response.rows;
1649
+ } catch (error) {
1650
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1651
+ throw new RsError("DUPLICATE", error.message);
1652
+ }
1653
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1654
+ }
1655
+ }
1656
+ logQueryDuration(startTime) {
1657
+ if (logger.level === "silly") {
1658
+ const [seconds, nanoseconds] = process.hrtime(startTime);
1659
+ const duration = seconds * 1e3 + nanoseconds / 1e6;
1660
+ logger.silly(`Query duration: ${duration.toFixed(2)}ms`);
1661
+ }
1662
+ }
1663
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1664
+ if (logger.level !== "silly") return;
1665
+ let sqlStatement = "";
1666
+ if (options.length === 0) {
1667
+ sqlStatement = query;
1242
1668
  } else {
1243
- query += format2.literal(value);
1669
+ let stringIndex = 0;
1670
+ sqlStatement = query.replace(/\$\d+/g, () => {
1671
+ const value = options[stringIndex++];
1672
+ if (typeof value === "number") return value.toString();
1673
+ return format3.literal(value);
1674
+ });
1244
1675
  }
1245
- query += strings[index + 1];
1246
- });
1247
- return query;
1248
- }
1676
+ const formattedSql = sqlFormat(sqlStatement, {
1677
+ language: "postgresql",
1678
+ linesBetweenQueries: 2,
1679
+ indentStyle: "standard",
1680
+ keywordCase: "upper",
1681
+ useTabs: true,
1682
+ tabWidth: 4
1683
+ });
1684
+ let initiator = "Anonymous";
1685
+ if ("userId" in queryMetadata && queryMetadata.userId)
1686
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1687
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1688
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1689
+ ${formattedSql}`);
1690
+ }
1691
+ };
1692
+
1693
+ // src/restura/sql/PsqlPool.ts
1694
+ var { Pool } = pg;
1695
+ var PsqlPool = class extends PsqlConnection {
1696
+ constructor(poolConfig) {
1697
+ super();
1698
+ this.poolConfig = poolConfig;
1699
+ this.pool = new Pool(poolConfig);
1700
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1701
+ logger.info("Connected to PostgreSQL database");
1702
+ }).catch((error) => {
1703
+ logger.error("Error connecting to database", error);
1704
+ process.exit(1);
1705
+ });
1706
+ }
1707
+ async query(query, values) {
1708
+ return this.pool.query(query, values);
1709
+ }
1710
+ };
1249
1711
 
1250
1712
  // src/restura/sql/SqlEngine.ts
1251
1713
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
@@ -1313,11 +1775,11 @@ var SqlEngine = class {
1313
1775
  return returnValue;
1314
1776
  }
1315
1777
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1316
- var _a;
1778
+ var _a2;
1317
1779
  if (!routeData.request) return value;
1318
1780
  const data = req.data;
1319
1781
  if (typeof value === "string") {
1320
- (_a = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1782
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1321
1783
  const requestParam = routeData.request.find((item) => {
1322
1784
  return item.name === param.replace("$", "");
1323
1785
  });
@@ -1330,9 +1792,9 @@ var SqlEngine = class {
1330
1792
  return value;
1331
1793
  }
1332
1794
  replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1333
- var _a;
1795
+ var _a2;
1334
1796
  if (typeof value === "string") {
1335
- (_a = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1797
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1336
1798
  param = param.replace("#", "");
1337
1799
  const globalParamValue = req.requesterDetails[param];
1338
1800
  if (!globalParamValue)
@@ -1351,32 +1813,86 @@ var SqlEngine = class {
1351
1813
  // src/restura/sql/filterPsqlParser.ts
1352
1814
  import peg from "pegjs";
1353
1815
  var filterSqlGrammar = `
1816
+ {
1817
+ // ported from pg-format but intentionally will add double quotes to every column
1818
+ function quoteSqlIdentity(value) {
1819
+ if (value === undefined || value === null) {
1820
+ throw new Error('SQL identifier cannot be null or undefined');
1821
+ } else if (value === false) {
1822
+ return '"f"';
1823
+ } else if (value === true) {
1824
+ return '"t"';
1825
+ } else if (value instanceof Date) {
1826
+ // return '"' + formatDate(value.toISOString()) + '"';
1827
+ } else if (value instanceof Buffer) {
1828
+ throw new Error('SQL identifier cannot be a buffer');
1829
+ } else if (Array.isArray(value) === true) {
1830
+ var temp = [];
1831
+ for (var i = 0; i < value.length; i++) {
1832
+ if (Array.isArray(value[i]) === true) {
1833
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1834
+ } else {
1835
+ // temp.push(quoteIdent(value[i]));
1836
+ }
1837
+ }
1838
+ return temp.toString();
1839
+ } else if (value === Object(value)) {
1840
+ throw new Error('SQL identifier cannot be an object');
1841
+ }
1842
+
1843
+ var ident = value.toString().slice(0); // create copy
1844
+
1845
+ // do not quote a valid, unquoted identifier
1846
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1847
+ // return ident;
1848
+ // }
1849
+
1850
+ var quoted = '"';
1851
+
1852
+ for (var i = 0; i < ident.length; i++) {
1853
+ var c = ident[i];
1854
+ if (c === '"') {
1855
+ quoted += c + c;
1856
+ } else {
1857
+ quoted += c;
1858
+ }
1859
+ }
1860
+
1861
+ quoted += '"';
1862
+
1863
+ return quoted;
1864
+ };
1865
+ }
1866
+
1354
1867
  start = expressionList
1355
1868
 
1869
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1870
+
1356
1871
  expressionList =
1357
- leftExpression:expression operator:operator rightExpression:expressionList
1872
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1358
1873
  { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1359
1874
  / expression
1360
1875
 
1361
1876
  expression =
1362
- negate:negate?"(" "column:" column:column ","? value:value? ","? type:type? ")"
1363
- {return \`\${negate? "!" : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1877
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1878
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1364
1879
  /
1365
- negate:negate?"("expression:expressionList")" { return \`\${negate? "!" : ""}(\${expression})\`; }
1880
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1366
1881
 
1367
1882
  negate = "!"
1368
1883
 
1369
1884
  operator = "and"i / "or"i
1370
1885
 
1371
1886
 
1372
- column = left:text "." right:text { return \`\${format.ident(left)}.\${format.ident(right)}\`; }
1887
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1373
1888
  /
1374
- text:text { return format.ident(text); }
1889
+ text:text { return quoteSqlIdentity(text); }
1375
1890
 
1376
1891
 
1377
- text = text:[a-z0-9-_:@]i+ { return text.join("");}
1892
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
1378
1893
 
1379
- type = "type:" type:typeString { return type; }
1894
+
1895
+ type = "type" _ ":" _ type:typeString { return type; }
1380
1896
  typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1381
1897
  text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1382
1898
  text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
@@ -1386,8 +1902,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
1386
1902
  text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1387
1903
  text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1388
1904
  text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1389
-
1390
- value = "value:" value:text { return value; }
1905
+
1906
+ value = "value" _ ":" value:text { return value; }
1907
+
1391
1908
 
1392
1909
  `;
1393
1910
  var filterPsqlParser = peg.generate(filterSqlGrammar, {
@@ -1397,18 +1914,224 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1397
1914
  var filterPsqlParser_default = filterPsqlParser;
1398
1915
 
1399
1916
  // src/restura/sql/PsqlEngine.ts
1917
+ var { Client, types } = pg2;
1918
+ var systemUser = {
1919
+ role: "",
1920
+ host: "",
1921
+ ipAddress: "",
1922
+ isSystemUser: true
1923
+ };
1400
1924
  var PsqlEngine = class extends SqlEngine {
1401
- constructor(psqlConnectionPool) {
1925
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1402
1926
  super();
1403
1927
  this.psqlConnectionPool = psqlConnectionPool;
1928
+ this.setupPgReturnTypes();
1929
+ if (shouldListenForDbTriggers) {
1930
+ this.setupTriggerListeners = this.listenForDbTriggers();
1931
+ }
1404
1932
  }
1405
- async diffDatabaseToSchema(schema) {
1406
- console.log(schema);
1407
- return Promise.resolve("");
1933
+ async close() {
1934
+ if (this.triggerClient) {
1935
+ await this.triggerClient.end();
1936
+ }
1937
+ }
1938
+ setupPgReturnTypes() {
1939
+ const TIMESTAMPTZ_OID = 1184;
1940
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1941
+ return val === null ? null : new Date(val).toISOString();
1942
+ });
1943
+ const BIGINT_OID = 20;
1944
+ types.setTypeParser(BIGINT_OID, (val) => {
1945
+ return val === null ? null : Number(val);
1946
+ });
1947
+ }
1948
+ async listenForDbTriggers() {
1949
+ this.triggerClient = new Client({
1950
+ user: this.psqlConnectionPool.poolConfig.user,
1951
+ host: this.psqlConnectionPool.poolConfig.host,
1952
+ database: this.psqlConnectionPool.poolConfig.database,
1953
+ password: this.psqlConnectionPool.poolConfig.password,
1954
+ port: this.psqlConnectionPool.poolConfig.port,
1955
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1956
+ });
1957
+ await this.triggerClient.connect();
1958
+ const promises = [];
1959
+ promises.push(this.triggerClient.query("LISTEN insert"));
1960
+ promises.push(this.triggerClient.query("LISTEN update"));
1961
+ promises.push(this.triggerClient.query("LISTEN delete"));
1962
+ await Promise.all(promises);
1963
+ this.triggerClient.on("notification", async (msg) => {
1964
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1965
+ const payload = ObjectUtils4.safeParse(msg.payload);
1966
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1967
+ }
1968
+ });
1969
+ }
1970
+ async handleTrigger(payload, mutationType) {
1971
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
1972
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
1973
+ }
1974
+ }
1975
+ async createDatabaseFromSchema(schema, connection) {
1976
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1977
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1978
+ return sqlFullStatement;
1408
1979
  }
1409
1980
  generateDatabaseSchemaFromSchema(schema) {
1410
- console.log(schema);
1411
- return "";
1981
+ const sqlStatements = [];
1982
+ const indexes = [];
1983
+ const triggers = [];
1984
+ for (const table of schema.database) {
1985
+ if (table.notify) {
1986
+ triggers.push(this.createInsertTriggers(table.name, table.notify));
1987
+ triggers.push(this.createUpdateTrigger(table.name, table.notify));
1988
+ triggers.push(this.createDeleteTrigger(table.name, table.notify));
1989
+ }
1990
+ let sql = `CREATE TABLE "${table.name}"
1991
+ ( `;
1992
+ const tableColumns = [];
1993
+ for (const column of table.columns) {
1994
+ let columnSql = "";
1995
+ columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
1996
+ let value = column.value;
1997
+ if (column.type === "JSON") value = "";
1998
+ if (column.type === "JSONB") value = "";
1999
+ if (column.type === "DECIMAL" && value) {
2000
+ value = value.replace("-", ",").replace(/['"]/g, "");
2001
+ }
2002
+ if (value && column.type !== "ENUM") {
2003
+ columnSql += `(${value})`;
2004
+ } else if (column.length) columnSql += `(${column.length})`;
2005
+ if (column.isPrimary) {
2006
+ columnSql += " PRIMARY KEY ";
2007
+ }
2008
+ if (column.isUnique) {
2009
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
2010
+ }
2011
+ if (column.isNullable) columnSql += " NULL";
2012
+ else columnSql += " NOT NULL";
2013
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
2014
+ if (value && column.type === "ENUM") {
2015
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
2016
+ }
2017
+ tableColumns.push(columnSql);
2018
+ }
2019
+ sql += tableColumns.join(", \n");
2020
+ for (const index of table.indexes) {
2021
+ if (!index.isPrimaryKey) {
2022
+ let unique = " ";
2023
+ if (index.isUnique) unique = "UNIQUE ";
2024
+ indexes.push(
2025
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
2026
+ return `"${item}" ${index.order}`;
2027
+ }).join(", ")});`
2028
+ );
2029
+ }
2030
+ }
2031
+ sql += "\n);";
2032
+ sqlStatements.push(sql);
2033
+ }
2034
+ for (const table of schema.database) {
2035
+ if (!table.foreignKeys.length) continue;
2036
+ const sql = `ALTER TABLE "${table.name}" `;
2037
+ const constraints = [];
2038
+ for (const foreignKey of table.foreignKeys) {
2039
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2040
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2041
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2042
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2043
+ constraints.push(constraint);
2044
+ }
2045
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2046
+ }
2047
+ for (const table of schema.database) {
2048
+ if (!table.checkConstraints.length) continue;
2049
+ const sql = `ALTER TABLE "${table.name}" `;
2050
+ const constraints = [];
2051
+ for (const check of table.checkConstraints) {
2052
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2053
+ constraints.push(constraint);
2054
+ }
2055
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2056
+ }
2057
+ sqlStatements.push(indexes.join("\n"));
2058
+ sqlStatements.push(triggers.join("\n"));
2059
+ return sqlStatements.join("\n\n");
2060
+ }
2061
+ async getScratchPool() {
2062
+ var _a2, _b;
2063
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
2064
+ `SELECT *
2065
+ FROM pg_database
2066
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
2067
+ [],
2068
+ systemUser
2069
+ );
2070
+ if (scratchDbExists.length === 0) {
2071
+ await this.psqlConnectionPool.runQuery(
2072
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
2073
+ [],
2074
+ systemUser
2075
+ );
2076
+ }
2077
+ const scratchPool = new PsqlPool({
2078
+ host: this.psqlConnectionPool.poolConfig.host,
2079
+ port: this.psqlConnectionPool.poolConfig.port,
2080
+ user: this.psqlConnectionPool.poolConfig.user,
2081
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2082
+ password: this.psqlConnectionPool.poolConfig.password,
2083
+ max: this.psqlConnectionPool.poolConfig.max,
2084
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2085
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2086
+ });
2087
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2088
+ await scratchPool.runQuery(
2089
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2090
+ [],
2091
+ systemUser
2092
+ );
2093
+ const schemaComment = await this.psqlConnectionPool.runQuery(
2094
+ `SELECT pg_description.description
2095
+ FROM pg_description
2096
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2097
+ WHERE pg_namespace.nspname = 'public';`,
2098
+ [],
2099
+ systemUser
2100
+ );
2101
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2102
+ await scratchPool.runQuery(
2103
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
2104
+ [],
2105
+ systemUser
2106
+ );
2107
+ }
2108
+ return scratchPool;
2109
+ }
2110
+ async diffDatabaseToSchema(schema) {
2111
+ const scratchPool = await this.getScratchPool();
2112
+ await this.createDatabaseFromSchema(schema, scratchPool);
2113
+ const originalClient = new Client({
2114
+ database: this.psqlConnectionPool.poolConfig.database,
2115
+ user: this.psqlConnectionPool.poolConfig.user,
2116
+ password: this.psqlConnectionPool.poolConfig.password,
2117
+ host: this.psqlConnectionPool.poolConfig.host,
2118
+ port: this.psqlConnectionPool.poolConfig.port
2119
+ });
2120
+ const scratchClient = new Client({
2121
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2122
+ user: this.psqlConnectionPool.poolConfig.user,
2123
+ password: this.psqlConnectionPool.poolConfig.password,
2124
+ host: this.psqlConnectionPool.poolConfig.host,
2125
+ port: this.psqlConnectionPool.poolConfig.port
2126
+ });
2127
+ const promises = [originalClient.connect(), scratchClient.connect()];
2128
+ await Promise.all(promises);
2129
+ const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
2130
+ const [info1, info2] = await Promise.all(infoPromises);
2131
+ const diff = getDiff(info1, info2);
2132
+ const endPromises = [originalClient.end(), scratchClient.end()];
2133
+ await Promise.all(endPromises);
2134
+ return diff.join("\n");
1412
2135
  }
1413
2136
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1414
2137
  if (!item.subquery) return "";
@@ -1422,8 +2145,7 @@ var PsqlEngine = class extends SqlEngine {
1422
2145
  )) {
1423
2146
  return "'[]'";
1424
2147
  }
1425
- return `COALESCE((
1426
- SELECT JSON_AGG(JSON_BUILD_OBJECT(
2148
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1427
2149
  ${item.subquery.properties.map((nestedItem) => {
1428
2150
  if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1429
2151
  ...routeData.joins,
@@ -1432,7 +2154,7 @@ var PsqlEngine = class extends SqlEngine {
1432
2154
  return;
1433
2155
  }
1434
2156
  if (nestedItem.subquery) {
1435
- return `"${nestedItem.name}", ${this.createNestedSelect(
2157
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1436
2158
  // recursion
1437
2159
  req,
1438
2160
  schema,
@@ -1443,7 +2165,7 @@ var PsqlEngine = class extends SqlEngine {
1443
2165
  )}`;
1444
2166
  }
1445
2167
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1446
- }).filter(Boolean).join(",")}
2168
+ }).filter(Boolean).join(", ")}
1447
2169
  ))
1448
2170
  FROM
1449
2171
  "${item.subquery.table}"
@@ -1458,16 +2180,19 @@ var PsqlEngine = class extends SqlEngine {
1458
2180
  parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
1459
2181
  });
1460
2182
  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
- ];
2183
+ const createdItem = await this.psqlConnectionPool.queryOne(
2184
+ query,
2185
+ sqlParams,
2186
+ req.requesterDetails
2187
+ );
2188
+ const insertId = createdItem.id;
2189
+ const whereId = {
2190
+ tableName: routeData.table,
2191
+ value: insertId,
2192
+ columnName: "id",
2193
+ operator: "="
2194
+ };
2195
+ const whereData = [whereId];
1471
2196
  req.data = { id: insertId };
1472
2197
  return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
1473
2198
  }
@@ -1486,7 +2211,9 @@ var PsqlEngine = class extends SqlEngine {
1486
2211
  let selectStatement = "SELECT \n";
1487
2212
  selectStatement += ` ${selectColumns.map((item) => {
1488
2213
  if (item.subquery) {
1489
- return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${item.name}`;
2214
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2215
+ item.name
2216
+ )}`;
1490
2217
  }
1491
2218
  return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
1492
2219
  }).join(",\n ")}
@@ -1519,29 +2246,31 @@ var PsqlEngine = class extends SqlEngine {
1519
2246
  );
1520
2247
  } else if (routeData.type === "PAGED") {
1521
2248
  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
- ],
2249
+ const pagePromise = this.psqlConnectionPool.runQuery(
2250
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2251
+ sqlParams,
1531
2252
  req.requesterDetails
1532
2253
  );
2254
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2255
+ ${sqlStatement};`;
2256
+ const totalPromise = this.psqlConnectionPool.runQuery(
2257
+ totalQuery,
2258
+ sqlParams,
2259
+ req.requesterDetails
2260
+ );
2261
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1533
2262
  let total = 0;
1534
- if (ObjectUtils4.isArrayWithData(pageResults)) {
1535
- total = pageResults[1][0].total;
2263
+ if (ObjectUtils4.isArrayWithData(totalResponse)) {
2264
+ total = totalResponse[0].total;
1536
2265
  }
1537
- return { data: pageResults[0], total };
2266
+ return { data: pageResults, total };
1538
2267
  } else {
1539
2268
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1540
2269
  }
1541
2270
  }
1542
2271
  async executeUpdateRequest(req, routeData, schema) {
1543
2272
  const sqlParams = [];
1544
- const _a = req.body, { id } = _a, bodyNoId = __objRest(_a, ["id"]);
2273
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
1545
2274
  const table = schema.database.find((item) => {
1546
2275
  return item.name === routeData.table;
1547
2276
  });
@@ -1552,10 +2281,10 @@ ${sqlStatement};`,
1552
2281
  for (const assignment of routeData.assignments) {
1553
2282
  const column = table.columns.find((column2) => column2.name === assignment.name);
1554
2283
  if (!column) continue;
1555
- const assignmentWithPrefix = escapeColumnName(`${routeData.table}.${assignment.name}`);
2284
+ const assignmentEscaped = escapeColumnName(assignment.name);
1556
2285
  if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
1557
- bodyNoId[assignmentWithPrefix] = Number(assignment.value);
1558
- else bodyNoId[assignmentWithPrefix] = assignment.value;
2286
+ bodyNoId[assignmentEscaped] = Number(assignment.value);
2287
+ else bodyNoId[assignmentEscaped] = assignment.value;
1559
2288
  }
1560
2289
  const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1561
2290
  const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
@@ -1573,10 +2302,12 @@ ${sqlStatement};`,
1573
2302
  req.requesterDetails.role,
1574
2303
  sqlParams
1575
2304
  );
1576
- let deleteStatement = `DELETE
1577
- FROM "${routeData.table}" ${joinStatement}`;
1578
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1579
- deleteStatement += ";";
2305
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2306
+ if (whereClause.replace(/\s/g, "") === "") {
2307
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2308
+ }
2309
+ const deleteStatement = `
2310
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
1580
2311
  await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1581
2312
  return true;
1582
2313
  }
@@ -1587,7 +2318,7 @@ ${sqlStatement};`,
1587
2318
  throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
1588
2319
  if (item.custom) {
1589
2320
  const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
1590
- joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)} ON ${customReplaced}
2321
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON ${customReplaced}
1591
2322
  `;
1592
2323
  } else {
1593
2324
  joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON "${baseTable}"."${item.localColumnName}" = ${escapeColumnName(item.alias ? item.alias : item.table)}.${escapeColumnName(
@@ -1639,38 +2370,37 @@ ${sqlStatement};`,
1639
2370
  );
1640
2371
  let operator = item.operator;
1641
2372
  if (operator === "LIKE") {
1642
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2373
+ item.value = `'%${item.value}%'`;
1643
2374
  } else if (operator === "STARTS WITH") {
1644
2375
  operator = "LIKE";
1645
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2376
+ item.value = `'${item.value}%'`;
1646
2377
  } else if (operator === "ENDS WITH") {
1647
2378
  operator = "LIKE";
1648
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2379
+ item.value = `'%${item.value}'`;
1649
2380
  }
1650
2381
  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}
2382
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
1653
2383
  `;
1654
2384
  });
1655
2385
  const data = req.data;
1656
2386
  if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
1657
2387
  let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1658
- var _a;
2388
+ var _a2;
1659
2389
  const requestParam = routeData.request.find((item) => {
1660
2390
  return item.name === value.replace("$", "");
1661
2391
  });
1662
2392
  if (!requestParam)
1663
2393
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1664
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2394
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1665
2395
  });
1666
2396
  statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
1667
- var _a;
2397
+ var _a2;
1668
2398
  const requestParam = routeData.request.find((item) => {
1669
2399
  return item.name === value.replace("#", "");
1670
2400
  });
1671
2401
  if (!requestParam)
1672
2402
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1673
- return ((_a = data[requestParam.name]) == null ? void 0 : _a.toString()) || "";
2403
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1674
2404
  });
1675
2405
  statement = filterPsqlParser_default.parse(statement);
1676
2406
  if (whereClause.startsWith("WHERE")) {
@@ -1683,82 +2413,259 @@ ${sqlStatement};`,
1683
2413
  }
1684
2414
  return whereClause;
1685
2415
  }
1686
- };
2416
+ createUpdateTrigger(tableName, notify) {
2417
+ if (!notify) return "";
2418
+ if (notify === "ALL") {
2419
+ return `
2420
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2421
+ RETURNS TRIGGER AS $$
2422
+ DECLARE
2423
+ query_metadata JSON;
2424
+ BEGIN
2425
+ SELECT INTO query_metadata
2426
+ (regexp_match(
2427
+ current_query(),
2428
+ '^--QUERY_METADATA\\(({.*})', 'n'
2429
+ ))[1]::json;
1687
2430
 
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}`);
2431
+ PERFORM pg_notify(
2432
+ 'update',
2433
+ json_build_object(
2434
+ 'table', '${tableName}',
2435
+ 'queryMetadata', query_metadata,
2436
+ 'changedId', NEW.id,
2437
+ 'record', NEW,
2438
+ 'previousRecord', OLD
2439
+ )::text
2440
+ );
2441
+ RETURN NEW;
2442
+ END;
2443
+ $$ LANGUAGE plpgsql;
2444
+
2445
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2446
+ AFTER UPDATE ON "${tableName}"
2447
+ FOR EACH ROW
2448
+ EXECUTE FUNCTION notify_${tableName}_update();
2449
+ `;
1719
2450
  }
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}`);
2451
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2452
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2453
+ return `
2454
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2455
+ RETURNS TRIGGER AS $$
2456
+ DECLARE
2457
+ query_metadata JSON;
2458
+ BEGIN
2459
+ SELECT INTO query_metadata
2460
+ (regexp_match(
2461
+ current_query(),
2462
+ '^--QUERY_METADATA\\(({.*})', 'n'
2463
+ ))[1]::json;
2464
+
2465
+ PERFORM pg_notify(
2466
+ 'update',
2467
+ json_build_object(
2468
+ 'table', '${tableName}',
2469
+ 'queryMetadata', query_metadata,
2470
+ 'changedId', NEW.id,
2471
+ 'record', json_build_object(
2472
+ ${notifyColumnNewBuildString}
2473
+ ),
2474
+ 'previousRecord', json_build_object(
2475
+ ${notifyColumnOldBuildString}
2476
+ )
2477
+ )::text
2478
+ );
2479
+ RETURN NEW;
2480
+ END;
2481
+ $$ LANGUAGE plpgsql;
2482
+
2483
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2484
+ AFTER UPDATE ON "${tableName}"
2485
+ FOR EACH ROW
2486
+ EXECUTE FUNCTION notify_${tableName}_update();
2487
+ `;
2488
+ }
2489
+ createDeleteTrigger(tableName, notify) {
2490
+ if (!notify) return "";
2491
+ if (notify === "ALL") {
2492
+ return `
2493
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2494
+ RETURNS TRIGGER AS $$
2495
+ DECLARE
2496
+ query_metadata JSON;
2497
+ BEGIN
2498
+ SELECT INTO query_metadata
2499
+ (regexp_match(
2500
+ current_query(),
2501
+ '^--QUERY_METADATA\\(({.*})', 'n'
2502
+ ))[1]::json;
2503
+
2504
+ PERFORM pg_notify(
2505
+ 'delete',
2506
+ json_build_object(
2507
+ 'table', '${tableName}',
2508
+ 'queryMetadata', query_metadata,
2509
+ 'deletedId', OLD.id,
2510
+ 'previousRecord', OLD
2511
+ )::text
2512
+ );
2513
+ RETURN NEW;
2514
+ END;
2515
+ $$ LANGUAGE plpgsql;
2516
+
2517
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2518
+ AFTER DELETE ON "${tableName}"
2519
+ FOR EACH ROW
2520
+ EXECUTE FUNCTION notify_${tableName}_delete();
2521
+ `;
1736
2522
  }
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
- });
2523
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2524
+ return `
2525
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2526
+ RETURNS TRIGGER AS $$
2527
+ DECLARE
2528
+ query_metadata JSON;
2529
+ BEGIN
2530
+ SELECT INTO query_metadata
2531
+ (regexp_match(
2532
+ current_query(),
2533
+ '^--QUERY_METADATA\\(({.*})', 'n'
2534
+ ))[1]::json;
2535
+
2536
+ PERFORM pg_notify(
2537
+ 'delete',
2538
+ json_build_object(
2539
+ 'table', '${tableName}',
2540
+ 'queryMetadata', query_metadata,
2541
+ 'deletedId', OLD.id,
2542
+ 'previousRecord', json_build_object(
2543
+ ${notifyColumnOldBuildString}
2544
+ )
2545
+ )::text
2546
+ );
2547
+ RETURN NEW;
2548
+ END;
2549
+ $$ LANGUAGE plpgsql;
2550
+
2551
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2552
+ AFTER DELETE ON "${tableName}"
2553
+ FOR EACH ROW
2554
+ EXECUTE FUNCTION notify_${tableName}_delete();
2555
+ `;
2556
+ }
2557
+ createInsertTriggers(tableName, notify) {
2558
+ if (!notify) return "";
2559
+ if (notify === "ALL") {
2560
+ return `
2561
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2562
+ RETURNS TRIGGER AS $$
2563
+ DECLARE
2564
+ query_metadata JSON;
2565
+ BEGIN
2566
+ SELECT INTO query_metadata
2567
+ (regexp_match(
2568
+ current_query(),
2569
+ '^--QUERY_METADATA\\(({.*})', 'n'
2570
+ ))[1]::json;
2571
+
2572
+ PERFORM pg_notify(
2573
+ 'insert',
2574
+ json_build_object(
2575
+ 'table', '${tableName}',
2576
+ 'queryMetadata', query_metadata,
2577
+ 'insertedId', NEW.id,
2578
+ 'record', NEW
2579
+ )::text
2580
+ );
2581
+
2582
+ RETURN NEW;
2583
+ END;
2584
+ $$ LANGUAGE plpgsql;
2585
+
2586
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2587
+ AFTER INSERT ON "${tableName}"
2588
+ FOR EACH ROW
2589
+ EXECUTE FUNCTION notify_${tableName}_insert();
2590
+ `;
1750
2591
  }
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}`);
2592
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2593
+ return `
2594
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2595
+ RETURNS TRIGGER AS $$
2596
+ DECLARE
2597
+ query_metadata JSON;
2598
+ BEGIN
2599
+ SELECT INTO query_metadata
2600
+ (regexp_match(
2601
+ current_query(),
2602
+ '^--QUERY_METADATA\\(({.*})', 'n'
2603
+ ))[1]::json;
2604
+
2605
+ PERFORM pg_notify(
2606
+ 'insert',
2607
+ json_build_object(
2608
+ 'table', '${tableName}',
2609
+ 'queryMetadata', query_metadata,
2610
+ 'insertedId', NEW.id,
2611
+ 'record', json_build_object(
2612
+ ${notifyColumnNewBuildString}
2613
+ )
2614
+ )::text
2615
+ );
2616
+
2617
+ RETURN NEW;
2618
+ END;
2619
+ $$ LANGUAGE plpgsql;
2620
+
2621
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2622
+ AFTER INSERT ON "${tableName}"
2623
+ FOR EACH ROW
2624
+ EXECUTE FUNCTION notify_${tableName}_insert();
2625
+ `;
2626
+ }
2627
+ schemaToPsqlType(column) {
2628
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2629
+ if (column.type === "ENUM") return `TEXT`;
2630
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2631
+ if (column.type === "MEDIUMINT") return "INT";
2632
+ return column.type;
2633
+ }
2634
+ };
2635
+
2636
+ // src/restura/utils/TempCache.ts
2637
+ import fs3 from "fs";
2638
+ import path3 from "path";
2639
+ import { DateUtils } from "@redskytech/core-utils";
2640
+ import { FileUtils as FileUtils2 } from "@restura/internal";
2641
+ import Bluebird3 from "bluebird";
2642
+ import * as os2 from "os";
2643
+ var TempCache = class {
2644
+ constructor(location) {
2645
+ this.maxDurationDays = 7;
2646
+ this.location = location || os2.tmpdir();
2647
+ FileUtils2.ensureDir(this.location).catch((e) => {
2648
+ throw e;
2649
+ });
2650
+ }
2651
+ async cleanup() {
2652
+ const fileList = await fs3.promises.readdir(this.location);
2653
+ await Bluebird3.map(
2654
+ fileList,
2655
+ async (file) => {
2656
+ const fullFilePath = path3.join(this.location, file);
2657
+ const fileStats = await fs3.promises.stat(fullFilePath);
2658
+ if (DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2659
+ logger.info(`Deleting old temp file: ${file}`);
2660
+ await fs3.promises.unlink(fullFilePath);
2661
+ }
2662
+ },
2663
+ { concurrency: 10 }
2664
+ );
1757
2665
  }
1758
2666
  };
1759
2667
 
1760
2668
  // src/restura/restura.ts
1761
- var { types } = pg2;
1762
2669
  var ResturaEngine = class {
1763
2670
  constructor() {
1764
2671
  this.publicEndpoints = {
@@ -1777,9 +2684,10 @@ var ResturaEngine = class {
1777
2684
  */
1778
2685
  async init(app, authenticationHandler, psqlConnectionPool) {
1779
2686
  this.resturaConfig = config2.validate("restura", resturaConfigSchema);
2687
+ this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
2688
+ new TempCache(this.resturaConfig.fileTempCachePath);
1780
2689
  this.psqlConnectionPool = psqlConnectionPool;
1781
- this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
1782
- setupPgReturnTypes();
2690
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
1783
2691
  await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
1784
2692
  this.authenticationHandler = authenticationHandler;
1785
2693
  app.use(compression());
@@ -1843,10 +2751,7 @@ var ResturaEngine = class {
1843
2751
  * @returns A promise that resolves when the API has been successfully generated and written to the output file.
1844
2752
  */
1845
2753
  async generateApiFromSchema(outputFile, providedSchema) {
1846
- fs3.writeFileSync(
1847
- outputFile,
1848
- await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1849
- );
2754
+ fs4.writeFileSync(outputFile, await apiGenerator(providedSchema));
1850
2755
  }
1851
2756
  /**
1852
2757
  * Generates a model from the provided schema and writes it to the specified output file.
@@ -1856,10 +2761,15 @@ var ResturaEngine = class {
1856
2761
  * @returns A promise that resolves when the model has been successfully written to the output file.
1857
2762
  */
1858
2763
  async generateModelFromSchema(outputFile, providedSchema) {
1859
- fs3.writeFileSync(
1860
- outputFile,
1861
- await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1862
- );
2764
+ fs4.writeFileSync(outputFile, await modelGenerator(providedSchema));
2765
+ }
2766
+ /**
2767
+ * Generates the ambient module declaration for Restura global types and writes it to the specified output file.
2768
+ * These types are used sometimes in the CustomTypes
2769
+ * @param outputFile
2770
+ */
2771
+ generateResturaGlobalTypes(outputFile) {
2772
+ fs4.writeFileSync(outputFile, resturaGlobalTypesGenerator());
1863
2773
  }
1864
2774
  /**
1865
2775
  * Retrieves the latest file system schema for Restura.
@@ -1868,11 +2778,11 @@ var ResturaEngine = class {
1868
2778
  * @throws {Error} If the schema file is missing or the schema is not valid.
1869
2779
  */
1870
2780
  async getLatestFileSystemSchema() {
1871
- if (!fs3.existsSync(this.resturaConfig.schemaFilePath)) {
2781
+ if (!fs4.existsSync(this.resturaConfig.schemaFilePath)) {
1872
2782
  logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
1873
2783
  throw new Error("Missing restura schema file");
1874
2784
  }
1875
- const schemaFileData = fs3.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2785
+ const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
1876
2786
  const schema = ObjectUtils5.safeParse(schemaFileData);
1877
2787
  const isValid = await isSchemaValid(schema);
1878
2788
  if (!isValid) {
@@ -1881,28 +2791,6 @@ var ResturaEngine = class {
1881
2791
  }
1882
2792
  return schema;
1883
2793
  }
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
2794
  async reloadEndpoints() {
1907
2795
  this.schema = await this.getLatestFileSystemSchema();
1908
2796
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
@@ -1931,30 +2819,10 @@ var ResturaEngine = class {
1931
2819
  logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
1932
2820
  }
1933
2821
  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
- );
2822
+ if (!fs4.existsSync(this.resturaConfig.generatedTypesPath)) {
2823
+ fs4.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1957
2824
  }
2825
+ this.updateTypes();
1958
2826
  }
1959
2827
  resturaAuthentication(req, res, next) {
1960
2828
  if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
@@ -1962,7 +2830,7 @@ var ResturaEngine = class {
1962
2830
  }
1963
2831
  async previewCreateSchema(req, res) {
1964
2832
  try {
1965
- const schemaDiff = { commands: "", endPoints: [], globalParams: [], roles: [], customTypes: false };
2833
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
1966
2834
  res.send({ data: schemaDiff });
1967
2835
  } catch (err) {
1968
2836
  res.status(400).send(err);
@@ -1970,7 +2838,7 @@ var ResturaEngine = class {
1970
2838
  }
1971
2839
  async updateSchema(req, res) {
1972
2840
  try {
1973
- this.schema = req.data;
2841
+ this.schema = sortObjectKeysAlphabetically(req.data);
1974
2842
  await this.storeFileSystemSchema();
1975
2843
  await this.reloadEndpoints();
1976
2844
  await this.updateTypes();
@@ -1981,11 +2849,12 @@ var ResturaEngine = class {
1981
2849
  }
1982
2850
  }
1983
2851
  async updateTypes() {
1984
- await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2852
+ await this.generateApiFromSchema(path4.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1985
2853
  await this.generateModelFromSchema(
1986
- path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2854
+ path4.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1987
2855
  this.schema
1988
2856
  );
2857
+ this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
1989
2858
  }
1990
2859
  async getSchema(req, res) {
1991
2860
  res.send({ data: this.schema });
@@ -1993,19 +2862,38 @@ var ResturaEngine = class {
1993
2862
  async getSchemaAndTypes(req, res) {
1994
2863
  try {
1995
2864
  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);
2865
+ const apiText = await apiGenerator(schema);
2866
+ const modelsText = await modelGenerator(schema);
1999
2867
  res.send({ schema, api: apiText, models: modelsText });
2000
2868
  } catch (err) {
2001
2869
  res.status(400).send({ error: err });
2002
2870
  }
2003
2871
  }
2872
+ async getMulterFilesIfAny(req, res, routeData) {
2873
+ var _a2;
2874
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2875
+ if (!this.isCustomRoute(routeData)) return;
2876
+ if (!routeData.fileUploadType) {
2877
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2878
+ }
2879
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2880
+ return new Promise((resolve2, reject) => {
2881
+ multerFileUploadFunction(req, res, (err) => {
2882
+ if (err) {
2883
+ logger.warn("Multer error: " + err);
2884
+ reject(err);
2885
+ }
2886
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2887
+ resolve2();
2888
+ });
2889
+ });
2890
+ }
2004
2891
  async executeRouteLogic(req, res, next) {
2005
2892
  try {
2006
2893
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
2007
2894
  this.validateAuthorization(req, routeData);
2008
- validateRequestParams(req, routeData, this.customTypeValidation);
2895
+ await this.getMulterFilesIfAny(req, res, routeData);
2896
+ requestValidator(req, routeData, this.customTypeValidation);
2009
2897
  if (this.isCustomRoute(routeData)) {
2010
2898
  await this.runCustomRouteLogic(req, res, routeData);
2011
2899
  return;
@@ -2041,22 +2929,10 @@ var ResturaEngine = class {
2041
2929
  return acc + StringUtils3.capitalizeFirst(cur);
2042
2930
  }, "")}`;
2043
2931
  const customFunction = customApi[functionName];
2044
- if (!customFunction) throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented`);
2932
+ if (!customFunction)
2933
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2045
2934
  await customFunction(req, res, routeData);
2046
2935
  }
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
2936
  async storeFileSystemSchema() {
2061
2937
  const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
2062
2938
  parser: "json"
@@ -2068,7 +2944,7 @@ var ResturaEngine = class {
2068
2944
  printWidth: 120,
2069
2945
  singleQuote: true
2070
2946
  }));
2071
- fs3.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2947
+ fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2072
2948
  }
2073
2949
  resetPublicEndpoints() {
2074
2950
  this.publicEndpoints = {
@@ -2085,13 +2961,13 @@ var ResturaEngine = class {
2085
2961
  if (!routeData.roles.includes(role))
2086
2962
  throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
2087
2963
  }
2088
- getRouteData(method, baseUrl, path4) {
2964
+ getRouteData(method, baseUrl, path5) {
2089
2965
  const endpoint = this.schema.endpoints.find((item) => {
2090
2966
  return item.baseUrl === baseUrl;
2091
2967
  });
2092
2968
  if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
2093
2969
  const route = endpoint.routes.find((item) => {
2094
- return item.method === method && item.path === path4;
2970
+ return item.method === method && item.path === path5;
2095
2971
  });
2096
2972
  if (!route) throw new RsError("NOT_FOUND", "Route not found");
2097
2973
  return route;
@@ -2112,6 +2988,9 @@ __decorateClass([
2112
2988
  __decorateClass([
2113
2989
  boundMethod
2114
2990
  ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
2991
+ __decorateClass([
2992
+ boundMethod
2993
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
2115
2994
  __decorateClass([
2116
2995
  boundMethod
2117
2996
  ], ResturaEngine.prototype, "executeRouteLogic", 1);
@@ -2121,24 +3000,54 @@ __decorateClass([
2121
3000
  __decorateClass([
2122
3001
  boundMethod
2123
3002
  ], 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
3003
  var restura = new ResturaEngine();
3004
+
3005
+ // src/restura/sql/PsqlTransaction.ts
3006
+ import pg3 from "pg";
3007
+ var { Client: Client2 } = pg3;
3008
+ var PsqlTransaction = class extends PsqlConnection {
3009
+ constructor(clientConfig, instanceId) {
3010
+ super(instanceId);
3011
+ this.clientConfig = clientConfig;
3012
+ this.client = new Client2(clientConfig);
3013
+ this.connectPromise = this.client.connect();
3014
+ this.beginTransactionPromise = this.beginTransaction();
3015
+ }
3016
+ async close() {
3017
+ if (this.client) {
3018
+ await this.client.end();
3019
+ }
3020
+ }
3021
+ async beginTransaction() {
3022
+ await this.connectPromise;
3023
+ return this.client.query("BEGIN");
3024
+ }
3025
+ async rollback() {
3026
+ return this.query("ROLLBACK");
3027
+ }
3028
+ async commit() {
3029
+ return this.query("COMMIT");
3030
+ }
3031
+ async release() {
3032
+ return this.client.end();
3033
+ }
3034
+ async query(query, values) {
3035
+ await this.connectPromise;
3036
+ await this.beginTransactionPromise;
3037
+ return this.client.query(query, values);
3038
+ }
3039
+ };
2136
3040
  export {
2137
3041
  HtmlStatusCodes,
3042
+ PsqlConnection,
3043
+ PsqlEngine,
2138
3044
  PsqlPool,
3045
+ PsqlTransaction,
2139
3046
  RsError,
2140
3047
  SQL,
2141
3048
  escapeColumnName,
3049
+ eventManager_default as eventManager,
3050
+ filterPsqlParser_default as filterPsqlParser,
2142
3051
  insertObjectQuery,
2143
3052
  isValueNumber2 as isValueNumber,
2144
3053
  logger,