@restura/core 0.1.0-alpha.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/index.d.mts +1439 -177
  2. package/dist/index.d.ts +1439 -177
  3. package/dist/index.js +1767 -708
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1751 -705
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +17 -6
  8. package/dist/acorn-SW5GI5G7.mjs +0 -3016
  9. package/dist/acorn-SW5GI5G7.mjs.map +0 -1
  10. package/dist/angular-FYEH6QOL.mjs +0 -1547
  11. package/dist/angular-FYEH6QOL.mjs.map +0 -1
  12. package/dist/babel-V6GZHMYX.mjs +0 -6911
  13. package/dist/babel-V6GZHMYX.mjs.map +0 -1
  14. package/dist/chunk-TL4KRYOF.mjs +0 -58
  15. package/dist/chunk-TL4KRYOF.mjs.map +0 -1
  16. package/dist/estree-67ZCSSSI.mjs +0 -4396
  17. package/dist/estree-67ZCSSSI.mjs.map +0 -1
  18. package/dist/flow-SJW7PRXX.mjs +0 -26365
  19. package/dist/flow-SJW7PRXX.mjs.map +0 -1
  20. package/dist/glimmer-PF2X22V2.mjs +0 -2995
  21. package/dist/glimmer-PF2X22V2.mjs.map +0 -1
  22. package/dist/graphql-NOJ5HX7K.mjs +0 -1253
  23. package/dist/graphql-NOJ5HX7K.mjs.map +0 -1
  24. package/dist/html-ROPIWVPQ.mjs +0 -2780
  25. package/dist/html-ROPIWVPQ.mjs.map +0 -1
  26. package/dist/index.cjs +0 -34
  27. package/dist/index.cjs.map +0 -1
  28. package/dist/index.d.cts +0 -3
  29. package/dist/markdown-PFYT4MSP.mjs +0 -3423
  30. package/dist/markdown-PFYT4MSP.mjs.map +0 -1
  31. package/dist/meriyah-5NLZXLMA.mjs +0 -2356
  32. package/dist/meriyah-5NLZXLMA.mjs.map +0 -1
  33. package/dist/postcss-ITO6IEN5.mjs +0 -5027
  34. package/dist/postcss-ITO6IEN5.mjs.map +0 -1
  35. package/dist/typescript-6IE7K56Q.mjs +0 -13392
  36. package/dist/typescript-6IE7K56Q.mjs.map +0 -1
  37. package/dist/yaml-DB2OVPLH.mjs +0 -4230
  38. package/dist/yaml-DB2OVPLH.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -44,16 +44,10 @@ 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
- level: z.enum(["info", "warn", "error", "debug"]).default("info")
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
- generatedTypesPath: z.string().default(process.cwd() + "/src/@types")
50
+ level: z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
57
51
  });
58
52
 
59
53
  // src/logger/logger.ts
@@ -91,8 +85,140 @@ var logger = winston.createLogger({
91
85
  ]
92
86
  });
93
87
 
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
+
94
220
  // src/restura/restura.ts
95
- import { ObjectUtils as ObjectUtils5 } from "@redskytech/core-utils";
221
+ import { ObjectUtils as ObjectUtils5, StringUtils as StringUtils3 } from "@redskytech/core-utils";
96
222
  import { config as config2 } from "@restura/internal";
97
223
 
98
224
  // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
@@ -145,13 +271,25 @@ function boundMethod(target, key, descriptor) {
145
271
  import bodyParser from "body-parser";
146
272
  import compression from "compression";
147
273
  import cookieParser from "cookie-parser";
148
- import { createHash } from "crypto";
149
274
  import * as express from "express";
150
- import fs2 from "fs";
151
- import path2 from "path";
275
+ import fs4 from "fs";
276
+ import path4 from "path";
152
277
  import * as prettier3 from "prettier";
153
278
 
154
- // src/restura/errors.ts
279
+ // src/restura/RsError.ts
280
+ var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
281
+ HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
282
+ HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
283
+ HtmlStatusCodes2[HtmlStatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
284
+ HtmlStatusCodes2[HtmlStatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
285
+ HtmlStatusCodes2[HtmlStatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
286
+ HtmlStatusCodes2[HtmlStatusCodes2["CONFLICT"] = 409] = "CONFLICT";
287
+ HtmlStatusCodes2[HtmlStatusCodes2["VERSION_OUT_OF_DATE"] = 418] = "VERSION_OUT_OF_DATE";
288
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVER_ERROR"] = 500] = "SERVER_ERROR";
289
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
290
+ HtmlStatusCodes2[HtmlStatusCodes2["NETWORK_CONNECT_TIMEOUT"] = 599] = "NETWORK_CONNECT_TIMEOUT";
291
+ return HtmlStatusCodes2;
292
+ })(HtmlStatusCodes || {});
155
293
  var RsError = class _RsError {
156
294
  constructor(errCode, message) {
157
295
  this.err = errCode;
@@ -162,6 +300,9 @@ var RsError = class _RsError {
162
300
  static htmlStatus(code) {
163
301
  return htmlStatusMap[code];
164
302
  }
303
+ static isRsError(error) {
304
+ return error instanceof _RsError;
305
+ }
165
306
  };
166
307
  var htmlStatusMap = {
167
308
  UNKNOWN_ERROR: 500 /* SERVER_ERROR */,
@@ -200,12 +341,171 @@ var htmlStatusMap = {
200
341
  SCHEMA_ERROR: 500 /* SERVER_ERROR */
201
342
  };
202
343
 
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
361
+ };
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);
376
+ }
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
+ });
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"
417
+ });
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);
456
+ }
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;
498
+
499
+ // src/restura/generators/apiGenerator.ts
500
+ import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
501
+ import prettier from "prettier";
502
+
203
503
  // src/restura/sql/SqlUtils.ts
204
504
  var SqlUtils = class _SqlUtils {
205
505
  static convertDatabaseTypeToTypescript(type, value) {
206
506
  type = type.toLocaleLowerCase();
207
507
  if (type.startsWith("tinyint") || type.startsWith("boolean")) return "boolean";
208
- 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"))
209
509
  return "number";
210
510
  if (type === "json") {
211
511
  if (!value) return "object";
@@ -226,7 +526,7 @@ var SqlUtils = class _SqlUtils {
226
526
  }
227
527
  };
228
528
 
229
- // src/restura/ResponseValidator.ts
529
+ // src/restura/validators/ResponseValidator.ts
230
530
  var ResponseValidator = class _ResponseValidator {
231
531
  constructor(schema) {
232
532
  this.database = schema.database;
@@ -291,9 +591,9 @@ var ResponseValidator = class _ResponseValidator {
291
591
  return { validator: "any" };
292
592
  }
293
593
  getTypeFromTable(selector, name) {
294
- const path3 = selector.split(".");
295
- if (path3.length === 0 || path3.length > 2 || path3[0] === "") return { validator: "any", isOptional: false };
296
- const tableName = path3.length == 2 ? path3[0] : name, columnName = path3.length == 2 ? path3[1] : path3[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];
297
597
  const table = this.database.find((t) => t.name == tableName);
298
598
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
299
599
  if (!table || !column) return { validator: "any", isOptional: false };
@@ -375,9 +675,7 @@ var ResponseValidator = class _ResponseValidator {
375
675
  }
376
676
  };
377
677
 
378
- // src/restura/apiGenerator.ts
379
- import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
380
- import prettier from "prettier";
678
+ // src/restura/generators/apiGenerator.ts
381
679
  var ApiTree = class _ApiTree {
382
680
  constructor(namespace, database) {
383
681
  this.database = database;
@@ -475,7 +773,7 @@ var ApiTree = class _ApiTree {
475
773
  break;
476
774
  }
477
775
  }
478
- return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
776
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
479
777
  }).join(";\n")}${ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
480
778
  `;
481
779
  modelString += `}`;
@@ -489,30 +787,37 @@ var ApiTree = class _ApiTree {
489
787
  return `export type Res = CustomTypes.${route.responseType}[]`;
490
788
  else return `export type Res = CustomTypes.${route.responseType}`;
491
789
  }
492
- return `export interface Res ${this.getFields(route.response)}`;
790
+ return `export interface Res ${this.getFields(route.response, route.table, route.joins)}`;
493
791
  }
494
- getFields(fields) {
495
- const nameFields = fields.map((f) => this.getNameAndType(f));
792
+ getFields(fields, routeBaseTable, joins) {
793
+ const nameFields = fields.map((f) => this.getNameAndType(f, routeBaseTable, joins));
496
794
  const nested = `{
497
795
  ${nameFields.join(";\n ")}${ObjectUtils.isArrayWithData(nameFields) ? ";" : ""}
498
796
  }`;
499
797
  return nested;
500
798
  }
501
- getNameAndType(p) {
502
- let responseType = "any", optional = false, array = false;
799
+ getNameAndType(p, routeBaseTable, joins) {
800
+ let responseType = "any", isNullable = false, array = false;
503
801
  if (p.selector) {
504
- ({ 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
+ }
505
810
  } else if (p.subquery) {
506
- responseType = this.getFields(p.subquery.properties);
811
+ responseType = this.getFields(p.subquery.properties, p.subquery.table, p.subquery.joins);
507
812
  array = true;
508
813
  }
509
- return `${p.name}${optional ? "?" : ""}:${responseType}${array ? "[]" : ""}`;
814
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
510
815
  }
511
816
  getTypeFromTable(selector, name) {
512
- const path3 = selector.split(".");
513
- if (path3.length === 0 || path3.length > 2 || path3[0] === "") return { responseType: "any", optional: false };
514
- let tableName = path3.length == 2 ? path3[0] : name;
515
- const columnName = path3.length == 2 ? path3[1] : path3[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];
516
821
  let table = this.database.find((t) => t.name == tableName);
517
822
  if (!table && tableName.includes("_")) {
518
823
  const tableAliasSplit = tableName.split("_");
@@ -520,18 +825,19 @@ var ApiTree = class _ApiTree {
520
825
  table = this.database.find((t) => t.name == tableName);
521
826
  }
522
827
  const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
523
- if (!table || !column) return { responseType: "any", optional: false };
828
+ if (!table || !column) return { responseType: "any", isNullable: false };
524
829
  return {
525
830
  responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
526
- optional: column.roles.length > 0 || column.isNullable
831
+ isNullable: column.roles.length > 0 || column.isNullable
527
832
  };
528
833
  }
529
834
  };
530
- function pathToNamespaces(path3) {
531
- return path3.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);
532
837
  }
533
- function apiGenerator(schema, schemaHash) {
534
- 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
+ `;
535
841
  const rootNamespace = ApiTree.createRootNode(schema.database);
536
842
  for (const endpoint of schema.endpoints) {
537
843
  const endpointNamespaces = pathToNamespaces(endpoint.baseUrl);
@@ -546,7 +852,7 @@ function apiGenerator(schema, schemaHash) {
546
852
  apiString += `
547
853
 
548
854
  declare namespace CustomTypes {
549
- ${schema.customTypes}
855
+ ${schema.customTypes.join("\n")}
550
856
  }`;
551
857
  }
552
858
  return prettier.format(apiString, __spreadValues({
@@ -561,6 +867,100 @@ function apiGenerator(schema, schemaHash) {
561
867
  }));
562
868
  }
563
869
 
870
+ // src/restura/generators/customTypeValidationGenerator.ts
871
+ import fs2 from "fs";
872
+ import path2, { resolve } from "path";
873
+ import tmp from "tmp";
874
+ import * as TJS from "typescript-json-schema";
875
+ function customTypeValidationGenerator(currentSchema) {
876
+ const schemaObject = {};
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);
882
+ if (!customInterfaceNames) return {};
883
+ const temporaryFile = tmp.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
884
+ fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes.join("\n"));
885
+ const compilerOptions = {
886
+ strictNullChecks: true,
887
+ skipLibCheck: true
888
+ // Needed if we are processing ES modules
889
+ };
890
+ const program = TJS.getProgramFromFiles(
891
+ [
892
+ resolve(temporaryFile.name),
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")
896
+ ],
897
+ compilerOptions
898
+ );
899
+ customInterfaceNames.forEach((item) => {
900
+ const ddlSchema = TJS.generateSchema(program, item, {
901
+ required: true
902
+ });
903
+ schemaObject[item] = ddlSchema || {};
904
+ });
905
+ temporaryFile.removeCallback();
906
+ return schemaObject;
907
+ }
908
+
909
+ // src/restura/generators/modelGenerator.ts
910
+ import { StringUtils as StringUtils2 } from "@redskytech/core-utils";
911
+ import prettier2 from "prettier";
912
+ function modelGenerator(schema) {
913
+ let modelString = `/** Auto generated file. DO NOT MODIFY **/
914
+
915
+ `;
916
+ modelString += `declare namespace Model {
917
+ `;
918
+ for (const table of schema.database) {
919
+ modelString += convertTable(table);
920
+ }
921
+ modelString += `}`;
922
+ return prettier2.format(modelString, __spreadValues({
923
+ parser: "typescript"
924
+ }, {
925
+ trailingComma: "none",
926
+ tabWidth: 4,
927
+ useTabs: true,
928
+ endOfLine: "lf",
929
+ printWidth: 120,
930
+ singleQuote: true
931
+ }));
932
+ }
933
+ function convertTable(table) {
934
+ let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
935
+ `;
936
+ for (const column of table.columns) {
937
+ modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
938
+ `;
939
+ }
940
+ modelString += ` }
941
+ `;
942
+ return modelString;
943
+ }
944
+
945
+ // src/restura/generators/resturaGlobalTypesGenerator.ts
946
+ function resturaGlobalTypesGenerator() {
947
+ return `/** Auto generated file. DO NOT MODIFY **/
948
+ /** This file contains types that may be used in the CustomTypes of Restura **/
949
+ /** For example export interface MyPagedQuery extends Restura.PageQuery { } **/
950
+
951
+ declare namespace Restura {
952
+ export type StandardOrderTypes = 'ASC' | 'DESC' | 'RAND' | 'NONE';
953
+ export interface PageQuery {
954
+ page?: number;
955
+ perPage?: number;
956
+ sortBy?: string;
957
+ sortOrder?: StandardOrderTypes;
958
+ filter?: string;
959
+ }
960
+ }
961
+ `;
962
+ }
963
+
564
964
  // src/restura/middleware/addApiResponseFunctions.ts
