@restura/core 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -878,7 +878,7 @@ declare namespace Restura {
878
878
  }
879
879
 
880
880
  // src/restura/restura.ts
881
- import { ObjectUtils as ObjectUtils5, StringUtils as StringUtils3 } from "@redskytech/core-utils";
881
+ import { ObjectUtils as ObjectUtils4, StringUtils as StringUtils3 } from "@redskytech/core-utils";
882
882
  import { config as config2 } from "@restura/internal";
883
883
 
884
884
  // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
@@ -1093,7 +1093,120 @@ import fs2 from "fs";
1093
1093
  import path2, { resolve } from "path";
1094
1094
  import tmp from "tmp";
1095
1095
  import * as TJS from "typescript-json-schema";
1096
- function customTypeValidationGenerator(currentSchema) {
1096
+
1097
+ // src/restura/generators/schemaGeneratorUtils.ts
1098
+ function buildRouteSchema(requestParams) {
1099
+ const properties = {};
1100
+ const required = [];
1101
+ for (const param of requestParams) {
1102
+ if (param.required) {
1103
+ required.push(param.name);
1104
+ }
1105
+ const propertySchema = buildPropertySchemaFromRequest(param);
1106
+ properties[param.name] = propertySchema;
1107
+ }
1108
+ return {
1109
+ type: "object",
1110
+ properties,
1111
+ ...required.length > 0 && { required },
1112
+ // Only include if not empty
1113
+ additionalProperties: false
1114
+ };
1115
+ }
1116
+ function buildPropertySchemaFromRequest(param) {
1117
+ const propertySchema = {};
1118
+ const typeCheckValidator = param.validator.find((v) => v.type === "TYPE_CHECK");
1119
+ const oneOfValidator = param.validator.find((v) => v.type === "ONE_OF");
1120
+ if (oneOfValidator && Array.isArray(oneOfValidator.value)) {
1121
+ propertySchema.enum = oneOfValidator.value;
1122
+ if (!typeCheckValidator && oneOfValidator.value.length > 0) {
1123
+ const firstValue = oneOfValidator.value[0];
1124
+ propertySchema.type = typeof firstValue === "number" ? "number" : "string";
1125
+ return propertySchema;
1126
+ }
1127
+ }
1128
+ if (!typeCheckValidator) {
1129
+ return propertySchema;
1130
+ }
1131
+ const typeValue = typeCheckValidator.value;
1132
+ if (typeof typeValue === "string" && typeValue.endsWith("[]")) {
1133
+ const itemType = typeValue.replace("[]", "");
1134
+ if (param.isNullable) {
1135
+ propertySchema.type = ["array", "null"];
1136
+ } else {
1137
+ propertySchema.type = "array";
1138
+ }
1139
+ propertySchema.items = {
1140
+ type: mapTypeToJsonSchemaType(itemType)
1141
+ };
1142
+ applyArrayValidators(propertySchema, param);
1143
+ } else {
1144
+ if (param.isNullable) {
1145
+ propertySchema.type = [mapTypeToJsonSchemaType(typeValue), "null"];
1146
+ } else {
1147
+ propertySchema.type = mapTypeToJsonSchemaType(typeValue);
1148
+ }
1149
+ const type = propertySchema.type;
1150
+ const isNumericType = type === "number" || type === "integer" || Array.isArray(type) && (type.includes("number") || type.includes("integer"));
1151
+ const isStringType = type === "string" || Array.isArray(type) && type.includes("string");
1152
+ if (isNumericType) {
1153
+ applyNumericValidators(propertySchema, param);
1154
+ } else if (isStringType) {
1155
+ applyStringValidators(propertySchema, param);
1156
+ }
1157
+ }
1158
+ return propertySchema;
1159
+ }
1160
+ function mapTypeToJsonSchemaType(type) {
1161
+ if (typeof type !== "string") {
1162
+ throw new Error(`Invalid type for JSON Schema mapping: ${type}`);
1163
+ }
1164
+ switch (type) {
1165
+ case "number":
1166
+ return "number";
1167
+ case "string":
1168
+ return "string";
1169
+ case "boolean":
1170
+ return "boolean";
1171
+ case "object":
1172
+ return "object";
1173
+ default:
1174
+ throw new Error(`Unknown type: ${type}`);
1175
+ }
1176
+ }
1177
+ function applyNumericValidators(propertySchema, param) {
1178
+ for (const validator of param.validator) {
1179
+ if (validator.type === "MIN" && typeof validator.value === "number") {
1180
+ propertySchema.minimum = validator.value;
1181
+ }
1182
+ if (validator.type === "MAX" && typeof validator.value === "number") {
1183
+ propertySchema.maximum = validator.value;
1184
+ }
1185
+ }
1186
+ }
1187
+ function applyStringValidators(propertySchema, param) {
1188
+ for (const validator of param.validator) {
1189
+ if (validator.type === "MIN" && typeof validator.value === "number") {
1190
+ propertySchema.minLength = validator.value;
1191
+ }
1192
+ if (validator.type === "MAX" && typeof validator.value === "number") {
1193
+ propertySchema.maxLength = validator.value;
1194
+ }
1195
+ }
1196
+ }
1197
+ function applyArrayValidators(propertySchema, param) {
1198
+ for (const validator of param.validator) {
1199
+ if (validator.type === "MIN" && typeof validator.value === "number") {
1200
+ propertySchema.minItems = validator.value;
1201
+ }
1202
+ if (validator.type === "MAX" && typeof validator.value === "number") {
1203
+ propertySchema.maxItems = validator.value;
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ // src/restura/generators/customTypeValidationGenerator.ts
1209
+ function customTypeValidationGenerator(currentSchema, ignoreGeneratedTypes = false) {
1097
1210
  const schemaObject = {};
1098
1211
  const customInterfaceNames = currentSchema.customTypes.map((customType) => {
1099
1212
  const matches = customType.match(/(?<=interface\s)(\w+)|(?<=type\s)(\w+)/g);
@@ -1111,9 +1224,11 @@ function customTypeValidationGenerator(currentSchema) {
1111
1224
  const program = TJS.getProgramFromFiles(
1112
1225
  [
1113
1226
  resolve(temporaryFile.name),
1114
- path2.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"),
1115
- path2.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"),
1116
- path2.join(restura.resturaConfig.generatedTypesPath, "api.d.ts")
1227
+ ...ignoreGeneratedTypes ? [] : [
1228
+ path2.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"),
1229
+ path2.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"),
1230
+ path2.join(restura.resturaConfig.generatedTypesPath, "api.d.ts")
1231
+ ]
1117
1232
  ],
1118
1233
  compilerOptions
1119
1234
  );
@@ -1124,6 +1239,35 @@ function customTypeValidationGenerator(currentSchema) {
1124
1239
  schemaObject[item] = ddlSchema || {};
1125
1240
  });
1126
1241
  temporaryFile.removeCallback();
1242
+ for (const endpoint of currentSchema.endpoints) {
1243
+ for (const route of endpoint.routes) {
1244
+ if (route.type !== "CUSTOM_ONE" && route.type !== "CUSTOM_ARRAY" && route.type !== "CUSTOM_PAGED") continue;
1245
+ if (!route.request || !Array.isArray(route.request)) continue;
1246
+ const routeKey = `${route.method}:${route.path}`;
1247
+ schemaObject[routeKey] = buildRouteSchema(route.request);
1248
+ }
1249
+ }
1250
+ return schemaObject;
1251
+ }
1252
+
1253
+ // src/restura/generators/standardTypeValidationGenerator.ts
1254
+ function standardTypeValidationGenerator(currentSchema) {
1255
+ const schemaObject = {};
1256
+ for (const endpoint of currentSchema.endpoints) {
1257
+ for (const route of endpoint.routes) {
1258
+ if (route.type !== "ONE" && route.type !== "ARRAY" && route.type !== "PAGED") continue;
1259
+ const routeKey = `${route.method}:${route.path}`;
1260
+ if (!route.request || route.request.length === 0) {
1261
+ schemaObject[routeKey] = {
1262
+ type: "object",
1263
+ properties: {},
1264
+ additionalProperties: false
1265
+ };
1266
+ } else {
1267
+ schemaObject[routeKey] = buildRouteSchema(route.request);
1268
+ }
1269
+ }
1270
+ }
1127
1271
  return schemaObject;
1128
1272
  }
1129
1273
 
@@ -1496,174 +1640,41 @@ async function isSchemaValid(schemaToCheck) {
1496
1640
  }
1497
1641
 
1498
1642
  // src/restura/validators/requestValidator.ts
1499
- import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
1500
1643
  import jsonschema from "jsonschema";
1501
- import { z as z4 } from "zod";
1502
-
1503
- // src/restura/utils/utils.ts
1504
- function addQuotesToStrings(variable) {
1505
- if (typeof variable === "string") {
1506
- return `'${variable}'`;
1507
- } else if (Array.isArray(variable)) {
1508
- const arrayWithQuotes = variable.map(addQuotesToStrings);
1509
- return arrayWithQuotes;
1510
- } else {
1511
- return variable;
1512
- }
1513
- }
1514
- function sortObjectKeysAlphabetically(obj) {
1515
- if (Array.isArray(obj)) {
1516
- return obj.map(sortObjectKeysAlphabetically);
1517
- } else if (obj !== null && typeof obj === "object") {
1518
- return Object.keys(obj).sort().reduce((sorted, key) => {
1519
- sorted[key] = sortObjectKeysAlphabetically(obj[key]);
1520
- return sorted;
1521
- }, {});
1522
- }
1523
- return obj;
1524
- }
1525
-
1526
- // src/restura/validators/requestValidator.ts
1527
- function requestValidator(req, routeData, validationSchema) {
1528
- const requestData = getRequestData(req);
1529
- req.data = requestData;
1530
- if (routeData.request === void 0) {
1531
- if (routeData.type !== "CUSTOM_ONE" && routeData.type !== "CUSTOM_ARRAY" && routeData.type !== "CUSTOM_PAGED")
1532
- throw new RsError("BAD_REQUEST", `No request parameters provided for standard request.`);
1644
+ function requestValidator(req, routeData, customValidationSchema, standardValidationSchema) {
1645
+ let schemaForCoercion;
1646
+ if (routeData.type === "ONE" || routeData.type === "ARRAY" || routeData.type === "PAGED") {
1647
+ const routeKey = `${routeData.method}:${routeData.path}`;
1648
+ schemaForCoercion = standardValidationSchema[routeKey];
1649
+ if (!schemaForCoercion) {
1650
+ throw new RsError("BAD_REQUEST", `No schema found for standard request route: ${routeKey}.`);
1651
+ }
1652
+ } else if (routeData.type === "CUSTOM_ONE" || routeData.type === "CUSTOM_ARRAY" || routeData.type === "CUSTOM_PAGED") {
1533
1653
  if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
1534
- if (!routeData.requestType) throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
1535
- const currentInterface = validationSchema[routeData.requestType];
1536
- const validator = new jsonschema.Validator();
1537
- const strictSchema = {
1654
+ if (!routeData.requestType && !routeData.request)
1655
+ throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
1656
+ const routeKey = `${routeData.method}:${routeData.path}`;
1657
+ const currentInterface = customValidationSchema[routeData.requestType || routeKey];
1658
+ schemaForCoercion = {
1538
1659
  ...currentInterface,
1539
1660
  additionalProperties: false
1540
1661
  };
1541
- const executeValidation = validator.validate(req.data, strictSchema);
1542
- if (!executeValidation.valid) {
1543
- throw new RsError(
1544
- "BAD_REQUEST",
1545
- `Request custom setup has failed the following check: (${executeValidation.errors})`
1546
- );
1547
- }
1548
- return;
1549
- }
1550
- Object.keys(req.data).forEach((requestParamName) => {
1551
- const requestParam = routeData.request.find((param) => param.name === requestParamName);
1552
- if (!requestParam) {
1553
- throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not allowed`);
1554
- }
1555
- });
1556
- routeData.request.forEach((requestParam) => {
1557
- const requestValue = requestData[requestParam.name];
1558
- if (requestParam.required && requestValue === void 0)
1559
- throw new RsError("BAD_REQUEST", `Request param (${requestParam.name}) is required but missing`);
1560
- else if (!requestParam.required && requestValue === void 0) return;
1561
- validateRequestSingleParam(requestValue, requestParam);
1562
- });
1563
- }
1564
- function validateRequestSingleParam(requestValue, requestParam) {
1565
- if (requestParam.isNullable && requestValue === null) return;
1566
- requestParam.validator.forEach((validator) => {
1567
- switch (validator.type) {
1568
- case "TYPE_CHECK":
1569
- performTypeCheck(requestValue, validator, requestParam.name);
1570
- break;
1571
- case "MIN":
1572
- performMinCheck(requestValue, validator, requestParam.name);
1573
- break;
1574
- case "MAX":
1575
- performMaxCheck(requestValue, validator, requestParam.name);
1576
- break;
1577
- case "ONE_OF":
1578
- performOneOfCheck(requestValue, validator, requestParam.name);
1579
- break;
1580
- }
1581
- });
1582
- }
1583
- function isValidType(type, requestValue) {
1584
- try {
1585
- expectValidType(type, requestValue);
1586
- return true;
1587
- } catch {
1588
- return false;
1589
- }
1590
- }
1591
- function expectValidType(type, requestValue) {
1592
- if (type === "number") {
1593
- return z4.number().parse(requestValue);
1594
- }
1595
- if (type === "string") {
1596
- return z4.string().parse(requestValue);
1597
- }
1598
- if (type === "boolean") {
1599
- return z4.boolean().parse(requestValue);
1600
- }
1601
- if (type === "string[]") {
1602
- return z4.array(z4.string()).parse(requestValue);
1603
- }
1604
- if (type === "number[]") {
1605
- return z4.array(z4.number()).parse(requestValue);
1606
- }
1607
- if (type === "any[]") {
1608
- return z4.array(z4.any()).parse(requestValue);
1609
- }
1610
- if (type === "object") {
1611
- return z4.object({}).strict().parse(requestValue);
1612
- }
1613
- }
1614
- function performTypeCheck(requestValue, validator, requestParamName) {
1615
- if (!isValidType(validator.value, requestValue)) {
1616
- throw new RsError(
1617
- "BAD_REQUEST",
1618
- `Request param (${requestParamName}) with value (${addQuotesToStrings(requestValue)}) is not of type (${validator.value})`
1619
- );
1662
+ } else {
1663
+ throw new RsError("BAD_REQUEST", `Invalid route type: ${routeData.type}`);
1620
1664
  }
1621
- try {
1622
- validatorDataSchemeValue.parse(validator.value);
1623
- } catch {
1624
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not a valid type`);
1665
+ const requestData = getRequestData(req, schemaForCoercion);
1666
+ req.data = requestData;
1667
+ const validator = new jsonschema.Validator();
1668
+ const executeValidation = validator.validate(req.data, schemaForCoercion);
1669
+ if (!executeValidation.valid) {
1670
+ const errorMessages = executeValidation.errors.map((err) => {
1671
+ const property = err.property.replace("instance.", "");
1672
+ return `${property}: ${err.message}`;
1673
+ }).join(", ");
1674
+ throw new RsError("BAD_REQUEST", `Request validation failed: ${errorMessages}`);
1625
1675
  }
1626
1676
  }
1627
- function expectOnlyNumbers(requestValue, validator, requestParamName) {
1628
- if (!isValueNumber(requestValue))
1629
- throw new RsError(
1630
- "BAD_REQUEST",
1631
- `Request param (${requestParamName}) with value (${requestValue}) is not of type number`
1632
- );
1633
- if (!isValueNumber(validator.value))
1634
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value} is not of type number`);
1635
- }
1636
- function performMinCheck(requestValue, validator, requestParamName) {
1637
- expectOnlyNumbers(requestValue, validator, requestParamName);
1638
- if (requestValue < validator.value)
1639
- throw new RsError(
1640
- "BAD_REQUEST",
1641
- `Request param (${requestParamName}) with value (${requestValue}) is less than (${validator.value})`
1642
- );
1643
- }
1644
- function performMaxCheck(requestValue, validator, requestParamName) {
1645
- expectOnlyNumbers(requestValue, validator, requestParamName);
1646
- if (requestValue > validator.value)
1647
- throw new RsError(
1648
- "BAD_REQUEST",
1649
- `Request param (${requestParamName}) with value (${requestValue}) is more than (${validator.value})`
1650
- );
1651
- }
1652
- function performOneOfCheck(requestValue, validator, requestParamName) {
1653
- if (!ObjectUtils2.isArrayWithData(validator.value))
1654
- throw new RsError("SCHEMA_ERROR", `Schema validator value (${validator.value}) is not of type array`);
1655
- if (typeof requestValue === "object")
1656
- throw new RsError("BAD_REQUEST", `Request param (${requestParamName}) is not of type string or number`);
1657
- if (!validator.value.includes(requestValue))
1658
- throw new RsError(
1659
- "BAD_REQUEST",
1660
- `Request param (${requestParamName}) with value (${requestValue}) is not one of (${validator.value.join(", ")})`
1661
- );
1662
- }
1663
- function isValueNumber(value) {
1664
- return !isNaN(Number(value));
1665
- }
1666
- function getRequestData(req) {
1677
+ function getRequestData(req, schema) {
1667
1678
  let body = "";
1668
1679
  if (req.method === "GET" || req.method === "DELETE") {
1669
1680
  body = "query";
@@ -1671,50 +1682,74 @@ function getRequestData(req) {
1671
1682
  body = "body";
1672
1683
  }
1673
1684
  const bodyData = req[body];
1674
- if (bodyData && body === "query") {
1675
- const normalizedData = {};
1676
- for (const attr in bodyData) {
1677
- if (attr.includes("[]") && !(bodyData[attr] instanceof Array)) {
1678
- bodyData[attr] = [bodyData[attr]];
1685
+ if (bodyData && body === "query" && schema) {
1686
+ return coerceBySchema(bodyData, schema);
1687
+ }
1688
+ return bodyData;
1689
+ }
1690
+ function coerceBySchema(data, schema) {
1691
+ const normalized = {};
1692
+ const properties = schema.properties || {};
1693
+ for (const attr in data) {
1694
+ const cleanAttr = attr.replace(/\[\]$/, "");
1695
+ const isArrayNotation = attr.includes("[]");
1696
+ let value = data[attr];
1697
+ const propertySchema = properties[cleanAttr];
1698
+ if (isArrayNotation && !Array.isArray(value)) {
1699
+ value = [value];
1700
+ }
1701
+ if (!propertySchema) {
1702
+ normalized[cleanAttr] = value;
1703
+ continue;
1704
+ }
1705
+ if (Array.isArray(value)) {
1706
+ const itemSchema = Array.isArray(propertySchema.items) ? propertySchema.items[0] : propertySchema.items || { type: "string" };
1707
+ normalized[cleanAttr] = value.map((item) => coerceValue(item, itemSchema));
1708
+ } else {
1709
+ normalized[cleanAttr] = coerceValue(value, propertySchema);
1710
+ }
1711
+ }
1712
+ return normalized;
1713
+ }
1714
+ function coerceValue(value, propertySchema) {
1715
+ if (value === void 0 || value === null) {
1716
+ return value;
1717
+ }
1718
+ const targetType = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type;
1719
+ if (value === "") {
1720
+ return targetType === "string" ? "" : void 0;
1721
+ }
1722
+ switch (targetType) {
1723
+ case "number":
1724
+ case "integer":
1725
+ const num = Number(value);
1726
+ return isNaN(num) ? value : num;
1727
+ case "boolean":
1728
+ if (value === "true") return true;
1729
+ if (value === "false") return false;
1730
+ if (typeof value === "string") {
1731
+ return value === "true" || value === "1";
1679
1732
  }
1680
- const cleanAttr = attr.replace(/\[\]$/, "");
1681
- if (bodyData[attr] instanceof Array) {
1682
- const parsedList = bodyData[attr].map((value) => {
1683
- if (value === "true") return true;
1684
- if (value === "false") return false;
1685
- if (value === void 0) return void 0;
1686
- if (value === "") return "";
1687
- const parsed = ObjectUtils2.safeParse(value);
1688
- return isNaN(Number(parsed)) ? parsed : Number(parsed);
1689
- });
1690
- normalizedData[cleanAttr] = parsedList;
1691
- } else {
1692
- let value = bodyData[attr];
1693
- if (value === "true") {
1694
- value = true;
1695
- } else if (value === "false") {
1696
- value = false;
1697
- } else if (value === void 0) {
1698
- value = void 0;
1699
- } else if (value === "") {
1700
- value = "";
1701
- } else {
1702
- value = ObjectUtils2.safeParse(value);
1703
- if (!isNaN(Number(value))) {
1704
- value = Number(value);
1705
- }
1733
+ return Boolean(value);
1734
+ case "string":
1735
+ return String(value);
1736
+ case "object":
1737
+ if (typeof value === "string") {
1738
+ try {
1739
+ return JSON.parse(value);
1740
+ } catch {
1741
+ return value;
1706
1742
  }
1707
- normalizedData[cleanAttr] = value;
1708
1743
  }
1709
- }
1710
- return normalizedData;
1744
+ return value;
1745
+ default:
1746
+ return value;
1711
1747
  }
1712
- return bodyData;
1713
1748
  }
1714
1749
 
1715
1750
  // src/restura/middleware/schemaValidation.ts
1716
1751
  async function schemaValidation(req, res, next) {
1717
- req.data = getRequestData(req);
1752
+ req.data = getRequestData(req, {});
1718
1753
  try {
1719
1754
  resturaSchema.parse(req.data);
1720
1755
  next();
@@ -1725,22 +1760,22 @@ async function schemaValidation(req, res, next) {
1725
1760
  }
1726
1761
 
1727
1762
  // src/restura/schemas/resturaConfigSchema.ts
1728
- import { z as z5 } from "zod";
1763
+ import { z as z4 } from "zod";
1729
1764
  var isTsx = process.argv[1]?.endsWith(".ts");
1730
1765
  var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
1731
1766
  var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
1732
- var resturaConfigSchema = z5.object({
1733
- authToken: z5.string().min(1, "Missing Restura Auth Token"),
1734
- sendErrorStackTrace: z5.boolean().default(false),
1735
- schemaFilePath: z5.string().default(process.cwd() + "/restura.schema.json"),
1736
- customApiFolderPath: z5.string().default(process.cwd() + customApiFolderPath),
1737
- generatedTypesPath: z5.string().default(process.cwd() + "/src/@types"),
1738
- fileTempCachePath: z5.string().optional(),
1739
- scratchDatabaseSuffix: z5.string().optional()
1767
+ var resturaConfigSchema = z4.object({
1768
+ authToken: z4.string().min(1, "Missing Restura Auth Token"),
1769
+ sendErrorStackTrace: z4.boolean().default(false),
1770
+ schemaFilePath: z4.string().default(process.cwd() + "/restura.schema.json"),
1771
+ customApiFolderPath: z4.string().default(process.cwd() + customApiFolderPath),
1772
+ generatedTypesPath: z4.string().default(process.cwd() + "/src/@types"),
1773
+ fileTempCachePath: z4.string().optional(),
1774
+ scratchDatabaseSuffix: z4.string().optional()
1740
1775
  });
1741
1776
 
1742
1777
  // src/restura/sql/PsqlEngine.ts
1743
- import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
1778
+ import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
1744
1779
  import getDiff from "@wmfs/pg-diff-sync";
1745
1780
  import pgInfo from "@wmfs/pg-info";
1746
1781
  import pg2 from "pg";
@@ -1752,7 +1787,7 @@ import pg from "pg";
1752
1787
  import crypto from "crypto";
1753
1788
  import format2 from "pg-format";
1754
1789
  import { format as sqlFormat } from "sql-formatter";
1755
- import { z as z6 } from "zod";
1790
+ import { z as z5 } from "zod";
1756
1791
 
1757
1792
  // src/restura/sql/PsqlUtils.ts
1758
1793
  import format from "pg-format";
@@ -1788,20 +1823,25 @@ function insertObjectQuery(table, obj) {
1788
1823
  INSERT INTO "${table}" (${columns})
1789
1824
  VALUES (${values})
1790
1825
  RETURNING *`;
1791
- query = query.replace(/'(\?)'/, "?");
1826
+ query = query.replace(/'(\?)'/g, "?");
1792
1827
  return query;
1793
1828
  }
1794
- function updateObjectQuery(table, obj, whereStatement) {
1829
+ function updateObjectQuery(table, obj, whereStatement, incrementSyncVersion = false) {
1795
1830
  const setArray = [];
1796
1831
  for (const i in obj) {
1797
- setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
1832
+ let value = SQL`${obj[i]}`;
1833
+ value = value.replace(/'(\?)'/g, "?");
1834
+ setArray.push(`${escapeColumnName(i)} = ` + value);
1835
+ }
1836
+ if (incrementSyncVersion) {
1837
+ setArray.push(`"syncVersion" = "syncVersion" + 1`);
1798
1838
  }
1799
1839
  return `
1800
- UPDATE ${escapeColumnName(table)}
1801
- SET ${setArray.join(", ")} ${whereStatement}
1802
- RETURNING *`;
1840
+ UPDATE ${escapeColumnName(table)}
1841
+ SET ${setArray.join(", ")} ${whereStatement}
1842
+ RETURNING *`;
1803
1843
  }
1804
- function isValueNumber2(value) {
1844
+ function isValueNumber(value) {
1805
1845
  return !isNaN(Number(value));
1806
1846
  }
1807
1847
  function SQL(strings, ...values) {
@@ -1835,9 +1875,9 @@ var PsqlConnection = class {
1835
1875
  const startTime = process.hrtime();
1836
1876
  try {
1837
1877
  const response = await this.query(queryMetadata + formattedQuery, options);
1838
- this.logSqlStatement(formattedQuery, options, meta, startTime);
1839
1878
  if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
1840
1879
  else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
1880
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
1841
1881
  return response.rows[0];
1842
1882
  } catch (error) {
1843
1883
  this.logSqlStatement(formattedQuery, options, meta, startTime);
@@ -1853,10 +1893,10 @@ var PsqlConnection = class {
1853
1893
  try {
1854
1894
  return zodSchema.parse(result);
1855
1895
  } catch (error) {
1856
- if (error instanceof z6.ZodError) {
1896
+ if (error instanceof z5.ZodError) {
1857
1897
  logger.error("Invalid data returned from database:");
1858
1898
  logger.silly("\n" + JSON.stringify(result, null, 2));
1859
- logger.error("\n" + z6.prettifyError(error));
1899
+ logger.error("\n" + z5.prettifyError(error));
1860
1900
  } else {
1861
1901
  logger.error(error);
1862
1902
  }
@@ -1884,12 +1924,12 @@ var PsqlConnection = class {
1884
1924
  async runQuerySchema(query, params, requesterDetails, zodSchema) {
1885
1925
  const result = await this.runQuery(query, params, requesterDetails);
1886
1926
  try {
1887
- return z6.array(zodSchema).parse(result);
1927
+ return z5.array(zodSchema).parse(result);
1888
1928
  } catch (error) {
1889
- if (error instanceof z6.ZodError) {
1929
+ if (error instanceof z5.ZodError) {
1890
1930
  logger.error("Invalid data returned from database:");
1891
1931
  logger.silly("\n" + JSON.stringify(result, null, 2));
1892
- logger.error("\n" + z6.prettifyError(error));
1932
+ logger.error("\n" + z5.prettifyError(error));
1893
1933
  } else {
1894
1934
  logger.error(error);
1895
1935
  }
@@ -1962,7 +2002,7 @@ var PsqlPool = class extends PsqlConnection {
1962
2002
  };
1963
2003
 
1964
2004
  // src/restura/sql/SqlEngine.ts
1965
- import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
2005
+ import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
1966
2006
  var SqlEngine = class {
1967
2007
  async runQueryForRoute(req, routeData, schema) {
1968
2008
  if (!this.canRequesterAccessTable(
@@ -2005,18 +2045,18 @@ var SqlEngine = class {
2005
2045
  const columnSchema = tableSchema.columns.find((item2) => item2.name === columnName);
2006
2046
  if (!columnSchema)
2007
2047
  throw new RsError("SCHEMA_ERROR", `Column ${columnName} not found in table ${tableName}`);
2008
- if (ObjectUtils3.isArrayWithData(columnSchema.roles)) {
2048
+ if (ObjectUtils2.isArrayWithData(columnSchema.roles)) {
2009
2049
  if (!requesterRole) return false;
2010
2050
  return columnSchema.roles.includes(requesterRole);
2011
2051
  }
2012
- if (ObjectUtils3.isArrayWithData(columnSchema.scopes)) {
2052
+ if (ObjectUtils2.isArrayWithData(columnSchema.scopes)) {
2013
2053
  if (!requesterScopes) return false;
2014
2054
  return columnSchema.scopes.every((scope) => requesterScopes.includes(scope));
2015
2055
  }
2016
2056
  return true;
2017
2057
  }
2018
2058
  if (item.subquery) {
2019
- return ObjectUtils3.isArrayWithData(
2059
+ return ObjectUtils2.isArrayWithData(
2020
2060
  item.subquery.properties.filter((nestedItem) => {
2021
2061
  return this.canRequesterAccessColumn(requesterRole, requesterScopes, schema, nestedItem, joins);
2022
2062
  })
@@ -2026,11 +2066,11 @@ var SqlEngine = class {
2026
2066
  }
2027
2067
  canRequesterAccessTable(requesterRole, requesterScopes, schema, tableName) {
2028
2068
  const tableSchema = this.getTableSchema(schema, tableName);
2029
- if (ObjectUtils3.isArrayWithData(tableSchema.roles)) {
2069
+ if (ObjectUtils2.isArrayWithData(tableSchema.roles)) {
2030
2070
  if (!requesterRole) return false;
2031
2071
  return tableSchema.roles.includes(requesterRole);
2032
2072
  }
2033
- if (ObjectUtils3.isArrayWithData(tableSchema.scopes)) {
2073
+ if (ObjectUtils2.isArrayWithData(tableSchema.scopes)) {
2034
2074
  if (!requesterScopes) return false;
2035
2075
  return tableSchema.scopes.some((scope) => requesterScopes.includes(scope));
2036
2076
  }
@@ -2306,7 +2346,7 @@ var PsqlEngine = class extends SqlEngine {
2306
2346
  });
2307
2347
  this.triggerClient.on("notification", async (msg) => {
2308
2348
  if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
2309
- const payload = ObjectUtils4.safeParse(msg.payload);
2349
+ const payload = ObjectUtils3.safeParse(msg.payload);
2310
2350
  await this.handleTrigger(payload, msg.channel.toUpperCase());
2311
2351
  }
2312
2352
  });
@@ -2480,7 +2520,7 @@ var PsqlEngine = class extends SqlEngine {
2480
2520
  }
2481
2521
  createNestedSelect(req, schema, item, routeData, sqlParams) {
2482
2522
  if (!item.subquery) return "";
2483
- if (!ObjectUtils4.isArrayWithData(
2523
+ if (!ObjectUtils3.isArrayWithData(
2484
2524
  item.subquery.properties.filter((nestedItem) => {
2485
2525
  return this.canRequesterAccessColumn(
2486
2526
  req.requesterDetails.role,
@@ -2516,7 +2556,7 @@ var PsqlEngine = class extends SqlEngine {
2516
2556
  }
2517
2557
  return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
2518
2558
  }).filter(Boolean).join(", ")}
2519
- ))
2559
+ ))
2520
2560
  FROM
2521
2561
  "${item.subquery.table}"
2522
2562
  ${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema, sqlParams)}
@@ -2624,7 +2664,7 @@ var PsqlEngine = class extends SqlEngine {
2624
2664
  );
2625
2665
  const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
2626
2666
  let total = 0;
2627
- if (ObjectUtils4.isArrayWithData(totalResponse)) {
2667
+ if (ObjectUtils3.isArrayWithData(totalResponse)) {
2628
2668
  total = totalResponse[0].total;
2629
2669
  }
2630
2670
  return { data: pageResults, total };
@@ -2632,9 +2672,19 @@ var PsqlEngine = class extends SqlEngine {
2632
2672
  throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
2633
2673
  }
2634
2674
  }
2675
+ /**
2676
+ * Executes an update request. The request will pull out the id and baseSyncVersion from the request body.
2677
+ * (If Present) The baseSyncVersion is used to check if the record has been modified since the last sync.
2678
+ * If the update fails because the baseSyncVersion has changed, a conflict error will be thrown.
2679
+ * IDs can not be updated using this method.
2680
+ * @param req - The request object.
2681
+ * @param routeData - The route data object.
2682
+ * @param schema - The schema object.
2683
+ * @returns The response object.
2684
+ */
2635
2685
  async executeUpdateRequest(req, routeData, schema) {
2636
2686
  const sqlParams = [];
2637
- const { id, baseModifiedOn, ...bodyNoId } = req.body;
2687
+ const { id, baseSyncVersion, ...bodyNoId } = req.body;
2638
2688
  const table = schema.database.find((item) => {
2639
2689
  return item.name === routeData.table;
2640
2690
  });
@@ -2642,28 +2692,24 @@ var PsqlEngine = class extends SqlEngine {
2642
2692
  if (table.columns.find((column) => column.name === "modifiedOn")) {
2643
2693
  bodyNoId.modifiedOn = (/* @__PURE__ */ new Date()).toISOString();
2644
2694
  }
2645
- for (const assignment of routeData.assignments) {
2646
- const column = table.columns.find((column2) => column2.name === assignment.name);
2647
- if (!column) continue;
2648
- const assignmentEscaped = escapeColumnName(assignment.name);
2649
- if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
2650
- bodyNoId[assignmentEscaped] = Number(assignment.value);
2651
- else bodyNoId[assignmentEscaped] = assignment.value;
2652
- }
2695
+ let incrementSyncVersion = false;
2696
+ if (table.columns.find((column) => column.name === "syncVersion")) incrementSyncVersion = true;
2697
+ (routeData.assignments || []).forEach((assignment) => {
2698
+ bodyNoId[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
2699
+ });
2653
2700
  let whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
2654
2701
  const originalWhereClause = whereClause;
2655
2702
  const originalSqlParams = [...sqlParams];
2656
- if (baseModifiedOn) {
2657
- const replacedBaseModifiedOn = this.replaceParamKeywords(baseModifiedOn, routeData, req, sqlParams);
2658
- const modifiedOnCheck = whereClause ? `${whereClause} AND "modifiedOn" = ?` : `"modifiedOn" = ?`;
2659
- sqlParams.push(replacedBaseModifiedOn.toString());
2660
- whereClause = modifiedOnCheck;
2703
+ if (baseSyncVersion) {
2704
+ const syncVersionCheck = whereClause ? `${whereClause} AND "syncVersion" = ?` : `"syncVersion" = ?`;
2705
+ sqlParams.push(baseSyncVersion.toString());
2706
+ whereClause = syncVersionCheck;
2661
2707
  }
2662
- const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
2708
+ const query = updateObjectQuery(routeData.table, bodyNoId, whereClause, incrementSyncVersion);
2663
2709
  try {
2664
2710
  await this.psqlConnectionPool.queryOne(query, [...sqlParams], req.requesterDetails);
2665
2711
  } catch (error) {
2666
- if (!baseModifiedOn || !(error instanceof RsError) || error.err !== "NOT_FOUND") throw error;
2712
+ if (!baseSyncVersion || !(error instanceof RsError) || error.err !== "NOT_FOUND") throw error;
2667
2713
  let isConflict = false;
2668
2714
  try {
2669
2715
  await this.psqlConnectionPool.queryOne(
@@ -2677,7 +2723,7 @@ var PsqlEngine = class extends SqlEngine {
2677
2723
  if (isConflict)
2678
2724
  throw new RsError(
2679
2725
  "CONFLICT",
2680
- "The record has been modified since the baseModifiedOn value was provided."
2726
+ "The record has been modified since the baseSyncVersion value was provided."
2681
2727
  );
2682
2728
  throw error;
2683
2729
  }
@@ -3074,6 +3120,19 @@ var TempCache = class {
3074
3120
  }
3075
3121
  };
3076
3122
 
3123
+ // src/restura/utils/utils.ts
3124
+ function sortObjectKeysAlphabetically(obj) {
3125
+ if (Array.isArray(obj)) {
3126
+ return obj.map(sortObjectKeysAlphabetically);
3127
+ } else if (obj !== null && typeof obj === "object") {
3128
+ return Object.keys(obj).sort().reduce((sorted, key) => {
3129
+ sorted[key] = sortObjectKeysAlphabetically(obj[key]);
3130
+ return sorted;
3131
+ }, {});
3132
+ }
3133
+ return obj;
3134
+ }
3135
+
3077
3136
  // src/restura/restura.ts
3078
3137
  var ResturaEngine = class {
3079
3138
  // Make public so other modules can access without re-parsing the config
@@ -3092,6 +3151,7 @@ var ResturaEngine = class {
3092
3151
  responseValidator;
3093
3152
  authenticationHandler;
3094
3153
  customTypeValidation;
3154
+ standardTypeValidation;
3095
3155
  psqlConnectionPool;
3096
3156
  psqlEngine;
3097
3157
  /**
@@ -3201,7 +3261,7 @@ var ResturaEngine = class {
3201
3261
  throw new Error("Missing restura schema file");
3202
3262
  }
3203
3263
  const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
3204
- const schema = ObjectUtils5.safeParse(schemaFileData);
3264
+ const schema = ObjectUtils4.safeParse(schemaFileData);
3205
3265
  const isValid = await isSchemaValid(schema);
3206
3266
  if (!isValid) {
3207
3267
  logger.error("Schema is not valid");
@@ -3212,6 +3272,7 @@ var ResturaEngine = class {
3212
3272
  async reloadEndpoints() {
3213
3273
  this.schema = await this.getLatestFileSystemSchema();
3214
3274
  this.customTypeValidation = customTypeValidationGenerator(this.schema);
3275
+ this.standardTypeValidation = standardTypeValidationGenerator(this.schema);
3215
3276
  this.resturaRouter = express.Router();
3216
3277
  this.resetPublicEndpoints();
3217
3278
  let routeCount = 0;
@@ -3311,7 +3372,12 @@ var ResturaEngine = class {
3311
3372
  const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
3312
3373
  this.validateAuthorization(req, routeData);
3313
3374
  await this.getMulterFilesIfAny(req, res, routeData);
3314
- requestValidator(req, routeData, this.customTypeValidation);
3375
+ requestValidator(
3376
+ req,
3377
+ routeData,
3378
+ this.customTypeValidation,
3379
+ this.standardTypeValidation
3380
+ );
3315
3381
  if (this.isCustomRoute(routeData)) {
3316
3382
  await this.runCustomRouteLogic(req, res, routeData);
3317
3383
  return;
@@ -3488,7 +3554,7 @@ export {
3488
3554
  filterPsqlParser_default as filterPsqlParser,
3489
3555
  insertObjectQuery,
3490
3556
  isSchemaValid,
3491
- isValueNumber2 as isValueNumber,
3557
+ isValueNumber,
3492
3558
  logger,
3493
3559
  modelGenerator,
3494
3560
  questionMarksToOrderedParams,