@restura/core 1.4.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.d.ts +2 -1
- package/dist/index.js +293 -241
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
|
1535
|
-
|
|
1536
|
-
const
|
|
1537
|
-
const
|
|
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
|
-
|
|
1542
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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
|
|
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
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return
|
|
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
|
-
|
|
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
|
|
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 =
|
|
1733
|
-
authToken:
|
|
1734
|
-
sendErrorStackTrace:
|
|
1735
|
-
schemaFilePath:
|
|
1736
|
-
customApiFolderPath:
|
|
1737
|
-
generatedTypesPath:
|
|
1738
|
-
fileTempCachePath:
|
|
1739
|
-
scratchDatabaseSuffix:
|
|
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
|
|
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
|
|
1790
|
+
import { z as z5 } from "zod";
|
|
1756
1791
|
|
|
1757
1792
|
// src/restura/sql/PsqlUtils.ts
|
|
1758
1793
|
import format from "pg-format";
|
|
@@ -1788,13 +1823,15 @@ 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
1829
|
function updateObjectQuery(table, obj, whereStatement, incrementSyncVersion = false) {
|
|
1795
1830
|
const setArray = [];
|
|
1796
1831
|
for (const i in obj) {
|
|
1797
|
-
|
|
1832
|
+
let value = SQL`${obj[i]}`;
|
|
1833
|
+
value = value.replace(/'(\?)'/g, "?");
|
|
1834
|
+
setArray.push(`${escapeColumnName(i)} = ` + value);
|
|
1798
1835
|
}
|
|
1799
1836
|
if (incrementSyncVersion) {
|
|
1800
1837
|
setArray.push(`"syncVersion" = "syncVersion" + 1`);
|
|
@@ -1804,7 +1841,7 @@ function updateObjectQuery(table, obj, whereStatement, incrementSyncVersion = fa
|
|
|
1804
1841
|
SET ${setArray.join(", ")} ${whereStatement}
|
|
1805
1842
|
RETURNING *`;
|
|
1806
1843
|
}
|
|
1807
|
-
function
|
|
1844
|
+
function isValueNumber(value) {
|
|
1808
1845
|
return !isNaN(Number(value));
|
|
1809
1846
|
}
|
|
1810
1847
|
function SQL(strings, ...values) {
|
|
@@ -1856,10 +1893,10 @@ var PsqlConnection = class {
|
|
|
1856
1893
|
try {
|
|
1857
1894
|
return zodSchema.parse(result);
|
|
1858
1895
|
} catch (error) {
|
|
1859
|
-
if (error instanceof
|
|
1896
|
+
if (error instanceof z5.ZodError) {
|
|
1860
1897
|
logger.error("Invalid data returned from database:");
|
|
1861
1898
|
logger.silly("\n" + JSON.stringify(result, null, 2));
|
|
1862
|
-
logger.error("\n" +
|
|
1899
|
+
logger.error("\n" + z5.prettifyError(error));
|
|
1863
1900
|
} else {
|
|
1864
1901
|
logger.error(error);
|
|
1865
1902
|
}
|
|
@@ -1887,12 +1924,12 @@ var PsqlConnection = class {
|
|
|
1887
1924
|
async runQuerySchema(query, params, requesterDetails, zodSchema) {
|
|
1888
1925
|
const result = await this.runQuery(query, params, requesterDetails);
|
|
1889
1926
|
try {
|
|
1890
|
-
return
|
|
1927
|
+
return z5.array(zodSchema).parse(result);
|
|
1891
1928
|
} catch (error) {
|
|
1892
|
-
if (error instanceof
|
|
1929
|
+
if (error instanceof z5.ZodError) {
|
|
1893
1930
|
logger.error("Invalid data returned from database:");
|
|
1894
1931
|
logger.silly("\n" + JSON.stringify(result, null, 2));
|
|
1895
|
-
logger.error("\n" +
|
|
1932
|
+
logger.error("\n" + z5.prettifyError(error));
|
|
1896
1933
|
} else {
|
|
1897
1934
|
logger.error(error);
|
|
1898
1935
|
}
|
|
@@ -1965,7 +2002,7 @@ var PsqlPool = class extends PsqlConnection {
|
|
|
1965
2002
|
};
|
|
1966
2003
|
|
|
1967
2004
|
// src/restura/sql/SqlEngine.ts
|
|
1968
|
-
import { ObjectUtils as
|
|
2005
|
+
import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
|
|
1969
2006
|
var SqlEngine = class {
|
|
1970
2007
|
async runQueryForRoute(req, routeData, schema) {
|
|
1971
2008
|
if (!this.canRequesterAccessTable(
|
|
@@ -2008,18 +2045,18 @@ var SqlEngine = class {
|
|
|
2008
2045
|
const columnSchema = tableSchema.columns.find((item2) => item2.name === columnName);
|
|
2009
2046
|
if (!columnSchema)
|
|
2010
2047
|
throw new RsError("SCHEMA_ERROR", `Column ${columnName} not found in table ${tableName}`);
|
|
2011
|
-
if (
|
|
2048
|
+
if (ObjectUtils2.isArrayWithData(columnSchema.roles)) {
|
|
2012
2049
|
if (!requesterRole) return false;
|
|
2013
2050
|
return columnSchema.roles.includes(requesterRole);
|
|
2014
2051
|
}
|
|
2015
|
-
if (
|
|
2052
|
+
if (ObjectUtils2.isArrayWithData(columnSchema.scopes)) {
|
|
2016
2053
|
if (!requesterScopes) return false;
|
|
2017
2054
|
return columnSchema.scopes.every((scope) => requesterScopes.includes(scope));
|
|
2018
2055
|
}
|
|
2019
2056
|
return true;
|
|
2020
2057
|
}
|
|
2021
2058
|
if (item.subquery) {
|
|
2022
|
-
return
|
|
2059
|
+
return ObjectUtils2.isArrayWithData(
|
|
2023
2060
|
item.subquery.properties.filter((nestedItem) => {
|
|
2024
2061
|
return this.canRequesterAccessColumn(requesterRole, requesterScopes, schema, nestedItem, joins);
|
|
2025
2062
|
})
|
|
@@ -2029,11 +2066,11 @@ var SqlEngine = class {
|
|
|
2029
2066
|
}
|
|
2030
2067
|
canRequesterAccessTable(requesterRole, requesterScopes, schema, tableName) {
|
|
2031
2068
|
const tableSchema = this.getTableSchema(schema, tableName);
|
|
2032
|
-
if (
|
|
2069
|
+
if (ObjectUtils2.isArrayWithData(tableSchema.roles)) {
|
|
2033
2070
|
if (!requesterRole) return false;
|
|
2034
2071
|
return tableSchema.roles.includes(requesterRole);
|
|
2035
2072
|
}
|
|
2036
|
-
if (
|
|
2073
|
+
if (ObjectUtils2.isArrayWithData(tableSchema.scopes)) {
|
|
2037
2074
|
if (!requesterScopes) return false;
|
|
2038
2075
|
return tableSchema.scopes.some((scope) => requesterScopes.includes(scope));
|
|
2039
2076
|
}
|
|
@@ -2309,7 +2346,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2309
2346
|
});
|
|
2310
2347
|
this.triggerClient.on("notification", async (msg) => {
|
|
2311
2348
|
if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
|
|
2312
|
-
const payload =
|
|
2349
|
+
const payload = ObjectUtils3.safeParse(msg.payload);
|
|
2313
2350
|
await this.handleTrigger(payload, msg.channel.toUpperCase());
|
|
2314
2351
|
}
|
|
2315
2352
|
});
|
|
@@ -2483,7 +2520,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2483
2520
|
}
|
|
2484
2521
|
createNestedSelect(req, schema, item, routeData, sqlParams) {
|
|
2485
2522
|
if (!item.subquery) return "";
|
|
2486
|
-
if (!
|
|
2523
|
+
if (!ObjectUtils3.isArrayWithData(
|
|
2487
2524
|
item.subquery.properties.filter((nestedItem) => {
|
|
2488
2525
|
return this.canRequesterAccessColumn(
|
|
2489
2526
|
req.requesterDetails.role,
|
|
@@ -2519,7 +2556,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2519
2556
|
}
|
|
2520
2557
|
return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
|
|
2521
2558
|
}).filter(Boolean).join(", ")}
|
|
2522
|
-
))
|
|
2559
|
+
))
|
|
2523
2560
|
FROM
|
|
2524
2561
|
"${item.subquery.table}"
|
|
2525
2562
|
${this.generateJoinStatements(req, item.subquery.joins, item.subquery.table, routeData, schema, sqlParams)}
|
|
@@ -2627,7 +2664,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2627
2664
|
);
|
|
2628
2665
|
const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
|
|
2629
2666
|
let total = 0;
|
|
2630
|
-
if (
|
|
2667
|
+
if (ObjectUtils3.isArrayWithData(totalResponse)) {
|
|
2631
2668
|
total = totalResponse[0].total;
|
|
2632
2669
|
}
|
|
2633
2670
|
return { data: pageResults, total };
|
|
@@ -2657,14 +2694,9 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2657
2694
|
}
|
|
2658
2695
|
let incrementSyncVersion = false;
|
|
2659
2696
|
if (table.columns.find((column) => column.name === "syncVersion")) incrementSyncVersion = true;
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
const assignmentEscaped = escapeColumnName(assignment.name);
|
|
2664
|
-
if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
|
|
2665
|
-
bodyNoId[assignmentEscaped] = Number(assignment.value);
|
|
2666
|
-
else bodyNoId[assignmentEscaped] = assignment.value;
|
|
2667
|
-
}
|
|
2697
|
+
(routeData.assignments || []).forEach((assignment) => {
|
|
2698
|
+
bodyNoId[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
|
|
2699
|
+
});
|
|
2668
2700
|
let whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
2669
2701
|
const originalWhereClause = whereClause;
|
|
2670
2702
|
const originalSqlParams = [...sqlParams];
|
|
@@ -3088,6 +3120,19 @@ var TempCache = class {
|
|
|
3088
3120
|
}
|
|
3089
3121
|
};
|
|
3090
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
|
+
|
|
3091
3136
|
// src/restura/restura.ts
|
|
3092
3137
|
var ResturaEngine = class {
|
|
3093
3138
|
// Make public so other modules can access without re-parsing the config
|
|
@@ -3106,6 +3151,7 @@ var ResturaEngine = class {
|
|
|
3106
3151
|
responseValidator;
|
|
3107
3152
|
authenticationHandler;
|
|
3108
3153
|
customTypeValidation;
|
|
3154
|
+
standardTypeValidation;
|
|
3109
3155
|
psqlConnectionPool;
|
|
3110
3156
|
psqlEngine;
|
|
3111
3157
|
/**
|
|
@@ -3215,7 +3261,7 @@ var ResturaEngine = class {
|
|
|
3215
3261
|
throw new Error("Missing restura schema file");
|
|
3216
3262
|
}
|
|
3217
3263
|
const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
|
|
3218
|
-
const schema =
|
|
3264
|
+
const schema = ObjectUtils4.safeParse(schemaFileData);
|
|
3219
3265
|
const isValid = await isSchemaValid(schema);
|
|
3220
3266
|
if (!isValid) {
|
|
3221
3267
|
logger.error("Schema is not valid");
|
|
@@ -3226,6 +3272,7 @@ var ResturaEngine = class {
|
|
|
3226
3272
|
async reloadEndpoints() {
|
|
3227
3273
|
this.schema = await this.getLatestFileSystemSchema();
|
|
3228
3274
|
this.customTypeValidation = customTypeValidationGenerator(this.schema);
|
|
3275
|
+
this.standardTypeValidation = standardTypeValidationGenerator(this.schema);
|
|
3229
3276
|
this.resturaRouter = express.Router();
|
|
3230
3277
|
this.resetPublicEndpoints();
|
|
3231
3278
|
let routeCount = 0;
|
|
@@ -3325,7 +3372,12 @@ var ResturaEngine = class {
|
|
|
3325
3372
|
const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
|
|
3326
3373
|
this.validateAuthorization(req, routeData);
|
|
3327
3374
|
await this.getMulterFilesIfAny(req, res, routeData);
|
|
3328
|
-
requestValidator(
|
|
3375
|
+
requestValidator(
|
|
3376
|
+
req,
|
|
3377
|
+
routeData,
|
|
3378
|
+
this.customTypeValidation,
|
|
3379
|
+
this.standardTypeValidation
|
|
3380
|
+
);
|
|
3329
3381
|
if (this.isCustomRoute(routeData)) {
|
|
3330
3382
|
await this.runCustomRouteLogic(req, res, routeData);
|
|
3331
3383
|
return;
|
|
@@ -3502,7 +3554,7 @@ export {
|
|
|
3502
3554
|
filterPsqlParser_default as filterPsqlParser,
|
|
3503
3555
|
insertObjectQuery,
|
|
3504
3556
|
isSchemaValid,
|
|
3505
|
-
|
|
3557
|
+
isValueNumber,
|
|
3506
3558
|
logger,
|
|
3507
3559
|
modelGenerator,
|
|
3508
3560
|
questionMarksToOrderedParams,
|