565
965
  function addApiResponseFunctions(req, res, next) {
566
966
  res.sendData = function(data, statusCode = 200) {
@@ -589,12 +989,45 @@ function addApiResponseFunctions(req, res, next) {
589
989
  next();
590
990
  }
591
991
 
592
- // src/restura/validateRequestParams.ts
593
- import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
594
- import jsonschema from "jsonschema";
992
+ // src/restura/middleware/authenticateUser.ts
993
+ function authenticateUser(applicationAuthenticateHandler) {
994
+ return (req, res, next) => {
995
+ applicationAuthenticateHandler(req, res, (userDetails) => {
996
+ req.requesterDetails = __spreadValues({ host: req.hostname, ipAddress: req.ip || "" }, userDetails);
997
+ next();
998
+ });
999
+ };
1000
+ }
1001
+
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
595
1028
  import { z as z3 } from "zod";
596
1029
 
597
- // src/restura/types/validation.types.ts
1030
+ // src/restura/schemas/validatorDataSchema.ts
598
1031
  import { z as z2 } from "zod";
599
1032
  var validatorDataSchemeValue = z2.union([z2.string(), z2.array(z2.string()), z2.number(), z2.array(z2.number())]);
600
1033
  var validatorDataSchema = z2.object({
@@ -602,264 +1035,84 @@ var validatorDataSchema = z2.object({
602
1035
  value: validatorDataSchemeValue
603
1036
  }).strict();
604
1037
 
605
- // src/restura/utils/addQuotesToStrings.ts
606
- function addQuotesToStrings(variable) {
607
- if (typeof variable === "string") {
608
- return `'${variable}'`;
609
- } else if (Array.isArray(variable)) {
610
- const arrayWithQuotes = variable.map(addQuotesToStrings);
611
- return arrayWithQuotes;
612
- } else {
613
- return variable;
614
- }
615
- }
616
-
617
- // src/restura/validateRequestParams.ts
618
- function validateRequestParams(req, routeData, validationSchema) {
619
- const requestData = getRequestData(req);
620
- req.data = requestData;
621
- if (routeData.request === void 0) {
622
- if (routeData.type !== "CUSTOM_ONE" && routeData.type !== "CUSTOM_ARRAY" && routeData.type !== "CUSTOM_PAGED")
623
- throw new RsError("BAD_REQUEST", `No request parameters provided for standard request.`);
624
- if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
625
- if (!routeData.requestType) throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
626
- const currentInterface = validationSchema[routeData.requestType];
627
- const validator = new jsonschema.Validator();
628
- const executeValidation = validator.validate(req.data, currentInterface);
629
- if (!executeValidation.valid) {
630
- throw new RsError(
631
- "BAD_REQUEST",
632
- `Request custom setup has failed the following check: (${executeValidation.errors})`
633
- );
634
- }
635
- return;
636
- }
637
- Object.keys(req.data).forEach((requestParamName) => {
638
- const requestParam = routeData.request.find((param) => param.name === requestParamName);
639
- if (!requestParam) {
640
- throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not allowed`);
641
- }
642
- });
643
- routeData.request.forEach((requestParam) => {
644
- const requestValue = requestData[requestParam.name];
645
- if (requestParam.required && requestValue === void 0)
646
- throw new RsError("BAD_REQUEST", `Request param (${requestParam.name}) is required but missing`);
647
- else if (!requestParam.required && requestValue === void 0) return;
648
- validateRequestSingleParam(requestValue, requestParam);
649
- });
650
- }
651
- function validateRequestSingleParam(requestValue, requestParam) {
652
- requestParam.validator.forEach((validator) => {
653
- switch (validator.type) {
654
- case "TYPE_CHECK":
655
- performTypeCheck(requestValue, validator, requestParam.name);
656
- break;
657
- case "MIN":
658
- performMinCheck(requestValue, validator, requestParam.name);
659
- break;
660
- case "MAX":
661
- performMaxCheck(requestValue, validator, requestParam.name);
662
- break;
663
- case "ONE_OF":
664
- performOneOfCheck(requestValue, validator, requestParam.name);
665
- break;
666
- }
667
- });
668
- }
669
- function isValidType(type, requestValue) {
670
- try {
671
- expectValidType(type, requestValue);
672
- return true;
673
- } catch (e) {
674
- return false;
675
- }
676
- }
677
- function expectValidType(type, requestValue) {
678
- if (type === "number") {
679
- return z3.number().parse(requestValue);
680
- }
681
- if (type === "string") {
682
- return z3.string().parse(requestValue);
683
- }
684
- if (type === "boolean") {
685
- return z3.boolean().parse(requestValue);
686
- }
687
- if (type === "string[]") {
688
- return z3.array(z3.string()).parse(requestValue);
689
- }
690
- if (type === "number[]") {
691
- return z3.array(z3.number()).parse(requestValue);
692
- }
693
- if (type === "any[]") {
694
- return z3.array(z3.any()).parse(requestValue);
695
- }
696
- if (type === "object") {
697
- return z3.object({}).strict().parse(requestValue);
698
- }
699
- }
700
- function performTypeCheck(requestValue, validator, requestParamName) {
701
- if (!isValidType(validator.value, requestValue)) {
702
- throw new RsError(
703
- "BAD_REQUEST",
704
- `Request param (${requestParamName}) with value (${addQuotesToStrings(requestValue)}) is not of type (${validator.value})`
705
- );
706
- }
707
- try {
708
- validatorDataSchemeValue.parse(validator.value);
709
- } catch (e) {
710
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not a valid type`);
711
- }
712
- }
713
- function expectOnlyNumbers(requestValue, validator, requestParamName) {
714
- if (!isValueNumber(requestValue))
715
- throw new RsError(
716
- "BAD_REQUEST",
717
- `Request param (${requestParamName}) with value (${requestValue}) is not of type number`
718
- );
719
- if (!isValueNumber(validator.value))
720
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value} is not of type number`);
721
- }
722
- function performMinCheck(requestValue, validator, requestParamName) {
723
- expectOnlyNumbers(requestValue, validator, requestParamName);
724
- if (requestValue < validator.value)
725
- throw new RsError(
726
- "BAD_REQUEST",
727
- `Request param (${requestParamName}) with value (${requestValue}) is less than (${validator.value})`
728
- );
729
- }
730
- function performMaxCheck(requestValue, validator, requestParamName) {
731
- expectOnlyNumbers(requestValue, validator, requestParamName);
732
- if (requestValue > validator.value)
733
- throw new RsError(
734
- "BAD_REQUEST",
735
- `Request param (${requestParamName}) with value (${requestValue}) is more than (${validator.value})`
736
- );
737
- }
738
- function performOneOfCheck(requestValue, validator, requestParamName) {
739
- if (!ObjectUtils2.isArrayWithData(validator.value))
740
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not of type array`);
741
- if (typeof requestValue === "object")
742
- throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not of type string or number`);
743
- if (!validator.value.includes(requestValue))
744
- throw new RsError(
745
- "BAD_REQUEST",
746
- `Request param (${requestParamName}) with value (${requestValue}) is not one of (${validator.value.join(", ")})`
747
- );
748
- }
749
- function isValueNumber(value) {
750
- return !isNaN(Number(value));
751
- }
752
- function getRequestData(req) {
753
- let body = "";
754
- if (req.method === "GET" || req.method === "DELETE") {
755
- body = "query";
756
- } else {
757
- body = "body";
758
- }
759
- const bodyData = req[body];
760
- if (bodyData) {
761
- for (const attr in bodyData) {
762
- if (attr === "token") {
763
- delete bodyData[attr];
764
- continue;
765
- }
766
- if (bodyData[attr] instanceof Array) {
767
- const attrList = [];
768
- for (const value of bodyData[attr]) {
769
- if (isNaN(Number(value))) continue;
770
- attrList.push(Number(value));
771
- }
772
- if (ObjectUtils2.isArrayWithData(attrList)) {
773
- bodyData[attr] = attrList;
774
- }
775
- } else {
776
- bodyData[attr] = ObjectUtils2.safeParse(bodyData[attr]);
777
- if (isNaN(Number(bodyData[attr]))) continue;
778
- bodyData[attr] = Number(bodyData[attr]);
779
- }
780
- }
781
- }
782
- return bodyData;
783
- }
784
-
785
- // src/restura/restura.schema.ts
786
- import { z as z4 } from "zod";
787
- var orderBySchema = z4.object({
788
- columnName: z4.string(),
789
- order: z4.enum(["ASC", "DESC"]),
790
- tableName: z4.string()
1038
+ // src/restura/schemas/resturaSchema.ts
1039
+ var orderBySchema = z3.object({
1040
+ columnName: z3.string(),
1041
+ order: z3.enum(["ASC", "DESC"]),
1042
+ tableName: z3.string()
791
1043
  }).strict();
792
- var groupBySchema = z4.object({
793
- columnName: z4.string(),
794
- tableName: z4.string()
1044
+ var groupBySchema = z3.object({
1045
+ columnName: z3.string(),
1046
+ tableName: z3.string()
795
1047
  }).strict();
796
- var whereDataSchema = z4.object({
797
- tableName: z4.string().optional(),
798
- columnName: z4.string().optional(),
799
- operator: z4.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH"]).optional(),
800
- value: z4.string().or(z4.number()).optional(),
801
- custom: z4.string().optional(),
802
- conjunction: z4.enum(["AND", "OR"]).optional()
1048
+ var whereDataSchema = z3.object({
1049
+ tableName: z3.string().optional(),
1050
+ columnName: z3.string().optional(),
1051
+ operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH", "IS", "IS NOT"]).optional(),
1052
+ value: z3.string().or(z3.number()).optional(),
1053
+ custom: z3.string().optional(),
1054
+ conjunction: z3.enum(["AND", "OR"]).optional()
803
1055
  }).strict();
804
- var assignmentDataSchema = z4.object({
805
- name: z4.string(),
806
- value: z4.string()
1056
+ var assignmentDataSchema = z3.object({
1057
+ name: z3.string(),
1058
+ value: z3.string()
807
1059
  }).strict();
808
- var joinDataSchema = z4.object({
809
- table: z4.string(),
810
- localColumnName: z4.string().optional(),
811
- foreignColumnName: z4.string().optional(),
812
- custom: z4.string().optional(),
813
- type: z4.enum(["LEFT", "INNER"]),
814
- alias: z4.string().optional()
1060
+ var joinDataSchema = z3.object({
1061
+ table: z3.string(),
1062
+ localColumnName: z3.string().optional(),
1063
+ foreignColumnName: z3.string().optional(),
1064
+ custom: z3.string().optional(),
1065
+ type: z3.enum(["LEFT", "INNER"]),
1066
+ alias: z3.string().optional()
815
1067
  }).strict();
816
- var requestDataSchema = z4.object({
817
- name: z4.string(),
818
- required: z4.boolean(),
819
- validator: z4.array(validatorDataSchema)
1068
+ var requestDataSchema = z3.object({
1069
+ name: z3.string(),
1070
+ required: z3.boolean(),
1071
+ isNullable: z3.boolean().optional(),
1072
+ validator: z3.array(validatorDataSchema)
820
1073
  }).strict();
821
- var responseDataSchema = z4.object({
822
- name: z4.string(),
823
- selector: z4.string().optional(),
824
- subquery: z4.object({
825
- table: z4.string(),
826
- joins: z4.array(joinDataSchema),
827
- where: z4.array(whereDataSchema),
828
- properties: z4.array(z4.lazy(() => responseDataSchema)),
1074
+ var responseDataSchema = z3.object({
1075
+ name: z3.string(),
1076
+ selector: z3.string().optional(),
1077
+ subquery: z3.object({
1078
+ table: z3.string(),
1079
+ joins: z3.array(joinDataSchema),
1080
+ where: z3.array(whereDataSchema),
1081
+ properties: z3.array(z3.lazy(() => responseDataSchema)),
829
1082
  // Explicit type for the lazy schema
830
1083
  groupBy: groupBySchema.optional(),
831
1084
  orderBy: orderBySchema.optional()
832
1085
  }).optional()
833
1086
  }).strict();
834
- var routeDataBaseSchema = z4.object({
835
- method: z4.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
836
- name: z4.string(),
837
- description: z4.string(),
838
- path: z4.string(),
839
- roles: z4.array(z4.string())
1087
+ var routeDataBaseSchema = z3.object({
1088
+ method: z3.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
1089
+ name: z3.string(),
1090
+ description: z3.string(),
1091
+ path: z3.string(),
1092
+ roles: z3.array(z3.string())
840
1093
  }).strict();
841
1094
  var standardRouteSchema = routeDataBaseSchema.extend({
842
- type: z4.enum(["ONE", "ARRAY", "PAGED"]),
843
- table: z4.string(),
844
- joins: z4.array(joinDataSchema),
845
- assignments: z4.array(assignmentDataSchema),
846
- where: z4.array(whereDataSchema),
847
- request: z4.array(requestDataSchema),
848
- response: z4.array(responseDataSchema),
1095
+ type: z3.enum(["ONE", "ARRAY", "PAGED"]),
1096
+ table: z3.string(),
1097
+ joins: z3.array(joinDataSchema),
1098
+ assignments: z3.array(assignmentDataSchema),
1099
+ where: z3.array(whereDataSchema),
1100
+ request: z3.array(requestDataSchema),
1101
+ response: z3.array(responseDataSchema),
849
1102
  groupBy: groupBySchema.optional(),
850
1103
  orderBy: orderBySchema.optional()
851
1104
  }).strict();
852
1105
  var customRouteSchema = routeDataBaseSchema.extend({
853
- type: z4.enum(["CUSTOM_ONE", "CUSTOM_ARRAY", "CUSTOM_PAGED"]),
854
- responseType: z4.union([z4.string(), z4.enum(["string", "number", "boolean"])]),
855
- requestType: z4.string().optional(),
856
- request: z4.array(requestDataSchema).optional(),
857
- table: z4.undefined(),
858
- joins: z4.undefined(),
859
- assignments: z4.undefined(),
860
- fileUploadType: z4.enum(["SINGLE", "MULTIPLE"]).optional()
1106
+ type: z3.enum(["CUSTOM_ONE", "CUSTOM_ARRAY", "CUSTOM_PAGED"]),
1107
+ responseType: z3.union([z3.string(), z3.enum(["string", "number", "boolean"])]),
1108
+ requestType: z3.string().optional(),
1109
+ request: z3.array(requestDataSchema).optional(),
1110
+ table: z3.undefined(),
1111
+ joins: z3.undefined(),
1112
+ assignments: z3.undefined(),
1113
+ fileUploadType: z3.enum(["SINGLE", "MULTIPLE"]).optional()
861
1114
  }).strict();
862
- var postgresColumnNumericTypesSchema = z4.enum([
1115
+ var postgresColumnNumericTypesSchema = z3.enum([
863
1116
  "SMALLINT",
864
1117
  // 2 bytes, -32,768 to 32,767
865
1118
  "INTEGER",
@@ -879,7 +1132,7 @@ var postgresColumnNumericTypesSchema = z4.enum([
879
1132
  "BIGSERIAL"
880
1133
  // auto-incrementing big integer
881
1134
  ]);
882
- var postgresColumnStringTypesSchema = z4.enum([
1135
+ var postgresColumnStringTypesSchema = z3.enum([
883
1136
  "CHAR",
884
1137
  // fixed-length, blank-padded
885
1138
  "VARCHAR",
@@ -889,7 +1142,7 @@ var postgresColumnStringTypesSchema = z4.enum([
889
1142
  "BYTEA"
890
1143
  // binary data
891
1144
  ]);
892
- var postgresColumnDateTypesSchema = z4.enum([
1145
+ var postgresColumnDateTypesSchema = z3.enum([
893
1146
  "DATE",
894
1147
  // calendar date (year, month, day)
895
1148
  "TIMESTAMP",
@@ -901,7 +1154,13 @@ var postgresColumnDateTypesSchema = z4.enum([
901
1154
  "INTERVAL"
902
1155
  // time span
903
1156
  ]);
904
- var mariaDbColumnNumericTypesSchema = z4.enum([
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
+ ]);
1163
+ var mariaDbColumnNumericTypesSchema = z3.enum([
905
1164
  "BOOLEAN",
906
1165
  // 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
907
1166
  "TINYINT",
@@ -921,7 +1180,7 @@ var mariaDbColumnNumericTypesSchema = z4.enum([
921
1180
  "DOUBLE"
922
1181
  // 8 bytes Stored in 64-bit IEEE-754 floating point format. As such, the number of significant digits is about 15 and the range of values is approximately +/-1e308.
923
1182
  ]);
924
- var mariaDbColumnStringTypesSchema = z4.enum([
1183
+ var mariaDbColumnStringTypesSchema = z3.enum([
925
1184
  "CHAR",
926
1185
  // 1, 2, 4, or 8 bytes Holds letters and special characters of fixed length. Max length is 255. Default and minimum size is 1 byte.
927
1186
  "VARCHAR",
@@ -947,7 +1206,7 @@ var mariaDbColumnStringTypesSchema = z4.enum([
947
1206
  "ENUM"
948
1207
  // Enum type
949
1208
  ]);
950
- var mariaDbColumnDateTypesSchema = z4.enum([
1209
+ var mariaDbColumnDateTypesSchema = z3.enum([
951
1210
  "DATE",
952
1211
  // 4-bytes Date has year, month, and day.
953
1212
  "DATETIME",
@@ -957,34 +1216,35 @@ var mariaDbColumnDateTypesSchema = z4.enum([
957
1216
  "TIMESTAMP"
958
1217
  // 4-bytes Values are stored as the number of seconds since 1970-01-01 00:00:00 UTC, and optionally microseconds.
959
1218
  ]);
960
- var columnDataSchema = z4.object({
961
- name: z4.string(),
962
- type: z4.union([
1219
+ var columnDataSchema = z3.object({
1220
+ name: z3.string(),
1221
+ type: z3.union([
963
1222
  postgresColumnNumericTypesSchema,
964
1223
  postgresColumnStringTypesSchema,
965
1224
  postgresColumnDateTypesSchema,
1225
+ postgresColumnJsonTypesSchema,
966
1226
  mariaDbColumnNumericTypesSchema,
967
1227
  mariaDbColumnStringTypesSchema,
968
1228
  mariaDbColumnDateTypesSchema
969
1229
  ]),
970
- isNullable: z4.boolean(),
971
- roles: z4.array(z4.string()),
972
- comment: z4.string().optional(),
973
- default: z4.string().optional(),
974
- value: z4.string().optional(),
975
- isPrimary: z4.boolean().optional(),
976
- isUnique: z4.boolean().optional(),
977
- hasAutoIncrement: z4.boolean().optional(),
978
- length: z4.number().optional()
1230
+ isNullable: z3.boolean(),
1231
+ roles: z3.array(z3.string()),
1232
+ comment: z3.string().optional(),
1233
+ default: z3.string().optional(),
1234
+ value: z3.string().optional(),
1235
+ isPrimary: z3.boolean().optional(),
1236
+ isUnique: z3.boolean().optional(),
1237
+ hasAutoIncrement: z3.boolean().optional(),
1238
+ length: z3.number().optional()
979
1239
  }).strict();
980
- var indexDataSchema = z4.object({
981
- name: z4.string(),
982
- columns: z4.array(z4.string()),
983
- isUnique: z4.boolean(),
984
- isPrimaryKey: z4.boolean(),
985
- order: z4.enum(["ASC", "DESC"])
1240
+ var indexDataSchema = z3.object({
1241
+ name: z3.string(),
1242
+ columns: z3.array(z3.string()),
1243
+ isUnique: z3.boolean(),
1244
+ isPrimaryKey: z3.boolean(),
1245
+ order: z3.enum(["ASC", "DESC"])
986
1246
  }).strict();
987
- var foreignKeyActionsSchema = z4.enum([
1247
+ var foreignKeyActionsSchema = z3.enum([
988
1248
  "CASCADE",
989
1249
  // CASCADE action for foreign keys
990
1250
  "SET NULL",
@@ -996,42 +1256,43 @@ var foreignKeyActionsSchema = z4.enum([
996
1256
  "SET DEFAULT"
997
1257
  // SET DEFAULT action for foreign keys
998
1258
  ]);
999
- var foreignKeyDataSchema = z4.object({
1000
- name: z4.string(),
1001
- column: z4.string(),
1002
- refTable: z4.string(),
1003
- refColumn: z4.string(),
1259
+ var foreignKeyDataSchema = z3.object({
1260
+ name: z3.string(),
1261
+ column: z3.string(),
1262
+ refTable: z3.string(),
1263
+ refColumn: z3.string(),
1004
1264
  onDelete: foreignKeyActionsSchema,
1005
1265
  onUpdate: foreignKeyActionsSchema
1006
1266
  }).strict();
1007
- var checkConstraintDataSchema = z4.object({
1008
- name: z4.string(),
1009
- check: z4.string()
1267
+ var checkConstraintDataSchema = z3.object({
1268
+ name: z3.string(),
1269
+ check: z3.string()
1010
1270
  }).strict();
1011
- var tableDataSchema = z4.object({
1012
- name: z4.string(),
1013
- columns: z4.array(columnDataSchema),
1014
- indexes: z4.array(indexDataSchema),
1015
- foreignKeys: z4.array(foreignKeyDataSchema),
1016
- checkConstraints: z4.array(checkConstraintDataSchema),
1017
- roles: z4.array(z4.string())
1271
+ var tableDataSchema = z3.object({
1272
+ name: z3.string(),
1273
+ columns: z3.array(columnDataSchema),
1274
+ indexes: z3.array(indexDataSchema),
1275
+ foreignKeys: z3.array(foreignKeyDataSchema),
1276
+ checkConstraints: z3.array(checkConstraintDataSchema),
1277
+ roles: z3.array(z3.string()),
1278
+ notify: z3.union([z3.literal("ALL"), z3.array(z3.string())]).optional()
1018
1279
  }).strict();
1019
- var endpointDataSchema = z4.object({
1020
- name: z4.string(),
1021
- description: z4.string(),
1022
- baseUrl: z4.string(),
1023
- routes: z4.array(z4.union([standardRouteSchema, customRouteSchema]))
1280
+ var endpointDataSchema = z3.object({
1281
+ name: z3.string(),
1282
+ description: z3.string(),
1283
+ baseUrl: z3.string(),
1284
+ routes: z3.array(z3.union([standardRouteSchema, customRouteSchema]))
1024
1285
  }).strict();
1025
- var resturaZodSchema = z4.object({
1026
- database: z4.array(tableDataSchema),
1027
- endpoints: z4.array(endpointDataSchema),
1028
- globalParams: z4.array(z4.string()),
1029
- roles: z4.array(z4.string()),
1030
- customTypes: z4.string()
1286
+ var resturaSchema = z3.object({
1287
+ database: z3.array(tableDataSchema),
1288
+ endpoints: z3.array(endpointDataSchema),
1289
+ globalParams: z3.array(z3.string()),
1290
+ roles: z3.array(z3.string()),
1291
+ customTypes: z3.array(z3.string())
1031
1292
  }).strict();
1032
1293
  async function isSchemaValid(schemaToCheck) {
1033
1294
  try {
1034
- resturaZodSchema.parse(schemaToCheck);
1295
+ resturaSchema.parse(schemaToCheck);
1035
1296
  return true;
1036
1297
  } catch (error) {
1037
1298
  logger.error(error);
@@ -1039,11 +1300,210 @@ async function isSchemaValid(schemaToCheck) {
1039
1300
  }
1040
1301
  }
1041
1302
 
1303
+ // src/restura/validators/requestValidator.ts
1304
+ import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
1305
+ import jsonschema from "jsonschema";
1306
+ import { z as z4 } from "zod";
1307
+
1308
+ // src/restura/utils/utils.ts
1309
+ function addQuotesToStrings(variable) {
1310
+ if (typeof variable === "string") {
1311
+ return `'${variable}'`;
1312
+ } else if (Array.isArray(variable)) {
1313
+ const arrayWithQuotes = variable.map(addQuotesToStrings);
1314
+ return arrayWithQuotes;
1315
+ } else {
1316
+ return variable;
1317
+ }
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
+ }
1330
+
1331
+ // src/restura/validators/requestValidator.ts
1332
+ function requestValidator(req, routeData, validationSchema) {
1333
+ const requestData = getRequestData(req);
1334
+ req.data = requestData;
1335
+ if (routeData.request === void 0) {
1336
+ if (routeData.type !== "CUSTOM_ONE" && routeData.type !== "CUSTOM_ARRAY" && routeData.type !== "CUSTOM_PAGED")
1337
+ throw new RsError("BAD_REQUEST", `No request parameters provided for standard request.`);
1338
+ if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
1339
+ if (!routeData.requestType) throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
1340
+ const currentInterface = validationSchema[routeData.requestType];
1341
+ const validator = new jsonschema.Validator();
1342
+ const executeValidation = validator.validate(req.data, currentInterface);
1343
+ if (!executeValidation.valid) {
1344
+ throw new RsError(
1345
+ "BAD_REQUEST",
1346
+ `Request custom setup has failed the following check: (${executeValidation.errors})`
1347
+ );
1348
+ }
1349
+ return;
1350
+ }
1351
+ Object.keys(req.data).forEach((requestParamName) => {
1352
+ const requestParam = routeData.request.find((param) => param.name === requestParamName);
1353
+ if (!requestParam) {
1354
+ throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not allowed`);
1355
+ }
1356
+ });
1357
+ routeData.request.forEach((requestParam) => {
1358
+ const requestValue = requestData[requestParam.name];
1359
+ if (requestParam.required && requestValue === void 0)
1360
+ throw new RsError("BAD_REQUEST", `Request param (${requestParam.name}) is required but missing`);
1361
+ else if (!requestParam.required && requestValue === void 0) return;
1362
+ validateRequestSingleParam(requestValue, requestParam);
1363
+ });
1364
+ }
1365
+ function validateRequestSingleParam(requestValue, requestParam) {
1366
+ if (requestParam.isNullable && requestValue === null) return;
1367
+ requestParam.validator.forEach((validator) => {
1368
+ switch (validator.type) {
1369
+ case "TYPE_CHECK":
1370
+ performTypeCheck(requestValue, validator, requestParam.name);
1371
+ break;
1372
+ case "MIN":
1373
+ performMinCheck(requestValue, validator, requestParam.name);
1374
+ break;
1375
+ case "MAX":
1376
+ performMaxCheck(requestValue, validator, requestParam.name);
1377
+ break;
1378
+ case "ONE_OF":
1379
+ performOneOfCheck(requestValue, validator, requestParam.name);
1380
+ break;
1381
+ }
1382
+ });
1383
+ }
1384
+ function isValidType(type, requestValue) {
1385
+ try {
1386
+ expectValidType(type, requestValue);
1387
+ return true;
1388
+ } catch (e) {
1389
+ return false;
1390
+ }
1391
+ }
1392
+ function expectValidType(type, requestValue) {
1393
+ if (type === "number") {
1394
+ return z4.number().parse(requestValue);
1395
+ }
1396
+ if (type === "string") {
1397
+ return z4.string().parse(requestValue);
1398
+ }
1399
+ if (type === "boolean") {
1400
+ return z4.boolean().parse(requestValue);
1401
+ }
1402
+ if (type === "string[]") {
1403
+ return z4.array(z4.string()).parse(requestValue);
1404
+ }
1405
+ if (type === "number[]") {
1406
+ return z4.array(z4.number()).parse(requestValue);
1407
+ }
1408
+ if (type === "any[]") {
1409
+ return z4.array(z4.any()).parse(requestValue);
1410
+ }
1411
+ if (type === "object") {
1412
+ return z4.object({}).strict().parse(requestValue);
1413
+ }
1414
+ }
1415
+ function performTypeCheck(requestValue, validator, requestParamName) {
1416
+ if (!isValidType(validator.value, requestValue)) {
1417
+ throw new RsError(
1418
+ "BAD_REQUEST",
1419
+ `Request param (${requestParamName}) with value (${addQuotesToStrings(requestValue)}) is not of type (${validator.value})`
1420
+ );
1421
+ }
1422
+ try {
1423
+ validatorDataSchemeValue.parse(validator.value);
1424
+ } catch (e) {
1425
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not a valid type`);
1426
+ }
1427
+ }
1428
+ function expectOnlyNumbers(requestValue, validator, requestParamName) {
1429
+ if (!isValueNumber(requestValue))
1430
+ throw new RsError(
1431
+ "BAD_REQUEST",
1432
+ `Request param (${requestParamName}) with value (${requestValue}) is not of type number`
1433
+ );
1434
+ if (!isValueNumber(validator.value))
1435
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value} is not of type number`);
1436
+ }
1437
+ function performMinCheck(requestValue, validator, requestParamName) {
1438
+ expectOnlyNumbers(requestValue, validator, requestParamName);
1439
+ if (requestValue < validator.value)
1440
+ throw new RsError(
1441
+ "BAD_REQUEST",
1442
+ `Request param (${requestParamName}) with value (${requestValue}) is less than (${validator.value})`
1443
+ );
1444
+ }
1445
+ function performMaxCheck(requestValue, validator, requestParamName) {
1446
+ expectOnlyNumbers(requestValue, validator, requestParamName);
1447
+ if (requestValue > validator.value)
1448
+ throw new RsError(
1449
+ "BAD_REQUEST",
1450
+ `Request param (${requestParamName}) with value (${requestValue}) is more than (${validator.value})`
1451
+ );
1452
+ }
1453
+ function performOneOfCheck(requestValue, validator, requestParamName) {
1454
+ if (!ObjectUtils2.isArrayWithData(validator.value))
1455
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not of type array`);
1456
+ if (typeof requestValue === "object")
1457
+ throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not of type string or number`);
1458
+ if (!validator.value.includes(requestValue))
1459
+ throw new RsError(
1460
+ "BAD_REQUEST",
1461
+ `Request param (${requestParamName}) with value (${requestValue}) is not one of (${validator.value.join(", ")})`
1462
+ );
1463
+ }
1464
+ function isValueNumber(value) {
1465
+ return !isNaN(Number(value));
1466
+ }
1467
+ function getRequestData(req) {
1468
+ let body = "";
1469
+ if (req.method === "GET" || req.method === "DELETE") {
1470
+ body = "query";
1471
+ } else {
1472
+ body = "body";
1473
+ }
1474
+ const bodyData = req[body];
1475
+ if (bodyData && body === "query") {
1476
+ for (const attr in bodyData) {
1477
+ if (bodyData[attr] instanceof Array) {
1478
+ const attrList = [];
1479
+ for (const value of bodyData[attr]) {
1480
+ if (isNaN(Number(value))) continue;
1481
+ attrList.push(Number(value));
1482
+ }
1483
+ if (ObjectUtils2.isArrayWithData(attrList)) {
1484
+ bodyData[attr] = attrList;
1485
+ }
1486
+ } else {
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
+ }
1496
+ }
1497
+ }
1498
+ }
1499
+ return bodyData;
1500
+ }
1501
+
1042
1502
  // src/restura/middleware/schemaValidation.ts
