@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.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} ${info.message}`;
32
+ return `[${info.timestamp}] ${info.level} ${info.message}`;
64
33
  })
65
34
  );
66
35
  var logger = winston.createLogger({
67
- level: loggerConfig.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
- //defaultMeta: { service: 'user-service' },
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
- constructor() {
92
- this.actionHandlers = {
93
- DATABASE_ROW_DELETE: [],
94
- DATABASE_ROW_INSERT: [],
95
- DATABASE_COLUMN_UPDATE: []
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
- constructor() {
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 = (_a2 = process.argv[1]) == null ? void 0 : _a2.endsWith(".ts");
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 == null ? void 0 : table.columns.find((c) => c.name == columnName);
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 == null ? void 0 : table.columns.find((c) => c.name == columnName);
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, __spreadValues({
869
- parser: "typescript"
870
- }, {
871
- trailingComma: "none",
872
- tabWidth: 4,
873
- useTabs: true,
874
- endOfLine: "lf",
875
- printWidth: 120,
876
- singleQuote: true
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(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
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, __spreadValues({
933
- parser: "typescript"
934
- }, {
935
- trailingComma: "none",
936
- tabWidth: 4,
937
- useTabs: true,
938
- endOfLine: "lf",
939
- printWidth: 120,
940
- singleQuote: true
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 = __spreadValues({
970
+ const errorData = {
994
971
  err: shortError,
995
- msg
996
- }, restura.resturaConfig.sendErrorStackTrace && stack ? { stack } : {});
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/authenticateUser.ts
1003
- function authenticateUser(applicationAuthenticateHandler) {
980
+ // src/restura/middleware/authenticateRequester.ts
981
+ function authenticateRequester(applicationAuthenticateHandler) {
1004
982
  return (req, res, next) => {
1005
- applicationAuthenticateHandler(req, res, (userDetails) => {
1006
- req.requesterDetails = __spreadValues({ host: req.hostname, ipAddress: req.ip || "" }, userDetails);
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 (e) {
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 (e) {
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 _a;
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 = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
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 ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
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 = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
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 ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
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();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
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.doesRoleHavePermissionToTable(req.requesterDetails.role, schema, routeData.table))
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
- doesRoleHavePermissionToColumn(role, schema, item, joins) {
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
- const doesColumnHaveRoles = ObjectUtils3.isArrayWithData(columnSchema.roles);
1778
- if (!doesColumnHaveRoles) return true;
1779
- if (!role) return false;
1780
- return columnSchema.roles.includes(role);
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.doesRoleHavePermissionToColumn(role, schema, nestedItem, joins);
1771
+ return this.canRequesterAccessColumn(requesterRole, requesterScopes, schema, nestedItem, joins);
1786
1772
  })
1787
1773
  );
1788
1774
  }
1789
1775
  return false;
1790
1776
  }
1791
- doesRoleHavePermissionToTable(userRole, schema, tableName) {
1777
+ canRequesterAccessTable(requesterRole, requesterScopes, schema, tableName) {
1792
1778
  const tableSchema = this.getTableSchema(schema, tableName);
1793
- const doesTableHaveRoles = ObjectUtils3.isArrayWithData(tableSchema.roles);
1794
- if (!doesTableHaveRoles) return true;
1795
- if (!userRole) return false;
1796
- return tableSchema.roles.includes(userRole);
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
- (_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
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
- (_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
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 = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
1915
- /
1916
- text:text { return quoteSqlIdentity(text); }
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 getScratchPool() {
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.psqlConnectionPool.poolConfig.database}_scratch';`,
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.psqlConnectionPool.poolConfig.database + "_scratch",
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
- `SELECT pg_description.description
2122
- FROM pg_description
2123
- JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2124
- WHERE pg_namespace.nspname = 'public';`,
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 ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
2140
+ if (schemaComment[0]?.description) {
2129
2141
  await scratchPool.runQuery(
2130
- `COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
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.getScratchPool();
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.psqlConnectionPool.poolConfig.database + "_scratch",
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, userRole, sqlParams) {
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.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
2168
- ...routeData.joins,
2169
- ...item.subquery.joins
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.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
2178
- ...routeData.joins,
2179
- ...item.subquery.joins
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, userRole, sqlParams)}
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, __spreadValues(__spreadValues({}, req.data), parameterObj));
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, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
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.doesRoleHavePermissionToColumn(userRole, schema, item, routeData.joins))
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, userRole, sqlParams)} AS ${escapeColumnName(
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 _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
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, userRole, sqlParams) {
2361
+ generateJoinStatements(req, joins, baseTable, routeData, schema, sqlParams) {
2342
2362
  let joinStatements = "";
2343
2363
  joins.forEach((item) => {
2344
- if (!this.doesRoleHavePermissionToTable(userRole, schema, item.table))
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" && !!(data == null ? void 0 : data.filter)) {
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 ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
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 ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
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
- constructor() {
2701
- this.publicEndpoints = {
2702
- GET: [],
2703
- POST: [],
2704
- PUT: [],
2705
- PATCH: [],
2706
- DELETE: []
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/", authenticateUser(this.authenticationHandler));
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) this.publicEndpoints[route.method].push(fullUrl);
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(req, res) {
2920
+ async getSchema(_req, res) {
2890
2921
  res.send({ data: this.schema });
2891
2922
  }
2892
- async getSchemaAndTypes(req, res) {
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
- var _a2;
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), __spreadValues({
2968
- parser: "json"
2969
- }, {
2970
- trailingComma: "none",
2971
- tabWidth: 4,
2972
- useTabs: true,
2973
- endOfLine: "lf",
2974
- printWidth: 120,
2975
- singleQuote: true
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 role = req.requesterDetails.role;
2990
- if (routeData.roles.length === 0 || !role) return;
2991
- if (!routeData.roles.includes(role)) throw new RsError("FORBIDDEN", "Not authorized to access this endpoint");
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();