@restura/core 0.2.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +517 -472
- package/dist/index.d.ts +517 -472
- package/dist/index.js +387 -348
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +256 -218
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -14
package/dist/index.mjs
CHANGED
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
-
var __defProps = Object.defineProperties;
|
|
3
2
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
5
|
-
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
8
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
9
|
-
var __spreadValues = (a, b) => {
|
|
10
|
-
for (var prop in b || (b = {}))
|
|
11
|
-
if (__hasOwnProp.call(b, prop))
|
|
12
|
-
__defNormalProp(a, prop, b[prop]);
|
|
13
|
-
if (__getOwnPropSymbols)
|
|
14
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
15
|
-
if (__propIsEnum.call(b, prop))
|
|
16
|
-
__defNormalProp(a, prop, b[prop]);
|
|
17
|
-
}
|
|
18
|
-
return a;
|
|
19
|
-
};
|
|
20
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
21
|
-
var __objRest = (source, exclude) => {
|
|
22
|
-
var target = {};
|
|
23
|
-
for (var prop in source)
|
|
24
|
-
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
25
|
-
target[prop] = source[prop];
|
|
26
|
-
if (source != null && __getOwnPropSymbols)
|
|
27
|
-
for (var prop of __getOwnPropSymbols(source)) {
|
|
28
|
-
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
29
|
-
target[prop] = source[prop];
|
|
30
|
-
}
|
|
31
|
-
return target;
|
|
32
|
-
};
|
|
33
3
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
34
4
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
35
5
|
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
@@ -45,13 +15,12 @@ import winston from "winston";
|
|
|
45
15
|
import { format } from "logform";
|
|
46
16
|
|
|
47
17
|
// src/logger/loggerConfigSchema.ts
|
|
48
|
-
import { z } from "zod";
|
|
18
|
+
import { z } from "zod/v4";
|
|
49
19
|
var loggerConfigSchema = z.object({
|
|
50
20
|
level: z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
|
|
51
21
|
});
|
|
52
22
|
|
|
53
23
|
// src/logger/logger.ts
|
|
54
|
-
var loggerConfig = config.validate("logger", loggerConfigSchema);
|
|
55
24
|
var consoleFormat = format.combine(
|
|
56
25
|
format.timestamp({
|
|
57
26
|
format: "YYYY-MM-DD HH:mm:ss.sss"
|
|
@@ -60,11 +29,11 @@ var consoleFormat = format.combine(
|
|
|
60
29
|
format.padLevels(),
|
|
61
30
|
format.colorize({ all: true }),
|
|
62
31
|
format.printf((info) => {
|
|
63
|
-
return `[${info.timestamp}] ${info.level}
|
|
32
|
+
return `[${info.timestamp}] ${info.level} ${info.message}`;
|
|
64
33
|
})
|
|
65
34
|
);
|
|
66
35
|
var logger = winston.createLogger({
|
|
67
|
-
level:
|
|
36
|
+
level: "info",
|
|
68
37
|
format: format.combine(
|
|
69
38
|
format.timestamp({
|
|
70
39
|
format: "YYYY-MM-DD HH:mm:ss.sss"
|
|
@@ -72,29 +41,30 @@ var logger = winston.createLogger({
|
|
|
72
41
|
format.errors({ stack: true }),
|
|
73
42
|
format.json()
|
|
74
43
|
),
|
|
75
|
-
|
|
76
|
-
transports: [
|
|
77
|
-
//
|
|
78
|
-
// - Write to all logs with level `info` and below to `combined.log`
|
|
79
|
-
// - Write all logs error (and below) to `error.log`.
|
|
80
|
-
// - Write all logs to standard out.
|
|
81
|
-
//
|
|
82
|
-
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
|
83
|
-
// new winston.transports.File({ filename: 'combined.log' }),
|
|
84
|
-
new winston.transports.Console({ format: consoleFormat })
|
|
85
|
-
]
|
|
44
|
+
transports: [new winston.transports.Console({ format: consoleFormat })]
|
|
86
45
|
});
|
|
46
|
+
(async () => {
|
|
47
|
+
try {
|
|
48
|
+
const loggerConfig = await config.validate("logger", loggerConfigSchema);
|
|
49
|
+
if (loggerConfig.level) {
|
|
50
|
+
logger.level = loggerConfig.level;
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error("Failed to load logger config:", error);
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to initialize logger configuration: ${error instanceof Error ? error.message : String(error)}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
87
59
|
|
|
88
60
|
// src/restura/eventManager.ts
|
|
89
61
|
import Bluebird from "bluebird";
|
|
90
62
|
var EventManager = class {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
};
|
|
97
|
-
}
|
|
63
|
+
actionHandlers = {
|
|
64
|
+
DATABASE_ROW_DELETE: [],
|
|
65
|
+
DATABASE_ROW_INSERT: [],
|
|
66
|
+
DATABASE_COLUMN_UPDATE: []
|
|
67
|
+
};
|
|
98
68
|
addRowInsertHandler(onInsert, filter) {
|
|
99
69
|
this.actionHandlers.DATABASE_ROW_INSERT.push({
|
|
100
70
|
callback: onInsert,
|
|
@@ -291,6 +261,10 @@ var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
|
|
|
291
261
|
return HtmlStatusCodes2;
|
|
292
262
|
})(HtmlStatusCodes || {});
|
|
293
263
|
var RsError = class _RsError {
|
|
264
|
+
err;
|
|
265
|
+
msg;
|
|
266
|
+
status;
|
|
267
|
+
stack;
|
|
294
268
|
constructor(errCode, message) {
|
|
295
269
|
this.err = errCode;
|
|
296
270
|
this.msg = message || "";
|
|
@@ -443,9 +417,7 @@ import fs from "fs";
|
|
|
443
417
|
import path from "path";
|
|
444
418
|
import { FileUtils } from "@restura/internal";
|
|
445
419
|
var CustomApiFactory = class {
|
|
446
|
-
|
|
447
|
-
this.customApis = {};
|
|
448
|
-
}
|
|
420
|
+
customApis = {};
|
|
449
421
|
async loadApiFiles(baseFolderPath) {
|
|
450
422
|
const apiVersions = ["v1"];
|
|
451
423
|
for (const apiVersion of apiVersions) {
|
|
@@ -459,11 +431,10 @@ var CustomApiFactory = class {
|
|
|
459
431
|
return this.customApis[customApiName];
|
|
460
432
|
}
|
|
461
433
|
async addDirectory(directoryPath, apiVersion) {
|
|
462
|
-
var _a2;
|
|
463
434
|
const entries = await fs.promises.readdir(directoryPath, {
|
|
464
435
|
withFileTypes: true
|
|
465
436
|
});
|
|
466
|
-
const isTsx2 =
|
|
437
|
+
const isTsx2 = process.argv[1]?.endsWith(".ts");
|
|
467
438
|
const isTsNode2 = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
|
|
468
439
|
const extension = isTsx2 || isTsNode2 ? "ts" : "js";
|
|
469
440
|
const shouldEndWith = `.api.${apiVersion}.${extension}`;
|
|
@@ -528,6 +499,8 @@ var SqlUtils = class _SqlUtils {
|
|
|
528
499
|
|
|
529
500
|
// src/restura/validators/ResponseValidator.ts
|
|
530
501
|
var ResponseValidator = class _ResponseValidator {
|
|
502
|
+
rootMap;
|
|
503
|
+
database;
|
|
531
504
|
constructor(schema) {
|
|
532
505
|
this.database = schema.database;
|
|
533
506
|
this.rootMap = {};
|
|
@@ -603,7 +576,7 @@ var ResponseValidator = class _ResponseValidator {
|
|
|
603
576
|
if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { validator: "any", isOptional: false };
|
|
604
577
|
const tableName = path5.length == 2 ? path5[0] : name, columnName = path5.length == 2 ? path5[1] : path5[0];
|
|
605
578
|
const table = this.database.find((t) => t.name == tableName);
|
|
606
|
-
const column = table
|
|
579
|
+
const column = table?.columns.find((c) => c.name == columnName);
|
|
607
580
|
if (!table || !column) return { validator: "any", isOptional: false };
|
|
608
581
|
let validator = SqlUtils.convertDatabaseTypeToTypescript(
|
|
609
582
|
column.type,
|
|
@@ -687,10 +660,12 @@ var ResponseValidator = class _ResponseValidator {
|
|
|
687
660
|
var ApiTree = class _ApiTree {
|
|
688
661
|
constructor(namespace, database) {
|
|
689
662
|
this.database = database;
|
|
690
|
-
this.data = [];
|
|
691
663
|
this.namespace = namespace;
|
|
692
664
|
this.children = /* @__PURE__ */ new Map();
|
|
693
665
|
}
|
|
666
|
+
namespace;
|
|
667
|
+
data = [];
|
|
668
|
+
children;
|
|
694
669
|
static createRootNode(database) {
|
|
695
670
|
return new _ApiTree(null, database);
|
|
696
671
|
}
|
|
@@ -834,7 +809,7 @@ var ApiTree = class _ApiTree {
|
|
|
834
809
|
tableName = tableAliasSplit[1];
|
|
835
810
|
table = this.database.find((t) => t.name == tableName);
|
|
836
811
|
}
|
|
837
|
-
const column = table
|
|
812
|
+
const column = table?.columns.find((c) => c.name == columnName);
|
|
838
813
|
if (!table || !column) return { responseType: "any", isNullable: false };
|
|
839
814
|
return {
|
|
840
815
|
responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
|
|
@@ -865,16 +840,17 @@ function apiGenerator(schema) {
|
|
|
865
840
|
${schema.customTypes.join("\n")}
|
|
866
841
|
}`;
|
|
867
842
|
}
|
|
868
|
-
return prettier.format(apiString,
|
|
869
|
-
parser: "typescript"
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
843
|
+
return prettier.format(apiString, {
|
|
844
|
+
parser: "typescript",
|
|
845
|
+
...{
|
|
846
|
+
trailingComma: "none",
|
|
847
|
+
tabWidth: 4,
|
|
848
|
+
useTabs: true,
|
|
849
|
+
endOfLine: "lf",
|
|
850
|
+
printWidth: 120,
|
|
851
|
+
singleQuote: true
|
|
852
|
+
}
|
|
853
|
+
});
|
|
878
854
|
}
|
|
879
855
|
|
|
880
856
|
// src/restura/generators/customTypeValidationGenerator.ts
|
|
@@ -885,7 +861,7 @@ import * as TJS from "typescript-json-schema";
|
|
|
885
861
|
function customTypeValidationGenerator(currentSchema) {
|
|
886
862
|
const schemaObject = {};
|
|
887
863
|
const customInterfaceNames = currentSchema.customTypes.map((customType) => {
|
|
888
|
-
const matches = customType.match(
|
|
864
|
+
const matches = customType.match(/(?<=interface\s)(\w+)|(?<=type\s)(\w+)/g);
|
|
889
865
|
if (matches && matches.length > 0) return matches[0];
|
|
890
866
|
return "";
|
|
891
867
|
}).filter(Boolean);
|
|
@@ -929,16 +905,17 @@ function modelGenerator(schema) {
|
|
|
929
905
|
modelString += convertTable(table);
|
|
930
906
|
}
|
|
931
907
|
modelString += `}`;
|
|
932
|
-
return prettier2.format(modelString,
|
|
933
|
-
parser: "typescript"
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
908
|
+
return prettier2.format(modelString, {
|
|
909
|
+
parser: "typescript",
|
|
910
|
+
...{
|
|
911
|
+
trailingComma: "none",
|
|
912
|
+
tabWidth: 4,
|
|
913
|
+
useTabs: true,
|
|
914
|
+
endOfLine: "lf",
|
|
915
|
+
printWidth: 120,
|
|
916
|
+
singleQuote: true
|
|
917
|
+
}
|
|
918
|
+
});
|
|
942
919
|
}
|
|
943
920
|
function convertTable(table) {
|
|
944
921
|
let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
|
|
@@ -990,20 +967,21 @@ function addApiResponseFunctions(req, res, next) {
|
|
|
990
967
|
htmlStatusCode = 500;
|
|
991
968
|
}
|
|
992
969
|
}
|
|
993
|
-
const errorData =
|
|
970
|
+
const errorData = {
|
|
994
971
|
err: shortError,
|
|
995
|
-
msg
|
|
996
|
-
|
|
972
|
+
msg,
|
|
973
|
+
...restura.resturaConfig.sendErrorStackTrace && stack ? { stack } : {}
|
|
974
|
+
};
|
|
997
975
|
res.status(htmlStatusCode).send(errorData);
|
|
998
976
|
};
|
|
999
977
|
next();
|
|
1000
978
|
}
|
|
1001
979
|
|
|
1002
|
-
// src/restura/middleware/
|
|
1003
|
-
function
|
|
980
|
+
// src/restura/middleware/authenticateRequester.ts
|
|
981
|
+
function authenticateRequester(applicationAuthenticateHandler) {
|
|
1004
982
|
return (req, res, next) => {
|
|
1005
|
-
applicationAuthenticateHandler(req, res, (
|
|
1006
|
-
req.requesterDetails =
|
|
983
|
+
applicationAuthenticateHandler(req, res, (authenticatedRequesterDetails) => {
|
|
984
|
+
req.requesterDetails = { host: req.hostname, ipAddress: req.ip || "", ...authenticatedRequesterDetails };
|
|
1007
985
|
next();
|
|
1008
986
|
});
|
|
1009
987
|
};
|
|
@@ -1058,22 +1036,7 @@ var groupBySchema = z3.object({
|
|
|
1058
1036
|
var whereDataSchema = z3.object({
|
|
1059
1037
|
tableName: z3.string().optional(),
|
|
1060
1038
|
columnName: z3.string().optional(),
|
|
1061
|
-
operator: z3.enum([
|
|
1062
|
-
"=",
|
|
1063
|
-
"<",
|
|
1064
|
-
">",
|
|
1065
|
-
"<=",
|
|
1066
|
-
">=",
|
|
1067
|
-
"!=",
|
|
1068
|
-
"LIKE",
|
|
1069
|
-
"NOT LIKE",
|
|
1070
|
-
"IN",
|
|
1071
|
-
"NOT IN",
|
|
1072
|
-
"STARTS WITH",
|
|
1073
|
-
"ENDS WITH",
|
|
1074
|
-
"IS",
|
|
1075
|
-
"IS NOT"
|
|
1076
|
-
]).optional(),
|
|
1039
|
+
operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH", "IS", "IS NOT"]).optional(),
|
|
1077
1040
|
value: z3.string().or(z3.number()).optional(),
|
|
1078
1041
|
custom: z3.string().optional(),
|
|
1079
1042
|
conjunction: z3.enum(["AND", "OR"]).optional()
|
|
@@ -1109,13 +1072,15 @@ var responseDataSchema = z3.object({
|
|
|
1109
1072
|
orderBy: orderBySchema.optional()
|
|
1110
1073
|
}).optional(),
|
|
1111
1074
|
type: z3.string().optional()
|
|
1075
|
+
// Type allows you to override the type of the response, used in custom selectors
|
|
1112
1076
|
}).strict();
|
|
1113
1077
|
var routeDataBaseSchema = z3.object({
|
|
1114
1078
|
method: z3.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
|
1115
1079
|
name: z3.string(),
|
|
1116
1080
|
description: z3.string(),
|
|
1117
1081
|
path: z3.string(),
|
|
1118
|
-
roles: z3.array(z3.string())
|
|
1082
|
+
roles: z3.array(z3.string()),
|
|
1083
|
+
scopes: z3.array(z3.string())
|
|
1119
1084
|
}).strict();
|
|
1120
1085
|
var standardRouteSchema = routeDataBaseSchema.extend({
|
|
1121
1086
|
type: z3.enum(["ONE", "ARRAY", "PAGED"]),
|
|
@@ -1255,6 +1220,7 @@ var columnDataSchema = z3.object({
|
|
|
1255
1220
|
]),
|
|
1256
1221
|
isNullable: z3.boolean(),
|
|
1257
1222
|
roles: z3.array(z3.string()),
|
|
1223
|
+
scopes: z3.array(z3.string()),
|
|
1258
1224
|
comment: z3.string().optional(),
|
|
1259
1225
|
default: z3.string().optional(),
|
|
1260
1226
|
value: z3.string().optional(),
|
|
@@ -1301,6 +1267,7 @@ var tableDataSchema = z3.object({
|
|
|
1301
1267
|
foreignKeys: z3.array(foreignKeyDataSchema),
|
|
1302
1268
|
checkConstraints: z3.array(checkConstraintDataSchema),
|
|
1303
1269
|
roles: z3.array(z3.string()),
|
|
1270
|
+
scopes: z3.array(z3.string()),
|
|
1304
1271
|
notify: z3.union([z3.literal("ALL"), z3.array(z3.string())]).optional()
|
|
1305
1272
|
}).strict();
|
|
1306
1273
|
var endpointDataSchema = z3.object({
|
|
@@ -1314,6 +1281,7 @@ var resturaSchema = z3.object({
|
|
|
1314
1281
|
endpoints: z3.array(endpointDataSchema),
|
|
1315
1282
|
globalParams: z3.array(z3.string()),
|
|
1316
1283
|
roles: z3.array(z3.string()),
|
|
1284
|
+
scopes: z3.array(z3.string()),
|
|
1317
1285
|
customTypes: z3.array(z3.string())
|
|
1318
1286
|
}).strict();
|
|
1319
1287
|
async function isSchemaValid(schemaToCheck) {
|
|
@@ -1411,7 +1379,7 @@ function isValidType(type, requestValue) {
|
|
|
1411
1379
|
try {
|
|
1412
1380
|
expectValidType(type, requestValue);
|
|
1413
1381
|
return true;
|
|
1414
|
-
} catch
|
|
1382
|
+
} catch {
|
|
1415
1383
|
return false;
|
|
1416
1384
|
}
|
|
1417
1385
|
}
|
|
@@ -1447,7 +1415,7 @@ function performTypeCheck(requestValue, validator, requestParamName) {
|
|
|
1447
1415
|
}
|
|
1448
1416
|
try {
|
|
1449
1417
|
validatorDataSchemeValue.parse(validator.value);
|
|
1450
|
-
} catch
|
|
1418
|
+
} catch {
|
|
1451
1419
|
throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not a valid type`);
|
|
1452
1420
|
}
|
|
1453
1421
|
}
|
|
@@ -1538,9 +1506,8 @@ async function schemaValidation(req, res, next) {
|
|
|
1538
1506
|
}
|
|
1539
1507
|
|
|
1540
1508
|
// src/restura/schemas/resturaConfigSchema.ts
|
|
1541
|
-
import { z as z5 } from "zod";
|
|
1542
|
-
var
|
|
1543
|
-
var isTsx = (_a = process.argv[1]) == null ? void 0 : _a.endsWith(".ts");
|
|
1509
|
+
import { z as z5 } from "zod/v4";
|
|
1510
|
+
var isTsx = process.argv[1]?.endsWith(".ts");
|
|
1544
1511
|
var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
|
|
1545
1512
|
var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
|
|
1546
1513
|
var resturaConfigSchema = z5.object({
|
|
@@ -1549,7 +1516,8 @@ var resturaConfigSchema = z5.object({
|
|
|
1549
1516
|
schemaFilePath: z5.string().default(process.cwd() + "/restura.schema.json"),
|
|
1550
1517
|
customApiFolderPath: z5.string().default(process.cwd() + customApiFolderPath),
|
|
1551
1518
|
generatedTypesPath: z5.string().default(process.cwd() + "/src/@types"),
|
|
1552
|
-
fileTempCachePath: z5.string().optional()
|
|
1519
|
+
fileTempCachePath: z5.string().optional(),
|
|
1520
|
+
scratchDatabaseSuffix: z5.string().optional()
|
|
1553
1521
|
});
|
|
1554
1522
|
|
|
1555
1523
|
// src/restura/sql/PsqlEngine.ts
|
|
@@ -1635,13 +1603,14 @@ function SQL(strings, ...values) {
|
|
|
1635
1603
|
|
|
1636
1604
|
// src/restura/sql/PsqlConnection.ts
|
|
1637
1605
|
var PsqlConnection = class {
|
|
1606
|
+
instanceId;
|
|
1638
1607
|
constructor(instanceId) {
|
|
1639
1608
|
this.instanceId = instanceId || crypto.randomUUID();
|
|
1640
1609
|
}
|
|
1641
1610
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1642
1611
|
async queryOne(query, options, requesterDetails) {
|
|
1643
1612
|
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1644
|
-
const meta =
|
|
1613
|
+
const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
|
|
1645
1614
|
this.logSqlStatement(formattedQuery, options, meta);
|
|
1646
1615
|
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1647
1616
|
`;
|
|
@@ -1654,7 +1623,7 @@ var PsqlConnection = class {
|
|
|
1654
1623
|
return response.rows[0];
|
|
1655
1624
|
} catch (error) {
|
|
1656
1625
|
if (RsError.isRsError(error)) throw error;
|
|
1657
|
-
if (
|
|
1626
|
+
if (error?.routine === "_bt_check_unique") {
|
|
1658
1627
|
throw new RsError("DUPLICATE", error.message);
|
|
1659
1628
|
}
|
|
1660
1629
|
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
@@ -1663,7 +1632,7 @@ var PsqlConnection = class {
|
|
|
1663
1632
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1664
1633
|
async runQuery(query, options, requesterDetails) {
|
|
1665
1634
|
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1666
|
-
const meta =
|
|
1635
|
+
const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
|
|
1667
1636
|
this.logSqlStatement(formattedQuery, options, meta);
|
|
1668
1637
|
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1669
1638
|
`;
|
|
@@ -1673,7 +1642,7 @@ var PsqlConnection = class {
|
|
|
1673
1642
|
this.logQueryDuration(startTime);
|
|
1674
1643
|
return response.rows;
|
|
1675
1644
|
} catch (error) {
|
|
1676
|
-
if (
|
|
1645
|
+
if (error?.routine === "_bt_check_unique") {
|
|
1677
1646
|
throw new RsError("DUPLICATE", error.message);
|
|
1678
1647
|
}
|
|
1679
1648
|
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
@@ -1723,13 +1692,20 @@ var PsqlPool = class extends PsqlConnection {
|
|
|
1723
1692
|
super();
|
|
1724
1693
|
this.poolConfig = poolConfig;
|
|
1725
1694
|
this.pool = new Pool(poolConfig);
|
|
1726
|
-
this.queryOne("SELECT NOW();", [], {
|
|
1695
|
+
this.queryOne("SELECT NOW();", [], {
|
|
1696
|
+
isSystemUser: true,
|
|
1697
|
+
role: "",
|
|
1698
|
+
host: "localhost",
|
|
1699
|
+
ipAddress: "",
|
|
1700
|
+
scopes: []
|
|
1701
|
+
}).then(() => {
|
|
1727
1702
|
logger.info("Connected to PostgreSQL database");
|
|
1728
1703
|
}).catch((error) => {
|
|
1729
1704
|
logger.error("Error connecting to database", error);
|
|
1730
1705
|
process.exit(1);
|
|
1731
1706
|
});
|
|
1732
1707
|
}
|
|
1708
|
+
pool;
|
|
1733
1709
|
async query(query, values) {
|
|
1734
1710
|
return this.pool.query(query, values);
|
|
1735
1711
|
}
|
|
@@ -1739,7 +1715,12 @@ var PsqlPool = class extends PsqlConnection {
|
|
|
1739
1715
|
import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
|
|
1740
1716
|
var SqlEngine = class {
|
|
1741
1717
|
async runQueryForRoute(req, routeData, schema) {
|
|
1742
|
-
if (!this.
|
|
1718
|
+
if (!this.canRequesterAccessTable(
|
|
1719
|
+
req.requesterDetails.role,
|
|
1720
|
+
req.requesterDetails.scopes,
|
|
1721
|
+
schema,
|
|
1722
|
+
routeData.table
|
|
1723
|
+
))
|
|
1743
1724
|
throw new RsError("FORBIDDEN", "You do not have permission to access this table");
|
|
1744
1725
|
switch (routeData.method) {
|
|
1745
1726
|
case "POST":
|
|
@@ -1758,7 +1739,7 @@ var SqlEngine = class {
|
|
|
1758
1739
|
if (!tableSchema) throw new RsError("SCHEMA_ERROR", `Table ${tableName} not found in schema`);
|
|
1759
1740
|
return tableSchema;
|
|
1760
1741
|
}
|
|
1761
|
-
|
|
1742
|
+
canRequesterAccessColumn(requesterRole, requesterScopes, schema, item, joins) {
|
|
1762
1743
|
if (item.type) return true;
|
|
1763
1744
|
if (item.selector) {
|
|
1764
1745
|
let tableName = item.selector.split(".")[0];
|
|
@@ -1774,26 +1755,36 @@ var SqlEngine = class {
|
|
|
1774
1755
|
const columnSchema = tableSchema.columns.find((item2) => item2.name === columnName);
|
|
1775
1756
|
if (!columnSchema)
|
|
1776
1757
|
throw new RsError("SCHEMA_ERROR", `Column ${columnName} not found in table ${tableName}`);
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1758
|
+
if (ObjectUtils3.isArrayWithData(columnSchema.roles)) {
|
|
1759
|
+
if (!requesterRole) return false;
|
|
1760
|
+
return columnSchema.roles.includes(requesterRole);
|
|
1761
|
+
}
|
|
1762
|
+
if (ObjectUtils3.isArrayWithData(columnSchema.scopes)) {
|
|
1763
|
+
if (!requesterScopes) return false;
|
|
1764
|
+
return columnSchema.scopes.some((scope) => requesterScopes.includes(scope));
|
|
1765
|
+
}
|
|
1766
|
+
return true;
|
|
1781
1767
|
}
|
|
1782
1768
|
if (item.subquery) {
|
|
1783
1769
|
return ObjectUtils3.isArrayWithData(
|
|
1784
1770
|
item.subquery.properties.filter((nestedItem) => {
|
|
1785
|
-
return this.
|
|
1771
|
+
return this.canRequesterAccessColumn(requesterRole, requesterScopes, schema, nestedItem, joins);
|
|
1786
1772
|
})
|
|
1787
1773
|
);
|
|
1788
1774
|
}
|
|
1789
1775
|
return false;
|
|
1790
1776
|
}
|
|
1791
|
-
|
|
1777
|
+
canRequesterAccessTable(requesterRole, requesterScopes, schema, tableName) {
|
|
1792
1778
|
const tableSchema = this.getTableSchema(schema, tableName);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1779
|
+
if (ObjectUtils3.isArrayWithData(tableSchema.roles)) {
|
|
1780
|
+
if (!requesterRole) return false;
|
|
1781
|
+
return tableSchema.roles.includes(requesterRole);
|
|
1782
|
+
}
|
|
1783
|
+
if (ObjectUtils3.isArrayWithData(tableSchema.scopes)) {
|
|
1784
|
+
if (!requesterScopes) return false;
|
|
1785
|
+
return tableSchema.scopes.some((scope) => requesterScopes.includes(scope));
|
|
1786
|
+
}
|
|
1787
|
+
return true;
|
|
1797
1788
|
}
|
|
1798
1789
|
replaceParamKeywords(value, routeData, req, sqlParams) {
|
|
1799
1790
|
let returnValue = value;
|
|
@@ -1802,11 +1793,10 @@ var SqlEngine = class {
|
|
|
1802
1793
|
return returnValue;
|
|
1803
1794
|
}
|
|
1804
1795
|
replaceLocalParamKeywords(value, routeData, req, sqlParams) {
|
|
1805
|
-
var _a2;
|
|
1806
1796
|
if (!routeData.request) return value;
|
|
1807
1797
|
const data = req.data;
|
|
1808
1798
|
if (typeof value === "string") {
|
|
1809
|
-
|
|
1799
|
+
value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)?.forEach((param) => {
|
|
1810
1800
|
const requestParam = routeData.request.find((item) => {
|
|
1811
1801
|
return item.name === param.replace("$", "");
|
|
1812
1802
|
});
|
|
@@ -1819,9 +1809,8 @@ var SqlEngine = class {
|
|
|
1819
1809
|
return value;
|
|
1820
1810
|
}
|
|
1821
1811
|
replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
|
|
1822
|
-
var _a2;
|
|
1823
1812
|
if (typeof value === "string") {
|
|
1824
|
-
|
|
1813
|
+
value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)?.forEach((param) => {
|
|
1825
1814
|
param = param.replace("#", "");
|
|
1826
1815
|
const globalParamValue = req.requesterDetails[param];
|
|
1827
1816
|
if (!globalParamValue)
|
|
@@ -1911,10 +1900,32 @@ negate = "!"
|
|
|
1911
1900
|
operator = "and"i / "or"i
|
|
1912
1901
|
|
|
1913
1902
|
|
|
1914
|
-
column =
|
|
1915
|
-
|
|
1916
|
-
|
|
1903
|
+
column = first:text rest:("." text)* {
|
|
1904
|
+
const partsArray = [first];
|
|
1905
|
+
if (rest && rest.length > 0) {
|
|
1906
|
+
partsArray.push(...rest.map(item => item[1]));
|
|
1907
|
+
}
|
|
1917
1908
|
|
|
1909
|
+
if (partsArray.length > 3) {
|
|
1910
|
+
throw new SyntaxError('Column path cannot have more than 3 parts (table.column.jsonField)');
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
if (partsArray.length === 1) {
|
|
1914
|
+
return quoteSqlIdentity(partsArray[0]);
|
|
1915
|
+
}
|
|
1916
|
+
const tableName = quoteSqlIdentity(partsArray[0]);
|
|
1917
|
+
|
|
1918
|
+
// If we only have two parts (table.column), use regular dot notation
|
|
1919
|
+
if (partsArray.length === 2) {
|
|
1920
|
+
return tableName + "." + quoteSqlIdentity(partsArray[1]);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// For JSON paths (more than 2 parts), first part is a column, last part uses ->>
|
|
1924
|
+
const jsonColumn = quoteSqlIdentity(partsArray[1]);
|
|
1925
|
+
const lastPart = partsArray[partsArray.length - 1];
|
|
1926
|
+
const result = tableName + "." + jsonColumn + "->>'" + lastPart + "'";
|
|
1927
|
+
return result;
|
|
1928
|
+
}
|
|
1918
1929
|
|
|
1919
1930
|
text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
|
|
1920
1931
|
|
|
@@ -1944,19 +1955,24 @@ var filterPsqlParser_default = filterPsqlParser;
|
|
|
1944
1955
|
var { Client, types } = pg2;
|
|
1945
1956
|
var systemUser = {
|
|
1946
1957
|
role: "",
|
|
1958
|
+
scopes: [],
|
|
1947
1959
|
host: "",
|
|
1948
1960
|
ipAddress: "",
|
|
1949
1961
|
isSystemUser: true
|
|
1950
1962
|
};
|
|
1951
1963
|
var PsqlEngine = class extends SqlEngine {
|
|
1952
|
-
constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
|
|
1964
|
+
constructor(psqlConnectionPool, shouldListenForDbTriggers = false, scratchDatabaseSuffix = "") {
|
|
1953
1965
|
super();
|
|
1954
1966
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
1955
1967
|
this.setupPgReturnTypes();
|
|
1956
1968
|
if (shouldListenForDbTriggers) {
|
|
1957
1969
|
this.setupTriggerListeners = this.listenForDbTriggers();
|
|
1958
1970
|
}
|
|
1971
|
+
this.scratchDbName = `${psqlConnectionPool.poolConfig.database}_scratch${scratchDatabaseSuffix ? `_${scratchDatabaseSuffix}` : ""}`;
|
|
1959
1972
|
}
|
|
1973
|
+
setupTriggerListeners;
|
|
1974
|
+
triggerClient;
|
|
1975
|
+
scratchDbName = "";
|
|
1960
1976
|
async close() {
|
|
1961
1977
|
if (this.triggerClient) {
|
|
1962
1978
|
await this.triggerClient.end();
|
|
@@ -2085,27 +2101,22 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2085
2101
|
sqlStatements.push(triggers.join("\n"));
|
|
2086
2102
|
return sqlStatements.join("\n\n");
|
|
2087
2103
|
}
|
|
2088
|
-
async
|
|
2089
|
-
var _a2, _b;
|
|
2104
|
+
async getNewPublicSchemaAndScratchPool() {
|
|
2090
2105
|
const scratchDbExists = await this.psqlConnectionPool.runQuery(
|
|
2091
2106
|
`SELECT *
|
|
2092
2107
|
FROM pg_database
|
|
2093
|
-
WHERE datname = '${this.
|
|
2108
|
+
WHERE datname = '${this.scratchDbName}';`,
|
|
2094
2109
|
[],
|
|
2095
2110
|
systemUser
|
|
2096
2111
|
);
|
|
2097
2112
|
if (scratchDbExists.length === 0) {
|
|
2098
|
-
await this.psqlConnectionPool.runQuery(
|
|
2099
|
-
`CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
|
|
2100
|
-
[],
|
|
2101
|
-
systemUser
|
|
2102
|
-
);
|
|
2113
|
+
await this.psqlConnectionPool.runQuery(`CREATE DATABASE ${this.scratchDbName};`, [], systemUser);
|
|
2103
2114
|
}
|
|
2104
2115
|
const scratchPool = new PsqlPool({
|
|
2105
2116
|
host: this.psqlConnectionPool.poolConfig.host,
|
|
2106
2117
|
port: this.psqlConnectionPool.poolConfig.port,
|
|
2107
2118
|
user: this.psqlConnectionPool.poolConfig.user,
|
|
2108
|
-
database: this.
|
|
2119
|
+
database: this.scratchDbName,
|
|
2109
2120
|
password: this.psqlConnectionPool.poolConfig.password,
|
|
2110
2121
|
max: this.psqlConnectionPool.poolConfig.max,
|
|
2111
2122
|
idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
|
|
@@ -2118,16 +2129,17 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2118
2129
|
systemUser
|
|
2119
2130
|
);
|
|
2120
2131
|
const schemaComment = await this.psqlConnectionPool.runQuery(
|
|
2121
|
-
`
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2132
|
+
`
|
|
2133
|
+
SELECT pg_description.description
|
|
2134
|
+
FROM pg_description
|
|
2135
|
+
JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
|
|
2136
|
+
WHERE pg_namespace.nspname = 'public';`,
|
|
2125
2137
|
[],
|
|
2126
2138
|
systemUser
|
|
2127
2139
|
);
|
|
2128
|
-
if (
|
|
2140
|
+
if (schemaComment[0]?.description) {
|
|
2129
2141
|
await scratchPool.runQuery(
|
|
2130
|
-
`COMMENT ON SCHEMA public IS '${
|
|
2142
|
+
`COMMENT ON SCHEMA public IS '${schemaComment[0]?.description}';`,
|
|
2131
2143
|
[],
|
|
2132
2144
|
systemUser
|
|
2133
2145
|
);
|
|
@@ -2135,7 +2147,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2135
2147
|
return scratchPool;
|
|
2136
2148
|
}
|
|
2137
2149
|
async diffDatabaseToSchema(schema) {
|
|
2138
|
-
const scratchPool = await this.
|
|
2150
|
+
const scratchPool = await this.getNewPublicSchemaAndScratchPool();
|
|
2139
2151
|
await this.createDatabaseFromSchema(schema, scratchPool);
|
|
2140
2152
|
const originalClient = new Client({
|
|
2141
2153
|
database: this.psqlConnectionPool.poolConfig.database,
|
|
@@ -2145,7 +2157,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2145
2157
|
port: this.psqlConnectionPool.poolConfig.port
|
|
2146
2158
|
});
|
|
2147
2159
|
const scratchClient = new Client({
|
|
2148
|
-
database: this.
|
|
2160
|
+
database: this.scratchDbName,
|
|
2149
2161
|
user: this.psqlConnectionPool.poolConfig.user,
|
|
2150
2162
|
password: this.psqlConnectionPool.poolConfig.password,
|
|
2151
2163
|
host: this.psqlConnectionPool.poolConfig.host,
|
|
@@ -2160,24 +2172,30 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2160
2172
|
await Promise.all(endPromises);
|
|
2161
2173
|
return diff.join("\n");
|
|
2162
2174
|
}
|
|
2163
|
-
createNestedSelect(req, schema, item, routeData,
|
|
2175
|
+
createNestedSelect(req, schema, item, routeData, sqlParams) {
|
|
2164
2176
|
if (!item.subquery) return "";
|
|
2165
2177
|
if (!ObjectUtils4.isArrayWithData(
|
|
2166
2178
|
item.subquery.properties.filter((nestedItem) => {
|
|
2167
|
-
return this.
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2179
|
+
return this.canRequesterAccessColumn(
|
|
2180
|
+
req.requesterDetails.role,
|
|
2181
|
+
req.requesterDetails.scopes,
|
|
2182
|
+
schema,
|
|
2183
|
+
nestedItem,
|
|
2184
|
+
[...routeData.joins, ...item.subquery.joins]
|
|
2185
|
+
);
|
|
2171
2186
|
})
|
|
2172
2187
|
)) {
|
|
2173
2188
|
return "'[]'";
|
|
2174
2189
|
}
|
|
2175
2190
|
return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
|
|
2176
2191
|
${item.subquery.properties.map((nestedItem) => {
|
|
2177
|
-
if (!this.
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2192
|
+
if (!this.canRequesterAccessColumn(
|
|
2193
|
+
req.requesterDetails.role,
|
|
2194
|
+
req.requesterDetails.scopes,
|
|
2195
|
+
schema,
|
|
2196
|
+
nestedItem,
|
|
2197
|
+
[...routeData.joins, ...item.subquery.joins]
|
|
2198
|
+
)) {
|
|
2181
2199
|
return;
|
|
2182
2200
|
}
|
|
2183
2201
|
if (nestedItem.subquery) {
|
|
@@ -2187,7 +2205,6 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2187
2205
|
schema,
|
|
2188
2206
|
nestedItem,
|
|
2189
2207
|
routeData,
|
|
2190
|
-
userRole,
|
|
2191
2208
|
sqlParams
|
|
2192
2209
|
)}`;
|
|
2193
2210
|
}
|
|
@@ -2196,7 +2213,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2196
2213
|
))
|
|
2197
2214
|
FROM
|
|
2198
2215
|
"${item.subquery.table}"
|
|
2199
|
-
${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema,
|
|
2216
|
+
${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema, sqlParams)}
|
|
2200
2217
|
${this.generateWhereClause(req, item.subquery.where, routeData, sqlParams)}
|
|
2201
2218
|
), '[]')`;
|
|
2202
2219
|
}
|
|
@@ -2206,7 +2223,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2206
2223
|
(routeData.assignments || []).forEach((assignment) => {
|
|
2207
2224
|
parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
|
|
2208
2225
|
});
|
|
2209
|
-
const query = insertObjectQuery(routeData.table,
|
|
2226
|
+
const query = insertObjectQuery(routeData.table, { ...req.data, ...parameterObj });
|
|
2210
2227
|
const createdItem = await this.psqlConnectionPool.queryOne(
|
|
2211
2228
|
query,
|
|
2212
2229
|
sqlParams,
|
|
@@ -2221,24 +2238,29 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2221
2238
|
};
|
|
2222
2239
|
const whereData = [whereId];
|
|
2223
2240
|
req.data = { id: insertId };
|
|
2224
|
-
return this.executeGetRequest(req,
|
|
2241
|
+
return this.executeGetRequest(req, { ...routeData, where: whereData }, schema);
|
|
2225
2242
|
}
|
|
2226
2243
|
async executeGetRequest(req, routeData, schema) {
|
|
2227
2244
|
const DEFAULT_PAGED_PAGE_NUMBER = 0;
|
|
2228
2245
|
const DEFAULT_PAGED_PER_PAGE_NUMBER = 25;
|
|
2229
2246
|
const sqlParams = [];
|
|
2230
|
-
const userRole = req.requesterDetails.role;
|
|
2231
2247
|
let sqlStatement = "";
|
|
2232
2248
|
const selectColumns = [];
|
|
2233
2249
|
routeData.response.forEach((item) => {
|
|
2234
|
-
if (item.subquery || this.
|
|
2250
|
+
if (item.subquery || this.canRequesterAccessColumn(
|
|
2251
|
+
req.requesterDetails.role,
|
|
2252
|
+
req.requesterDetails.scopes,
|
|
2253
|
+
schema,
|
|
2254
|
+
item,
|
|
2255
|
+
routeData.joins
|
|
2256
|
+
))
|
|
2235
2257
|
selectColumns.push(item);
|
|
2236
2258
|
});
|
|
2237
2259
|
if (!selectColumns.length) throw new RsError("FORBIDDEN", `You do not have permission to access this data.`);
|
|
2238
2260
|
let selectStatement = "SELECT \n";
|
|
2239
2261
|
selectStatement += ` ${selectColumns.map((item) => {
|
|
2240
2262
|
if (item.subquery) {
|
|
2241
|
-
return `${this.createNestedSelect(req, schema, item, routeData,
|
|
2263
|
+
return `${this.createNestedSelect(req, schema, item, routeData, sqlParams)} AS ${escapeColumnName(
|
|
2242
2264
|
item.name
|
|
2243
2265
|
)}`;
|
|
2244
2266
|
}
|
|
@@ -2253,7 +2275,6 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2253
2275
|
routeData.table,
|
|
2254
2276
|
routeData,
|
|
2255
2277
|
schema,
|
|
2256
|
-
userRole,
|
|
2257
2278
|
sqlParams
|
|
2258
2279
|
);
|
|
2259
2280
|
sqlStatement += this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
@@ -2297,7 +2318,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2297
2318
|
}
|
|
2298
2319
|
async executeUpdateRequest(req, routeData, schema) {
|
|
2299
2320
|
const sqlParams = [];
|
|
2300
|
-
const
|
|
2321
|
+
const { id, ...bodyNoId } = req.body;
|
|
2301
2322
|
const table = schema.database.find((item) => {
|
|
2302
2323
|
return item.name === routeData.table;
|
|
2303
2324
|
});
|
|
@@ -2326,7 +2347,6 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2326
2347
|
routeData.table,
|
|
2327
2348
|
routeData,
|
|
2328
2349
|
schema,
|
|
2329
|
-
req.requesterDetails.role,
|
|
2330
2350
|
sqlParams
|
|
2331
2351
|
);
|
|
2332
2352
|
const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
@@ -2338,10 +2358,15 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
|
2338
2358
|
await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
|
|
2339
2359
|
return true;
|
|
2340
2360
|
}
|
|
2341
|
-
generateJoinStatements(req, joins, baseTable, routeData, schema,
|
|
2361
|
+
generateJoinStatements(req, joins, baseTable, routeData, schema, sqlParams) {
|
|
2342
2362
|
let joinStatements = "";
|
|
2343
2363
|
joins.forEach((item) => {
|
|
2344
|
-
if (!this.
|
|
2364
|
+
if (!this.canRequesterAccessTable(
|
|
2365
|
+
req.requesterDetails.role,
|
|
2366
|
+
req.requesterDetails.scopes,
|
|
2367
|
+
schema,
|
|
2368
|
+
item.table
|
|
2369
|
+
))
|
|
2345
2370
|
throw new RsError("FORBIDDEN", "You do not have permission to access this table");
|
|
2346
2371
|
if (item.custom) {
|
|
2347
2372
|
const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
|
|
@@ -2396,41 +2421,36 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
|
2396
2421
|
`Invalid where clause in route ${routeData.name}, missing required fields if not custom`
|
|
2397
2422
|
);
|
|
2398
2423
|
let operator = item.operator;
|
|
2399
|
-
let value = item.value;
|
|
2400
2424
|
if (operator === "LIKE") {
|
|
2401
|
-
value = `'%${value}%'`;
|
|
2402
|
-
} else if (operator === "NOT LIKE") {
|
|
2403
|
-
value = `'%${value}%'`;
|
|
2425
|
+
item.value = `'%${item.value}%'`;
|
|
2404
2426
|
} else if (operator === "STARTS WITH") {
|
|
2405
2427
|
operator = "LIKE";
|
|
2406
|
-
value = `'${value}%'`;
|
|
2428
|
+
item.value = `'${item.value}%'`;
|
|
2407
2429
|
} else if (operator === "ENDS WITH") {
|
|
2408
2430
|
operator = "LIKE";
|
|
2409
|
-
value = `'%${value}'`;
|
|
2431
|
+
item.value = `'%${item.value}'`;
|
|
2410
2432
|
}
|
|
2411
|
-
const replacedValue = this.replaceParamKeywords(value, routeData, req, sqlParams);
|
|
2433
|
+
const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
|
|
2412
2434
|
whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
|
|
2413
2435
|
`;
|
|
2414
2436
|
});
|
|
2415
2437
|
const data = req.data;
|
|
2416
|
-
if (routeData.type === "PAGED" && !!
|
|
2438
|
+
if (routeData.type === "PAGED" && !!data?.filter) {
|
|
2417
2439
|
let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
2418
|
-
var _a2;
|
|
2419
2440
|
const requestParam = routeData.request.find((item) => {
|
|
2420
2441
|
return item.name === value.replace("$", "");
|
|
2421
2442
|
});
|
|
2422
2443
|
if (!requestParam)
|
|
2423
2444
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
2424
|
-
return
|
|
2445
|
+
return data[requestParam.name]?.toString() || "";
|
|
2425
2446
|
});
|
|
2426
2447
|
statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
2427
|
-
var _a2;
|
|
2428
2448
|
const requestParam = routeData.request.find((item) => {
|
|
2429
2449
|
return item.name === value.replace("#", "");
|
|
2430
2450
|
});
|
|
2431
2451
|
if (!requestParam)
|
|
2432
2452
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
2433
|
-
return
|
|
2453
|
+
return data[requestParam.name]?.toString() || "";
|
|
2434
2454
|
});
|
|
2435
2455
|
statement = filterPsqlParser_default.parse(statement);
|
|
2436
2456
|
if (whereClause.startsWith("WHERE")) {
|
|
@@ -2671,8 +2691,9 @@ import { FileUtils as FileUtils2 } from "@restura/internal";
|
|
|
2671
2691
|
import Bluebird3 from "bluebird";
|
|
2672
2692
|
import * as os2 from "os";
|
|
2673
2693
|
var TempCache = class {
|
|
2694
|
+
location;
|
|
2695
|
+
maxDurationDays = 7;
|
|
2674
2696
|
constructor(location) {
|
|
2675
|
-
this.maxDurationDays = 7;
|
|
2676
2697
|
this.location = location || os2.tmpdir();
|
|
2677
2698
|
FileUtils2.ensureDir(this.location).catch((e) => {
|
|
2678
2699
|
throw e;
|
|
@@ -2697,15 +2718,24 @@ var TempCache = class {
|
|
|
2697
2718
|
|
|
2698
2719
|
// src/restura/restura.ts
|
|
2699
2720
|
var ResturaEngine = class {
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2721
|
+
// Make public so other modules can access without re-parsing the config
|
|
2722
|
+
resturaConfig;
|
|
2723
|
+
multerCommonUpload;
|
|
2724
|
+
resturaRouter;
|
|
2725
|
+
publicEndpoints = {
|
|
2726
|
+
GET: [],
|
|
2727
|
+
POST: [],
|
|
2728
|
+
PUT: [],
|
|
2729
|
+
PATCH: [],
|
|
2730
|
+
DELETE: []
|
|
2731
|
+
};
|
|
2732
|
+
expressApp;
|
|
2733
|
+
schema;
|
|
2734
|
+
responseValidator;
|
|
2735
|
+
authenticationHandler;
|
|
2736
|
+
customTypeValidation;
|
|
2737
|
+
psqlConnectionPool;
|
|
2738
|
+
psqlEngine;
|
|
2709
2739
|
/**
|
|
2710
2740
|
* Initializes the Restura engine with the provided Express application.
|
|
2711
2741
|
*
|
|
@@ -2713,11 +2743,11 @@ var ResturaEngine = class {
|
|
|
2713
2743
|
* @returns A promise that resolves when the initialization is complete.
|
|
2714
2744
|
*/
|
|
2715
2745
|
async init(app, authenticationHandler, psqlConnectionPool) {
|
|
2716
|
-
this.resturaConfig = config2.validate("restura", resturaConfigSchema);
|
|
2746
|
+
this.resturaConfig = await config2.validate("restura", resturaConfigSchema);
|
|
2717
2747
|
this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
|
|
2718
2748
|
new TempCache(this.resturaConfig.fileTempCachePath);
|
|
2719
2749
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
2720
|
-
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
|
|
2750
|
+
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true, this.resturaConfig.scratchDatabaseSuffix);
|
|
2721
2751
|
await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
|
|
2722
2752
|
this.authenticationHandler = authenticationHandler;
|
|
2723
2753
|
app.use(compression());
|
|
@@ -2726,7 +2756,7 @@ var ResturaEngine = class {
|
|
|
2726
2756
|
app.use(cookieParser());
|
|
2727
2757
|
app.disable("x-powered-by");
|
|
2728
2758
|
app.use("/", addApiResponseFunctions);
|
|
2729
|
-
app.use("/api/",
|
|
2759
|
+
app.use("/api/", authenticateRequester(this.authenticationHandler));
|
|
2730
2760
|
app.use("/restura", this.resturaAuthentication);
|
|
2731
2761
|
app.put(
|
|
2732
2762
|
"/restura/v1/schema",
|
|
@@ -2747,7 +2777,7 @@ var ResturaEngine = class {
|
|
|
2747
2777
|
}
|
|
2748
2778
|
/**
|
|
2749
2779
|
* Determines if a given endpoint is public based on the HTTP method and full URL. This
|
|
2750
|
-
* is determined on whether the endpoint in the schema has no roles assigned to it.
|
|
2780
|
+
* is determined on whether the endpoint in the schema has no roles or scopes assigned to it.
|
|
2751
2781
|
*
|
|
2752
2782
|
* @param method - The HTTP method (e.g., 'GET', 'POST', 'PUT', 'PATCH', 'DELETE').
|
|
2753
2783
|
* @param fullUrl - The full URL of the endpoint.
|
|
@@ -2836,7 +2866,8 @@ var ResturaEngine = class {
|
|
|
2836
2866
|
route.path = route.path.startsWith("/") ? route.path : `/${route.path}`;
|
|
2837
2867
|
route.path = route.path.endsWith("/") ? route.path.slice(0, -1) : route.path;
|
|
2838
2868
|
const fullUrl = `${baseUrl}${route.path}`;
|
|
2839
|
-
if (route.roles.length === 0
|
|
2869
|
+
if (route.roles.length === 0 && route.scopes.length === 0)
|
|
2870
|
+
this.publicEndpoints[route.method].push(fullUrl);
|
|
2840
2871
|
this.resturaRouter[route.method.toLowerCase()](
|
|
2841
2872
|
route.path,
|
|
2842
2873
|
// <-- Notice we only use path here since the baseUrl is already added to the router.
|
|
@@ -2886,10 +2917,10 @@ var ResturaEngine = class {
|
|
|
2886
2917
|
);
|
|
2887
2918
|
this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
|
|
2888
2919
|
}
|
|
2889
|
-
async getSchema(
|
|
2920
|
+
async getSchema(_req, res) {
|
|
2890
2921
|
res.send({ data: this.schema });
|
|
2891
2922
|
}
|
|
2892
|
-
async getSchemaAndTypes(
|
|
2923
|
+
async getSchemaAndTypes(_req, res) {
|
|
2893
2924
|
try {
|
|
2894
2925
|
const schema = await this.getLatestFileSystemSchema();
|
|
2895
2926
|
const apiText = await apiGenerator(schema);
|
|
@@ -2900,8 +2931,7 @@ var ResturaEngine = class {
|
|
|
2900
2931
|
}
|
|
2901
2932
|
}
|
|
2902
2933
|
async getMulterFilesIfAny(req, res, routeData) {
|
|
2903
|
-
|
|
2904
|
-
if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
|
|
2934
|
+
if (!req.header("content-type")?.includes("multipart/form-data")) return;
|
|
2905
2935
|
if (!this.isCustomRoute(routeData)) return;
|
|
2906
2936
|
if (!routeData.fileUploadType) {
|
|
2907
2937
|
throw new RsError("BAD_REQUEST", "File upload type not defined for route");
|
|
@@ -2964,16 +2994,17 @@ var ResturaEngine = class {
|
|
|
2964
2994
|
await customFunction(req, res, routeData);
|
|
2965
2995
|
}
|
|
2966
2996
|
async storeFileSystemSchema() {
|
|
2967
|
-
const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema),
|
|
2968
|
-
parser: "json"
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2997
|
+
const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), {
|
|
2998
|
+
parser: "json",
|
|
2999
|
+
...{
|
|
3000
|
+
trailingComma: "none",
|
|
3001
|
+
tabWidth: 4,
|
|
3002
|
+
useTabs: true,
|
|
3003
|
+
endOfLine: "lf",
|
|
3004
|
+
printWidth: 120,
|
|
3005
|
+
singleQuote: true
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
2977
3008
|
fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
|
|
2978
3009
|
}
|
|
2979
3010
|
resetPublicEndpoints() {
|
|
@@ -2986,9 +3017,13 @@ var ResturaEngine = class {
|
|
|
2986
3017
|
};
|
|
2987
3018
|
}
|
|
2988
3019
|
validateAuthorization(req, routeData) {
|
|
2989
|
-
const
|
|
2990
|
-
|
|
2991
|
-
if (
|
|
3020
|
+
const requesterRole = req.requesterDetails.role;
|
|
3021
|
+
const requesterScopes = req.requesterDetails.scopes;
|
|
3022
|
+
if (routeData.roles.length === 0 && routeData.scopes.length === 0) return;
|
|
3023
|
+
if (requesterRole && routeData.roles.length > 0 && !routeData.roles.includes(requesterRole))
|
|
3024
|
+
throw new RsError("FORBIDDEN", "Not authorized to access this endpoint");
|
|
3025
|
+
if (requesterScopes.length > 0 && routeData.scopes.length > 0 && !routeData.scopes.some((scope) => requesterScopes.includes(scope)))
|
|
3026
|
+
throw new RsError("FORBIDDEN", "Not authorized to access this endpoint");
|
|
2992
3027
|
}
|
|
2993
3028
|
getRouteData(method, baseUrl, path5) {
|
|
2994
3029
|
const endpoint = this.schema.endpoints.find((item) => {
|
|
@@ -3042,6 +3077,9 @@ var PsqlTransaction = class extends PsqlConnection {
|
|
|
3042
3077
|
this.connectPromise = this.client.connect();
|
|
3043
3078
|
this.beginTransactionPromise = this.beginTransaction();
|
|
3044
3079
|
}
|
|
3080
|
+
client;
|
|
3081
|
+
beginTransactionPromise;
|
|
3082
|
+
connectPromise;
|
|
3045
3083
|
async close() {
|
|
3046
3084
|
if (this.client) {
|
|
3047
3085
|
await this.client.end();
|