1043
1503
  async function schemaValidation(req, res, next) {
1044
1504
  req.data = getRequestData(req);
1045
1505
  try {
1046
- resturaZodSchema.parse(req.data);
1506
+ resturaSchema.parse(req.data);
1047
1507
  next();
1048
1508
  } catch (error) {
1049
1509
  logger.error(error);
@@ -1051,91 +1511,183 @@ async function schemaValidation(req, res, next) {
1051
1511
  }
1052
1512
  }
1053
1513
 
1054
- // src/restura/modelGenerator.ts
1055
- import { StringUtils as StringUtils2 } from "@redskytech/core-utils";
1056
- import prettier2 from "prettier";
1057
- function modelGenerator(schema, schemaHash) {
1058
- let modelString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/
1059
- `;
1060
- modelString += `declare namespace Model {
1061
- `;
1062
- for (const table of schema.database) {
1063
- modelString += convertTable(table);
1064
- }
1065
- modelString += `}`;
1066
- return prettier2.format(modelString, __spreadValues({
1067
- parser: "typescript"
1068
- }, {
1069
- trailingComma: "none",
1070
- tabWidth: 4,
1071
- useTabs: true,
1072
- endOfLine: "lf",
1073
- printWidth: 120,
1074
- singleQuote: true
1075
- }));
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
+ });
1528
+
1529
+ // src/restura/sql/PsqlEngine.ts
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
+
1542
+ // src/restura/sql/PsqlUtils.ts
1543
+ import format2 from "pg-format";
1544
+ function escapeColumnName(columnName) {
1545
+ if (columnName === void 0) return "";
1546
+ return `"${columnName.replace(/"/g, "")}"`.replace(".", '"."');
1076
1547
  }
