@restura/core 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,39 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
4
+ var __defProps = Object.defineProperties;
3
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
5
10
  var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
12
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __spreadValues = (a, b) => {
14
+ for (var prop in b || (b = {}))
15
+ if (__hasOwnProp.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ if (__getOwnPropSymbols)
18
+ for (var prop of __getOwnPropSymbols(b)) {
19
+ if (__propIsEnum.call(b, prop))
20
+ __defNormalProp(a, prop, b[prop]);
21
+ }
22
+ return a;
23
+ };
24
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
25
+ var __objRest = (source, exclude) => {
26
+ var target = {};
27
+ for (var prop in source)
28
+ if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
29
+ target[prop] = source[prop];
30
+ if (source != null && __getOwnPropSymbols)
31
+ for (var prop of __getOwnPropSymbols(source)) {
32
+ if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
33
+ target[prop] = source[prop];
34
+ }
35
+ return target;
36
+ };
6
37
  var __export = (target, all) => {
7
38
  for (var name in all)
8
39
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -15,21 +46,2869 @@ var __copyProps = (to, from, except, desc) => {
15
46
  }
16
47
  return to;
17
48
  };
49
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
50
+ // If the importer is in node compatibility mode or this is not an ESM
51
+ // file that has been converted to a CommonJS file using a Babel-
52
+ // compatible transform (i.e. "__esModule" has not been set), then set
53
+ // "default" to the CommonJS "module.exports" for node compatibility.
54
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
55
+ mod
56
+ ));
18
57
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
58
+ var __decorateClass = (decorators, target, key, kind) => {
59
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
60
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
61
+ if (decorator = decorators[i])
62
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
63
+ if (kind && result) __defProp(target, key, result);
64
+ return result;
65
+ };
19
66
 
20
67
  // src/index.ts
21
68
  var src_exports = {};
22
69
  __export(src_exports, {
23
- isEven: () => isEven
70
+ HtmlStatusCodes: () => HtmlStatusCodes,
71
+ PsqlConnection: () => PsqlConnection,
72
+ PsqlEngine: () => PsqlEngine,
73
+ PsqlPool: () => PsqlPool,
74
+ PsqlTransaction: () => PsqlTransaction,
75
+ RsError: () => RsError,
76
+ SQL: () => SQL,
77
+ escapeColumnName: () => escapeColumnName,
78
+ eventManager: () => eventManager_default,
79
+ insertObjectQuery: () => insertObjectQuery,
80
+ isValueNumber: () => isValueNumber2,
81
+ logger: () => logger,
82
+ questionMarksToOrderedParams: () => questionMarksToOrderedParams,
83
+ restura: () => restura,
84
+ updateObjectQuery: () => updateObjectQuery
24
85
  });
25
86
  module.exports = __toCommonJS(src_exports);
87
+
88
+ // src/logger/logger.ts
26
89
  var import_internal = require("@restura/internal");