1077
- function convertTable(table) {
1078
- let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
1079
- `;
1080
- for (const column of table.columns) {
1081
- modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
1082
- `;
1548
+ function questionMarksToOrderedParams(query) {
1549
+ let count = 1;
1550
+ let inSingleQuote = false;
1551
+ let inDoubleQuote = false;
1552
+ return query.replace(/('|"|\?)/g, (char) => {
1553
+ if (char === "'") {
1554
+ inSingleQuote = !inSingleQuote && !inDoubleQuote;
1555
+ return char;
1556
+ }
1557
+ if (char === '"') {
1558
+ inDoubleQuote = !inDoubleQuote && !inSingleQuote;
1559
+ return char;
1560
+ }
1561
+ if (char === "?" && !inSingleQuote && !inDoubleQuote) {
1562
+ return `$${count++}`;
1563
+ }
1564
+ return char;
1565
+ });
1566
+ }
1567
+ function insertObjectQuery(table, obj) {
1568
+ const keys = Object.keys(obj);
1569
+ const params = Object.values(obj);
1570
+ const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1571
+ const values = params.map((value) => SQL`${value}`).join(", ");
1572
+ let query = `
1573
+ INSERT INTO "${table}" (${columns})
1574
+ VALUES (${values})
1575
+ RETURNING *`;
1576
+ query = query.replace(/'(\?)'/, "?");
1577
+ return query;
1578
+ }
1579
+ function updateObjectQuery(table, obj, whereStatement) {
1580
+ const setArray = [];
1581
+ for (const i in obj) {
1582
+ setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1083
1583
  }
1084
- modelString += ` }
1085
- `;
1086
- return modelString;
1584
+ return `
1585
+ UPDATE ${escapeColumnName(table)}
1586
+ SET ${setArray.join(", ")} ${whereStatement}
1587
+ RETURNING *`;
1087
1588
  }
1088
-
1089
- // src/restura/middleware/authenticateUser.ts
1090
- function authenticateUser(applicationAuthenticateHandler) {
1091
- return (req, res, next) => {
1092
- applicationAuthenticateHandler(
1093
- req,
1094
- (userDetails) => {
1095
- req.requesterDetails = __spreadValues(__spreadValues({}, req.requesterDetails), userDetails);
1096
- next();
1097
- },
1098
- (errorMessage) => {
1099
- res.sendError("UNAUTHORIZED", errorMessage);
1100
- }
1101
- );
1102
- };
1589
+ function isValueNumber2(value) {
1590
+ return !isNaN(Number(value));
1591
+ }
1592
+ function SQL(strings, ...values) {
1593
+ let query = strings[0];
1594
+ values.forEach((value, index) => {
1595
+ if (typeof value === "boolean") {
1596
+ query += value;
1597
+ } else if (typeof value === "number") {
1598
+ query += value;
1599
+ } else if (Array.isArray(value)) {
1600
+ query += format2.literal(JSON.stringify(value)) + "::jsonb";
1601
+ } else {
1602
+ query += format2.literal(value);
1603
+ }
1604
+ query += strings[index + 1];
1605
+ });
1606
+ return query;
1103
1607
  }
1104
1608
 
1105
- // src/restura/customTypeValidationGenerator.ts
1106
- import fs from "fs";
1107
- import * as TJS from "typescript-json-schema";
1108
- import path, { resolve } from "path";
1109
- import tmp from "tmp";
1110
- import * as process2 from "process";
1111
- function customTypeValidationGenerator(currentSchema) {
1112
- const schemaObject = {};
1113
- const customInterfaceNames = currentSchema.customTypes.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
1114
- if (!customInterfaceNames) return {};
1115
- const temporaryFile = tmp.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
1116
- fs.writeFileSync(temporaryFile.name, currentSchema.customTypes);
1117
- const compilerOptions = {
1118
- strictNullChecks: true,
1119
- skipLibCheck: true
1120
- };
1121
- const program = TJS.getProgramFromFiles(
1122
- [
1123
- resolve(temporaryFile.name),
1124
- // find a way to remove
1125
- path.join(process2.cwd(), "src/@types/models.d.ts"),
1126
- path.join(process2.cwd(), "src/@types/api.d.ts")
1127
- ],
1128
- compilerOptions
1129
- );
1130
- customInterfaceNames.forEach((item) => {
1131
- const ddlSchema = TJS.generateSchema(program, item, {
1132
- required: true
1609
+ // src/restura/sql/PsqlConnection.ts
1610
+ var PsqlConnection = class {
1611
+ constructor(instanceId) {
1612
+ this.instanceId = instanceId || crypto.randomUUID();
1613
+ }
1614
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1615
+ async queryOne(query, options, requesterDetails) {
1616
+ const formattedQuery = questionMarksToOrderedParams(query);
1617
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1618
+ this.logSqlStatement(formattedQuery, options, meta);
1619
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1620
+ `;
1621
+ try {
1622
+ const response = await this.query(queryMetadata + formattedQuery, options);
1623
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1624
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1625
+ return response.rows[0];
1626
+ } catch (error) {
1627
+ if (RsError.isRsError(error)) throw error;
1628
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1629
+ throw new RsError("DUPLICATE", error.message);
1630
+ }
1631
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1632
+ }
1633
+ }
1634
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1635
+ async runQuery(query, options, requesterDetails) {
1636
+ const formattedQuery = questionMarksToOrderedParams(query);
1637
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1638
+ this.logSqlStatement(formattedQuery, options, meta);
1639
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1640
+ `;
1641
+ try {
1642
+ const response = await this.query(queryMetadata + formattedQuery, options);
1643
+ return response.rows;
1644
+ } catch (error) {
1645
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1646
+ throw new RsError("DUPLICATE", error.message);
1647
+ }
1648
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1649
+ }
1650
+ }
1651
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1652
+ if (logger.level !== "silly") return;
1653
+ let sqlStatement = "";
1654
+ if (options.length === 0) {
1655
+ sqlStatement = query;
1656
+ } else {
1657
+ let stringIndex = 0;
1658
+ sqlStatement = query.replace(/\$\d+/g, () => {
1659
+ const value = options[stringIndex++];
1660
+ if (typeof value === "number") return value.toString();
1661
+ return format3.literal(value);
1662
+ });
1663
+ }
1664
+ let initiator = "Anonymous";
1665
+ if ("userId" in queryMetadata && queryMetadata.userId)
1666
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1667
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1668
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1669
+ ${sqlStatement}`);
1670
+ }
1671
+ };
1672
+
1673
+ // src/restura/sql/PsqlPool.ts
1674
+ var { Pool } = pg;
1675
+ var PsqlPool = class extends PsqlConnection {
1676
+ constructor(poolConfig) {
1677
+ super();
1678
+ this.poolConfig = poolConfig;
1679
+ this.pool = new Pool(poolConfig);
1680
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1681
+ logger.info("Connected to PostgreSQL database");
1682
+ }).catch((error) => {
1683
+ logger.error("Error connecting to database", error);
1684
+ process.exit(1);
1133
1685
  });
1134
- schemaObject[item] = ddlSchema || {};
1135
- });
1136
- temporaryFile.removeCallback();
1137
- return schemaObject;
1138
- }
1686
+ }
1687
+ async query(query, values) {
1688
+ return this.pool.query(query, values);
1689
+ }
1690
+ };
1139
1691
 
1140
1692
  // src/restura/sql/SqlEngine.ts
1141
1693
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
@@ -1203,11 +1755,11 @@ var SqlEngine = class {
1203
1755
  return returnValue;
1204
1756
  }
1205
1757
  replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1206
- var _a;
1758
+ var _a2;
1207
1759
  if (!routeData.request) return value;
1208
1760
  const data = req.data;
1209
1761
  if (typeof value === "string") {
1210
- (_a = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1762
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1211
1763
  const requestParam = routeData.request.find((item) => {
1212
1764
  return item.name === param.replace("$", "");
1213
1765
  });
@@ -1220,13 +1772,16 @@ var SqlEngine = class {
1220
1772
  return value;
1221
1773
  }
1222
1774
  replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1223
- var _a;
1775
+ var _a2;
1224
1776
  if (typeof value === "string") {
1225
- (_a = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a.forEach((param) => {
1777
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1226
1778
  param = param.replace("#", "");
1227
1779
  const globalParamValue = req.requesterDetails[param];
1228
1780
  if (!globalParamValue)
1229
- throw new RsError("SCHEMA_ERROR", `Invalid global keyword clause in route ${routeData.name}`);
1781
+ throw new RsError(
1782
+ "SCHEMA_ERROR",
1783
+ `Invalid global keyword clause in route (${routeData.path}) when looking for (#${param})`
1784
+ );
1230
1785
  sqlParams.push(globalParamValue);
1231
1786
  });
1232
1787
  return value.replace(new RegExp(/#[a-zA-Z][a-zA-Z0-9_]+/g), "?");
@@ -1235,118 +1790,368 @@ var SqlEngine = class {
1235
1790
  }
1236
1791
  };
1237
1792
 
1238
- // src/restura/sql/filterSqlParser.ts
1793
+ // src/restura/sql/filterPsqlParser.ts
1239
1794
  import peg from "pegjs";
1240
1795
  var filterSqlGrammar = `
1796
+ {
1797
+ // ported from pg-format but intentionally will add double quotes to every column
1798
+ function quoteSqlIdentity(value) {
1799
+ if (value === undefined || value === null) {
1800
+ throw new Error('SQL identifier cannot be null or undefined');
1801
+ } else if (value === false) {
1802
+ return '"f"';
1803
+ } else if (value === true) {
1804
+ return '"t"';
1805
+ } else if (value instanceof Date) {
1806
+ // return '"' + formatDate(value.toISOString()) + '"';
1807
+ } else if (value instanceof Buffer) {
1808
+ throw new Error('SQL identifier cannot be a buffer');
1809
+ } else if (Array.isArray(value) === true) {
1810
+ var temp = [];
1811
+ for (var i = 0; i < value.length; i++) {
1812
+ if (Array.isArray(value[i]) === true) {
1813
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1814
+ } else {
1815
+ // temp.push(quoteIdent(value[i]));
1816
+ }
1817
+ }
1818
+ return temp.toString();
1819
+ } else if (value === Object(value)) {
1820
+ throw new Error('SQL identifier cannot be an object');
1821
+ }
1822
+
1823
+ var ident = value.toString().slice(0); // create copy
1824
+
1825
+ // do not quote a valid, unquoted identifier
1826
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1827
+ // return ident;
1828
+ // }
1829
+
1830
+ var quoted = '"';
1831
+
1832
+ for (var i = 0; i < ident.length; i++) {
1833
+ var c = ident[i];
1834
+ if (c === '"') {
1835
+ quoted += c + c;
1836
+ } else {
1837
+ quoted += c;
1838
+ }
1839
+ }
1840
+
1841
+ quoted += '"';
1842
+
1843
+ return quoted;
1844
+ };
1845
+ }
1846
+
1241
1847
  start = expressionList
1242
1848
 
1849
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1850
+
1243
1851
  expressionList =
1244
- leftExpression:expression operator:operator rightExpression:expressionList
1852
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1245
1853
  { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1246
1854
  / expression
1247
1855
 
1248
1856
  expression =
1249
- negate:negate?"(" "column:" column:column ","? value:value? ","? type:type? ")"
1250
- {return \`\${negate? "!" : ""}(\${type? type(column, value) : \`\${column} = \${mysql.escape(value)}\`})\`;}
1857
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1858
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1251
1859
  /
1252
- negate:negate?"("expression:expressionList")" { return \`\${negate? "!" : ""}(\${expression})\`; }
1860
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1253
1861
 
1254
1862
  negate = "!"
1255
1863
 
1256
1864
  operator = "and"i / "or"i
1257
1865
 
1258
- column = left:text "." right:text { return \`\${mysql.escapeId(left)}.\${mysql.escapeId(right)}\`; }
1259
- /
1260
- text:text { return mysql.escapeId(text); }
1866
+
1867
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1868
+ /
1869
+ text:text { return quoteSqlIdentity(text); }
1870
+
1261
1871
 
1872
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
1262
1873
 
1263
- text = text:[ a-z0-9-_:.@]i+ { return text.join("");}
1264
1874
 
1265
- type = "type:" type:typeString { return type; }
1266
- typeString = text:"startsWith" { return function(column, value) { return \`\${column} LIKE '\${mysql.escape(value).slice(1,-1)}%'\`; } } /
1267
- text:"endsWith" { return function(column, value) { return \`\${column} LIKE '%\${mysql.escape(value).slice(1,-1)}'\`; } } /
1268
- text:"contains" { return function(column, value) { return \`\${column} LIKE '%\${mysql.escape(value).slice(1,-1)}%'\`; } } /
1269
- text:"exact" { return function(column, value) { return \`\${column} = '\${mysql.escape(value).slice(1,-1)}'\`; } } /
1270
- text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= '\${mysql.escape(value).slice(1,-1)}'\`; } } /
1271
- text:"greaterThan" { return function(column, value) { return \`\${column} > '\${mysql.escape(value).slice(1,-1)}'\`; } } /
1272
- text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${mysql.escape(value).slice(1,-1)}'\`; } } /
1273
- text:"lessThan" { return function(column, value) { return \`\${column} < '\${mysql.escape(value).slice(1,-1)}'\`; } } /
1875
+ type = "type" _ ":" _ type:typeString { return type; }
1876
+ typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1877
+ text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1878
+ text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
1879
+ text:"exact" { return function(column, value) { return \`\${column} = '\${format.literal(value).slice(1,-1)}'\`; } } /
1880
+ text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= '\${format.literal(value).slice(1,-1)}'\`; } } /
1881
+ text:"greaterThan" { return function(column, value) { return \`\${column} > '\${format.literal(value).slice(1,-1)}'\`; } } /
1882
+ text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1883
+ text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1274
1884
  text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1275
-
1276
- value = "value:" value:text { return value; }
1277
1885
 
1278
- `;
1279
- var filterSqlParser = peg.generate(filterSqlGrammar, {
1280
- format: "commonjs"
1281
- // dependencies: { mysql: 'mysql' } // todo: figure out a better way to escape values depending on the database type
1282
- });
1283
- var filterSqlParser_default = filterSqlParser;
1886
+ value = "value" _ ":" value:text { return value; }
1284
1887
 
1285
- // src/restura/sql/PsqlEngine.ts
1286
- import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
1287
1888
 
1288
- // src/restura/sql/PsqlUtils.ts
1289
- import format2 from "pg-format";
1290
- function escapeColumnName(columnName) {
1291
- if (columnName === void 0) return "";
1292
- return `"${columnName.replace(/"/g, "")}"`.replace(".", '"."');
1293
- }
1294
- function questionMarksToOrderedParams(query) {
1295
- let count = 1;
1296
- return query.replace(/'\?'/g, () => `$${count++}`);
1297
- }
1298
- function insertObjectQuery(table, obj) {
1299
- const keys = Object.keys(obj);
1300
- const params = Object.values(obj);
1301
- const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1302
- const values = params.map((value) => SQL`${value}`).join(", ");
1303
- const query = `INSERT INTO "${table}" (${columns})
1304
- VALUES (${values})
1305
- RETURNING *`;
1306
- return query;
1307
- }
1308
- function updateObjectQuery(table, obj, whereStatement) {
1309
- const setArray = [];
1310
- for (const i in obj) {
1311
- setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1312
- }
1313
- return `UPDATE ${escapeColumnName(table)}
1314
- SET ${setArray.join(", ")} ${whereStatement}
1315
- RETURNING *`;
1316
- }
1317
- function isValueNumber2(value) {
1318
- return !isNaN(Number(value));
1319
- }
1320
- function SQL(strings, ...values) {
1321
- let query = strings[0];
1322
- values.forEach((value, index) => {
1323
- if (isValueNumber2(value)) {
1324
- query += value;
1325
- } else {
1326
- query += format2.literal(value);
1327
- }
1328
- query += strings[index + 1];
1329
- });
1330
- return query;
1331
- }
1889
+ `;
1890
+ var filterPsqlParser = peg.generate(filterSqlGrammar, {
1891
+ format: "commonjs",
1892
+ dependencies: { format: "pg-format" }
1893
+ });
1894
+ var filterPsqlParser_default = filterPsqlParser;
1332
1895
 
1333
1896
  // src/restura/sql/PsqlEngine.ts
1897
+ var { Client, types } = pg2;
1898
+ var systemUser = {
1899
+ role: "",
1900
+ host: "",
1901
+ ipAddress: "",
1902
+ isSystemUser: true
1903
+ };
1334
1904
  var PsqlEngine = class extends SqlEngine {
1335
- constructor(psqlConnectionPool) {
1905
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1336
1906
  super();
1337
1907
  this.psqlConnectionPool = psqlConnectionPool;
1908
+ this.setupPgReturnTypes();
1909
+ if (shouldListenForDbTriggers) {
1910
+ this.setupTriggerListeners = this.listenForDbTriggers();
1911
+ }
1338
1912
  }
1339
- async diffDatabaseToSchema(schema) {
1340
- console.log(schema);
1341
- return Promise.resolve("");
1913
+ async close() {
1914
+ if (this.triggerClient) {
1915
+ await this.triggerClient.end();
1916
+ }
1917
+ }
1918
+ setupPgReturnTypes() {
1919
+ const TIMESTAMPTZ_OID = 1184;
1920
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1921
+ return val === null ? null : new Date(val).toISOString();
1922
+ });
1923
+ const BIGINT_OID = 20;
1924
+ types.setTypeParser(BIGINT_OID, (val) => {
1925
+ return val === null ? null : Number(val);
1926
+ });
1927
+ }
1928
+ async listenForDbTriggers() {
1929
+ this.triggerClient = new Client({
1930
+ user: this.psqlConnectionPool.poolConfig.user,
1931
+ host: this.psqlConnectionPool.poolConfig.host,
1932
+ database: this.psqlConnectionPool.poolConfig.database,
1933
+ password: this.psqlConnectionPool.poolConfig.password,
1934
+ port: this.psqlConnectionPool.poolConfig.port,
1935
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1936
+ });
1937
+ await this.triggerClient.connect();
1938
+ const promises = [];
1939
+ promises.push(this.triggerClient.query("LISTEN insert"));
1940
+ promises.push(this.triggerClient.query("LISTEN update"));
1941
+ promises.push(this.triggerClient.query("LISTEN delete"));
1942
+ await Promise.all(promises);
1943
+ this.triggerClient.on("notification", async (msg) => {
1944
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1945
+ const payload = ObjectUtils4.safeParse(msg.payload);
1946
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1947
+ }
1948
+ });
1949
+ }
1950
+ async handleTrigger(payload, mutationType) {
1951
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
1952
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
1953
+ }
1954
+ }
1955
+ async createDatabaseFromSchema(schema, connection) {
1956
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1957
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1958
+ return sqlFullStatement;
1342
1959
  }
1343
1960
  generateDatabaseSchemaFromSchema(schema) {
1344
- console.log(schema);
1345
- return "";
1961
+ const sqlStatements = [];
1962
+ const indexes = [];
1963
+ const triggers = [];
1964
+ for (const table of schema.database) {
1965
+ if (table.notify) {
1966
+ triggers.push(this.createInsertTriggers(table.name, table.notify));
1967
+ triggers.push(this.createUpdateTrigger(table.name, table.notify));
1968
+ triggers.push(this.createDeleteTrigger(table.name, table.notify));
1969
+ }
1970
+ let sql = `CREATE TABLE "${table.name}"
1971
+ ( `;
1972
+ const tableColumns = [];
1973
+ for (const column of table.columns) {
1974
+ let columnSql = "";
1975
+ columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
1976
+ let value = column.value;
1977
+ if (column.type === "JSON") value = "";
1978
+ if (column.type === "JSONB") value = "";
1979
+ if (column.type === "DECIMAL" && value) {
1980
+ value = value.replace("-", ",").replace(/['"]/g, "");
1981
+ }
1982
+ if (value && column.type !== "ENUM") {
1983
+ columnSql += `(${value})`;
1984
+ } else if (column.length) columnSql += `(${column.length})`;
1985
+ if (column.isPrimary) {
1986
+ columnSql += " PRIMARY KEY ";
1987
+ }
1988
+ if (column.isUnique) {
1989
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
1990
+ }
1991
+ if (column.isNullable) columnSql += " NULL";
1992
+ else columnSql += " NOT NULL";
1993
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
1994
+ if (value && column.type === "ENUM") {
1995
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
1996
+ }
1997
+ tableColumns.push(columnSql);
1998
+ }
1999
+ sql += tableColumns.join(", \n");
2000
+ for (const index of table.indexes) {
2001
+ if (!index.isPrimaryKey) {
2002
+ let unique = " ";
2003
+ if (index.isUnique) unique = "UNIQUE ";
2004
+ indexes.push(
2005
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
2006
+ return `"${item}" ${index.order}`;
2007
+ }).join(", ")});`
2008
+ );
2009
+ }
2010
+ }
2011
+ sql += "\n);";
2012
+ sqlStatements.push(sql);
2013
+ }
2014
+ for (const table of schema.database) {
2015
+ if (!table.foreignKeys.length) continue;
2016
+ const sql = `ALTER TABLE "${table.name}" `;
2017
+ const constraints = [];
2018
+ for (const foreignKey of table.foreignKeys) {
2019
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2020
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2021
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2022
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2023
+ constraints.push(constraint);
2024
+ }
2025
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2026
+ }
2027
+ for (const table of schema.database) {
2028
+ if (!table.checkConstraints.length) continue;
2029
+ const sql = `ALTER TABLE "${table.name}" `;
2030
+ const constraints = [];
2031
+ for (const check of table.checkConstraints) {
2032
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2033
+ constraints.push(constraint);
2034
+ }
2035
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2036
+ }
2037
+ sqlStatements.push(indexes.join("\n"));
2038
+ sqlStatements.push(triggers.join("\n"));
2039
+ return sqlStatements.join("\n\n");
2040
+ }
2041
+ async getScratchPool() {
2042
+ var _a2, _b;
2043
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
2044
+ `SELECT *
2045
+ FROM pg_database
2046
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
2047
+ [],
2048
+ systemUser
2049
+ );
2050
+ if (scratchDbExists.length === 0) {
2051
+ await this.psqlConnectionPool.runQuery(
2052
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
2053
+ [],
2054
+ systemUser
2055
+ );
2056
+ }
2057
+ const scratchPool = new PsqlPool({
2058
+ host: this.psqlConnectionPool.poolConfig.host,
2059
+ port: this.psqlConnectionPool.poolConfig.port,
2060
+ user: this.psqlConnectionPool.poolConfig.user,
2061
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2062
+ password: this.psqlConnectionPool.poolConfig.password,
2063
+ max: this.psqlConnectionPool.poolConfig.max,
2064
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2065
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2066
+ });
2067
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2068
+ await scratchPool.runQuery(
2069
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2070
+ [],
2071
+ systemUser
2072
+ );
2073
+ const schemaComment = await this.psqlConnectionPool.runQuery(
2074
+ `SELECT pg_description.description
2075
+ FROM pg_description
2076
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2077
+ WHERE pg_namespace.nspname = 'public';`,
2078
+ [],
2079
+ systemUser
2080
+ );
2081
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2082
+ await scratchPool.runQuery(
2083
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
2084
+ [],
2085
+ systemUser
2086
+ );
2087
+ }
2088
+ return scratchPool;
2089
+ }
2090
+ async diffDatabaseToSchema(schema) {
2091
+ const scratchPool = await this.getScratchPool();
2092
+ await this.createDatabaseFromSchema(schema, scratchPool);
2093
+ const originalClient = new Client({
2094
+ database: this.psqlConnectionPool.poolConfig.database,
2095
+ user: this.psqlConnectionPool.poolConfig.user,
2096
+ password: this.psqlConnectionPool.poolConfig.password,
2097
+ host: this.psqlConnectionPool.poolConfig.host,
2098
+ port: this.psqlConnectionPool.poolConfig.port
2099
+ });
2100
+ const scratchClient = new Client({
2101
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
2102
+ user: this.psqlConnectionPool.poolConfig.user,
2103
+ password: this.psqlConnectionPool.poolConfig.password,
2104
+ host: this.psqlConnectionPool.poolConfig.host,
2105
+ port: this.psqlConnectionPool.poolConfig.port
2106
+ });
2107
+ const promises = [originalClient.connect(), scratchClient.connect()];
2108
+ await Promise.all(promises);
2109
+ const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
2110
+ const [info1, info2] = await Promise.all(infoPromises);
2111
+ const diff = getDiff(info1, info2);
2112
+ const endPromises = [originalClient.end(), scratchClient.end()];
2113
+ await Promise.all(endPromises);
2114
+ return diff.join("\n");
1346
2115
  }
1347
2116
  createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1348
- console.log(req, schema, item, routeData, userRole, sqlParams);
1349
- return "";
2117
+ if (!item.subquery) return "";
2118
+ if (!ObjectUtils4.isArrayWithData(
2119
+ item.subquery.properties.filter((nestedItem) => {
2120
+ return this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
2121
+ ...routeData.joins,
2122
+ ...item.subquery.joins
2123
+ ]);
2124
+ })
2125
+ )) {
2126
+ return "'[]'";
2127
+ }
2128
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
2129
+ ${item.subquery.properties.map((nestedItem) => {
2130
+ if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
2131
+ ...routeData.joins,
2132
+ ...item.subquery.joins
2133
+ ])) {
2134
+ return;
2135
+ }
2136
+ if (nestedItem.subquery) {
2137
+ return `'${nestedItem.name}', ${this.createNestedSelect(
2138
+ // recursion
2139
+ req,
2140
+ schema,
2141
+ nestedItem,
2142
+ routeData,
2143
+ userRole,
2144
+ sqlParams
2145
+ )}`;
2146
+ }
2147
+ return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
2148
+ }).filter(Boolean).join(", ")}
2149
+ ))
2150
+ FROM
2151
+ "${item.subquery.table}"
2152
+ ${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema, userRole, sqlParams)}
2153
+ ${this.generateWhereClause(req, item.subquery.where, routeData, sqlParams)}
2154
+ ), '[]')`;
1350
2155
  }
1351
2156
  async executeCreateRequest(req, routeData, schema) {
1352
2157
  const sqlParams = [];
@@ -1355,16 +2160,19 @@ var PsqlEngine = class extends SqlEngine {
1355
2160
  parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
1356
2161
  });
1357
2162
  const query = insertObjectQuery(routeData.table, __spreadValues(__spreadValues({}, req.data), parameterObj));
1358
- const createdItem = await this.psqlConnectionPool.queryOne(query, sqlParams);
1359
- const insertId = createdItem == null ? void 0 : createdItem.id;
1360
- const whereData = [
1361
- {
1362
- tableName: routeData.table,
1363
- value: insertId,
1364
- columnName: "id",
1365
- operator: "="
1366
- }
1367
- ];
2163
+ const createdItem = await this.psqlConnectionPool.queryOne(
2164
+ query,
2165
+ sqlParams,
2166
+ req.requesterDetails
2167
+ );
2168
+ const insertId = createdItem.id;
2169
+ const whereId = {
2170
+ tableName: routeData.table,
2171
+ value: insertId,
2172
+ columnName: "id",
2173
+ operator: "="
2174
+ };
2175
+ const whereData = [whereId];
1368
2176
  req.data = { id: insertId };
1369
2177
  return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
1370
2178
  }
@@ -1383,7 +2191,9 @@ var PsqlEngine = class extends SqlEngine {
1383
2191
  let selectStatement = "SELECT \n";
1384
2192
  selectStatement += ` ${selectColumns.map((item) => {
1385
2193
  if (item.subquery) {
1386
- return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${item.name}`;
2194
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2195
+ item.name
2196
+ )}`;
1387
2197
  }
1388
2198
  return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
1389
2199
  }).join(",\n ")}
@@ -1405,37 +2215,42 @@ var PsqlEngine = class extends SqlEngine {
1405
2215
  if (routeData.type === "ONE") {
1406
2216
  return this.psqlConnectionPool.queryOne(
1407
2217
  `${selectStatement}${sqlStatement}${groupByOrderByStatement};`,
1408
- sqlParams
2218
+ sqlParams,
2219
+ req.requesterDetails
1409
2220
  );
1410
2221
  } else if (routeData.type === "ARRAY") {
1411
2222
  return this.psqlConnectionPool.runQuery(
1412
2223
  `${selectStatement}${sqlStatement}${groupByOrderByStatement};`,
1413
- sqlParams
2224
+ sqlParams,
2225
+ req.requesterDetails
1414
2226
  );
1415
2227
  } else if (routeData.type === "PAGED") {
1416
2228
  const data = req.data;
1417
- const pageResults = await this.psqlConnectionPool.runQuery(
1418
- `${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT ? OFFSET ?;SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
1419
- ${sqlStatement};`,
1420
- [
1421
- ...sqlParams,
1422
- data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER,
1423
- (data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER,
1424
- ...sqlParams
1425
- ]
2229
+ const pagePromise = this.psqlConnectionPool.runQuery(
2230
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2231
+ sqlParams,
2232
+ req.requesterDetails
1426
2233
  );
2234
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2235
+ ${sqlStatement};`;
2236
+ const totalPromise = this.psqlConnectionPool.runQuery(
2237
+ totalQuery,
2238
+ sqlParams,
2239
+ req.requesterDetails
2240
+ );
2241
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
1427
2242
  let total = 0;
1428
- if (ObjectUtils4.isArrayWithData(pageResults)) {
1429
- total = pageResults[1][0].total;
2243
+ if (ObjectUtils4.isArrayWithData(totalResponse)) {
2244
+ total = totalResponse[0].total;
1430
2245
  }
1431
- return { data: pageResults[0], total };
2246
+ return { data: pageResults, total };
1432
2247
  } else {
1433
2248
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
1434
2249
  }
1435
2250
  }
1436
2251
  async executeUpdateRequest(req, routeData, schema) {
1437
2252
  const sqlParams = [];
1438
- const _a = req.body, { id } = _a, bodyNoId = __objRest(_a, ["id"]);
2253
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
1439
2254
  const table = schema.database.find((item) => {
1440
2255
  return item.name === routeData.table;
1441
2256
  });
@@ -1453,7 +2268,7 @@ ${sqlStatement};`,
1453
2268
  }
1454
2269
  const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1455
2270
  const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
1456
- await this.psqlConnectionPool.queryOne(query, [...sqlParams]);
2271
+ await this.psqlConnectionPool.queryOne(query, [...sqlParams], req.requesterDetails);
1457
2272
  return this.executeGetRequest(req, routeData, schema);
1458
2273
  }
1459
2274
  async executeDeleteRequest(req, routeData, schema) {
@@ -1467,20 +2282,32 @@ ${sqlStatement};`,
1467
2282
  req.requesterDetails.role,
1468
2283
  sqlParams
1469
2284
  );
1470
- let deleteStatement = `DELETE
1471
- FROM "${routeData.table}" ${joinStatement}`;
1472
- deleteStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
1473
- deleteStatement += ";";
1474
- await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams);
2285
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2286
+ if (whereClause.replace(/\s/g, "") === "") {
2287
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2288
+ }
2289
+ const deleteStatement = `
2290
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
2291
+ await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
1475
2292
  return true;
1476
2293
  }
1477
2294
  generateJoinStatements(req, joins, baseTable, routeData, schema, userRole, sqlParams) {
1478
- console.log(req, joins, baseTable, routeData, schema, userRole, sqlParams);
1479
- return "";
1480
- }
1481
- getTableSchema(schema, tableName) {
1482
- console.log(schema, tableName);
1483
- return {};
2295
+ let joinStatements = "";
2296
+ joins.forEach((item) => {
2297
+ if (!this.doesRoleHavePermissionToTable(userRole, schema, item.table))
2298
+ throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
2299
+ if (item.custom) {
2300
+ const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
2301
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)} ON ${customReplaced}
2302
+ `;
2303
+ } else {
2304
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON "${baseTable}"."${item.localColumnName}" = ${escapeColumnName(item.alias ? item.alias : item.table)}.${escapeColumnName(
2305
+ item.foreignColumnName
2306
+ )}
2307
+ `;
2308
+ }
2309
+ });
2310
+ return joinStatements;
1484
2311
  }
1485
2312
  generateGroupBy(routeData) {
1486
2313
  let groupBy = "";
@@ -1523,38 +2350,39 @@ ${sqlStatement};`,
1523
2350
  );