27
- function isEven(value) {
28
- (0, import_internal.log)("isEven called");
29
- return value % 2 === 0;
90
+ var import_winston = __toESM(require("winston"));
91
+ var import_logform = require("logform");
92
+
93
+ // src/config.schema.ts
94
+ var import_zod = require("zod");
95
+ var loggerConfigSchema = import_zod.z.object({
96
+ level: import_zod.z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
97
+ });
98
+ var _a;
99
+ var isTsx = (_a = process.argv[1]) == null ? void 0 : _a.endsWith(".ts");
100
+ var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
101
+ var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
102
+ var resturaConfigSchema = import_zod.z.object({
103
+ authToken: import_zod.z.string().min(1, "Missing Restura Auth Token"),
104
+ sendErrorStackTrace: import_zod.z.boolean().default(false),
105
+ schemaFilePath: import_zod.z.string().default(process.cwd() + "/restura.schema.json"),
106
+ customApiFolderPath: import_zod.z.string().default(process.cwd() + customApiFolderPath),
107
+ generatedTypesPath: import_zod.z.string().default(process.cwd() + "/src/@types"),
108
+ fileTempCachePath: import_zod.z.string().optional()
109
+ });
110
+
111
+ // src/logger/logger.ts
112
+ var loggerConfig = import_internal.config.validate("logger", loggerConfigSchema);
113
+ var consoleFormat = import_logform.format.combine(
114
+ import_logform.format.timestamp({
115
+ format: "YYYY-MM-DD HH:mm:ss.sss"
116
+ }),
117
+ import_logform.format.errors({ stack: true }),
118
+ import_logform.format.padLevels(),
119
+ import_logform.format.colorize({ all: true }),
120
+ import_logform.format.printf((info) => {
121
+ return `[${info.timestamp}] ${info.level} ${info.message}`;
122
+ })
123
+ );
124
+ var logger = import_winston.default.createLogger({
125
+ level: loggerConfig.level,
126
+ format: import_logform.format.combine(
127
+ import_logform.format.timestamp({
128
+ format: "YYYY-MM-DD HH:mm:ss.sss"
129
+ }),
130
+ import_logform.format.errors({ stack: true }),
131
+ import_logform.format.json()
132
+ ),
133
+ //defaultMeta: { service: 'user-service' },
134
+ transports: [
135
+ //
136
+ // - Write to all logs with level `info` and below to `combined.log`
137
+ // - Write all logs error (and below) to `error.log`.
138
+ // - Write all logs to standard out.
139
+ //
140
+ // new winston.transports.File({ filename: 'error.log', level: 'error' }),
141
+ // new winston.transports.File({ filename: 'combined.log' }),
142
+ new import_winston.default.transports.Console({ format: consoleFormat })
143
+ ]
144
+ });
145
+
146
+ // src/restura/errors.ts
147
+ var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
148
+ HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
149
+ HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
150
+ HtmlStatusCodes2[HtmlStatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
151
+ HtmlStatusCodes2[HtmlStatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
152
+ HtmlStatusCodes2[HtmlStatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
153
+ HtmlStatusCodes2[HtmlStatusCodes2["CONFLICT"] = 409] = "CONFLICT";
154
+ HtmlStatusCodes2[HtmlStatusCodes2["VERSION_OUT_OF_DATE"] = 418] = "VERSION_OUT_OF_DATE";
155
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVER_ERROR"] = 500] = "SERVER_ERROR";
156
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
157
+ HtmlStatusCodes2[HtmlStatusCodes2["NETWORK_CONNECT_TIMEOUT"] = 599] = "NETWORK_CONNECT_TIMEOUT";
158
+ return HtmlStatusCodes2;
159
+ })(HtmlStatusCodes || {});
160
+ var RsError = class _RsError {
161
+ constructor(errCode, message) {
162
+ this.err = errCode;
163
+ this.msg = message || "";
164
+ this.status = _RsError.htmlStatus(errCode);
165
+ this.stack = new Error().stack || "";
166
+ }
167
+ static htmlStatus(code) {
168
+ return htmlStatusMap[code];
169
+ }
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ static isRsError(error) {
172
+ return error instanceof _RsError;
173
+ }
174
+ };
175
+ var htmlStatusMap = {
176
+ UNKNOWN_ERROR: 500 /* SERVER_ERROR */,
177
+ NOT_FOUND: 404 /* NOT_FOUND */,
178
+ EMAIL_TAKEN: 409 /* CONFLICT */,
179
+ FORBIDDEN: 403 /* FORBIDDEN */,
180
+ CONFLICT: 409 /* CONFLICT */,
181
+ UNAUTHORIZED: 401 /* UNAUTHORIZED */,
182
+ UPDATE_FORBIDDEN: 403 /* FORBIDDEN */,
183
+ CREATE_FORBIDDEN: 403 /* FORBIDDEN */,
184
+ DELETE_FORBIDDEN: 403 /* FORBIDDEN */,
185
+ DELETE_FAILURE: 500 /* SERVER_ERROR */,
186
+ BAD_REQUEST: 400 /* BAD_REQUEST */,
187
+ INVALID_TOKEN: 401 /* UNAUTHORIZED */,
188
+ INCORRECT_EMAIL_OR_PASSWORD: 401 /* UNAUTHORIZED */,
189
+ DUPLICATE_TOKEN: 409 /* CONFLICT */,
190
+ DUPLICATE_USERNAME: 409 /* CONFLICT */,
191
+ DUPLICATE_EMAIL: 409 /* CONFLICT */,
192
+ DUPLICATE: 409 /* CONFLICT */,
193
+ EMAIL_NOT_VERIFIED: 400 /* BAD_REQUEST */,
194
+ UPDATE_WITHOUT_ID: 400 /* BAD_REQUEST */,
195
+ CONNECTION_ERROR: 599 /* NETWORK_CONNECT_TIMEOUT */,
196
+ INVALID_PAYMENT: 403 /* FORBIDDEN */,
197
+ DECLINED_PAYMENT: 403 /* FORBIDDEN */,
198
+ INTEGRATION_ERROR: 500 /* SERVER_ERROR */,
199
+ CANNOT_RESERVE: 403 /* FORBIDDEN */,
200
+ REFUND_FAILURE: 403 /* FORBIDDEN */,
201
+ INVALID_INVOICE: 403 /* FORBIDDEN */,
202
+ INVALID_COUPON: 403 /* FORBIDDEN */,
203
+ SERVICE_UNAVAILABLE: 503 /* SERVICE_UNAVAILABLE */,
204
+ METHOD_UNALLOWED: 405 /* METHOD_NOT_ALLOWED */,
205
+ LOGIN_EXPIRED: 401 /* UNAUTHORIZED */,
206
+ THIRD_PARTY_ERROR: 400 /* BAD_REQUEST */,
207
+ ACCESS_DENIED: 403 /* FORBIDDEN */,
208
+ DATABASE_ERROR: 500 /* SERVER_ERROR */,
209
+ SCHEMA_ERROR: 500 /* SERVER_ERROR */
210
+ };
211
+
212
+ // src/restura/restura.ts
213
+ var import_core_utils7 = require("@redskytech/core-utils");
214
+ var import_internal4 = require("@restura/internal");
215
+
216
+ // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
217
+ function _typeof(obj) {
218
+ if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
219
+ _typeof = function _typeof2(obj2) {
220
+ return typeof obj2;
221
+ };
222
+ } else {
223
+ _typeof = function _typeof2(obj2) {
224
+ return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
225
+ };
226
+ }
227
+ return _typeof(obj);
228
+ }
229
+ function boundMethod(target, key, descriptor) {
230
+ var fn = descriptor.value;
231
+ if (typeof fn !== "function") {
232
+ throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
233
+ }
234
+ var definingProperty = false;
235
+ return {
236
+ configurable: true,
237
+ get: function get() {
238
+ if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
239
+ return fn;
240
+ }
241
+ var boundFn = fn.bind(this);
242
+ definingProperty = true;
243
+ Object.defineProperty(this, key, {
244
+ configurable: true,
245
+ get: function get2() {
246
+ return boundFn;
247
+ },
248
+ set: function set(value) {
249
+ fn = value;
250
+ delete this[key];
251
+ }
252
+ });
253
+ definingProperty = false;
254
+ return boundFn;
255
+ },
256
+ set: function set(value) {
257
+ fn = value;
258
+ }
259
+ };
260
+ }
261
+
262
+ // src/restura/restura.ts
263
+ var import_body_parser = __toESM(require("body-parser"));
264
+ var import_compression = __toESM(require("compression"));
265
+ var import_cookie_parser = __toESM(require("cookie-parser"));
266
+ var import_crypto2 = require("crypto");
267
+ var express = __toESM(require("express"));
268
+ var import_fs4 = __toESM(require("fs"));
269
+ var import_path5 = __toESM(require("path"));
270
+ var import_pg3 = __toESM(require("pg"));
271
+ var prettier3 = __toESM(require("prettier"));
272
+
273
+ // src/restura/sql/SqlUtils.ts
274
+ var SqlUtils = class _SqlUtils {
275
+ static convertDatabaseTypeToTypescript(type, value) {
276
+ type = type.toLocaleLowerCase();
277
+ if (type.startsWith("tinyint") || type.startsWith("boolean")) return "boolean";
278
+ if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float"))
279
+ return "number";
280
+ if (type === "json") {
281
+ if (!value) return "object";
282
+ return value.split(",").map((val) => {
283
+ return val.replace(/['"]/g, "");
284
+ }).join(" | ");
285
+ }
286
+ if (type.startsWith("varchar") || type.indexOf("text") > -1 || type.startsWith("char") || type.indexOf("blob") > -1 || type.startsWith("binary"))
287
+ return "string";
288
+ if (type.startsWith("date") || type.startsWith("time")) return "string";
289
+ if (type.startsWith("enum")) return _SqlUtils.convertDatabaseEnumToStringUnion(value || type);
290
+ return "any";
291
+ }
292
+ static convertDatabaseEnumToStringUnion(type) {
293
+ return type.replace(/^enum\(|\)/g, "").split(",").map((value) => {
294
+ return `'${value.replace(/'/g, "")}'`;
295
+ }).join(" | ");
296
+ }
297
+ };
298
+
299
+ // src/restura/ResponseValidator.ts
300
+ var ResponseValidator = class _ResponseValidator {
301
+ constructor(schema) {
302
+ this.database = schema.database;
303
+ this.rootMap = {};
304
+ for (const endpoint of schema.endpoints) {
305
+ const endpointMap = {};
306
+ for (const route of endpoint.routes) {
307
+ if (_ResponseValidator.isCustomRoute(route)) {
308
+ endpointMap[`${route.method}:${route.path}`] = { validator: "any" };
309
+ continue;
310
+ }
311
+ endpointMap[`${route.method}:${route.path}`] = this.getRouteResponseType(route);
312
+ }
313
+ const endpointUrl = endpoint.baseUrl.endsWith("/") ? endpoint.baseUrl.slice(0, -1) : endpoint.baseUrl;
314
+ this.rootMap[endpointUrl] = { validator: endpointMap };
315
+ }
316
+ }
317
+ validateResponseParams(data, endpointUrl, routeData) {
318
+ if (!this.rootMap) {
319
+ throw new RsError("BAD_REQUEST", "Cannot validate response without type maps");
320
+ }
321
+ const routeMap = this.rootMap[endpointUrl].validator[`${routeData.method}:${routeData.path}`];
322
+ data = this.validateAndCoerceMap("_base", data, routeMap);
323
+ }
324
+ getRouteResponseType(route) {
325
+ const map = {};
326
+ for (const field of route.response) {
327
+ map[field.name] = this.getFieldResponseType(field, route.table);
328
+ }
329
+ if (route.type === "PAGED") {
330
+ return {
331
+ validator: {
332
+ data: { validator: map, isArray: true },
333
+ total: { validator: "number" }
334
+ }
335
+ };
336
+ }
337
+ if (route.method === "DELETE") {
338
+ return {
339
+ validator: "boolean"
340
+ };
341
+ }
342
+ return { validator: map, isArray: route.type === "ARRAY" };
343
+ }
344
+ getFieldResponseType(field, tableName) {
345
+ if (field.selector) {
346
+ return this.getTypeFromTable(field.selector, tableName);
347
+ } else if (field.subquery) {
348
+ const table = this.database.find((t) => t.name == tableName);
349
+ if (!table) return { isArray: true, validator: "any" };
350
+ const isOptional = table.roles.length > 0;
351
+ const validator = {};
352
+ for (const prop of field.subquery.properties) {
353
+ validator[prop.name] = this.getFieldResponseType(prop, field.subquery.table);
354
+ }
355
+ return {
356
+ isArray: true,
357
+ isOptional,
358
+ validator
359
+ };
360
+ }
361
+ return { validator: "any" };
362
+ }
363
+ getTypeFromTable(selector, name) {
364
+ const path5 = selector.split(".");
365
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { validator: "any", isOptional: false };
366
+ const tableName = path5.length == 2 ? path5[0] : name, columnName = path5.length == 2 ? path5[1] : path5[0];
367
+ const table = this.database.find((t) => t.name == tableName);
368
+ const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
369
+ if (!table || !column) return { validator: "any", isOptional: false };
370
+ let validator = SqlUtils.convertDatabaseTypeToTypescript(
371
+ column.type,
372
+ column.value
373
+ );
374
+ if (!_ResponseValidator.validatorIsValidString(validator)) validator = this.parseValidationEnum(validator);
375
+ return {
376
+ validator,
377
+ isOptional: column.roles.length > 0 || column.isNullable
378
+ };
379
+ }
380
+ parseValidationEnum(validator) {
381
+ let terms = validator.split("|");
382
+ terms = terms.map((v) => v.replace(/'/g, "").trim());
383
+ return terms;
384
+ }
385
+ validateAndCoerceMap(name, value, { isOptional, isArray, validator }) {
386
+ if (validator === "any") return value;
387
+ const valueType = typeof value;
388
+ if (value == null) {
389
+ if (isOptional) return value;
390
+ throw new RsError("DATABASE_ERROR", `Response param (${name}) is required`);
391
+ }
392
+ if (isArray) {
393
+ if (!Array.isArray(value)) {
394
+ throw new RsError(
395
+ "DATABASE_ERROR",
396
+ `Response param (${name}) is a/an ${valueType} instead of an array`
397
+ );
398
+ }
399
+ value.forEach((v, i) => this.validateAndCoerceMap(`${name}[${i}]`, v, { validator }));
400
+ return value;
401
+ }
402
+ if (typeof validator === "string") {
403
+ if (validator === "boolean" && valueType === "number") {
404
+ if (value !== 0 && value !== 1)
405
+ throw new RsError("DATABASE_ERROR", `Response param (${name}) is of the wrong type (${valueType})`);
406
+ return value === 1;
407
+ } else if (validator === "string" && valueType === "string") {
408
+ if (typeof value === "string" && value.match(
409
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.?\d*$|\d{2}:\d{2}:\d{2}.?\d*$|^\d{4}-\d{2}-\d{2}$/
410
+ )) {
411
+ const date = new Date(value);
412
+ if (date.toISOString() === "1970-01-01T00:00:00.000Z") return null;
413
+ const timezoneOffset = date.getTimezoneOffset() * 6e4;
414
+ return new Date(date.getTime() - timezoneOffset * 2).toISOString();
415
+ }
416
+ return value;
417
+ } else if (valueType === validator) {
418
+ return value;
419
+ } else if (valueType === "object") {
420
+ return value;
421
+ }
422
+ throw new RsError("DATABASE_ERROR", `Response param (${name}) is of the wrong type (${valueType})`);
423
+ }
424
+ if (Array.isArray(validator) && typeof value === "string") {
425
+ if (validator.includes(value)) return value;
426
+ throw new RsError("DATABASE_ERROR", `Response param (${name}) is not one of the enum options (${value})`);
427
+ }
428
+ if (valueType !== "object") {
429
+ throw new RsError("DATABASE_ERROR", `Response param (${name}) is of the wrong type (${valueType})`);
430
+ }
431
+ for (const prop in value) {
432
+ if (!validator[prop])
433
+ throw new RsError("DATABASE_ERROR", `Response param (${name}.${prop}) is not allowed`);
434
+ }
435
+ for (const prop in validator) {
436
+ value[prop] = this.validateAndCoerceMap(`${name}.${prop}`, value[prop], validator[prop]);
437
+ }
438
+ return value;
439
+ }
440
+ static isCustomRoute(route) {
441
+ return route.type === "CUSTOM_ONE" || route.type === "CUSTOM_ARRAY" || route.type === "CUSTOM_PAGED";
442
+ }
443
+ static validatorIsValidString(validator) {
444
+ return !validator.includes("|");
445
+ }
446
+ };
447
+
448
+ // src/restura/apiGenerator.ts
449
+ var import_core_utils = require("@redskytech/core-utils");
450
+ var import_prettier = __toESM(require("prettier"));
451
+ var ApiTree = class _ApiTree {
452
+ constructor(namespace, database) {
453
+ this.database = database;
454
+ this.data = [];
455
+ this.namespace = namespace;
456
+ this.children = /* @__PURE__ */ new Map();
457
+ }
458
+ static createRootNode(database) {
459
+ return new _ApiTree(null, database);
460
+ }
461
+ static isRouteData(data) {
462
+ return data.method !== void 0;
463
+ }
464
+ static isEndpointData(data) {
465
+ return data.routes !== void 0;
466
+ }
467
+ addData(namespaces, route) {
468
+ if (import_core_utils.ObjectUtils.isEmpty(namespaces)) {
469
+ this.data.push(route);
470
+ return;
471
+ }
472
+ const childName = namespaces[0];
473
+ this.children.set(childName, this.children.get(childName) || new _ApiTree(childName, this.database));
474
+ this.children.get(childName).addData(namespaces.slice(1), route);
475
+ }
476
+ createApiModels() {
477
+ let result = "";
478
+ for (const child of this.children.values()) {
479
+ result += child.createApiModelImpl(true);
480
+ }
481
+ return result;
482
+ }
483
+ createApiModelImpl(isBase) {
484
+ let result = ``;
485
+ for (const data of this.data) {
486
+ if (_ApiTree.isEndpointData(data)) {
487
+ result += _ApiTree.generateEndpointComments(data);
488
+ }
489
+ }
490
+ result += isBase ? `
491
+ declare namespace ${this.namespace} {` : `
492
+ export namespace ${this.namespace} {`;
493
+ for (const data of this.data) {
494
+ if (_ApiTree.isRouteData(data)) {
495
+ result += this.generateRouteModels(data);
496
+ }
497
+ }
498
+ for (const child of this.children.values()) {
499
+ result += child.createApiModelImpl(false);
500
+ }
501
+ result += "}";
502
+ return result;
503
+ }
504
+ static generateEndpointComments(endpoint) {
505
+ return `
506
+ // ${endpoint.name}
507
+ // ${endpoint.description}`;
508
+ }
509
+ generateRouteModels(route) {
510
+ let modelString = ``;
511
+ modelString += `
512
+ // ${route.name}
513
+ // ${route.description}
514
+ export namespace ${import_core_utils.StringUtils.capitalizeFirst(route.method.toLowerCase())} {
515
+ ${this.generateRequestParameters(route)}
516
+ ${this.generateResponseParameters(route)}
517
+ }`;
518
+ return modelString;
519
+ }
520
+ generateRequestParameters(route) {
521
+ let modelString = ``;
522
+ if (ResponseValidator.isCustomRoute(route) && route.requestType) {
523
+ modelString += `
524
+ export type Req = CustomTypes.${route.requestType}`;
525
+ return modelString;
526
+ }
527
+ if (!route.request) return modelString;
528
+ modelString += `
529
+ export interface Req{
530
+ ${route.request.map((p) => {
531
+ let requestType = "any";
532
+ const oneOfValidator = p.validator.find((v) => v.type === "ONE_OF");
533
+ const typeCheckValidator = p.validator.find((v) => v.type === "TYPE_CHECK");
534
+ if (oneOfValidator && import_core_utils.ObjectUtils.isArrayWithData(oneOfValidator.value)) {
535
+ requestType = oneOfValidator.value.map((v) => `'${v}'`).join(" | ");
536
+ } else if (typeCheckValidator) {
537
+ switch (typeCheckValidator.value) {
538
+ case "string":
539
+ case "number":
540
+ case "boolean":
541
+ case "string[]":
542
+ case "number[]":
543
+ case "any[]":
544
+ requestType = typeCheckValidator.value;
545
+ break;
546
+ }
547
+ }
548
+ return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
549
+ }).join(";\n")}${import_core_utils.ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
550
+ `;
551
+ modelString += `}`;
552
+ return modelString;
553
+ }
554
+ generateResponseParameters(route) {
555
+ if (ResponseValidator.isCustomRoute(route)) {
556
+ if (["number", "string", "boolean"].includes(route.responseType))
557
+ return `export type Res = ${route.responseType}`;
558
+ else if (["CUSTOM_ARRAY", "CUSTOM_PAGED"].includes(route.type))
559
+ return `export type Res = CustomTypes.${route.responseType}[]`;
560
+ else return `export type Res = CustomTypes.${route.responseType}`;
561
+ }
562
+ return `export interface Res ${this.getFields(route.response)}`;
563
+ }
564
+ getFields(fields) {
565
+ const nameFields = fields.map((f) => this.getNameAndType(f));
566
+ const nested = `{
567
+ ${nameFields.join(";\n ")}${import_core_utils.ObjectUtils.isArrayWithData(nameFields) ? ";" : ""}
568
+ }`;
569
+ return nested;
570
+ }
571
+ getNameAndType(p) {
572
+ let responseType = "any", isNullable = false, array = false;
573
+ if (p.selector) {
574
+ ({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
575
+ } else if (p.subquery) {
576
+ responseType = this.getFields(p.subquery.properties);
577
+ array = true;
578
+ }
579
+ return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
580
+ }
581
+ getTypeFromTable(selector, name) {
582
+ const path5 = selector.split(".");
583
+ if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { responseType: "any", isNullable: false };
584
+ let tableName = path5.length == 2 ? path5[0] : name;
585
+ const columnName = path5.length == 2 ? path5[1] : path5[0];
586
+ let table = this.database.find((t) => t.name == tableName);
587
+ if (!table && tableName.includes("_")) {
588
+ const tableAliasSplit = tableName.split("_");
589
+ tableName = tableAliasSplit[1];
590
+ table = this.database.find((t) => t.name == tableName);
591
+ }
592
+ const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
593
+ if (!table || !column) return { responseType: "any", isNullable: false };
594
+ return {
595
+ responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
596
+ isNullable: column.roles.length > 0 || column.isNullable
597
+ };
598
+ }
599
+ };
600
+ function pathToNamespaces(path5) {
601
+ return path5.split("/").map((e) => import_core_utils.StringUtils.toPascalCasing(e)).filter((e) => e);
602
+ }
603
+ function apiGenerator(schema, schemaHash) {
604
+ let apiString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/`;
605
+ const rootNamespace = ApiTree.createRootNode(schema.database);
606
+ for (const endpoint of schema.endpoints) {
607
+ const endpointNamespaces = pathToNamespaces(endpoint.baseUrl);
608
+ rootNamespace.addData(endpointNamespaces, endpoint);
609
+ for (const route of endpoint.routes) {
610
+ const fullNamespace = [...endpointNamespaces, ...pathToNamespaces(route.path)];
611
+ rootNamespace.addData(fullNamespace, route);
612
+ }
613
+ }
614
+ apiString += rootNamespace.createApiModels();
615
+ if (schema.customTypes.length > 0) {
616
+ apiString += `
617
+
618
+ declare namespace CustomTypes {
619
+ ${schema.customTypes}
620
+ }`;
621
+ }
622
+ return import_prettier.default.format(apiString, __spreadValues({
623
+ parser: "typescript"
624
+ }, {
625
+ trailingComma: "none",
626
+ tabWidth: 4,
627
+ useTabs: true,
628
+ endOfLine: "lf",
629
+ printWidth: 120,
630
+ singleQuote: true
631
+ }));
632
+ }
633
+
634
+ // src/restura/customApiFactory.ts
635
+ var import_fs = __toESM(require("fs"));
636
+ var import_path = __toESM(require("path"));
637
+ var import_bluebird = __toESM(require("bluebird"));
638
+ var import_internal2 = require("@restura/internal");
639
+ var CustomApiFactory = class {
640
+ constructor() {
641
+ this.customApis = {};
642
+ }
643
+ async loadApiFiles(baseFolderPath) {
644
+ const apiVersions = ["v1"];
645
+ for (const apiVersion of apiVersions) {
646
+ const apiVersionFolderPath = import_path.default.join(baseFolderPath, apiVersion);
647
+ const directoryExists = await import_internal2.fileUtils.existDir(apiVersionFolderPath);
648
+ if (!directoryExists) continue;
649
+ await this.addDirectory(apiVersionFolderPath, apiVersion);
650
+ }
651
+ }
652
+ getCustomApi(customApiName) {
653
+ return this.customApis[customApiName];
654
+ }
655
+ async addDirectory(directoryPath, apiVersion) {
656
+ var _a2;
657
+ const entries = await import_fs.default.promises.readdir(directoryPath, {
658
+ withFileTypes: true
659
+ });
660
+ const isTsx2 = (_a2 = process.argv[1]) == null ? void 0 : _a2.endsWith(".ts");
661
+ const isTsNode2 = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
662
+ const extension = isTsx2 || isTsNode2 ? "ts" : "js";
663
+ const shouldEndWith = `.api.${apiVersion}.${extension}`;
664
+ await import_bluebird.default.map(entries, async (entry) => {
665
+ if (entry.isFile()) {
666
+ if (entry.name.endsWith(shouldEndWith) === false) return;
667
+ try {
668
+ const importPath = `${import_path.default.join(directoryPath, entry.name)}`;
669
+ const ApiImport = await import(importPath);
670
+ const customApiClass = new ApiImport.default();
671
+ logger.info(`Registering custom API: ${ApiImport.default.name}`);
672
+ this.bindMethodsToInstance(customApiClass);
673
+ this.customApis[ApiImport.default.name] = customApiClass;
674
+ } catch (e) {
675
+ console.error(e);
676
+ }
677
+ }
678
+ });
679
+ }
680
+ bindMethodsToInstance(instance) {
681
+ const proto = Object.getPrototypeOf(instance);
682
+ Object.getOwnPropertyNames(proto).forEach((key) => {
683
+ const property = instance[key];
684
+ if (typeof property === "function" && key !== "constructor") {
685
+ instance[key] = property.bind(instance);
686
+ }
687
+ });
688
+ }
689
+ };
690
+ var customApiFactory = new CustomApiFactory();
691
+ var customApiFactory_default = customApiFactory;
692
+
693
+ // src/restura/customTypeValidationGenerator.ts
694
+ var import_fs2 = __toESM(require("fs"));
695
+ var TJS = __toESM(require("typescript-json-schema"));
696
+ var import_path2 = __toESM(require("path"));
697
+ var import_tmp = __toESM(require("tmp"));
698
+ var process2 = __toESM(require("process"));
699
+ function customTypeValidationGenerator(currentSchema) {
700
+ const schemaObject = {};
701
+ const customInterfaceNames = currentSchema.customTypes.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
702
+ if (!customInterfaceNames) return {};
703
+ const temporaryFile = import_tmp.default.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
704
+ import_fs2.default.writeFileSync(temporaryFile.name, currentSchema.customTypes);
705
+ const compilerOptions = {
706
+ strictNullChecks: true,
707
+ skipLibCheck: true
708
+ };
709
+ const program = TJS.getProgramFromFiles(
710
+ [
711
+ (0, import_path2.resolve)(temporaryFile.name),
712
+ // find a way to remove
713
+ import_path2.default.join(process2.cwd(), "src/@types/models.d.ts"),
714
+ import_path2.default.join(process2.cwd(), "src/@types/api.d.ts")
715
+ ],
716
+ compilerOptions
717
+ );
718
+ customInterfaceNames.forEach((item) => {
719
+ const ddlSchema = TJS.generateSchema(program, item, {
720
+ required: true
721
+ });
722
+ schemaObject[item] = ddlSchema || {};
723
+ });
724
+ temporaryFile.removeCallback();
725
+ return schemaObject;
726
+ }
727
+
728
+ // src/restura/middleware/addApiResponseFunctions.ts
729
+ function addApiResponseFunctions(req, res, next) {
730
+ res.sendData = function(data, statusCode = 200) {
731
+ res.status(statusCode).send({ data });
732
+ };
733
+ res.sendNoWrap = function(dataNoWrap, statusCode = 200) {
734
+ res.status(statusCode).send(dataNoWrap);
735
+ };
736
+ res.sendPaginated = function(pagedData, statusCode = 200) {
737
+ res.status(statusCode).send({ data: pagedData.data, total: pagedData.total });
738
+ };
739
+ res.sendError = function(shortError, msg, htmlStatusCode, stack) {
740
+ if (htmlStatusCode === void 0) {
741
+ if (RsError.htmlStatus(shortError) !== void 0) {
742
+ htmlStatusCode = RsError.htmlStatus(shortError);
743
+ } else {
744
+ htmlStatusCode = 500;
745
+ }
746
+ }
747
+ const errorData = __spreadValues({
748
+ err: shortError,
749
+ msg
750
+ }, restura.resturaConfig.sendErrorStackTrace && stack ? { stack } : {});
751
+ res.status(htmlStatusCode).send(errorData);
752
+ };
753
+ next();
754
+ }
755
+
756
+ // src/restura/middleware/authenticateUser.ts
757
+ function authenticateUser(applicationAuthenticateHandler) {
758
+ return (req, res, next) => {
759
+ applicationAuthenticateHandler(req, res, (userDetails) => {
760
+ req.requesterDetails = __spreadValues(__spreadValues({}, req.requesterDetails), userDetails);
761
+ next();
762
+ });
763
+ };
764
+ }
765
+
766
+ // src/restura/restura.schema.ts
767
+ var import_zod3 = require("zod");
768
+
769
+ // src/restura/types/validation.types.ts
770
+ var import_zod2 = require("zod");
771
+ var validatorDataSchemeValue = import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.number(), import_zod2.z.array(import_zod2.z.number())]);
772
+ var validatorDataSchema = import_zod2.z.object({
773
+ type: import_zod2.z.enum(["TYPE_CHECK", "MIN", "MAX", "ONE_OF"]),
774
+ value: validatorDataSchemeValue
775
+ }).strict();
776
+
777
+ // src/restura/restura.schema.ts
778
+ var orderBySchema = import_zod3.z.object({
779
+ columnName: import_zod3.z.string(),
780
+ order: import_zod3.z.enum(["ASC", "DESC"]),
781
+ tableName: import_zod3.z.string()
782
+ }).strict();
783
+ var groupBySchema = import_zod3.z.object({
784
+ columnName: import_zod3.z.string(),
785
+ tableName: import_zod3.z.string()
786
+ }).strict();
787
+ var whereDataSchema = import_zod3.z.object({
788
+ tableName: import_zod3.z.string().optional(),
789
+ columnName: import_zod3.z.string().optional(),
790
+ operator: import_zod3.z.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH"]).optional(),
791
+ value: import_zod3.z.string().or(import_zod3.z.number()).optional(),
792
+ custom: import_zod3.z.string().optional(),
793
+ conjunction: import_zod3.z.enum(["AND", "OR"]).optional()
794
+ }).strict();
795
+ var assignmentDataSchema = import_zod3.z.object({
796
+ name: import_zod3.z.string(),
797
+ value: import_zod3.z.string()
798
+ }).strict();
799
+ var joinDataSchema = import_zod3.z.object({
800
+ table: import_zod3.z.string(),
801
+ localColumnName: import_zod3.z.string().optional(),
802
+ foreignColumnName: import_zod3.z.string().optional(),
803
+ custom: import_zod3.z.string().optional(),
804
+ type: import_zod3.z.enum(["LEFT", "INNER"]),
805
+ alias: import_zod3.z.string().optional()
806
+ }).strict();
807
+ var requestDataSchema = import_zod3.z.object({
808
+ name: import_zod3.z.string(),
809
+ required: import_zod3.z.boolean(),
810
+ isNullable: import_zod3.z.boolean().optional(),
811
+ validator: import_zod3.z.array(validatorDataSchema)
812
+ }).strict();
813
+ var responseDataSchema = import_zod3.z.object({
814
+ name: import_zod3.z.string(),
815
+ selector: import_zod3.z.string().optional(),
816
+ subquery: import_zod3.z.object({
817
+ table: import_zod3.z.string(),
818
+ joins: import_zod3.z.array(joinDataSchema),
819
+ where: import_zod3.z.array(whereDataSchema),
820
+ properties: import_zod3.z.array(import_zod3.z.lazy(() => responseDataSchema)),
821
+ // Explicit type for the lazy schema
822
+ groupBy: groupBySchema.optional(),
823
+ orderBy: orderBySchema.optional()
824
+ }).optional()
825
+ }).strict();
826
+ var routeDataBaseSchema = import_zod3.z.object({
827
+ method: import_zod3.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
828
+ name: import_zod3.z.string(),
829
+ description: import_zod3.z.string(),
830
+ path: import_zod3.z.string(),
831
+ roles: import_zod3.z.array(import_zod3.z.string())
832
+ }).strict();
833
+ var standardRouteSchema = routeDataBaseSchema.extend({
834
+ type: import_zod3.z.enum(["ONE", "ARRAY", "PAGED"]),
835
+ table: import_zod3.z.string(),
836
+ joins: import_zod3.z.array(joinDataSchema),
837
+ assignments: import_zod3.z.array(assignmentDataSchema),
838
+ where: import_zod3.z.array(whereDataSchema),
839
+ request: import_zod3.z.array(requestDataSchema),
840
+ response: import_zod3.z.array(responseDataSchema),
841
+ groupBy: groupBySchema.optional(),
842
+ orderBy: orderBySchema.optional()
843
+ }).strict();
844
+ var customRouteSchema = routeDataBaseSchema.extend({
845
+ type: import_zod3.z.enum(["CUSTOM_ONE", "CUSTOM_ARRAY", "CUSTOM_PAGED"]),
846
+ responseType: import_zod3.z.union([import_zod3.z.string(), import_zod3.z.enum(["string", "number", "boolean"])]),
847
+ requestType: import_zod3.z.string().optional(),
848
+ request: import_zod3.z.array(requestDataSchema).optional(),
849
+ table: import_zod3.z.undefined(),
850
+ joins: import_zod3.z.undefined(),
851
+ assignments: import_zod3.z.undefined(),
852
+ fileUploadType: import_zod3.z.enum(["SINGLE", "MULTIPLE"]).optional()
853
+ }).strict();
854
+ var postgresColumnNumericTypesSchema = import_zod3.z.enum([
855
+ "SMALLINT",
856
+ // 2 bytes, -32,768 to 32,767
857
+ "INTEGER",
858
+ // 4 bytes, -2,147,483,648 to 2,147,483,647
859
+ "BIGINT",
860
+ // 8 bytes, -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
861
+ "DECIMAL",
862
+ // user-specified precision, exact numeric
863
+ "NUMERIC",
864
+ // same as DECIMAL
865
+ "REAL",
866
+ // 4 bytes, 6 decimal digits precision (single precision)
867
+ "DOUBLE PRECISION",
868
+ // 8 bytes, 15 decimal digits precision (double precision)
869
+ "SERIAL",
870
+ // auto-incrementing integer
871
+ "BIGSERIAL"
872
+ // auto-incrementing big integer
873
+ ]);
874
+ var postgresColumnStringTypesSchema = import_zod3.z.enum([
875
+ "CHAR",
876
+ // fixed-length, blank-padded
877
+ "VARCHAR",
878
+ // variable-length with limit
879
+ "TEXT",
880
+ // variable-length without limit
881
+ "BYTEA"
882
+ // binary data
883
+ ]);
884
+ var postgresColumnDateTypesSchema = import_zod3.z.enum([
885
+ "DATE",
886
+ // calendar date (year, month, day)
887
+ "TIMESTAMP",
888
+ // both date and time (without time zone)
889
+ "TIMESTAMPTZ",
890
+ // both date and time (with time zone)
891
+ "TIME",
892
+ // time of day (without time zone)
893
+ "INTERVAL"
894
+ // time span
895
+ ]);
896
+ var postgresColumnJsonTypesSchema = import_zod3.z.enum([
897
+ "JSON",
898
+ // stores JSON data as raw text
899
+ "JSONB"
900
+ // stores JSON data in a binary format, optimized for query performance
901
+ ]);
902
+ var mariaDbColumnNumericTypesSchema = import_zod3.z.enum([
903
+ "BOOLEAN",
904
+ // 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
905
+ "TINYINT",
906
+ // 1-byte A very small integer. Numeric value with scale 0. Signed: -126 to +127. Unsigned: 0 to 253.
907
+ "SMALLINT",
908
+ // 2-bytes A small integer. Signed: -32,766 to 32,767. Unsigned: 0 to 65,533.
909
+ "MEDIUMINT",
910
+ // 3-bytes A medium integer. Signed: -8388608 to 8388607. Unsigned: 0 to 16777215. Supported starting with MariaDB ColumnStore 1.4.2.
911
+ "INTEGER",
912
+ // 4-bytes A normal-size integer. Numeric value with scale 0. Signed: -2,147,483,646 to 2,147,483,647. Unsigned: 0 to 4,294,967,293
913
+ "BIGINT",
914
+ // 8-bytes A large integer. Numeric value with scale 0. Signed: -9,223,372,036,854,775,806 to +9,223,372,036,854,775,807 Unsigned: 0 to +18,446,744,073,709,551,613
915
+ "DECIMAL",
916
+ // 2, 4, or 8 bytes A packed fixed-point number that can have a specific total number of digits and with a set number of digits after a decimal. The maximum precision (total number of digits) that can be specified is 18.
917
+ "FLOAT",
918
+ // 4 bytes Stored in 32-bit IEEE-754 floating point format. As such, the number of significant digits is about 6, and the range of values is approximately +/- 1e38.
919
+ "DOUBLE"
920
+ // 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.
921
+ ]);
922
+ var mariaDbColumnStringTypesSchema = import_zod3.z.enum([
923
+ "CHAR",
924
+ // 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.
925
+ "VARCHAR",
926
+ // 1, 2, 4, or 8 bytes or 8-byte token Holds letters, numbers, and special characters of variable length. Max length = 8000 bytes or characters and minimum length = 1 byte or character.
927
+ "TINYTEXT",
928
+ // 255 bytes Holds a small amount of letters, numbers, and special characters of variable length. Supported from version 1.1.0 onwards.
929
+ "TINYBLOB",
930
+ // 255 bytes Holds a small amount of binary data of variable length. Supported from version 1.1.0 onwards.
931
+ "TEXT",
932
+ // 64 KB Holds letters, numbers, and special characters of variable length. Supported from version 1.1.0 onwards.
933
+ "BLOB",
934
+ // 64 KB Holds binary data of variable length. Supported from version 1.1.0 onwards.
935
+ "MEDIUMTEXT",
936
+ // 16 MB Holds a medium amount of letters, numbers, and special characters of variable length. Supported from version 1.1.0 onwards.
937
+ "MEDIUMBLOB",
938
+ // 16 MB Holds a medium amount of binary data of variable length. Supported from version 1.1.0 onwards.
939
+ "LONGTEXT",
940
+ // 1.96 GB Holds a large amount of letters, numbers, and special characters of variable length. Supported from version 1.1.0 onwards.
941
+ "JSON",
942
+ // Alias for LONGTEXT, creates a CONSTRAINT for JSON_VALID, holds a JSON-formatted string of plain text.
943
+ "LONGBLOB",
944
+ // 1.96 GB Holds a large amount of binary data of variable length. Supported from version 1.1.0 onwards.
945
+ "ENUM"
946
+ // Enum type
947
+ ]);
948
+ var mariaDbColumnDateTypesSchema = import_zod3.z.enum([
949
+ "DATE",
950
+ // 4-bytes Date has year, month, and day.
951
+ "DATETIME",
952
+ // 8-bytes A date and time combination. Supported range is 1000-01-01 00:00:00 to 9999-12-31 23:59:59. From version 1.2.0 microseconds are also supported.
953
+ "TIME",
954
+ // 8-bytes Holds hour, minute, second and optionally microseconds for time.
955
+ "TIMESTAMP"
956
+ // 4-bytes Values are stored as the number of seconds since 1970-01-01 00:00:00 UTC, and optionally microseconds.
957
+ ]);
958
+ var columnDataSchema = import_zod3.z.object({
959
+ name: import_zod3.z.string(),
960
+ type: import_zod3.z.union([
961
+ postgresColumnNumericTypesSchema,
962
+ postgresColumnStringTypesSchema,
963
+ postgresColumnDateTypesSchema,
964
+ postgresColumnJsonTypesSchema,
965
+ mariaDbColumnNumericTypesSchema,
966
+ mariaDbColumnStringTypesSchema,
967
+ mariaDbColumnDateTypesSchema
968
+ ]),
969
+ isNullable: import_zod3.z.boolean(),
970
+ roles: import_zod3.z.array(import_zod3.z.string()),
971
+ comment: import_zod3.z.string().optional(),
972
+ default: import_zod3.z.string().optional(),
973
+ value: import_zod3.z.string().optional(),
974
+ isPrimary: import_zod3.z.boolean().optional(),
975
+ isUnique: import_zod3.z.boolean().optional(),
976
+ hasAutoIncrement: import_zod3.z.boolean().optional(),
977
+ length: import_zod3.z.number().optional()
978
+ }).strict();
979
+ var indexDataSchema = import_zod3.z.object({
980
+ name: import_zod3.z.string(),
981
+ columns: import_zod3.z.array(import_zod3.z.string()),
982
+ isUnique: import_zod3.z.boolean(),
983
+ isPrimaryKey: import_zod3.z.boolean(),
984
+ order: import_zod3.z.enum(["ASC", "DESC"])
985
+ }).strict();
986
+ var foreignKeyActionsSchema = import_zod3.z.enum([
987
+ "CASCADE",
988
+ // CASCADE action for foreign keys
989
+ "SET NULL",
990
+ // SET NULL action for foreign keys
991
+ "RESTRICT",
992
+ // RESTRICT action for foreign keys
993
+ "NO ACTION",
994
+ // NO ACTION for foreign keys
995
+ "SET DEFAULT"
996
+ // SET DEFAULT action for foreign keys
997
+ ]);
998
+ var foreignKeyDataSchema = import_zod3.z.object({
999
+ name: import_zod3.z.string(),
1000
+ column: import_zod3.z.string(),
1001
+ refTable: import_zod3.z.string(),
1002
+ refColumn: import_zod3.z.string(),
1003
+ onDelete: foreignKeyActionsSchema,
1004
+ onUpdate: foreignKeyActionsSchema
1005
+ }).strict();
1006
+ var checkConstraintDataSchema = import_zod3.z.object({
1007
+ name: import_zod3.z.string(),
1008
+ check: import_zod3.z.string()
1009
+ }).strict();
1010
+ var tableDataSchema = import_zod3.z.object({
1011
+ name: import_zod3.z.string(),
1012
+ columns: import_zod3.z.array(columnDataSchema),
1013
+ indexes: import_zod3.z.array(indexDataSchema),
1014
+ foreignKeys: import_zod3.z.array(foreignKeyDataSchema),
1015
+ checkConstraints: import_zod3.z.array(checkConstraintDataSchema),
1016
+ roles: import_zod3.z.array(import_zod3.z.string())
1017
+ }).strict();
1018
+ var endpointDataSchema = import_zod3.z.object({
1019
+ name: import_zod3.z.string(),
1020
+ description: import_zod3.z.string(),
1021
+ baseUrl: import_zod3.z.string(),
1022
+ routes: import_zod3.z.array(import_zod3.z.union([standardRouteSchema, customRouteSchema]))
1023
+ }).strict();
1024
+ var resturaZodSchema = import_zod3.z.object({
1025
+ database: import_zod3.z.array(tableDataSchema),
1026
+ endpoints: import_zod3.z.array(endpointDataSchema),
1027
+ globalParams: import_zod3.z.array(import_zod3.z.string()),
1028
+ roles: import_zod3.z.array(import_zod3.z.string()),
1029
+ customTypes: import_zod3.z.string()
1030
+ }).strict();
1031
+ async function isSchemaValid(schemaToCheck) {
1032
+ try {
1033
+ resturaZodSchema.parse(schemaToCheck);
1034
+ return true;
1035
+ } catch (error) {
1036
+ logger.error(error);
1037
+ return false;
1038
+ }
1039
+ }
1040
+
1041
+ // src/restura/validateRequestParams.ts
1042
+ var import_core_utils2 = require("@redskytech/core-utils");
1043
+ var import_jsonschema = __toESM(require("jsonschema"));
1044
+ var import_zod4 = require("zod");
1045
+
1046
+ // src/restura/utils/addQuotesToStrings.ts
1047
+ function addQuotesToStrings(variable) {
1048
+ if (typeof variable === "string") {
1049
+ return `'${variable}'`;
1050
+ } else if (Array.isArray(variable)) {
1051
+ const arrayWithQuotes = variable.map(addQuotesToStrings);
1052
+ return arrayWithQuotes;
1053
+ } else {
1054
+ return variable;
1055
+ }
1056
+ }
1057
+
1058
+ // src/restura/validateRequestParams.ts
1059
+ function validateRequestParams(req, routeData, validationSchema) {
1060
+ const requestData = getRequestData(req);
1061
+ req.data = requestData;
1062
+ if (routeData.request === void 0) {
1063
+ if (routeData.type !== "CUSTOM_ONE" && routeData.type !== "CUSTOM_ARRAY" && routeData.type !== "CUSTOM_PAGED")
1064
+ throw new RsError("BAD_REQUEST", `No request parameters provided for standard request.`);
1065
+ if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
1066
+ if (!routeData.requestType) throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
1067
+ const currentInterface = validationSchema[routeData.requestType];
1068
+ const validator = new import_jsonschema.default.Validator();
1069
+ const executeValidation = validator.validate(req.data, currentInterface);
1070
+ if (!executeValidation.valid) {
1071
+ throw new RsError(
1072
+ "BAD_REQUEST",
1073
+ `Request custom setup has failed the following check: (${executeValidation.errors})`
1074
+ );
1075
+ }
1076
+ return;
1077
+ }
1078
+ Object.keys(req.data).forEach((requestParamName) => {
1079
+ const requestParam = routeData.request.find((param) => param.name === requestParamName);
1080
+ if (!requestParam) {
1081
+ throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not allowed`);
1082
+ }
1083
+ });
1084
+ routeData.request.forEach((requestParam) => {
1085
+ const requestValue = requestData[requestParam.name];
1086
+ if (requestParam.required && requestValue === void 0)
1087
+ throw new RsError("BAD_REQUEST", `Request param (${requestParam.name}) is required but missing`);
1088
+ else if (!requestParam.required && requestValue === void 0) return;
1089
+ validateRequestSingleParam(requestValue, requestParam);
1090
+ });
1091
+ }
1092
+ function validateRequestSingleParam(requestValue, requestParam) {
1093
+ if (requestParam.isNullable && requestValue === null) return;
1094
+ requestParam.validator.forEach((validator) => {
1095
+ switch (validator.type) {
1096
+ case "TYPE_CHECK":
1097
+ performTypeCheck(requestValue, validator, requestParam.name);
1098
+ break;
1099
+ case "MIN":
1100
+ performMinCheck(requestValue, validator, requestParam.name);
1101
+ break;
1102
+ case "MAX":
1103
+ performMaxCheck(requestValue, validator, requestParam.name);
1104
+ break;
1105
+ case "ONE_OF":
1106
+ performOneOfCheck(requestValue, validator, requestParam.name);
1107
+ break;
1108
+ }
1109
+ });
1110
+ }
1111
+ function isValidType(type, requestValue) {
1112
+ try {
1113
+ expectValidType(type, requestValue);
1114
+ return true;
1115
+ } catch (e) {
1116
+ return false;
1117
+ }
1118
+ }
1119
+ function expectValidType(type, requestValue) {
1120
+ if (type === "number") {
1121
+ return import_zod4.z.number().parse(requestValue);
1122
+ }
1123
+ if (type === "string") {
1124
+ return import_zod4.z.string().parse(requestValue);
1125
+ }
1126
+ if (type === "boolean") {
1127
+ return import_zod4.z.boolean().parse(requestValue);
1128
+ }
1129
+ if (type === "string[]") {
1130
+ return import_zod4.z.array(import_zod4.z.string()).parse(requestValue);
1131
+ }
1132
+ if (type === "number[]") {
1133
+ return import_zod4.z.array(import_zod4.z.number()).parse(requestValue);
1134
+ }
1135
+ if (type === "any[]") {
1136
+ return import_zod4.z.array(import_zod4.z.any()).parse(requestValue);
1137
+ }
1138
+ if (type === "object") {
1139
+ return import_zod4.z.object({}).strict().parse(requestValue);
1140
+ }
1141
+ }
1142
+ function performTypeCheck(requestValue, validator, requestParamName) {
1143
+ if (!isValidType(validator.value, requestValue)) {
1144
+ throw new RsError(
1145
+ "BAD_REQUEST",
1146
+ `Request param (${requestParamName}) with value (${addQuotesToStrings(requestValue)}) is not of type (${validator.value})`
1147
+ );
1148
+ }
1149
+ try {
1150
+ validatorDataSchemeValue.parse(validator.value);
1151
+ } catch (e) {
1152
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not a valid type`);
1153
+ }
1154
+ }
1155
+ function expectOnlyNumbers(requestValue, validator, requestParamName) {
1156
+ if (!isValueNumber(requestValue))
1157
+ throw new RsError(
1158
+ "BAD_REQUEST",
1159
+ `Request param (${requestParamName}) with value (${requestValue}) is not of type number`
1160
+ );
1161
+ if (!isValueNumber(validator.value))
1162
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value} is not of type number`);
1163
+ }
1164
+ function performMinCheck(requestValue, validator, requestParamName) {
1165
+ expectOnlyNumbers(requestValue, validator, requestParamName);
1166
+ if (requestValue < validator.value)
1167
+ throw new RsError(
1168
+ "BAD_REQUEST",
1169
+ `Request param (${requestParamName}) with value (${requestValue}) is less than (${validator.value})`
1170
+ );
1171
+ }
1172
+ function performMaxCheck(requestValue, validator, requestParamName) {
1173
+ expectOnlyNumbers(requestValue, validator, requestParamName);
1174
+ if (requestValue > validator.value)
1175
+ throw new RsError(
1176
+ "BAD_REQUEST",
1177
+ `Request param (${requestParamName}) with value (${requestValue}) is more than (${validator.value})`
1178
+ );
1179
+ }
1180
+ function performOneOfCheck(requestValue, validator, requestParamName) {
1181
+ if (!import_core_utils2.ObjectUtils.isArrayWithData(validator.value))
1182
+ throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not of type array`);
1183
+ if (typeof requestValue === "object")
1184
+ throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not of type string or number`);
1185
+ if (!validator.value.includes(requestValue))
1186
+ throw new RsError(
1187
+ "BAD_REQUEST",
1188
+ `Request param (${requestParamName}) with value (${requestValue}) is not one of (${validator.value.join(", ")})`
1189
+ );
1190
+ }
1191
+ function isValueNumber(value) {
1192
+ return !isNaN(Number(value));
1193
+ }
1194
+ function getRequestData(req) {
1195
+ let body = "";
1196
+ if (req.method === "GET" || req.method === "DELETE") {
1197
+ body = "query";
1198
+ } else {
1199
+ body = "body";
1200
+ }
1201
+ const bodyData = req[body];
1202
+ if (bodyData && body === "query") {
1203
+ for (const attr in bodyData) {
1204
+ if (bodyData[attr] instanceof Array) {
1205
+ const attrList = [];
1206
+ for (const value of bodyData[attr]) {
1207
+ if (isNaN(Number(value))) continue;
1208
+ attrList.push(Number(value));
1209
+ }
1210
+ if (import_core_utils2.ObjectUtils.isArrayWithData(attrList)) {
1211
+ bodyData[attr] = attrList;
1212
+ }
1213
+ } else {
1214
+ bodyData[attr] = import_core_utils2.ObjectUtils.safeParse(bodyData[attr]);
1215
+ if (isNaN(Number(bodyData[attr]))) continue;
1216
+ bodyData[attr] = Number(bodyData[attr]);
1217
+ }
1218
+ }
1219
+ }
1220
+ return bodyData;
1221
+ }
1222
+
1223
+ // src/restura/middleware/schemaValidation.ts
1224
+ async function schemaValidation(req, res, next) {
1225
+ req.data = getRequestData(req);
1226
+ try {
1227
+ resturaZodSchema.parse(req.data);
1228
+ next();
1229
+ } catch (error) {
1230
+ logger.error(error);
1231
+ res.sendError("BAD_REQUEST", error, 400 /* BAD_REQUEST */);
1232
+ }
1233
+ }
1234
+
1235
+ // src/restura/modelGenerator.ts
1236
+ var import_core_utils3 = require("@redskytech/core-utils");
1237
+ var import_prettier2 = __toESM(require("prettier"));
1238
+ function modelGenerator(schema, schemaHash) {
1239
+ let modelString = `/** Auto generated file from Schema Hash (${schemaHash}). DO NOT MODIFY **/
1240
+ `;
1241
+ modelString += `declare namespace Model {
1242
+ `;
1243
+ for (const table of schema.database) {
1244
+ modelString += convertTable(table);
1245
+ }
1246
+ modelString += `}`;
1247
+ return import_prettier2.default.format(modelString, __spreadValues({
1248
+ parser: "typescript"
1249
+ }, {
1250
+ trailingComma: "none",
1251
+ tabWidth: 4,
1252
+ useTabs: true,
1253
+ endOfLine: "lf",
1254
+ printWidth: 120,
1255
+ singleQuote: true
1256
+ }));
1257
+ }
1258
+ function convertTable(table) {
1259
+ let modelString = ` export interface ${import_core_utils3.StringUtils.capitalizeFirst(table.name)} {
1260
+ `;
1261
+ for (const column of table.columns) {
1262
+ modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
1263
+ `;
1264
+ }
1265
+ modelString += ` }
1266
+ `;
1267
+ return modelString;
1268
+ }
1269
+
1270
+ // src/restura/sql/PsqlEngine.ts
1271
+ var import_core_utils5 = require("@redskytech/core-utils");
1272
+ var import_pg_diff_sync = __toESM(require("@wmfs/pg-diff-sync"));
1273
+ var import_pg_info = __toESM(require("@wmfs/pg-info"));
1274
+ var import_pg2 = __toESM(require("pg"));
1275
+
1276
+ // src/restura/sql/PsqlPool.ts
1277
+ var import_pg = __toESM(require("pg"));
1278
+
1279
+ // src/restura/sql/PsqlConnection.ts
1280
+ var import_pg_format2 = __toESM(require("pg-format"));
1281
+
1282
+ // src/restura/sql/PsqlUtils.ts
1283
+ var import_pg_format = __toESM(require("pg-format"));
1284
+ function escapeColumnName(columnName) {
1285
+ if (columnName === void 0) return "";
1286
+ return `"${columnName.replace(/"/g, "")}"`.replace(".", '"."');
1287
+ }
1288
+ function questionMarksToOrderedParams(query) {
1289
+ let count = 1;
1290
+ return query.replace(/'\?'|\?/g, () => `$${count++}`);
1291
+ }
1292
+ function insertObjectQuery(table, obj) {
1293
+ const keys = Object.keys(obj);
1294
+ const params = Object.values(obj);
1295
+ const columns = keys.map((column) => escapeColumnName(column)).join(", ");
1296
+ const values = params.map((value) => SQL`${value}`).join(", ");
1297
+ const query = `
1298
+ INSERT INTO "${table}" (${columns})
1299
+ VALUES (${values})
1300
+ RETURNING *`;
1301
+ return query;
1302
+ }
1303
+ function updateObjectQuery(table, obj, whereStatement) {
1304
+ const setArray = [];
1305
+ for (const i in obj) {
1306
+ setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1307
+ }
1308
+ return `
1309
+ UPDATE ${escapeColumnName(table)}
1310
+ SET ${setArray.join(", ")} ${whereStatement}
1311
+ RETURNING *`;
1312
+ }
1313
+ function isValueNumber2(value) {
1314
+ return !isNaN(Number(value));
1315
+ }
1316
+ function SQL(strings, ...values) {
1317
+ let query = strings[0];
1318
+ values.forEach((value, index) => {
1319
+ if (typeof value === "boolean") {
1320
+ query += value;
1321
+ } else if (typeof value === "number") {
1322
+ query += value;
1323
+ } else {
1324
+ query += import_pg_format.default.literal(value);
1325
+ }
1326
+ query += strings[index + 1];
1327
+ });
1328
+ return query;
1329
+ }
1330
+
1331
+ // src/restura/sql/PsqlConnection.ts
1332
+ var import_crypto = __toESM(require("crypto"));
1333
+ var PsqlConnection = class {
1334
+ constructor() {
1335
+ this.instanceId = import_crypto.default.randomUUID();
1336
+ }
1337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1338
+ async queryOne(query, options, requesterDetails) {
1339
+ const formattedQuery = questionMarksToOrderedParams(query);
1340
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1341
+ this.logSqlStatement(formattedQuery, options, meta);
1342
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1343
+ `;
1344
+ try {
1345
+ const response = await this.query(queryMetadata + formattedQuery, options);
1346
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1347
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1348
+ return response.rows[0];
1349
+ } catch (error) {
1350
+ console.error(error, query, options);
1351
+ if (RsError.isRsError(error)) throw error;
1352
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1353
+ throw new RsError("DUPLICATE", error.message);
1354
+ }
1355
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1356
+ }
1357
+ }
1358
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1359
+ async runQuery(query, options, requesterDetails) {
1360
+ const formattedQuery = questionMarksToOrderedParams(query);
1361
+ const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
1362
+ this.logSqlStatement(formattedQuery, options, meta);
1363
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
1364
+ `;
1365
+ try {
1366
+ const response = await this.query(queryMetadata + formattedQuery, options);
1367
+ return response.rows;
1368
+ } catch (error) {
1369
+ console.error(error, query, options);
1370
+ if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
1371
+ throw new RsError("DUPLICATE", error.message);
1372
+ }
1373
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
1374
+ }
1375
+ }
1376
+ logSqlStatement(query, options, queryMetadata, prefix = "") {
1377
+ if (logger.level !== "silly") return;
1378
+ let sqlStatement = "";
1379
+ if (options.length === 0) {
1380
+ sqlStatement = query;
1381
+ } else {
1382
+ let stringIndex = 0;
1383
+ sqlStatement = query.replace(/\$\d+/g, () => {
1384
+ const value = options[stringIndex++];
1385
+ if (typeof value === "number") return value.toString();
1386
+ return import_pg_format2.default.literal(value);
1387
+ });
1388
+ }
1389
+ let initiator = "Anonymous";
1390
+ if ("userId" in queryMetadata && queryMetadata.userId)
1391
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
1392
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
1393
+ logger.silly(`${prefix}query by ${initiator}, Query ->
1394
+ ${sqlStatement}`);
1395
+ }
1396
+ };
1397
+
1398
+ // src/restura/sql/PsqlPool.ts
1399
+ var { Pool } = import_pg.default;
1400
+ var PsqlPool = class extends PsqlConnection {
1401
+ constructor(poolConfig) {
1402
+ super();
1403
+ this.poolConfig = poolConfig;
1404
+ this.pool = new Pool(poolConfig);
1405
+ this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
1406
+ logger.info("Connected to PostgreSQL database");
1407
+ }).catch((error) => {
1408
+ logger.error("Error connecting to database", error);
1409
+ process.exit(1);
1410
+ });
1411
+ }
1412
+ async query(query, values) {
1413
+ return this.pool.query(query, values);
1414
+ }
1415
+ };
1416
+
1417
+ // src/restura/sql/SqlEngine.ts
1418
+ var import_core_utils4 = require("@redskytech/core-utils");
1419
+ var SqlEngine = class {
1420
+ async runQueryForRoute(req, routeData, schema) {
1421
+ if (!this.doesRoleHavePermissionToTable(req.requesterDetails.role, schema, routeData.table))
1422
+ throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
1423
+ switch (routeData.method) {
1424
+ case "POST":
1425
+ return this.executeCreateRequest(req, routeData, schema);
1426
+ case "GET":
1427
+ return this.executeGetRequest(req, routeData, schema);
1428
+ case "PUT":
1429
+ case "PATCH":
1430
+ return this.executeUpdateRequest(req, routeData, schema);
1431
+ case "DELETE":
1432
+ return this.executeDeleteRequest(req, routeData, schema);
1433
+ }
1434
+ }
1435
+ getTableSchema(schema, tableName) {
1436
+ const tableSchema = schema.database.find((item) => item.name === tableName);
1437
+ if (!tableSchema) throw new RsError("SCHEMA_ERROR", `Table ${tableName} not found in schema`);
1438
+ return tableSchema;
1439
+ }
1440
+ doesRoleHavePermissionToColumn(role, schema, item, joins) {
1441
+ if (item.selector) {
1442
+ let tableName = item.selector.split(".")[0];
1443
+ const columnName = item.selector.split(".")[1];
1444
+ let tableSchema = schema.database.find((item2) => item2.name === tableName);
1445
+ if (!tableSchema) {
1446
+ const join = joins.find((join2) => join2.alias === tableName);
1447
+ if (!join) throw new RsError("SCHEMA_ERROR", `Table ${tableName} not found in schema`);
1448
+ tableName = join.table;
1449
+ tableSchema = schema.database.find((item2) => item2.name === tableName);
1450
+ }
1451
+ if (!tableSchema) throw new RsError("SCHEMA_ERROR", `Table ${tableName} not found in schema`);
1452
+ const columnSchema = tableSchema.columns.find((item2) => item2.name === columnName);
1453
+ if (!columnSchema)
1454
+ throw new RsError("SCHEMA_ERROR", `Column ${columnName} not found in table ${tableName}`);
1455
+ const doesColumnHaveRoles = import_core_utils4.ObjectUtils.isArrayWithData(columnSchema.roles);
1456
+ if (!doesColumnHaveRoles) return true;
1457
+ if (!role) return false;
1458
+ return columnSchema.roles.includes(role);
1459
+ }
1460
+ if (item.subquery) {
1461
+ return import_core_utils4.ObjectUtils.isArrayWithData(
1462
+ item.subquery.properties.filter((nestedItem) => {
1463
+ return this.doesRoleHavePermissionToColumn(role, schema, nestedItem, joins);
1464
+ })
1465
+ );
1466
+ }
1467
+ return false;
1468
+ }
1469
+ doesRoleHavePermissionToTable(userRole, schema, tableName) {
1470
+ const tableSchema = this.getTableSchema(schema, tableName);
1471
+ const doesTableHaveRoles = import_core_utils4.ObjectUtils.isArrayWithData(tableSchema.roles);
1472
+ if (!doesTableHaveRoles) return true;
1473
+ if (!userRole) return false;
1474
+ return tableSchema.roles.includes(userRole);
1475
+ }
1476
+ replaceParamKeywords(value, routeData, req, sqlParams) {
1477
+ let returnValue = value;
1478
+ returnValue = this.replaceLocalParamKeywords(returnValue, routeData, req, sqlParams);
1479
+ returnValue = this.replaceGlobalParamKeywords(returnValue, routeData, req, sqlParams);
1480
+ return returnValue;
1481
+ }
1482
+ replaceLocalParamKeywords(value, routeData, req, sqlParams) {
1483
+ var _a2;
1484
+ if (!routeData.request) return value;
1485
+ const data = req.data;
1486
+ if (typeof value === "string") {
1487
+ (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1488
+ const requestParam = routeData.request.find((item) => {
1489
+ return item.name === param.replace("$", "");
1490
+ });
1491
+ if (!requestParam)
1492
+ throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
1493
+ sqlParams.push(data[requestParam.name]);
1494
+ });
1495
+ return value.replace(new RegExp(/\$[a-zA-Z][a-zA-Z0-9_]+/g), "?");
1496
+ }
1497
+ return value;
1498
+ }
1499
+ replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
1500
+ var _a2;
1501
+ if (typeof value === "string") {
1502
+ (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
1503
+ param = param.replace("#", "");
1504
+ const globalParamValue = req.requesterDetails[param];
1505
+ if (!globalParamValue)
1506
+ throw new RsError(
1507
+ "SCHEMA_ERROR",
1508
+ `Invalid global keyword clause in route (${routeData.path}) when looking for (#${param})`
1509
+ );
1510
+ sqlParams.push(globalParamValue);
1511
+ });
1512
+ return value.replace(new RegExp(/#[a-zA-Z][a-zA-Z0-9_]+/g), "?");
1513
+ }
1514
+ return value;
1515
+ }
1516
+ };
1517
+
1518
+ // src/restura/sql/filterPsqlParser.ts
1519
+ var import_pegjs = __toESM(require("pegjs"));
1520
+ var filterSqlGrammar = `
1521
+ {
1522
+ // ported from pg-format but intentionally will add double quotes to every column
1523
+ function quoteSqlIdentity(value) {
1524
+ if (value === undefined || value === null) {
1525
+ throw new Error('SQL identifier cannot be null or undefined');
1526
+ } else if (value === false) {
1527
+ return '"f"';
1528
+ } else if (value === true) {
1529
+ return '"t"';
1530
+ } else if (value instanceof Date) {
1531
+ // return '"' + formatDate(value.toISOString()) + '"';
1532
+ } else if (value instanceof Buffer) {
1533
+ throw new Error('SQL identifier cannot be a buffer');
1534
+ } else if (Array.isArray(value) === true) {
1535
+ var temp = [];
1536
+ for (var i = 0; i < value.length; i++) {
1537
+ if (Array.isArray(value[i]) === true) {
1538
+ throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
1539
+ } else {
1540
+ // temp.push(quoteIdent(value[i]));
1541
+ }
1542
+ }
1543
+ return temp.toString();
1544
+ } else if (value === Object(value)) {
1545
+ throw new Error('SQL identifier cannot be an object');
1546
+ }
1547
+
1548
+ var ident = value.toString().slice(0); // create copy
1549
+
1550
+ // do not quote a valid, unquoted identifier
1551
+ // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
1552
+ // return ident;
1553
+ // }
1554
+
1555
+ var quoted = '"';
1556
+
1557
+ for (var i = 0; i < ident.length; i++) {
1558
+ var c = ident[i];
1559
+ if (c === '"') {
1560
+ quoted += c + c;
1561
+ } else {
1562
+ quoted += c;
1563
+ }
1564
+ }
1565
+
1566
+ quoted += '"';
1567
+
1568
+ return quoted;
1569
+ };
1570
+ }
1571
+
1572
+ start = expressionList
1573
+
1574
+ _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
1575
+
1576
+ expressionList =
1577
+ leftExpression:expression _ operator:operator _ rightExpression:expressionList
1578
+ { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
1579
+ / expression
1580
+
1581
+ expression =
1582
+ negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
1583
+ {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
1584
+ /
1585
+ negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
1586
+
1587
+ negate = "!"
1588
+
1589
+ operator = "and"i / "or"i
1590
+
1591
+
1592
+ column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1593
+ /
1594
+ text:text { return quoteSqlIdentity(text); }
1595
+
1596
+
1597
+ text = text:[a-z0-9 \\t\\r\\n\\-_:@]i+ { return text.join(""); }
1598
+
1599
+ type = "type" _ ":" _ type:typeString { return type; }
1600
+ typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
1601
+ text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
1602
+ text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
1603
+ text:"exact" { return function(column, value) { return \`\${column} = '\${format.literal(value).slice(1,-1)}'\`; } } /
1604
+ text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= '\${format.literal(value).slice(1,-1)}'\`; } } /
1605
+ text:"greaterThan" { return function(column, value) { return \`\${column} > '\${format.literal(value).slice(1,-1)}'\`; } } /
1606
+ text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
1607
+ text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
1608
+ text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
1609
+
1610
+ value = "value" _ ":" value:text { return value; }
1611
+
1612
+
1613
+ `;
1614
+ var filterPsqlParser = import_pegjs.default.generate(filterSqlGrammar, {
1615
+ format: "commonjs",
1616
+ dependencies: { format: "pg-format" }
1617
+ });
1618
+ var filterPsqlParser_default = filterPsqlParser;
1619
+
1620
+ // src/restura/eventManager.ts
1621
+ var import_bluebird2 = __toESM(require("bluebird"));
1622
+ var EventManager = class {
1623
+ constructor() {
1624
+ this.actionHandlers = {
1625
+ DATABASE_ROW_DELETE: [],
1626
+ DATABASE_ROW_INSERT: [],
1627
+ DATABASE_COLUMN_UPDATE: []
1628
+ };
1629
+ }
1630
+ addRowInsertHandler(onInsert, filter) {
1631
+ this.actionHandlers.DATABASE_ROW_INSERT.push({
1632
+ callback: onInsert,
1633
+ filter
1634
+ });
1635
+ }
1636
+ addColumnChangeHandler(onUpdate, filter) {
1637
+ this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
1638
+ callback: onUpdate,
1639
+ filter
1640
+ });
1641
+ }
1642
+ addRowDeleteHandler(onDelete, filter) {
1643
+ this.actionHandlers.DATABASE_ROW_DELETE.push({
1644
+ callback: onDelete,
1645
+ filter
1646
+ });
1647
+ }
1648
+ async fireActionFromDbTrigger(sqlMutationData, result) {
1649
+ if (sqlMutationData.mutationType === "INSERT") {
1650
+ await this.fireInsertActions(sqlMutationData, result);
1651
+ } else if (sqlMutationData.mutationType === "UPDATE") {
1652
+ await this.fireUpdateActions(sqlMutationData, result);
1653
+ } else if (sqlMutationData.mutationType === "DELETE") {
1654
+ await this.fireDeleteActions(sqlMutationData, result);
1655
+ }
1656
+ }
1657
+ async fireInsertActions(data, triggerResult) {
1658
+ await import_bluebird2.default.map(
1659
+ this.actionHandlers.DATABASE_ROW_INSERT,
1660
+ ({ callback, filter }) => {
1661
+ if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
1662
+ const insertData = {
1663
+ tableName: triggerResult.table,
1664
+ insertId: triggerResult.record.id,
1665
+ insertObject: triggerResult.record,
1666
+ queryMetadata: data.queryMetadata
1667
+ };
1668
+ callback(insertData, data.queryMetadata);
1669
+ },
1670
+ { concurrency: 10 }
1671
+ );
1672
+ }
1673
+ async fireDeleteActions(data, triggerResult) {
1674
+ await import_bluebird2.default.map(
1675
+ this.actionHandlers.DATABASE_ROW_DELETE,
1676
+ ({ callback, filter }) => {
1677
+ if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
1678
+ const deleteData = {
1679
+ tableName: triggerResult.table,
1680
+ deletedRow: triggerResult.previousRecord,
1681
+ queryMetadata: data.queryMetadata
1682
+ };
1683
+ callback(deleteData, data.queryMetadata);
1684
+ },
1685
+ { concurrency: 10 }
1686
+ );
1687
+ }
1688
+ async fireUpdateActions(data, triggerResult) {
1689
+ await import_bluebird2.default.map(
1690
+ this.actionHandlers.DATABASE_COLUMN_UPDATE,
1691
+ ({ callback, filter }) => {
1692
+ if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
1693
+ const columnChangeData = {
1694
+ tableName: triggerResult.table,
1695
+ rowId: triggerResult.record.id,
1696
+ newData: triggerResult.record,
1697
+ oldData: triggerResult.previousRecord,
1698
+ queryMetadata: data.queryMetadata
1699
+ };
1700
+ callback(columnChangeData, data.queryMetadata);
1701
+ },
1702
+ { concurrency: 10 }
1703
+ );
1704
+ }
1705
+ hasHandlersForEventType(eventType, filter, triggerResult) {
1706
+ if (filter) {
1707
+ switch (eventType) {
1708
+ case "DATABASE_ROW_INSERT":
1709
+ case "DATABASE_ROW_DELETE":
1710
+ if (filter.tableName && filter.tableName !== triggerResult.table) return false;
1711
+ break;
1712
+ case "DATABASE_COLUMN_UPDATE":
1713
+ const filterColumnChange = filter;
1714
+ if (filterColumnChange.tableName !== filter.tableName) return false;
1715
+ if (!filterColumnChange.columns.some((item) => {
1716
+ const updatedColumns = Object.keys(
1717
+ changedValues(triggerResult.record, triggerResult.previousRecord)
1718
+ );
1719
+ return updatedColumns.includes(item);
1720
+ }))
1721
+ return false;
1722
+ break;
1723
+ }
1724
+ }
1725
+ return true;
1726
+ }
1727
+ };
1728
+ var eventManager = new EventManager();
1729
+ var eventManager_default = eventManager;
1730
+ function changedValues(record, previousRecord) {
1731
+ const changed = {};
1732
+ for (const i in previousRecord) {
1733
+ if (previousRecord[i] !== record[i]) {
1734
+ if (typeof previousRecord[i] === "object" && typeof record[i] === "object") {
1735
+ const nestedChanged = changedValues(record[i], previousRecord[i]);
1736
+ if (Object.keys(nestedChanged).length > 0) {
1737
+ changed[i] = record[i];
1738
+ }
1739
+ } else {
1740
+ changed[i] = record[i];
1741
+ }
1742
+ }
1743
+ }
1744
+ return changed;
1745
+ }
1746
+
1747
+ // src/restura/sql/PsqlEngine.ts
1748
+ var { Client } = import_pg2.default;
1749
+ var systemUser = {
1750
+ role: "",
1751
+ host: "",
1752
+ ipAddress: "",
1753
+ isSystemUser: true
1754
+ };
1755
+ var PsqlEngine = class extends SqlEngine {
1756
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
1757
+ super();
1758
+ this.psqlConnectionPool = psqlConnectionPool;
1759
+ if (shouldListenForDbTriggers) {
1760
+ this.setupTriggerListeners = this.listenForDbTriggers();
1761
+ }
1762
+ }
1763
+ async close() {
1764
+ if (this.triggerClient) {
1765
+ await this.triggerClient.end();
1766
+ }
1767
+ }
1768
+ async listenForDbTriggers() {
1769
+ this.triggerClient = new Client({
1770
+ user: this.psqlConnectionPool.poolConfig.user,
1771
+ host: this.psqlConnectionPool.poolConfig.host,
1772
+ database: this.psqlConnectionPool.poolConfig.database,
1773
+ password: this.psqlConnectionPool.poolConfig.password,
1774
+ port: this.psqlConnectionPool.poolConfig.port,
1775
+ connectionTimeoutMillis: 2e3
1776
+ });
1777
+ await this.triggerClient.connect();
1778
+ const promises = [];
1779
+ promises.push(this.triggerClient.query("LISTEN insert"));
1780
+ promises.push(this.triggerClient.query("LISTEN update"));
1781
+ promises.push(this.triggerClient.query("LISTEN delete"));
1782
+ await Promise.all(promises);
1783
+ this.triggerClient.on("notification", async (msg) => {
1784
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
1785
+ const payload = import_core_utils5.ObjectUtils.safeParse(msg.payload);
1786
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
1787
+ }
1788
+ });
1789
+ }
1790
+ async handleTrigger(payload, mutationType) {
1791
+ const findRequesterDetailsRegex = /^--QUERY_METADATA\(\{.*\}\)/;
1792
+ const match = payload.query.match(findRequesterDetailsRegex);
1793
+ if (match) {
1794
+ const jsonString = match[0].slice(match[0].indexOf("{"), match[0].lastIndexOf("}") + 1);
1795
+ const queryMetadata = import_core_utils5.ObjectUtils.safeParse(jsonString);
1796
+ const triggerFromThisInstance = queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId;
1797
+ if (!triggerFromThisInstance) {
1798
+ return;
1799
+ }
1800
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata, mutationType }, payload);
1801
+ }
1802
+ }
1803
+ async createDatabaseFromSchema(schema, connection) {
1804
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
1805
+ await connection.runQuery(sqlFullStatement, [], systemUser);
1806
+ return sqlFullStatement;
1807
+ }
1808
+ generateDatabaseSchemaFromSchema(schema) {
1809
+ const sqlStatements = [];
1810
+ const indexes = [];
1811
+ const triggers = [];
1812
+ for (const table of schema.database) {
1813
+ triggers.push(this.createInsertTriggers(table.name));
1814
+ triggers.push(this.createUpdateTrigger(table.name));
1815
+ triggers.push(this.createDeleteTrigger(table.name));
1816
+ let sql = `CREATE TABLE "${table.name}"
1817
+ ( `;
1818
+ const tableColumns = [];
1819
+ for (const column of table.columns) {
1820
+ let columnSql = "";
1821
+ columnSql += ` "${column.name}" ${schemaToPsqlType(column)}`;
1822
+ let value = column.value;
1823
+ if (column.type === "JSON") value = "";
1824
+ if (column.type === "JSONB") value = "";
1825
+ if (column.type === "DECIMAL" && value) {
1826
+ value = value.replace("-", ",").replace(/['"]/g, "");
1827
+ }
1828
+ if (value && column.type !== "ENUM") {
1829
+ columnSql += `(${value})`;
1830
+ } else if (column.length) columnSql += `(${column.length})`;
1831
+ if (column.isPrimary) {
1832
+ columnSql += " PRIMARY KEY ";
1833
+ }
1834
+ if (column.isUnique) {
1835
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
1836
+ }
1837
+ if (column.isNullable) columnSql += " NULL";
1838
+ else columnSql += " NOT NULL";
1839
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
1840
+ if (value && column.type === "ENUM") {
1841
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
1842
+ }
1843
+ tableColumns.push(columnSql);
1844
+ }
1845
+ sql += tableColumns.join(", \n");
1846
+ for (const index of table.indexes) {
1847
+ if (!index.isPrimaryKey) {
1848
+ let unique = " ";
1849
+ if (index.isUnique) unique = "UNIQUE ";
1850
+ indexes.push(
1851
+ ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
1852
+ return `"${item}" ${index.order}`;
1853
+ }).join(", ")});`
1854
+ );
1855
+ }
1856
+ }
1857
+ sql += "\n);";
1858
+ sqlStatements.push(sql);
1859
+ }
1860
+ for (const table of schema.database) {
1861
+ if (!table.foreignKeys.length) continue;
1862
+ const sql = `ALTER TABLE "${table.name}" `;
1863
+ const constraints = [];
1864
+ for (const foreignKey of table.foreignKeys) {
1865
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
1866
+ FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
1867
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
1868
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
1869
+ constraints.push(constraint);
1870
+ }
1871
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1872
+ }
1873
+ for (const table of schema.database) {
1874
+ if (!table.checkConstraints.length) continue;
1875
+ const sql = `ALTER TABLE "${table.name}" `;
1876
+ const constraints = [];
1877
+ for (const check of table.checkConstraints) {
1878
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
1879
+ constraints.push(constraint);
1880
+ }
1881
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
1882
+ }
1883
+ sqlStatements.push(indexes.join("\n"));
1884
+ sqlStatements.push(triggers.join("\n"));
1885
+ return sqlStatements.join("\n\n");
1886
+ }
1887
+ async getScratchPool() {
1888
+ var _a2, _b;
1889
+ const scratchDbExists = await this.psqlConnectionPool.runQuery(
1890
+ `SELECT *
1891
+ FROM pg_database
1892
+ WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
1893
+ [],
1894
+ systemUser
1895
+ );
1896
+ if (scratchDbExists.length === 0) {
1897
+ await this.psqlConnectionPool.runQuery(
1898
+ `CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
1899
+ [],
1900
+ systemUser
1901
+ );
1902
+ }
1903
+ const scratchPool = new PsqlPool({
1904
+ host: this.psqlConnectionPool.poolConfig.host,
1905
+ port: this.psqlConnectionPool.poolConfig.port,
1906
+ user: this.psqlConnectionPool.poolConfig.user,
1907
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1908
+ password: this.psqlConnectionPool.poolConfig.password,
1909
+ max: this.psqlConnectionPool.poolConfig.max,
1910
+ idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
1911
+ connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
1912
+ });
1913
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
1914
+ await scratchPool.runQuery(
1915
+ `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
1916
+ [],
1917
+ systemUser
1918
+ );
1919
+ const schemaComment = await this.psqlConnectionPool.runQuery(
1920
+ `SELECT pg_description.description
1921
+ FROM pg_description
1922
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
1923
+ WHERE pg_namespace.nspname = 'public';`,
1924
+ [],
1925
+ systemUser
1926
+ );
1927
+ if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
1928
+ await scratchPool.runQuery(
1929
+ `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
1930
+ [],
1931
+ systemUser
1932
+ );
1933
+ }
1934
+ return scratchPool;
1935
+ }
1936
+ async diffDatabaseToSchema(schema) {
1937
+ const scratchPool = await this.getScratchPool();
1938
+ await this.createDatabaseFromSchema(schema, scratchPool);
1939
+ const originalClient = new Client({
1940
+ database: this.psqlConnectionPool.poolConfig.database,
1941
+ user: this.psqlConnectionPool.poolConfig.user,
1942
+ password: this.psqlConnectionPool.poolConfig.password,
1943
+ host: this.psqlConnectionPool.poolConfig.host,
1944
+ port: this.psqlConnectionPool.poolConfig.port
1945
+ });
1946
+ const scratchClient = new Client({
1947
+ database: this.psqlConnectionPool.poolConfig.database + "_scratch",
1948
+ user: this.psqlConnectionPool.poolConfig.user,
1949
+ password: this.psqlConnectionPool.poolConfig.password,
1950
+ host: this.psqlConnectionPool.poolConfig.host,
1951
+ port: this.psqlConnectionPool.poolConfig.port
1952
+ });
1953
+ const promises = [originalClient.connect(), scratchClient.connect()];
1954
+ await Promise.all(promises);
1955
+ const infoPromises = [(0, import_pg_info.default)({ client: originalClient }), (0, import_pg_info.default)({ client: scratchClient })];
1956
+ const [info1, info2] = await Promise.all(infoPromises);
1957
+ const diff = (0, import_pg_diff_sync.default)(info1, info2);
1958
+ const endPromises = [originalClient.end(), scratchClient.end()];
1959
+ await Promise.all(endPromises);
1960
+ return diff.join("\n");
1961
+ }
1962
+ createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
1963
+ if (!item.subquery) return "";
1964
+ if (!import_core_utils5.ObjectUtils.isArrayWithData(
1965
+ item.subquery.properties.filter((nestedItem) => {
1966
+ return this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1967
+ ...routeData.joins,
1968
+ ...item.subquery.joins
1969
+ ]);
1970
+ })
1971
+ )) {
1972
+ return "'[]'";
1973
+ }
1974
+ return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
1975
+ ${item.subquery.properties.map((nestedItem) => {
1976
+ if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
1977
+ ...routeData.joins,
1978
+ ...item.subquery.joins
1979
+ ])) {
1980
+ return;
1981
+ }
1982
+ if (nestedItem.subquery) {
1983
+ return `'${nestedItem.name}', ${this.createNestedSelect(
1984
+ // recursion
1985
+ req,
1986
+ schema,
1987
+ nestedItem,
1988
+ routeData,
1989
+ userRole,
1990
+ sqlParams
1991
+ )}`;
1992
+ }
1993
+ return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
1994
+ }).filter(Boolean).join(", ")}
1995
+ ))
1996
+ FROM
1997
+ "${item.subquery.table}"
1998
+ ${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema, userRole, sqlParams)}
1999
+ ${this.generateWhereClause(req, item.subquery.where, routeData, sqlParams)}
2000
+ ), '[]')`;
2001
+ }
2002
+ async executeCreateRequest(req, routeData, schema) {
2003
+ const sqlParams = [];
2004
+ const parameterObj = {};
2005
+ (routeData.assignments || []).forEach((assignment) => {
2006
+ parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
2007
+ });
2008
+ const query = insertObjectQuery(routeData.table, __spreadValues(__spreadValues({}, req.data), parameterObj));
2009
+ const createdItem = await this.psqlConnectionPool.queryOne(
2010
+ query,
2011
+ sqlParams,
2012
+ req.requesterDetails
2013
+ );
2014
+ const insertId = createdItem.id;
2015
+ const whereId = {
2016
+ tableName: routeData.table,
2017
+ value: insertId,
2018
+ columnName: "id",
2019
+ operator: "="
2020
+ };
2021
+ const whereData = [whereId];
2022
+ req.data = { id: insertId };
2023
+ return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
2024
+ }
2025
+ async executeGetRequest(req, routeData, schema) {
2026
+ const DEFAULT_PAGED_PAGE_NUMBER = 0;
2027
+ const DEFAULT_PAGED_PER_PAGE_NUMBER = 25;
2028
+ const sqlParams = [];
2029
+ const userRole = req.requesterDetails.role;
2030
+ let sqlStatement = "";
2031
+ const selectColumns = [];
2032
+ routeData.response.forEach((item) => {
2033
+ if (item.subquery || this.doesRoleHavePermissionToColumn(userRole, schema, item, routeData.joins))
2034
+ selectColumns.push(item);
2035
+ });
2036
+ if (!selectColumns.length) throw new RsError("UNAUTHORIZED", `You do not have permission to access this data.`);
2037
+ let selectStatement = "SELECT \n";
2038
+ selectStatement += ` ${selectColumns.map((item) => {
2039
+ if (item.subquery) {
2040
+ return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
2041
+ item.name
2042
+ )}`;
2043
+ }
2044
+ return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
2045
+ }).join(",\n ")}
2046
+ `;
2047
+ sqlStatement += `FROM "${routeData.table}"
2048
+ `;
2049
+ sqlStatement += this.generateJoinStatements(
2050
+ req,
2051
+ routeData.joins,
2052
+ routeData.table,
2053
+ routeData,
2054
+ schema,
2055
+ userRole,
2056
+ sqlParams
2057
+ );
2058
+ sqlStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2059
+ let groupByOrderByStatement = this.generateGroupBy(routeData);
2060
+ groupByOrderByStatement += this.generateOrderBy(req, routeData);
2061
+ if (routeData.type === "ONE") {
2062
+ return this.psqlConnectionPool.queryOne(
2063
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement};`,
2064
+ sqlParams,
2065
+ req.requesterDetails
2066
+ );
2067
+ } else if (routeData.type === "ARRAY") {
2068
+ return this.psqlConnectionPool.runQuery(
2069
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement};`,
2070
+ sqlParams,
2071
+ req.requesterDetails
2072
+ );
2073
+ } else if (routeData.type === "PAGED") {
2074
+ const data = req.data;
2075
+ const pagePromise = this.psqlConnectionPool.runQuery(
2076
+ `${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
2077
+ sqlParams,
2078
+ req.requesterDetails
2079
+ );
2080
+ const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
2081
+ ${sqlStatement};`;
2082
+ const totalPromise = this.psqlConnectionPool.runQuery(
2083
+ totalQuery,
2084
+ sqlParams,
2085
+ req.requesterDetails
2086
+ );
2087
+ const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
2088
+ let total = 0;
2089
+ if (import_core_utils5.ObjectUtils.isArrayWithData(totalResponse)) {
2090
+ total = totalResponse[0].total;
2091
+ }
2092
+ return { data: pageResults, total };
2093
+ } else {
2094
+ throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
2095
+ }
2096
+ }
2097
+ async executeUpdateRequest(req, routeData, schema) {
2098
+ const sqlParams = [];
2099
+ const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
2100
+ const table = schema.database.find((item) => {
2101
+ return item.name === routeData.table;
2102
+ });
2103
+ if (!table) throw new RsError("UNKNOWN_ERROR", "Unknown table.");
2104
+ if (table.columns.find((column) => column.name === "modifiedOn")) {
2105
+ bodyNoId.modifiedOn = (/* @__PURE__ */ new Date()).toISOString();
2106
+ }
2107
+ for (const assignment of routeData.assignments) {
2108
+ const column = table.columns.find((column2) => column2.name === assignment.name);
2109
+ if (!column) continue;
2110
+ const assignmentWithPrefix = escapeColumnName(`${routeData.table}.${assignment.name}`);
2111
+ if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
2112
+ bodyNoId[assignmentWithPrefix] = Number(assignment.value);
2113
+ else bodyNoId[assignmentWithPrefix] = assignment.value;
2114
+ }
2115
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2116
+ const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
2117
+ await this.psqlConnectionPool.queryOne(query, [...sqlParams], req.requesterDetails);
2118
+ return this.executeGetRequest(req, routeData, schema);
2119
+ }
2120
+ async executeDeleteRequest(req, routeData, schema) {
2121
+ const sqlParams = [];
2122
+ const joinStatement = this.generateJoinStatements(
2123
+ req,
2124
+ routeData.joins,
2125
+ routeData.table,
2126
+ routeData,
2127
+ schema,
2128
+ req.requesterDetails.role,
2129
+ sqlParams
2130
+ );
2131
+ const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2132
+ if (whereClause.replace(/\s/g, "") === "") {
2133
+ throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
2134
+ }
2135
+ const deleteStatement = `
2136
+ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
2137
+ await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
2138
+ return true;
2139
+ }
2140
+ generateJoinStatements(req, joins, baseTable, routeData, schema, userRole, sqlParams) {
2141
+ let joinStatements = "";
2142
+ joins.forEach((item) => {
2143
+ if (!this.doesRoleHavePermissionToTable(userRole, schema, item.table))
2144
+ throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
2145
+ if (item.custom) {
2146
+ const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
2147
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)} ON ${customReplaced}
2148
+ `;
2149
+ } else {
2150
+ joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON "${baseTable}"."${item.localColumnName}" = ${escapeColumnName(item.alias ? item.alias : item.table)}.${escapeColumnName(
2151
+ item.foreignColumnName
2152
+ )}
2153
+ `;
2154
+ }
2155
+ });
2156
+ return joinStatements;
2157
+ }
2158
+ generateGroupBy(routeData) {
2159
+ let groupBy = "";
2160
+ if (routeData.groupBy) {
2161
+ groupBy = `GROUP BY ${escapeColumnName(routeData.groupBy.tableName)}.${escapeColumnName(routeData.groupBy.columnName)}
2162
+ `;
2163
+ }
2164
+ return groupBy;
2165
+ }
2166
+ generateOrderBy(req, routeData) {
2167
+ let orderBy = "";
2168
+ const orderOptions = {
2169
+ ASC: "ASC",
2170
+ DESC: "DESC"
2171
+ };
2172
+ const data = req.data;
2173
+ if (routeData.type === "PAGED" && "sortBy" in data) {
2174
+ const sortOrder = orderOptions[data.sortOrder] || "ASC";
2175
+ orderBy = `ORDER BY ${escapeColumnName(data.sortBy)} ${sortOrder}
2176
+ `;
2177
+ } else if (routeData.orderBy) {
2178
+ const sortOrder = orderOptions[routeData.orderBy.order] || "ASC";
2179
+ orderBy = `ORDER BY ${escapeColumnName(routeData.orderBy.tableName)}.${escapeColumnName(routeData.orderBy.columnName)} ${sortOrder}
2180
+ `;
2181
+ }
2182
+ return orderBy;
2183
+ }
2184
+ generateWhereClause(req, where, routeData, sqlParams) {
2185
+ let whereClause = "";
2186
+ where.forEach((item, index) => {
2187
+ if (index === 0) whereClause = "WHERE ";
2188
+ if (item.custom) {
2189
+ whereClause += this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
2190
+ return;
2191
+ }
2192
+ if (item.operator === void 0 || item.value === void 0 || item.columnName === void 0 || item.tableName === void 0)
2193
+ throw new RsError(
2194
+ "SCHEMA_ERROR",
2195
+ `Invalid where clause in route ${routeData.name}, missing required fields if not custom`
2196
+ );
2197
+ let operator = item.operator;
2198
+ if (operator === "LIKE") {
2199
+ item.value = `'%${item.value}%'`;
2200
+ } else if (operator === "STARTS WITH") {
2201
+ operator = "LIKE";
2202
+ item.value = `'${item.value}%'`;
2203
+ } else if (operator === "ENDS WITH") {
2204
+ operator = "LIKE";
2205
+ item.value = `'%${item.value}'`;
2206
+ }
2207
+ const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
2208
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
2209
+ `;
2210
+ });
2211
+ const data = req.data;
2212
+ if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
2213
+ let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
2214
+ var _a2;
2215
+ const requestParam = routeData.request.find((item) => {
2216
+ return item.name === value.replace("$", "");
2217
+ });
2218
+ if (!requestParam)
2219
+ throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
2220
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
2221
+ });
2222
+ statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
2223
+ var _a2;
2224
+ const requestParam = routeData.request.find((item) => {
2225
+ return item.name === value.replace("#", "");
2226
+ });
2227
+ if (!requestParam)
2228
+ throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
2229
+ return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
2230
+ });
2231
+ statement = filterPsqlParser_default.parse(statement);
2232
+ if (whereClause.startsWith("WHERE")) {
2233
+ whereClause += ` AND (${statement})
2234
+ `;
2235
+ } else {
2236
+ whereClause += `WHERE ${statement}
2237
+ `;
2238
+ }
2239
+ }
2240
+ return whereClause;
2241
+ }
2242
+ createUpdateTrigger(tableName) {
2243
+ return `
2244
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2245
+ RETURNS TRIGGER AS $$
2246
+ BEGIN
2247
+ PERFORM pg_notify('update', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2248
+ RETURN NEW;
2249
+ END;
2250
+ $$ LANGUAGE plpgsql;
2251
+
2252
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2253
+ AFTER UPDATE ON "${tableName}"
2254
+ FOR EACH ROW
2255
+ EXECUTE FUNCTION notify_${tableName}_update();
2256
+ `;
2257
+ }
2258
+ createDeleteTrigger(tableName) {
2259
+ return `
2260
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2261
+ RETURNS TRIGGER AS $$
2262
+ BEGIN
2263
+ PERFORM pg_notify('delete', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2264
+ RETURN NEW;
2265
+ END;
2266
+ $$ LANGUAGE plpgsql;
2267
+
2268
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2269
+ AFTER DELETE ON "${tableName}"
2270
+ FOR EACH ROW
2271
+ EXECUTE FUNCTION notify_${tableName}_delete();
2272
+ `;
2273
+ }
2274
+ createInsertTriggers(tableName) {
2275
+ return `
2276
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2277
+ RETURNS TRIGGER AS $$
2278
+ BEGIN
2279
+ PERFORM pg_notify('insert', JSON_BUILD_OBJECT('table', '${tableName}', 'query', current_query(), 'record', NEW, 'previousRecord', OLD)::text);
2280
+ RETURN NEW;
2281
+ END;
2282
+ $$ LANGUAGE plpgsql;
2283
+
2284
+ CREATE TRIGGER "${tableName}_insert"
2285
+ AFTER INSERT ON "${tableName}"
2286
+ FOR EACH ROW
2287
+ EXECUTE FUNCTION notify_${tableName}_insert();
2288
+ `;
2289
+ }
2290
+ };
2291
+ __decorateClass([
2292
+ boundMethod
2293
+ ], PsqlEngine.prototype, "handleTrigger", 1);
2294
+ __decorateClass([
2295
+ boundMethod
2296
+ ], PsqlEngine.prototype, "createUpdateTrigger", 1);
2297
+ __decorateClass([
2298
+ boundMethod
2299
+ ], PsqlEngine.prototype, "createDeleteTrigger", 1);
2300
+ __decorateClass([
2301
+ boundMethod
2302
+ ], PsqlEngine.prototype, "createInsertTriggers", 1);
2303
+ function schemaToPsqlType(column) {
2304
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2305
+ if (column.type === "ENUM") return `TEXT`;
2306
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2307
+ if (column.type === "MEDIUMINT") return "INT";
2308
+ return column.type;
30
2309
  }
2310
+
2311
+ // src/restura/compareSchema.ts
2312
+ var import_lodash = __toESM(require("lodash.clonedeep"));
2313
+ var CompareSchema = class {
2314
+ async diffSchema(newSchema, latestSchema, psqlEngine) {
2315
+ const endPoints = this.diffEndPoints(newSchema.endpoints[0].routes, latestSchema.endpoints[0].routes);
2316
+ const globalParams = this.diffStringArray(newSchema.globalParams, latestSchema.globalParams);
2317
+ const roles = this.diffStringArray(newSchema.roles, latestSchema.roles);
2318
+ let commands = "";
2319
+ if (JSON.stringify(newSchema.database) !== JSON.stringify(latestSchema.database))
2320
+ commands = await psqlEngine.diffDatabaseToSchema(newSchema);
2321
+ const customTypes = newSchema.customTypes !== latestSchema.customTypes;
2322
+ const schemaPreview = { endPoints, globalParams, roles, commands, customTypes };
2323
+ return schemaPreview;
2324
+ }
2325
+ diffStringArray(newArray, originalArray) {
2326
+ const stringsDiff = [];
2327
+ const originalClone = new Set(originalArray);
2328
+ newArray.forEach((item) => {
2329
+ const originalIndex = originalClone.has(item);
2330
+ if (!originalIndex) {
2331
+ stringsDiff.push({
2332
+ name: item,
2333
+ changeType: "NEW"
2334
+ });
2335
+ } else {
2336
+ originalClone.delete(item);
2337
+ }
2338
+ });
2339
+ originalClone.forEach((item) => {
2340
+ stringsDiff.push({
2341
+ name: item,
2342
+ changeType: "DELETED"
2343
+ });
2344
+ });
2345
+ return stringsDiff;
2346
+ }
2347
+ diffEndPoints(newEndPoints, originalEndpoints) {
2348
+ const originalClone = (0, import_lodash.default)(originalEndpoints);
2349
+ const diffObj = [];
2350
+ newEndPoints.forEach((endPoint) => {
2351
+ const { path: path5, method } = endPoint;
2352
+ const endPointIndex = originalClone.findIndex((original) => {
2353
+ return original.path === endPoint.path && original.method === endPoint.method;
2354
+ });
2355
+ if (endPointIndex === -1) {
2356
+ diffObj.push({
2357
+ name: `${method} ${path5}`,
2358
+ changeType: "NEW"
2359
+ });
2360
+ } else {
2361
+ const original = originalClone.findIndex((original2) => {
2362
+ return this.compareEndPoints(endPoint, original2);
2363
+ });
2364
+ if (original === -1) {
2365
+ diffObj.push({
2366
+ name: `${method} ${path5}`,
2367
+ changeType: "MODIFIED"
2368
+ });
2369
+ }
2370
+ originalClone.splice(endPointIndex, 1);
2371
+ }
2372
+ });
2373
+ originalClone.forEach((original) => {
2374
+ const { path: path5, method } = original;
2375
+ diffObj.push({
2376
+ name: `${method} ${path5}`,
2377
+ changeType: "DELETED"
2378
+ });
2379
+ });
2380
+ return diffObj;
2381
+ }
2382
+ compareEndPoints(endPoint1, endPoint2) {
2383
+ return JSON.stringify(endPoint1) === JSON.stringify(endPoint2);
2384
+ }
2385
+ };
2386
+ __decorateClass([
2387
+ boundMethod
2388
+ ], CompareSchema.prototype, "diffSchema", 1);
2389
+ __decorateClass([
2390
+ boundMethod
2391
+ ], CompareSchema.prototype, "diffStringArray", 1);
2392
+ __decorateClass([
2393
+ boundMethod
2394
+ ], CompareSchema.prototype, "diffEndPoints", 1);
2395
+ __decorateClass([
2396
+ boundMethod
2397
+ ], CompareSchema.prototype, "compareEndPoints", 1);
2398
+ var compareSchema = new CompareSchema();
2399
+ var compareSchema_default = compareSchema;
2400
+
2401
+ // src/restura/middleware/getMulterUploadSingleton.ts
2402
+ var import_multer = __toESM(require("multer"));
2403
+ var import_path3 = require("path");
2404
+ var os = __toESM(require("os"));
2405
+ var OneHundredMB = 100 * 1024 * 1024;
2406
+ var commonUpload = null;
2407
+ var getMulterUploadSingleton = (directory) => {
2408
+ if (commonUpload) return commonUpload;
2409
+ const storage = import_multer.default.diskStorage({
2410
+ destination: directory || os.tmpdir(),
2411
+ filename: function(request, file, cb) {
2412
+ const extension = (0, import_path3.extname)(file.originalname);
2413
+ const uniqueName = Date.now() + "-" + Math.round(Math.random() * 1e3);
2414
+ cb(null, `${uniqueName}${extension}`);
2415
+ }
2416
+ });
2417
+ commonUpload = (0, import_multer.default)({
2418
+ storage,
2419
+ limits: {
2420
+ fileSize: OneHundredMB
2421
+ }
2422
+ });
2423
+ return commonUpload;
2424
+ };
2425
+
2426
+ // src/restura/utils/TempCache.ts
2427
+ var import_fs3 = __toESM(require("fs"));
2428
+ var import_path4 = __toESM(require("path"));
2429
+ var import_core_utils6 = require("@redskytech/core-utils");
2430
+ var import_bluebird3 = __toESM(require("bluebird"));
2431
+ var os2 = __toESM(require("os"));
2432
+ var import_internal3 = require("@restura/internal");
2433
+ var TempCache = class {
2434
+ constructor(location) {
2435
+ this.maxDurationDays = 7;
2436
+ this.location = location || os2.tmpdir();
2437
+ import_internal3.fileUtils.ensureDir(this.location).catch((e) => {
2438
+ throw e;
2439
+ });
2440
+ }
2441
+ async cleanup() {
2442
+ const fileList = await import_fs3.default.promises.readdir(this.location);
2443
+ await import_bluebird3.default.map(
2444
+ fileList,
2445
+ async (file) => {
2446
+ const fullFilePath = import_path4.default.join(this.location, file);
2447
+ const fileStats = await import_fs3.default.promises.stat(fullFilePath);
2448
+ if (import_core_utils6.DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
2449
+ logger.info(`Deleting old temp file: ${file}`);
2450
+ await import_fs3.default.promises.unlink(fullFilePath);
2451
+ }
2452
+ },
2453
+ { concurrency: 10 }
2454
+ );
2455
+ }
2456
+ };
2457
+
2458
+ // src/restura/restura.ts
2459
+ var { types } = import_pg3.default;
2460
+ var ResturaEngine = class {
2461
+ constructor() {
2462
+ this.publicEndpoints = {
2463
+ GET: [],
2464
+ POST: [],
2465
+ PUT: [],
2466
+ PATCH: [],
2467
+ DELETE: []
2468
+ };
2469
+ }
2470
+ /**
2471
+ * Initializes the Restura engine with the provided Express application.
2472
+ *
2473
+ * @param app - The Express application instance to initialize with Restura.
2474
+ * @returns A promise that resolves when the initialization is complete.
2475
+ */
2476
+ async init(app, authenticationHandler, psqlConnectionPool) {
2477
+ this.resturaConfig = import_internal4.config.validate("restura", resturaConfigSchema);
2478
+ this.multerCommonUpload = getMulterUploadSingleton(this.resturaConfig.fileTempCachePath);
2479
+ new TempCache(this.resturaConfig.fileTempCachePath);
2480
+ this.psqlConnectionPool = psqlConnectionPool;
2481
+ this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
2482
+ setupPgReturnTypes();
2483
+ await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
2484
+ this.authenticationHandler = authenticationHandler;
2485
+ app.use((0, import_compression.default)());
2486
+ app.use(import_body_parser.default.json({ limit: "32mb" }));
2487
+ app.use(import_body_parser.default.urlencoded({ limit: "32mb", extended: false }));
2488
+ app.use((0, import_cookie_parser.default)());
2489
+ app.disable("x-powered-by");
2490
+ app.use("/", addApiResponseFunctions);
2491
+ app.use("/api/", authenticateUser(this.authenticationHandler));
2492
+ app.use("/restura", this.resturaAuthentication);
2493
+ app.put(
2494
+ "/restura/v1/schema",
2495
+ schemaValidation,
2496
+ this.updateSchema
2497
+ );
2498
+ app.post(
2499
+ "/restura/v1/schema/preview",
2500
+ schemaValidation,
2501
+ this.previewCreateSchema
2502
+ );
2503
+ app.get("/restura/v1/schema", this.getSchema);
2504
+ app.get("/restura/v1/schema/types", this.getSchemaAndTypes);
2505
+ this.expressApp = app;
2506
+ await this.reloadEndpoints();
2507
+ await this.validateGeneratedTypesFolder();
2508
+ logger.info("Restura Engine Initialized");
2509
+ }
2510
+ /**
2511
+ * Determines if a given endpoint is public based on the HTTP method and full URL. This
2512
+ * is determined on whether the endpoint in the schema has no roles assigned to it.
2513
+ *
2514
+ * @param method - The HTTP method (e.g., 'GET', 'POST', 'PUT', 'PATCH', 'DELETE').
2515
+ * @param fullUrl - The full URL of the endpoint.
2516
+ * @returns A boolean indicating whether the endpoint is public.
2517
+ */
2518
+ isEndpointPublic(method, fullUrl) {
2519
+ if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) return false;
2520
+ return this.publicEndpoints[method].includes(fullUrl);
2521
+ }
2522
+ /**
2523
+ * Checks if an endpoint exists for a given HTTP method and full URL.
2524
+ *
2525
+ * @param method - The HTTP method to check (e.g., 'GET', 'POST', 'PUT', 'PATCH', 'DELETE').
2526
+ * @param fullUrl - The full URL of the endpoint to check.
2527
+ * @returns `true` if the endpoint exists, otherwise `false`.
2528
+ */
2529
+ doesEndpointExist(method, fullUrl) {
2530
+ return this.schema.endpoints.some((endpoint) => {
2531
+ if (!fullUrl.startsWith(endpoint.baseUrl)) return false;
2532
+ const pathWithoutBaseUrl = fullUrl.replace(endpoint.baseUrl, "");
2533
+ return endpoint.routes.some((route) => {
2534
+ return route.method === method && route.path === pathWithoutBaseUrl;
2535
+ });
2536
+ });
2537
+ }
2538
+ /**
2539
+ * Generates an API from the provided schema and writes it to the specified output file.
2540
+ *
2541
+ * @param outputFile - The path to the file where the generated API will be written.
2542
+ * @param providedSchema - The schema from which the API will be generated.
2543
+ * @returns A promise that resolves when the API has been successfully generated and written to the output file.
2544
+ */
2545
+ async generateApiFromSchema(outputFile, providedSchema) {
2546
+ import_fs4.default.writeFileSync(
2547
+ outputFile,
2548
+ await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
2549
+ );
2550
+ }
2551
+ /**
2552
+ * Generates a model from the provided schema and writes it to the specified output file.
2553
+ *
2554
+ * @param outputFile - The path to the file where the generated model will be written.
2555
+ * @param providedSchema - The schema from which the model will be generated.
2556
+ * @returns A promise that resolves when the model has been successfully written to the output file.
2557
+ */
2558
+ async generateModelFromSchema(outputFile, providedSchema) {
2559
+ import_fs4.default.writeFileSync(
2560
+ outputFile,
2561
+ await modelGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
2562
+ );
2563
+ }
2564
+ /**
2565
+ * Retrieves the latest file system schema for Restura.
2566
+ *
2567
+ * @returns {Promise<ResturaSchema>} A promise that resolves to the latest Restura schema.
2568
+ * @throws {Error} If the schema file is missing or the schema is not valid.
2569
+ */
2570
+ async getLatestFileSystemSchema() {
2571
+ if (!import_fs4.default.existsSync(this.resturaConfig.schemaFilePath)) {
2572
+ logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
2573
+ throw new Error("Missing restura schema file");
2574
+ }
2575
+ const schemaFileData = import_fs4.default.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
2576
+ const schema = import_core_utils7.ObjectUtils.safeParse(schemaFileData);
2577
+ const isValid = await isSchemaValid(schema);
2578
+ if (!isValid) {
2579
+ logger.error("Schema is not valid");
2580
+ throw new Error("Schema is not valid");
2581
+ }
2582
+ return schema;
2583
+ }
2584
+ /**
2585
+ * Asynchronously generates and retrieves hashes for the provided schema and related generated files.
2586
+ *
2587
+ * @param providedSchema - The schema for which hashes need to be generated.
2588
+ * @returns A promise that resolves to an object containing:
2589
+ * - `schemaHash`: The hash of the provided schema.
2590
+ * - `apiCreatedSchemaHash`: The hash extracted from the generated `api.d.ts` file.
2591
+ * - `modelCreatedSchemaHash`: The hash extracted from the generated `models.d.ts` file.
2592
+ */
2593
+ async getHashes(providedSchema) {
2594
+ var _a2, _b, _c, _d;
2595
+ const schemaHash = await this.generateHashForSchema(providedSchema);
2596
+ const apiFile = import_fs4.default.readFileSync(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
2597
+ const apiCreatedSchemaHash = (_b = (_a2 = apiFile.toString().match(/\((.*)\)/)) == null ? void 0 : _a2[1]) != null ? _b : "";
2598
+ const modelFile = import_fs4.default.readFileSync(import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
2599
+ const modelCreatedSchemaHash = (_d = (_c = modelFile.toString().match(/\((.*)\)/)) == null ? void 0 : _c[1]) != null ? _d : "";
2600
+ return {
2601
+ schemaHash,
2602
+ apiCreatedSchemaHash,
2603
+ modelCreatedSchemaHash
2604
+ };
2605
+ }
2606
+ async reloadEndpoints() {
2607
+ this.schema = await this.getLatestFileSystemSchema();
2608
+ this.customTypeValidation = customTypeValidationGenerator(this.schema);
2609
+ this.resturaRouter = express.Router();
2610
+ this.resetPublicEndpoints();
2611
+ let routeCount = 0;
2612
+ for (const endpoint of this.schema.endpoints) {
2613
+ const baseUrl = endpoint.baseUrl.endsWith("/") ? endpoint.baseUrl.slice(0, -1) : endpoint.baseUrl;
2614
+ this.expressApp.use(baseUrl, (req, res, next) => {
2615
+ this.resturaRouter(req, res, next);
2616
+ });
2617
+ for (const route of endpoint.routes) {
2618
+ route.path = route.path.startsWith("/") ? route.path : `/${route.path}`;
2619
+ route.path = route.path.endsWith("/") ? route.path.slice(0, -1) : route.path;
2620
+ const fullUrl = `${baseUrl}${route.path}`;
2621
+ if (route.roles.length === 0) this.publicEndpoints[route.method].push(fullUrl);
2622
+ this.resturaRouter[route.method.toLowerCase()](
2623
+ route.path,
2624
+ // <-- Notice we only use path here since the baseUrl is already added to the router.
2625
+ this.executeRouteLogic
2626
+ );
2627
+ routeCount++;
2628
+ }
2629
+ }
2630
+ this.responseValidator = new ResponseValidator(this.schema);
2631
+ logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
2632
+ }
2633
+ async validateGeneratedTypesFolder() {
2634
+ if (!import_fs4.default.existsSync(this.resturaConfig.generatedTypesPath)) {
2635
+ import_fs4.default.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
2636
+ }
2637
+ const hasApiFile = import_fs4.default.existsSync(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
2638
+ const hasModelsFile = import_fs4.default.existsSync(import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
2639
+ if (!hasApiFile) {
2640
+ await this.generateApiFromSchema(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2641
+ }
2642
+ if (!hasModelsFile) {
2643
+ await this.generateModelFromSchema(
2644
+ import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2645
+ this.schema
2646
+ );
2647
+ }
2648
+ const hashes = await this.getHashes(this.schema);
2649
+ if (hashes.schemaHash !== hashes.apiCreatedSchemaHash) {
2650
+ await this.generateApiFromSchema(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2651
+ }
2652
+ if (hashes.schemaHash !== hashes.modelCreatedSchemaHash) {
2653
+ await this.generateModelFromSchema(
2654
+ import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2655
+ this.schema
2656
+ );
2657
+ }
2658
+ }
2659
+ resturaAuthentication(req, res, next) {
2660
+ if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
2661
+ else next();
2662
+ }
2663
+ async previewCreateSchema(req, res) {
2664
+ try {
2665
+ const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
2666
+ res.send({ data: schemaDiff });
2667
+ } catch (err) {
2668
+ res.status(400).send(err);
2669
+ }
2670
+ }
2671
+ async updateSchema(req, res) {
2672
+ try {
2673
+ this.schema = req.data;
2674
+ await this.storeFileSystemSchema();
2675
+ await this.reloadEndpoints();
2676
+ await this.updateTypes();
2677
+ res.send({ data: "success" });
2678
+ } catch (err) {
2679
+ if (err instanceof Error) res.status(400).send(err.message);
2680
+ else res.status(400).send("Unknown error");
2681
+ }
2682
+ }
2683
+ async updateTypes() {
2684
+ await this.generateApiFromSchema(import_path5.default.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
2685
+ await this.generateModelFromSchema(
2686
+ import_path5.default.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
2687
+ this.schema
2688
+ );
2689
+ }
2690
+ async getSchema(req, res) {
2691
+ res.send({ data: this.schema });
2692
+ }
2693
+ async getSchemaAndTypes(req, res) {
2694
+ try {
2695
+ const schema = await this.getLatestFileSystemSchema();
2696
+ const schemaHash = await this.generateHashForSchema(schema);
2697
+ const apiText = await apiGenerator(schema, schemaHash);
2698
+ const modelsText = await modelGenerator(schema, schemaHash);
2699
+ res.send({ schema, api: apiText, models: modelsText });
2700
+ } catch (err) {
2701
+ res.status(400).send({ error: err });
2702
+ }
2703
+ }
2704
+ async getMulterFilesIfAny(req, res, routeData) {
2705
+ var _a2;
2706
+ if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
2707
+ if (!this.isCustomRoute(routeData)) return;
2708
+ if (!routeData.fileUploadType) {
2709
+ throw new RsError("BAD_REQUEST", "File upload type not defined for route");
2710
+ }
2711
+ const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
2712
+ return new Promise((resolve2, reject) => {
2713
+ multerFileUploadFunction(req, res, (err) => {
2714
+ if (err) {
2715
+ logger.warn("Multer error: " + err);
2716
+ reject(err);
2717
+ }
2718
+ if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
2719
+ resolve2();
2720
+ });
2721
+ });
2722
+ }
2723
+ async executeRouteLogic(req, res, next) {
2724
+ try {
2725
+ const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
2726
+ this.validateAuthorization(req, routeData);
2727
+ await this.getMulterFilesIfAny(req, res, routeData);
2728
+ validateRequestParams(req, routeData, this.customTypeValidation);
2729
+ if (this.isCustomRoute(routeData)) {
2730
+ await this.runCustomRouteLogic(req, res, routeData);
2731
+ return;
2732
+ }
2733
+ const data = await this.psqlEngine.runQueryForRoute(
2734
+ req,
2735
+ routeData,
2736
+ this.schema
2737
+ );
2738
+ this.responseValidator.validateResponseParams(data, req.baseUrl, routeData);
2739
+ if (routeData.type === "PAGED") res.sendNoWrap(data);
2740
+ else res.sendData(data);
2741
+ } catch (e) {
2742
+ next(e);
2743
+ }
2744
+ }
2745
+ isCustomRoute(route) {
2746
+ return route.type === "CUSTOM_ONE" || route.type === "CUSTOM_ARRAY" || route.type === "CUSTOM_PAGED";
2747
+ }
2748
+ async runCustomRouteLogic(req, res, routeData) {
2749
+ const version = req.baseUrl.split("/")[2];
2750
+ let domain = routeData.path.split("/")[1];
2751
+ domain = domain.split("-").reduce((acc, value, index) => {
2752
+ if (index === 0) acc = value;
2753
+ else acc += import_core_utils7.StringUtils.capitalizeFirst(value);
2754
+ return acc;
2755
+ }, "");
2756
+ const customApiName = `${import_core_utils7.StringUtils.capitalizeFirst(domain)}Api${import_core_utils7.StringUtils.capitalizeFirst(version)}`;
2757
+ const customApi = customApiFactory_default.getCustomApi(customApiName);
2758
+ if (!customApi) throw new RsError("NOT_FOUND", `API domain ${domain}-${version} not found`);
2759
+ const functionName = `${routeData.method.toLowerCase()}${routeData.path.replace(new RegExp("-", "g"), "/").split("/").reduce((acc, cur) => {
2760
+ if (cur === "") return acc;
2761
+ return acc + import_core_utils7.StringUtils.capitalizeFirst(cur);
2762
+ }, "")}`;
2763
+ const customFunction = customApi[functionName];
2764
+ if (!customFunction)
2765
+ throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
2766
+ await customFunction(req, res, routeData);
2767
+ }
2768
+ async generateHashForSchema(providedSchema) {
2769
+ const schemaPrettyStr = await prettier3.format(JSON.stringify(providedSchema), __spreadValues({
2770
+ parser: "json"
2771
+ }, {
2772
+ trailingComma: "none",
2773
+ tabWidth: 4,
2774
+ useTabs: true,
2775
+ endOfLine: "lf",
2776
+ printWidth: 120,
2777
+ singleQuote: true
2778
+ }));
2779
+ return (0, import_crypto2.createHash)("sha256").update(schemaPrettyStr).digest("hex");
2780
+ }
2781
+ async storeFileSystemSchema() {
2782
+ const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
2783
+ parser: "json"
2784
+ }, {
2785
+ trailingComma: "none",
2786
+ tabWidth: 4,
2787
+ useTabs: true,
2788
+ endOfLine: "lf",
2789
+ printWidth: 120,
2790
+ singleQuote: true
2791
+ }));
2792
+ import_fs4.default.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
2793
+ }
2794
+ resetPublicEndpoints() {
2795
+ this.publicEndpoints = {
2796
+ GET: [],
2797
+ POST: [],
2798
+ PUT: [],
2799
+ PATCH: [],
2800
+ DELETE: []
2801
+ };
2802
+ }
2803
+ validateAuthorization(req, routeData) {
2804
+ const role = req.requesterDetails.role;
2805
+ if (routeData.roles.length === 0 || !role) return;
2806
+ if (!routeData.roles.includes(role))
2807
+ throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
2808
+ }
2809
+ getRouteData(method, baseUrl, path5) {
2810
+ const endpoint = this.schema.endpoints.find((item) => {
2811
+ return item.baseUrl === baseUrl;
2812
+ });
2813
+ if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
2814
+ const route = endpoint.routes.find((item) => {
2815
+ return item.method === method && item.path === path5;
2816
+ });
2817
+ if (!route) throw new RsError("NOT_FOUND", "Route not found");
2818
+ return route;
2819
+ }
2820
+ };
2821
+ __decorateClass([
2822
+ boundMethod
2823
+ ], ResturaEngine.prototype, "resturaAuthentication", 1);
2824
+ __decorateClass([
2825
+ boundMethod
2826
+ ], ResturaEngine.prototype, "previewCreateSchema", 1);
2827
+ __decorateClass([
2828
+ boundMethod
2829
+ ], ResturaEngine.prototype, "updateSchema", 1);
2830
+ __decorateClass([
2831
+ boundMethod
2832
+ ], ResturaEngine.prototype, "getSchema", 1);
2833
+ __decorateClass([
2834
+ boundMethod
2835
+ ], ResturaEngine.prototype, "getSchemaAndTypes", 1);
2836
+ __decorateClass([
2837
+ boundMethod
2838
+ ], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
2839
+ __decorateClass([
2840
+ boundMethod
2841
+ ], ResturaEngine.prototype, "executeRouteLogic", 1);
2842
+ __decorateClass([
2843
+ boundMethod
2844
+ ], ResturaEngine.prototype, "isCustomRoute", 1);
2845
+ __decorateClass([
2846
+ boundMethod
2847
+ ], ResturaEngine.prototype, "runCustomRouteLogic", 1);
2848
+ function setupPgReturnTypes() {
2849
+ const TIMESTAMPTZ_OID = 1184;
2850
+ types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
2851
+ return val === null ? null : new Date(val).toISOString();
2852
+ });
2853
+ const BIGINT_OID = 20;
2854
+ types.setTypeParser(BIGINT_OID, (val) => {
2855
+ return val === null ? null : Number(val);
2856
+ });
2857
+ }
2858
+ setupPgReturnTypes();
2859
+ var restura = new ResturaEngine();
2860
+
2861
+ // src/restura/sql/PsqlTransaction.ts
2862
+ var import_pg4 = __toESM(require("pg"));
2863
+ var { Client: Client2 } = import_pg4.default;
2864
+ var PsqlTransaction = class extends PsqlConnection {
2865
+ constructor(clientConfig) {
2866
+ super();
2867
+ this.clientConfig = clientConfig;
2868
+ this.client = new Client2(clientConfig);
2869
+ this.connectPromise = this.client.connect();
2870
+ this.beginTransactionPromise = this.beginTransaction();
2871
+ }
2872
+ async close() {
2873
+ if (this.client) {
2874
+ await this.client.end();
2875
+ }
2876
+ }
2877
+ async beginTransaction() {
2878
+ await this.connectPromise;
2879
+ return this.client.query("BEGIN");
2880
+ }
2881
+ async rollback() {
2882
+ return this.query("ROLLBACK");
2883
+ }
2884
+ async commit() {
2885
+ return this.query("COMMIT");
2886
+ }
2887
+ async release() {
2888
+ return this.client.end();
2889
+ }
2890
+ async query(query, values) {
2891
+ await this.connectPromise;
2892
+ await this.beginTransactionPromise;
2893
+ return this.client.query(query, values);
2894
+ }
2895
+ };
31
2896
  // Annotate the CommonJS export names for ESM import in node:
32
2897
  0 && (module.exports = {
33
- isEven
2898
+ HtmlStatusCodes,
2899
+ PsqlConnection,
2900
+ PsqlEngine,
2901
+ PsqlPool,
2902
+ PsqlTransaction,
2903
+ RsError,
2904
+ SQL,
2905
+ escapeColumnName,
2906
+ eventManager,
2907
+ insertObjectQuery,
2908
+ isValueNumber,
2909
+ logger,
2910
+ questionMarksToOrderedParams,
2911
+ restura,
2912
+ updateObjectQuery
34
2913
  });
35
2914
  //# sourceMappingURL=index.js.map