1524
2351
  let operator = item.operator;
1525
2352
  if (operator === "LIKE") {
1526
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}%`;
2353
+ item.value = `'%${item.value}%'`;
1527
2354
  } else if (operator === "STARTS WITH") {
1528
2355
  operator = "LIKE";
1529
- sqlParams[sqlParams.length - 1] = `${sqlParams[sqlParams.length - 1]}%`;
2356
+ item.value = `'${item.value}%'`;
1530
2357
  } else if (operator === "ENDS WITH") {
1531
2358
  operator = "LIKE";
1532
- sqlParams[sqlParams.length - 1] = `%${sqlParams[sqlParams.length - 1]}`;
2359
+ item.value = `'%${item.value}'`;
1533
2360
  }
1534
2361
  const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
1535
- const escapedValue = SQL`${replacedValue}`;
1536
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
2362
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
1537
2363
  `;
1538
2364
  });
1539
2365
  const data = req.data;
1540
2366
  if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
1541
2367
  let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
2368
+ var _a2;
1542
2369
  const requestParam = routeData.request.find((item) => {
1543
2370
  return item.name === value.replace("$", "");
1544
2371
  });
1545
2372
  if (!requestParam)
1546
2373
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1547
- return data[requestParam.name];
2374
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1548
2375
  });
1549
2376
  statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
2377
+ var _a2;
1550
2378
  const requestParam = routeData.request.find((item) => {
1551
2379
  return item.name === value.replace("#", "");
1552
2380
  });
1553
2381
  if (!requestParam)
1554
2382
  throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1555
- return data[requestParam.name];
2383
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
1556
2384
  });
1557
- statement = filterSqlParser_default.parse(statement);
2385
+ statement = filterPsqlParser_default.parse(statement);
1558
2386
  if (whereClause.startsWith("WHERE")) {
1559
2387
  whereClause += ` AND (${statement})
1560
2388
  `;
@@ -1565,47 +2393,255 @@ ${sqlStatement};`,
1565
2393
  }
1566
2394
  return whereClause;
1567
2395
  }
1568
- };
2396
+ createUpdateTrigger(tableName, notify) {
2397
+ if (!notify) return "";
2398
+ if (notify === "ALL") {
2399
+ return `
2400
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2401
+ RETURNS TRIGGER AS $$
2402
+ DECLARE
2403
+ query_metadata JSON;
2404
+ BEGIN
2405
+ SELECT INTO query_metadata
2406
+ (regexp_match(
2407
+ current_query(),
2408
+ '^--QUERY_METADATA\\(({.*})', 'n'
2409
+ ))[1]::json;
1569
2410
 
1570
- // src/restura/restura.ts
1571
- import { types } from "pg";
2411
+ PERFORM pg_notify(
2412
+ 'update',
2413
+ json_build_object(
2414
+ 'table', '${tableName}',
2415
+ 'queryMetadata', query_metadata,
2416
+ 'changedId', NEW.id,
2417
+ 'record', NEW,
2418
+ 'previousRecord', OLD
2419
+ )::text
2420
+ );
2421
+ RETURN NEW;
2422
+ END;
2423
+ $$ LANGUAGE plpgsql;
1572
2424
 
1573
- // src/restura/sql/PsqlPool.ts
1574
- import { Pool } from "pg";
1575
- var PsqlPool = class {
1576
- constructor(poolConfig) {
1577
- this.poolConfig = poolConfig;
1578
- this.pool = new Pool(poolConfig);
1579
- }
1580
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1581
- async queryOne(query, options) {
1582
- const formattedQuery = questionMarksToOrderedParams(query);
1583
- try {
1584
- const response = await this.pool.query(formattedQuery, options);
1585
- return response.rows[0];
1586
- } catch (error) {
1587
- console.error(error, query, options);
1588
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1589
- throw new RsError("DUPLICATE", error.message);
1590
- }
1591
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2425
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2426
+ AFTER UPDATE ON "${tableName}"
2427
+ FOR EACH ROW
2428
+ EXECUTE FUNCTION notify_${tableName}_update();
2429
+ `;
1592
2430
  }
1593
- }
1594
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1595
- async runQuery(query, options) {
1596
- const formattedQuery = questionMarksToOrderedParams(query);
1597
- const queryUpdated = query.replace(/[\t\n]/g, " ");
1598
- console.log(queryUpdated, options);
1599
- try {
1600
- const response = await this.pool.query(formattedQuery, options);
1601
- return response.rows;
1602
- } catch (error) {
1603
- console.error(error, query, options);
1604
- if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1605
- throw new RsError("DUPLICATE", error.message);
1606
- }
1607
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2431
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2432
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2433
+ return `
2434
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2435
+ RETURNS TRIGGER AS $$
2436
+ DECLARE
2437
+ query_metadata JSON;
2438
+ BEGIN
2439
+ SELECT INTO query_metadata
2440
+ (regexp_match(
2441
+ current_query(),
2442
+ '^--QUERY_METADATA\\(({.*})', 'n'
2443
+ ))[1]::json;
2444
+
2445
+ PERFORM pg_notify(
2446
+ 'update',
2447
+ json_build_object(
2448
+ 'table', '${tableName}',
2449
+ 'queryMetadata', query_metadata,
2450
+ 'changedId', NEW.id,
2451
+ 'record', json_build_object(
2452
+ ${notifyColumnNewBuildString}
2453
+ ),
2454
+ 'previousRecord', json_build_object(
2455
+ ${notifyColumnOldBuildString}
2456
+ )
2457
+ )::text
2458
+ );
2459
+ RETURN NEW;
2460
+ END;
2461
+ $$ LANGUAGE plpgsql;
2462
+
2463
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2464
+ AFTER UPDATE ON "${tableName}"
2465
+ FOR EACH ROW
2466
+ EXECUTE FUNCTION notify_${tableName}_update();
2467
+ `;
2468
+ }
2469
+ createDeleteTrigger(tableName, notify) {
2470
+ if (!notify) return "";
2471
+ if (notify === "ALL") {
2472
+ return `
2473
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2474
+ RETURNS TRIGGER AS $$
2475
+ DECLARE
2476
+ query_metadata JSON;
2477
+ BEGIN
2478
+ SELECT INTO query_metadata
2479
+ (regexp_match(
2480
+ current_query(),
2481
+ '^--QUERY_METADATA\\(({.*})', 'n'
2482
+ ))[1]::json;
2483
+
2484
+ PERFORM pg_notify(
2485
+ 'delete',
2486
+ json_build_object(
2487
+ 'table', '${tableName}',
2488
+ 'queryMetadata', query_metadata,
2489
+ 'deletedId', OLD.id,
2490
+ 'previousRecord', OLD
2491
+ )::text
2492
+ );
2493
+ RETURN NEW;
2494
+ END;
2495
+ $$ LANGUAGE plpgsql;
2496
+
2497
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2498
+ AFTER DELETE ON "${tableName}"
2499
+ FOR EACH ROW
2500
+ EXECUTE FUNCTION notify_${tableName}_delete();
2501
+ `;
2502
+ }
2503
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2504
+ return `
2505
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2506
+ RETURNS TRIGGER AS $$
2507
+ DECLARE
2508
+ query_metadata JSON;
2509
+ BEGIN
2510
+ SELECT INTO query_metadata
2511
+ (regexp_match(
2512
+ current_query(),
2513
+ '^--QUERY_METADATA\\(({.*})', 'n'
2514
+ ))[1]::json;
2515
+
2516
+ PERFORM pg_notify(
2517
+ 'delete',
2518
+ json_build_object(
2519
+ 'table', '${tableName}',
2520
+ 'queryMetadata', query_metadata,
2521
+ 'deletedId', OLD.id,
2522
+ 'previousRecord', json_build_object(
2523
+ ${notifyColumnOldBuildString}
2524
+ )
2525
+ )::text
2526
+ );
2527
+ RETURN NEW;
2528
+ END;
2529
+ $$ LANGUAGE plpgsql;
2530
+
2531
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2532
+ AFTER DELETE ON "${tableName}"
2533
+ FOR EACH ROW
2534
+ EXECUTE FUNCTION notify_${tableName}_delete();
2535
+ `;
2536
+ }
2537
+ createInsertTriggers(tableName, notify) {
2538
+ if (!notify) return "";
2539
+ if (notify === "ALL") {
2540
+ return `
2541
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2542
+ RETURNS TRIGGER AS $$
2543
+ DECLARE
2544
+ query_metadata JSON;
2545
+ BEGIN
2546
+ SELECT INTO query_metadata
2547
+ (regexp_match(
2548
+ current_query(),
2549
+ '^--QUERY_METADATA\\(({.*})', 'n'
2550
+ ))[1]::json;
2551
+
2552
+ PERFORM pg_notify(
2553
+ 'insert',
2554
+ json_build_object(
2555
+ 'table', '${tableName}',
2556
+ 'queryMetadata', query_metadata,
2557
+ 'insertedId', NEW.id,
2558
+ 'record', NEW
2559
+ )::text
2560
+ );
2561
+
2562
+ RETURN NEW;
2563
+ END;
2564
+ $$ LANGUAGE plpgsql;
2565
+
2566
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2567
+ AFTER INSERT ON "${tableName}"
2568
+ FOR EACH ROW
2569
+ EXECUTE FUNCTION notify_${tableName}_insert();
2570
+ `;
1608
2571
  }
2572
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2573
+ return `
2574
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2575
+ RETURNS TRIGGER AS $$
2576
+ DECLARE
2577
+ query_metadata JSON;
2578
+ BEGIN
2579
+ SELECT INTO query_metadata
2580
+ (regexp_match(
2581
+ current_query(),
2582
+ '^--QUERY_METADATA\\(({.*})', 'n'
2583
+ ))[1]::json;
2584
+
2585
+ PERFORM pg_notify(
2586
+ 'insert',
2587
+ json_build_object(
2588
+ 'table', '${tableName}',
2589
+ 'queryMetadata', query_metadata,
2590
+ 'insertedId', NEW.id,
2591
+ 'record', json_build_object(
2592
+ ${notifyColumnNewBuildString}
2593
+ )
2594
+ )::text
2595
+ );
2596
+
2597
+ RETURN NEW;
2598
+ END;
2599
+ $$ LANGUAGE plpgsql;
2600
+
2601
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2602
+ AFTER INSERT ON "${tableName}"
2603
+ FOR EACH ROW
2604
+ EXECUTE FUNCTION notify_${tableName}_insert();
2605
+ `;
2606
+ }
2607
+ schemaToPsqlType(column) {
2608
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2609
+ if (column.type === "ENUM") return `TEXT`;
2610
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2611
+ if (column.type === "MEDIUMINT") return "INT";
2612
+ return column.type;
2613
+ }
2614
+ };
2615
+
2616
+ // src/restura/utils/TempCache.ts
2617
+ import fs3 from "fs";
2618
+ import path3 from "path";
2619
+ import { DateUtils } from "@redskytech/core-utils";
2620
+ import { FileUtils as FileUtils2 } from "@restura/internal";
2621
+ import Bluebird3 from "bluebird";
2622
+ import * as os2 from "os";
2623
+ var TempCache = class {
2624
+ constructor(location) {
2625
+ this.maxDurationDays = 7;
2626
+ this.location = location || os2.tmpdir();
2627
+ FileUtils2.ensureDir(this.location).catch((e) => {
2628
+ throw e;
2629
+ });
2630
+ }
2631
+ async cleanup() {
2632
+ const fileList = await fs3.promises.readdir(this.location);
2633
+ await Bluebird3.map(
2634
+ fileList,
2635
+ async (file) => {
2636
+ const fullFilePath = path3.join(this.location, file);
2637
+ const fileStats = await fs3.promises.stat(fullFilePath);
2638
+ if (DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2639
+ logger.info(`Deleting old temp file: ${file}`);
2640
+ await fs3.promises.unlink(fullFilePath);
2641
+ }
2642
+ },
2643
+ { concurrency: 10 }
2644
+ );
1609
2645
  }
1610
2646
  };
1611
2647
 
@@ -1627,10 +2663,12 @@ var ResturaEngine = class {
1627
2663
  * @returns A promise that resolves when the initialization is complete.
1628
2664
  */
1629
2665
  async init(app, authenticationHandler, psqlConnectionPool) {
1630
- this.psqlConnectionPool = psqlConnectionPool;
1631
- this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
1632
- setupPgReturnTypes();
1633
2666
  this.resturaConfig = config2.validate("restura", resturaConfigSchema);
2667
+ this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
2668
+ new TempCache(this.resturaConfig.fileTempCachePath);
2669
+ this.psqlConnectionPool = psqlConnectionPool;
2670
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
2671
+ await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
1634
2672
  this.authenticationHandler = authenticationHandler;
1635
2673
  app.use(compression());
1636
2674
  app.use(bodyParser.json({ limit: "32mb" }));
@@ -1693,10 +2731,7 @@ var ResturaEngine = class {
1693
2731
  * @returns A promise that resolves when the API has been successfully generated and written to the output file.
1694
2732
  */
1695
2733
  async generateApiFromSchema(outputFile, providedSchema) {
1696
- fs2.writeFileSync(
1697
- outputFile,
1698
- await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1699
- );
2734
+ fs4.writeFileSync(outputFile, await apiGenerator(providedSchema));
1700
2735
  }
1701
2736
  /**
1702
2737
  * Generates a model from the provided schema and writes it to the specified output file.
@@ -1706,10 +2741,15 @@ var ResturaEngine = class {
1706
2741
  * @returns A promise that resolves when the model has been successfully written to the output file.
1707
2742
  */
1708
2743
  async generateModelFromSchema(outputFile, providedSchema) {
1709
- fs2.writeFileSync(
1710
- outputFile,
1711
- await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
1712
- );
2744
+ fs4.writeFileSync(outputFile, await modelGenerator(providedSchema));
2745
+ }
2746
+ /**
2747
+ * Generates the ambient module declaration for Restura global types and writes it to the specified output file.
2748
+ * These types are used sometimes in the CustomTypes
2749
+ * @param outputFile
2750
+ */
2751
+ generateResturaGlobalTypes(outputFile) {
2752
+ fs4.writeFileSync(outputFile, resturaGlobalTypesGenerator());
1713
2753
  }
1714
2754
  /**
1715
2755
  * Retrieves the latest file system schema for Restura.
@@ -1718,11 +2758,11 @@ var ResturaEngine = class {
1718
2758
  * @throws {Error} If the schema file is missing or the schema is not valid.
1719
2759
  */
1720
2760
  async getLatestFileSystemSchema() {
1721
- if (!fs2.existsSync(this.resturaConfig.schemaFilePath)) {
2761
+ if (!fs4.existsSync(this.resturaConfig.schemaFilePath)) {
1722
2762
  logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
1723
2763
  throw new Error("Missing restura schema file");
1724
2764
  }
1725
- const schemaFileData = fs2.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2765
+ const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
1726
2766
  const schema = ObjectUtils5.safeParse(schemaFileData);
1727
2767
  const isValid = await isSchemaValid(schema);
1728
2768
  if (!isValid) {
@@ -1731,28 +2771,6 @@ var ResturaEngine = class {
1731
2771
  }
1732
2772
  return schema;
1733
2773
  }
1734
- /**
1735
- * Asynchronously generates and retrieves hashes for the provided schema and related generated files.
1736
- *
1737
- * @param providedSchema - The schema for which hashes need to be generated.
1738
- * @returns A promise that resolves to an object containing:
1739
- * - `schemaHash`: The hash of the provided schema.
1740
- * - `apiCreatedSchemaHash`: The hash extracted from the generated `api.d.ts` file.
1741
- * - `modelCreatedSchemaHash`: The hash extracted from the generated `models.d.ts` file.
1742
- */
1743
- async getHashes(providedSchema) {
1744
- var _a, _b, _c, _d;
1745
- const schemaHash = await this.generateHashForSchema(providedSchema);
1746
- const apiFile = fs2.readFileSync(path2.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1747
- const apiCreatedSchemaHash = (_b = (_a = apiFile.toString().match(/\((.*)\)/)) == null ? void 0 : _a[1]) != null ? _b : "";
1748
- const modelFile = fs2.readFileSync(path2.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1749
- const modelCreatedSchemaHash = (_d = (_c = modelFile.toString().match(/\((.*)\)/)) == null ? void 0 : _c[1]) != null ? _d : "";
1750
- return {
1751
- schemaHash,
1752
- apiCreatedSchemaHash,
1753
- modelCreatedSchemaHash
1754
- };
1755
- }
1756
2774
  async reloadEndpoints() {
1757
2775
  this.schema = await this.getLatestFileSystemSchema();
1758
2776
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
@@ -1781,30 +2799,10 @@ var ResturaEngine = class {
1781
2799
  logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
1782
2800
  }
1783
2801
  async validateGeneratedTypesFolder() {
1784
- if (!fs2.existsSync(this.resturaConfig.generatedTypesPath)) {
1785
- fs2.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1786
- }
1787
- const hasApiFile = fs2.existsSync(path2.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
1788
- const hasModelsFile = fs2.existsSync(path2.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
1789
- if (!hasApiFile) {
1790
- await this.generateApiFromSchema(path2.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1791
- }
1792
- if (!hasModelsFile) {
1793
- await this.generateModelFromSchema(
1794
- path2.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1795
- this.schema
1796
- );
1797
- }
1798
- const hashes = await this.getHashes(this.schema);
1799
- if (hashes.schemaHash !== hashes.apiCreatedSchemaHash) {
1800
- await this.generateApiFromSchema(path2.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1801
- }
1802
- if (hashes.schemaHash !== hashes.modelCreatedSchemaHash) {
1803
- await this.generateModelFromSchema(
1804
- path2.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1805
- this.schema
1806
- );
2802
+ if (!fs4.existsSync(this.resturaConfig.generatedTypesPath)) {
2803
+ fs4.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
1807
2804
  }
2805
+ this.updateTypes();
1808
2806
  }
1809
2807
  resturaAuthentication(req, res, next) {
1810
2808
  if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
@@ -1812,7 +2810,7 @@ var ResturaEngine = class {
1812
2810
  }
1813
2811
  async previewCreateSchema(req, res) {
1814
2812
  try {
1815
- const schemaDiff = { commands: "", endPoints: [], globalParams: [], roles: [], customTypes: false };
2813
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
1816
2814
  res.send({ data: schemaDiff });
1817
2815
  } catch (err) {
1818
2816
  res.status(400).send(err);
@@ -1820,7 +2818,7 @@ var ResturaEngine = class {
1820
2818
  }
1821
2819
  async updateSchema(req, res) {
1822
2820
  try {
1823
- this.schema = req.data;
2821
+ this.schema = sortObjectKeysAlphabetically(req.data);
1824
2822
  await this.storeFileSystemSchema();
1825
2823
  await this.reloadEndpoints();
1826
2824
  await this.updateTypes();
@@ -1831,11 +2829,12 @@ var ResturaEngine = class {
1831
2829
  }
1832
2830
  }
1833
2831
  async updateTypes() {
1834
- await this.generateApiFromSchema(path2.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2832
+ await this.generateApiFromSchema(path4.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
1835
2833
  await this.generateModelFromSchema(
1836
- path2.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2834
+ path4.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
1837
2835
  this.schema
1838
2836
  );
2837
+ this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
1839
2838
  }
1840
2839
  async getSchema(req, res) {
1841
2840
  res.send({ data: this.schema });
@@ -1843,24 +2842,48 @@ var ResturaEngine = class {
1843
2842
  async getSchemaAndTypes(req, res) {
1844
2843
  try {
1845
2844
  const schema = await this.getLatestFileSystemSchema();
1846
- const schemaHash = await this.generateHashForSchema(schema);
1847
- const apiText = await apiGenerator(schema, schemaHash);
1848
- const modelsText = await modelGenerator(schema, schemaHash);
2845
+ const apiText = await apiGenerator(schema);
2846
+ const modelsText = await modelGenerator(schema);
1849
2847
  res.send({ schema, api: apiText, models: modelsText });
1850
2848
  } catch (err) {
1851
2849
  res.status(400).send({ error: err });
1852
2850
  }
1853
2851
  }
2852
+ async getMulterFilesIfAny(req, res, routeData) {
2853
+ var _a2;
2854
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2855
+ if (!this.isCustomRoute(routeData)) return;
2856
+ if (!routeData.fileUploadType) {
2857
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2858
+ }
2859
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2860
+ return new Promise((resolve2, reject) => {
2861
+ multerFileUploadFunction(req, res, (err) => {
2862
+ if (err) {
2863
+ logger.warn("Multer error: " + err);
2864
+ reject(err);
2865
+ }
2866
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2867
+ resolve2();
2868
+ });
2869
+ });
2870
+ }
1854
2871
  async executeRouteLogic(req, res, next) {
1855
2872
  try {
1856
2873
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
1857
2874
  this.validateAuthorization(req, routeData);
1858
- validateRequestParams(req, routeData, this.customTypeValidation);
2875
+ await this.getMulterFilesIfAny(req, res, routeData);
2876
+ requestValidator(req, routeData, this.customTypeValidation);
2877
+ if (this.isCustomRoute(routeData)) {
2878
+ await this.runCustomRouteLogic(req, res, routeData);
2879
+ return;
2880
+ }
1859
2881
  const data = await this.psqlEngine.runQueryForRoute(
1860
2882
  req,
1861
2883
  routeData,
1862
2884
  this.schema
1863
2885
  );
2886
+ this.responseValidator.validateResponseParams(data, req.baseUrl, routeData);
1864
2887
  if (routeData.type === "PAGED") res.sendNoWrap(data);
1865
2888
  else res.sendData(data);
1866
2889
  } catch (e) {
@@ -1870,46 +2893,25 @@ var ResturaEngine = class {
1870
2893
  isCustomRoute(route) {
1871
2894
  return route.type === "CUSTOM_ONE" || route.type === "CUSTOM_ARRAY" || route.type === "CUSTOM_PAGED";
1872
2895
  }
1873
- // @boundMethod
1874
- // private async runCustomRouteLogic<T>(req: RsRequest<T>, res: RsResponse<T>, routeData: RouteData) {
1875
- // const version = req.baseUrl.split('/')[2];
1876
- // let domain = routeData.path.split('/')[1];
1877
- // domain = domain.split('-').reduce((acc, value, index) => {
1878
- // if (index === 0) acc = value;
1879
- // else acc += StringUtils.capitalizeFirst(value);
1880
- // return acc;
1881
- // }, '');
1882
- // const customApiName = `${StringUtils.capitalizeFirst(domain)}Api${StringUtils.capitalizeFirst(version)}`;
1883
- // const customApi = apiFactory.getCustomApi(customApiName);
1884
- // if (!customApi) throw new RsError('NOT_FOUND', `API domain ${domain}-${version} not found`);
1885
- // const functionName = `${routeData.method.toLowerCase()}${routeData.path
1886
- // .replace(new RegExp('-', 'g'), '/')
1887
- // .split('/')
1888
- // .reduce((acc, cur) => {
1889
- // if (cur === '') return acc;
1890
- // return acc + StringUtils.capitalizeFirst(cur);
1891
- // }, '')}`;
1892
- // // @ts-expect-error - Here we are dynamically calling the function from a custom class, not sure how to typescript this
1893
- // const customFunction = customApi[functionName] as (
1894
- // req: RsRequest<T>,
1895
- // res: RsResponse<T>,
1896
- // routeData: RouteData
1897
- // ) => Promise<void>;
1898
- // if (!customFunction) throw new RsError('NOT_FOUND', `API path ${routeData.path} not implemented`);
1899
- // await customFunction(req, res, routeData);
1900
- // }
1901
- async generateHashForSchema(providedSchema) {
1902
- const schemaPrettyStr = await prettier3.format(JSON.stringify(providedSchema), __spreadValues({
1903
- parser: "json"
1904
- }, {
1905
- trailingComma: "none",
1906
- tabWidth: 4,
1907
- useTabs: true,
1908
- endOfLine: "lf",
1909
- printWidth: 120,
1910
- singleQuote: true
1911
- }));
1912
- return createHash("sha256").update(schemaPrettyStr).digest("hex");
2896
+ async runCustomRouteLogic(req, res, routeData) {
2897
+ const version = req.baseUrl.split("/")[2];
2898
+ let domain = routeData.path.split("/")[1];
2899
+ domain = domain.split("-").reduce((acc, value, index) => {
2900
+ if (index === 0) acc = value;
2901
+ else acc += StringUtils3.capitalizeFirst(value);
2902
+ return acc;
2903
+ }, "");
2904
+ const customApiName = `${StringUtils3.capitalizeFirst(domain)}Api${StringUtils3.capitalizeFirst(version)}`;
2905
+ const customApi = customApiFactory_default.getCustomApi(customApiName);
2906
+ if (!customApi) throw new RsError("NOT_FOUND", `API domain ${domain}-${version} not found`);
2907
+ const functionName = `${routeData.method.toLowerCase()}${routeData.path.replace(new RegExp("-", "g"), "/").split("/").reduce((acc, cur) => {
2908
+ if (cur === "") return acc;
2909
+ return acc + StringUtils3.capitalizeFirst(cur);
2910
+ }, "")}`;
2911
+ const customFunction = customApi[functionName];
2912
+ if (!customFunction)
2913
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2914
+ await customFunction(req, res, routeData);
1913
2915
  }
1914
2916
  async storeFileSystemSchema() {
1915
2917
  const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
@@ -1922,7 +2924,7 @@ var ResturaEngine = class {
1922
2924
  printWidth: 120,
1923
2925
  singleQuote: true
1924
2926
  }));
1925
- fs2.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2927
+ fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
1926
2928
  }
1927
2929
  resetPublicEndpoints() {
1928
2930
  this.publicEndpoints = {
@@ -1939,13 +2941,13 @@ var ResturaEngine = class {
1939
2941
  if (!routeData.roles.includes(role))
1940
2942
  throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
1941
2943
  }
1942
- getRouteData(method, baseUrl, path3) {
2944
+ getRouteData(method, baseUrl, path5) {
1943
2945
  const endpoint = this.schema.endpoints.find((item) => {
1944
2946
  return item.baseUrl === baseUrl;
1945
2947
  });
1946
2948
  if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
1947
2949
  const route = endpoint.routes.find((item) => {
1948
- return item.method === method && item.path === path3;
2950
+ return item.method === method && item.path === path5;
1949
2951
  });
1950
2952
  if (!route) throw new RsError("NOT_FOUND", "Route not found");
1951
2953
  return route;
@@ -1966,27 +2968,71 @@ __decorateClass([
1966
2968
  __decorateClass([
1967
2969
  boundMethod
1968
2970
  ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
2971
+ __decorateClass([
2972
+ boundMethod
2973
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
1969
2974
  __decorateClass([
1970
2975
  boundMethod
1971
2976
  ], ResturaEngine.prototype, "executeRouteLogic", 1);
1972
2977
  __decorateClass([
1973
2978
  boundMethod
1974
2979
  ], ResturaEngine.prototype, "isCustomRoute", 1);
1975
- var setupPgReturnTypes = () => {
1976
- const TIMESTAMPTZ_OID = 1184;
1977
- types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
1978
- return val === null ? null : new Date(val).toISOString();
1979
- });
1980
- const BIGINT_OID = 20;
1981
- types.setTypeParser(BIGINT_OID, (val) => {
1982
- return val === null ? null : Number(val);
1983
- });
1984
- };
1985
- setupPgReturnTypes();
2980
+ __decorateClass([
2981
+ boundMethod
2982
+ ], ResturaEngine.prototype, "runCustomRouteLogic", 1);
1986
2983
  var restura = new ResturaEngine();
2984
+
2985
+ // src/restura/sql/PsqlTransaction.ts
2986
+ import pg3 from "pg";
2987
+ var { Client: Client2 } = pg3;
2988
+ var PsqlTransaction = class extends PsqlConnection {
2989
+ constructor(clientConfig, instanceId) {
2990
+ super(instanceId);
2991
+ this.clientConfig = clientConfig;
2992
+ this.client = new Client2(clientConfig);
2993
+ this.connectPromise = this.client.connect();
2994
+ this.beginTransactionPromise = this.beginTransaction();
2995
+ }
2996
+ async close() {
2997
+ if (this.client) {
2998
+ await this.client.end();
2999
+ }
3000
+ }
3001
+ async beginTransaction() {
3002
+ await this.connectPromise;
3003
+ return this.client.query("BEGIN");
3004
+ }
3005
+ async rollback() {
3006
+ return this.query("ROLLBACK");
3007
+ }
3008
+ async commit() {
3009
+ return this.query("COMMIT");
3010
+ }
3011
+ async release() {
3012
+ return this.client.end();
3013
+ }
3014
+ async query(query, values) {
3015
+ await this.connectPromise;
3016
+ await this.beginTransactionPromise;
3017
+ return this.client.query(query, values);
3018
+ }
3019
+ };
1987
3020
  export {
3021
+ HtmlStatusCodes,
3022
+ PsqlConnection,
3023
+ PsqlEngine,
1988
3024
  PsqlPool,
3025
+ PsqlTransaction,
3026
+ RsError,
3027
+ SQL,
3028
+ escapeColumnName,
3029
+ eventManager_default as eventManager,
3030
+ filterPsqlParser_default as filterPsqlParser,
3031
+ insertObjectQuery,
3032
+ isValueNumber2 as isValueNumber,
1989
3033
  logger,
1990
- restura
3034
+ questionMarksToOrderedParams,
3035
+ restura,
3036
+ updateObjectQuery
1991
3037
  };
1992
3038
  //# sourceMappingURL=index.mjs.map