@restura/core 1.5.0 → 1.7.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 +174 -141
- package/dist/index.js +411 -177
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -197,7 +197,7 @@ var EventManager = class {
|
|
|
197
197
|
async fireInsertActions(data, triggerResult) {
|
|
198
198
|
await Bluebird.map(
|
|
199
199
|
this.actionHandlers.DATABASE_ROW_INSERT,
|
|
200
|
-
({ callback, filter }) => {
|
|
200
|
+
async ({ callback, filter }) => {
|
|
201
201
|
if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
|
|
202
202
|
const insertData = {
|
|
203
203
|
tableName: triggerResult.table,
|
|
@@ -205,7 +205,11 @@ var EventManager = class {
|
|
|
205
205
|
insertObject: triggerResult.record,
|
|
206
206
|
queryMetadata: data.queryMetadata
|
|
207
207
|
};
|
|
208
|
-
|
|
208
|
+
try {
|
|
209
|
+
await callback(insertData, data.queryMetadata);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error(`Error firing insert action for table ${triggerResult.table}`, error);
|
|
212
|
+
}
|
|
209
213
|
},
|
|
210
214
|
{ concurrency: 10 }
|
|
211
215
|
);
|
|
@@ -213,7 +217,7 @@ var EventManager = class {
|
|
|
213
217
|
async fireDeleteActions(data, triggerResult) {
|
|
214
218
|
await Bluebird.map(
|
|
215
219
|
this.actionHandlers.DATABASE_ROW_DELETE,
|
|
216
|
-
({ callback, filter }) => {
|
|
220
|
+
async ({ callback, filter }) => {
|
|
217
221
|
if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
|
|
218
222
|
const deleteData = {
|
|
219
223
|
tableName: triggerResult.table,
|
|
@@ -221,7 +225,11 @@ var EventManager = class {
|
|
|
221
225
|
deletedRow: triggerResult.previousRecord,
|
|
222
226
|
queryMetadata: data.queryMetadata
|
|
223
227
|
};
|
|
224
|
-
|
|
228
|
+
try {
|
|
229
|
+
await callback(deleteData, data.queryMetadata);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.error(`Error firing delete action for table ${triggerResult.table}`, error);
|
|
232
|
+
}
|
|
225
233
|
},
|
|
226
234
|
{ concurrency: 10 }
|
|
227
235
|
);
|
|
@@ -229,7 +237,7 @@ var EventManager = class {
|
|
|
229
237
|
async fireUpdateActions(data, triggerResult) {
|
|
230
238
|
await Bluebird.map(
|
|
231
239
|
this.actionHandlers.DATABASE_COLUMN_UPDATE,
|
|
232
|
-
({ callback, filter }) => {
|
|
240
|
+
async ({ callback, filter }) => {
|
|
233
241
|
if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
|
|
234
242
|
const columnChangeData = {
|
|
235
243
|
tableName: triggerResult.table,
|
|
@@ -238,7 +246,11 @@ var EventManager = class {
|
|
|
238
246
|
oldData: triggerResult.previousRecord,
|
|
239
247
|
queryMetadata: data.queryMetadata
|
|
240
248
|
};
|
|
241
|
-
|
|
249
|
+
try {
|
|
250
|
+
await callback(columnChangeData, data.queryMetadata);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
logger.error(`Error firing update action for table ${triggerResult.table}`, error);
|
|
253
|
+
}
|
|
242
254
|
},
|
|
243
255
|
{ concurrency: 10 }
|
|
244
256
|
);
|
|
@@ -1092,10 +1104,10 @@ var customApiFactory_default = customApiFactory;
|
|
|
1092
1104
|
import fs2 from "fs";
|
|
1093
1105
|
import path2, { resolve } from "path";
|
|
1094
1106
|
import tmp from "tmp";
|
|
1095
|
-
import
|
|
1107
|
+
import { createGenerator } from "ts-json-schema-generator";
|
|
1096
1108
|
|
|
1097
1109
|
// src/restura/generators/schemaGeneratorUtils.ts
|
|
1098
|
-
function buildRouteSchema(requestParams) {
|
|
1110
|
+
function buildRouteSchema(routeKey, requestParams) {
|
|
1099
1111
|
const properties = {};
|
|
1100
1112
|
const required = [];
|
|
1101
1113
|
for (const param of requestParams) {
|
|
@@ -1105,13 +1117,19 @@ function buildRouteSchema(requestParams) {
|
|
|
1105
1117
|
const propertySchema = buildPropertySchemaFromRequest(param);
|
|
1106
1118
|
properties[param.name] = propertySchema;
|
|
1107
1119
|
}
|
|
1108
|
-
|
|
1120
|
+
const schemaDefinition = {
|
|
1109
1121
|
type: "object",
|
|
1110
1122
|
properties,
|
|
1111
1123
|
...required.length > 0 && { required },
|
|
1112
|
-
// Only include if not empty
|
|
1113
1124
|
additionalProperties: false
|
|
1114
1125
|
};
|
|
1126
|
+
return {
|
|
1127
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1128
|
+
$ref: `#/definitions/${routeKey}`,
|
|
1129
|
+
definitions: {
|
|
1130
|
+
[routeKey]: schemaDefinition
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1115
1133
|
}
|
|
1116
1134
|
function buildPropertySchemaFromRequest(param) {
|
|
1117
1135
|
const propertySchema = {};
|
|
@@ -1215,28 +1233,31 @@ function customTypeValidationGenerator(currentSchema, ignoreGeneratedTypes = fal
|
|
|
1215
1233
|
}).filter(Boolean);
|
|
1216
1234
|
if (!customInterfaceNames) return {};
|
|
1217
1235
|
const temporaryFile = tmp.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1236
|
+
const additionalImports = ignoreGeneratedTypes ? "" : [
|
|
1237
|
+
`/// <reference path="${toForwardSlashPath(path2.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"))}" />`,
|
|
1238
|
+
`/// <reference path="${toForwardSlashPath(path2.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"))}" />`,
|
|
1239
|
+
`/// <reference path="${toForwardSlashPath(path2.join(restura.resturaConfig.generatedTypesPath, "api.d.ts"))}" />`
|
|
1240
|
+
].join("\n") + "\n";
|
|
1241
|
+
const typesWithExport = currentSchema.customTypes.map((type) => {
|
|
1242
|
+
if (!type.trim().startsWith("export ")) {
|
|
1243
|
+
return "export " + type;
|
|
1244
|
+
}
|
|
1245
|
+
return type;
|
|
1246
|
+
});
|
|
1247
|
+
fs2.writeFileSync(temporaryFile.name, additionalImports + typesWithExport.join("\n"));
|
|
1248
|
+
const config3 = {
|
|
1249
|
+
path: resolve(temporaryFile.name),
|
|
1250
|
+
tsconfig: path2.join(process.cwd(), "tsconfig.json"),
|
|
1251
|
+
skipTypeCheck: true
|
|
1223
1252
|
};
|
|
1224
|
-
const
|
|
1225
|
-
[
|
|
1226
|
-
resolve(temporaryFile.name),
|
|
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
|
-
]
|
|
1232
|
-
],
|
|
1233
|
-
compilerOptions
|
|
1234
|
-
);
|
|
1253
|
+
const generator = createGenerator(config3);
|
|
1235
1254
|
customInterfaceNames.forEach((item) => {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1255
|
+
try {
|
|
1256
|
+
const ddlSchema = generator.createSchema(item);
|
|
1257
|
+
schemaObject[item] = ddlSchema || {};
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
logger.error("Failed to generate schema for custom type: " + item, error);
|
|
1260
|
+
}
|
|
1240
1261
|
});
|
|
1241
1262
|
temporaryFile.removeCallback();
|
|
1242
1263
|
for (const endpoint of currentSchema.endpoints) {
|
|
@@ -1244,11 +1265,14 @@ function customTypeValidationGenerator(currentSchema, ignoreGeneratedTypes = fal
|
|
|
1244
1265
|
if (route.type !== "CUSTOM_ONE" && route.type !== "CUSTOM_ARRAY" && route.type !== "CUSTOM_PAGED") continue;
|
|
1245
1266
|
if (!route.request || !Array.isArray(route.request)) continue;
|
|
1246
1267
|
const routeKey = `${route.method}:${route.path}`;
|
|
1247
|
-
schemaObject[routeKey] = buildRouteSchema(route.request);
|
|
1268
|
+
schemaObject[routeKey] = buildRouteSchema(routeKey, route.request);
|
|
1248
1269
|
}
|
|
1249
1270
|
}
|
|
1250
1271
|
return schemaObject;
|
|
1251
1272
|
}
|
|
1273
|
+
function toForwardSlashPath(path5) {
|
|
1274
|
+
return path5.replaceAll("\\", "/");
|
|
1275
|
+
}
|
|
1252
1276
|
|
|
1253
1277
|
// src/restura/generators/standardTypeValidationGenerator.ts
|
|
1254
1278
|
function standardTypeValidationGenerator(currentSchema) {
|
|
@@ -1259,12 +1283,18 @@ function standardTypeValidationGenerator(currentSchema) {
|
|
|
1259
1283
|
const routeKey = `${route.method}:${route.path}`;
|
|
1260
1284
|
if (!route.request || route.request.length === 0) {
|
|
1261
1285
|
schemaObject[routeKey] = {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1286
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
1287
|
+
$ref: `#/definitions/${routeKey}`,
|
|
1288
|
+
definitions: {
|
|
1289
|
+
[routeKey]: {
|
|
1290
|
+
type: "object",
|
|
1291
|
+
properties: {},
|
|
1292
|
+
additionalProperties: false
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1265
1295
|
};
|
|
1266
1296
|
} else {
|
|
1267
|
-
schemaObject[routeKey] = buildRouteSchema(route.request);
|
|
1297
|
+
schemaObject[routeKey] = buildRouteSchema(routeKey, route.request);
|
|
1268
1298
|
}
|
|
1269
1299
|
}
|
|
1270
1300
|
}
|
|
@@ -1300,6 +1330,18 @@ function addApiResponseFunctions(req, res, next) {
|
|
|
1300
1330
|
next();
|
|
1301
1331
|
}
|
|
1302
1332
|
|
|
1333
|
+
// src/restura/middleware/addDeprecationResponse.ts
|
|
1334
|
+
function addDeprecationResponse(req, res, next) {
|
|
1335
|
+
const deprecation = req.routeData?.deprecation;
|
|
1336
|
+
if (deprecation) {
|
|
1337
|
+
const { date, message } = deprecation;
|
|
1338
|
+
const dateObject = new Date(date);
|
|
1339
|
+
res.set("Deprecation", `@${dateObject.getTime().toString()}`);
|
|
1340
|
+
res.set("Deprecation-Message", message ?? "This endpoint is deprecated and will be removed in the future.");
|
|
1341
|
+
}
|
|
1342
|
+
next();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1303
1345
|
// src/restura/middleware/authenticateRequester.ts
|
|
1304
1346
|
function authenticateRequester(applicationAuthenticateHandler) {
|
|
1305
1347
|
return (req, res, next) => {
|
|
@@ -1422,6 +1464,10 @@ var routeDataBaseSchema = z3.object({
|
|
|
1422
1464
|
name: z3.string(),
|
|
1423
1465
|
description: z3.string(),
|
|
1424
1466
|
path: z3.string(),
|
|
1467
|
+
deprecation: z3.object({
|
|
1468
|
+
date: z3.iso.datetime(),
|
|
1469
|
+
message: z3.string().optional()
|
|
1470
|
+
}).optional(),
|
|
1425
1471
|
roles: z3.array(z3.string()),
|
|
1426
1472
|
scopes: z3.array(z3.string())
|
|
1427
1473
|
}).strict();
|
|
@@ -1574,7 +1620,8 @@ var indexDataSchema = z3.object({
|
|
|
1574
1620
|
columns: z3.array(z3.string()),
|
|
1575
1621
|
isUnique: z3.boolean(),
|
|
1576
1622
|
isPrimaryKey: z3.boolean(),
|
|
1577
|
-
order: z3.enum(["ASC", "DESC"])
|
|
1623
|
+
order: z3.enum(["ASC", "DESC"]),
|
|
1624
|
+
where: z3.string().optional()
|
|
1578
1625
|
}).strict();
|
|
1579
1626
|
var foreignKeyActionsSchema = z3.enum([
|
|
1580
1627
|
"CASCADE",
|
|
@@ -1641,31 +1688,81 @@ async function isSchemaValid(schemaToCheck) {
|
|
|
1641
1688
|
|
|
1642
1689
|
// src/restura/validators/requestValidator.ts
|
|
1643
1690
|
import jsonschema from "jsonschema";
|
|
1691
|
+
function deepResolveSchemaRefs(schema, definitions, seen = /* @__PURE__ */ new Set()) {
|
|
1692
|
+
if (!schema || typeof schema !== "object") {
|
|
1693
|
+
return schema;
|
|
1694
|
+
}
|
|
1695
|
+
if ("$ref" in schema && typeof schema.$ref === "string") {
|
|
1696
|
+
const refPath = schema.$ref;
|
|
1697
|
+
if (refPath.startsWith("#/definitions/") && definitions) {
|
|
1698
|
+
const defName = refPath.substring("#/definitions/".length);
|
|
1699
|
+
if (seen.has(defName)) {
|
|
1700
|
+
return { type: "object", properties: {} };
|
|
1701
|
+
}
|
|
1702
|
+
const resolved = definitions[defName];
|
|
1703
|
+
if (resolved) {
|
|
1704
|
+
seen.add(defName);
|
|
1705
|
+
return deepResolveSchemaRefs(resolved, definitions, seen);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return schema;
|
|
1709
|
+
}
|
|
1710
|
+
const result = {};
|
|
1711
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1712
|
+
if (key === "definitions") {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (value && typeof value === "object") {
|
|
1716
|
+
if (Array.isArray(value)) {
|
|
1717
|
+
result[key] = value.map(
|
|
1718
|
+
(item) => typeof item === "object" ? deepResolveSchemaRefs(item, definitions, new Set(seen)) : item
|
|
1719
|
+
);
|
|
1720
|
+
} else {
|
|
1721
|
+
result[key] = deepResolveSchemaRefs(value, definitions, new Set(seen));
|
|
1722
|
+
}
|
|
1723
|
+
} else {
|
|
1724
|
+
result[key] = value;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return result;
|
|
1728
|
+
}
|
|
1729
|
+
function resolveSchemaRef(schema, definitions) {
|
|
1730
|
+
return deepResolveSchemaRefs(schema, definitions);
|
|
1731
|
+
}
|
|
1644
1732
|
function requestValidator(req, routeData, customValidationSchema, standardValidationSchema) {
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
} else if (routeData.type === "CUSTOM_ONE" || routeData.type === "CUSTOM_ARRAY" || routeData.type === "CUSTOM_PAGED") {
|
|
1733
|
+
const routeKey = `${routeData.method}:${routeData.path}`;
|
|
1734
|
+
const isCustom = routeData.type === "CUSTOM_ONE" || routeData.type === "CUSTOM_ARRAY" || routeData.type === "CUSTOM_PAGED";
|
|
1735
|
+
const isStandard = routeData.type === "ONE" || routeData.type === "ARRAY" || routeData.type === "PAGED";
|
|
1736
|
+
if (!isStandard && !isCustom) {
|
|
1737
|
+
throw new RsError("BAD_REQUEST", `Invalid route type: ${routeData.type}`);
|
|
1738
|
+
}
|
|
1739
|
+
if (isCustom) {
|
|
1653
1740
|
if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
|
|
1654
1741
|
if (!routeData.requestType && !routeData.request)
|
|
1655
1742
|
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 = {
|
|
1659
|
-
...currentInterface,
|
|
1660
|
-
additionalProperties: false
|
|
1661
|
-
};
|
|
1662
|
-
} else {
|
|
1663
|
-
throw new RsError("BAD_REQUEST", `Invalid route type: ${routeData.type}`);
|
|
1664
1743
|
}
|
|
1744
|
+
const schemaKey = isCustom ? routeData.requestType || routeKey : routeKey;
|
|
1745
|
+
if (!schemaKey) throw new RsError("BAD_REQUEST", `No schema key defined for request: ${routeKey}.`);
|
|
1746
|
+
const schemaDictionary = isCustom ? customValidationSchema : standardValidationSchema;
|
|
1747
|
+
const schemaRoot = schemaDictionary[schemaKey];
|
|
1748
|
+
if (!schemaRoot) {
|
|
1749
|
+
const requestType = isCustom ? "custom" : "standard";
|
|
1750
|
+
throw new RsError("BAD_REQUEST", `No schema found for ${requestType} request: ${schemaKey}.`);
|
|
1751
|
+
}
|
|
1752
|
+
const schemaForValidation = schemaRoot;
|
|
1753
|
+
const schemaDefinitions = schemaRoot.definitions;
|
|
1754
|
+
const rawInterface = schemaRoot.definitions[schemaKey];
|
|
1755
|
+
const schemaForCoercion = isCustom ? resolveSchemaRef(rawInterface, schemaDefinitions) : rawInterface;
|
|
1665
1756
|
const requestData = getRequestData(req, schemaForCoercion);
|
|
1666
1757
|
req.data = requestData;
|
|
1667
1758
|
const validator = new jsonschema.Validator();
|
|
1668
|
-
|
|
1759
|
+
if (schemaDefinitions) {
|
|
1760
|
+
for (const [defName, defSchema] of Object.entries(schemaDefinitions)) {
|
|
1761
|
+
validator.addSchema(defSchema, `/definitions/${defName}`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
const resolvedSchema = resolveSchemaRef(schemaForValidation, schemaDefinitions);
|
|
1765
|
+
const executeValidation = validator.validate(req.data, resolvedSchema);
|
|
1669
1766
|
if (!executeValidation.valid) {
|
|
1670
1767
|
const errorMessages = executeValidation.errors.map((err) => {
|
|
1671
1768
|
const property = err.property.replace("instance.", "");
|
|
@@ -1785,7 +1882,6 @@ import pg from "pg";
|
|
|
1785
1882
|
|
|
1786
1883
|
// src/restura/sql/PsqlConnection.ts
|
|
1787
1884
|
import crypto from "crypto";
|
|
1788
|
-
import format2 from "pg-format";
|
|
1789
1885
|
import { format as sqlFormat } from "sql-formatter";
|
|
1790
1886
|
import { z as z5 } from "zod";
|
|
1791
1887
|
|
|
@@ -1860,6 +1956,13 @@ function SQL(strings, ...values) {
|
|
|
1860
1956
|
});
|
|
1861
1957
|
return query;
|
|
1862
1958
|
}
|
|
1959
|
+
function toSqlLiteral(value) {
|
|
1960
|
+
if (value === null || value === void 0) return "NULL";
|
|
1961
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "NULL";
|
|
1962
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
1963
|
+
if (Array.isArray(value)) return `ARRAY[${value.map((v) => toSqlLiteral(v)).join(", ")}]`;
|
|
1964
|
+
return format.literal(value);
|
|
1965
|
+
}
|
|
1863
1966
|
|
|
1864
1967
|
// src/restura/sql/PsqlConnection.ts
|
|
1865
1968
|
var PsqlConnection = class {
|
|
@@ -1938,21 +2041,11 @@ var PsqlConnection = class {
|
|
|
1938
2041
|
}
|
|
1939
2042
|
logSqlStatement(query, options, queryMetadata, startTime, prefix = "") {
|
|
1940
2043
|
if (logger.level !== "trace" && logger.level !== "silly") return;
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
const paramIndex = parseInt(match.substring(1)) - 1;
|
|
1947
|
-
if (paramIndex < 0 || paramIndex >= options.length) {
|
|
1948
|
-
return "INVALID_PARAM_INDEX";
|
|
1949
|
-
}
|
|
1950
|
-
const value = options[paramIndex];
|
|
1951
|
-
if (typeof value === "number") return value.toString();
|
|
1952
|
-
if (typeof value === "boolean") return value.toString();
|
|
1953
|
-
return format2.literal(value);
|
|
1954
|
-
});
|
|
1955
|
-
}
|
|
2044
|
+
const sqlStatement = query.replace(/\$(\d+)/g, (_, num) => {
|
|
2045
|
+
const paramIndex = parseInt(num) - 1;
|
|
2046
|
+
if (paramIndex >= options.length) return "INVALID_PARAM_INDEX";
|
|
2047
|
+
return toSqlLiteral(options[paramIndex]);
|
|
2048
|
+
});
|
|
1956
2049
|
const formattedSql = sqlFormat(sqlStatement, {
|
|
1957
2050
|
language: "postgresql",
|
|
1958
2051
|
linesBetweenQueries: 2,
|
|
@@ -1969,8 +2062,7 @@ var PsqlConnection = class {
|
|
|
1969
2062
|
if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
|
|
1970
2063
|
logger.silly(`${prefix}query by ${initiator}, Query ->
|
|
1971
2064
|
${formattedSql}`, {
|
|
1972
|
-
|
|
1973
|
-
_meta: { durationNs: nanoseconds }
|
|
2065
|
+
durationMs
|
|
1974
2066
|
});
|
|
1975
2067
|
}
|
|
1976
2068
|
};
|
|
@@ -2132,124 +2224,246 @@ var SqlEngine = class {
|
|
|
2132
2224
|
|
|
2133
2225
|
// src/restura/sql/filterPsqlParser.ts
|
|
2134
2226
|
import peg from "pegjs";
|
|
2135
|
-
var
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
|
|
2227
|
+
var initializers = `
|
|
2228
|
+
// Quotes a SQL identifier (column/table name) with double quotes, escaping any embedded quotes
|
|
2229
|
+
function quoteSqlIdentity(value) {
|
|
2230
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// Unescape special characters in values: \\, -> , | \\| -> | | \\\\ -> \\
|
|
2234
|
+
function unescapeValue(str) {
|
|
2235
|
+
var result = '';
|
|
2236
|
+
for (var i = 0; i < str.length; i++) {
|
|
2237
|
+
if (str[i] === '\\\\' && i + 1 < str.length) {
|
|
2238
|
+
var next = str[i + 1];
|
|
2239
|
+
if (next === ',' || next === '|' || next === '\\\\') {
|
|
2240
|
+
result += next;
|
|
2241
|
+
i++;
|
|
2242
|
+
} else {
|
|
2243
|
+
result += str[i];
|
|
2244
|
+
}
|
|
2154
2245
|
} else {
|
|
2155
|
-
|
|
2246
|
+
result += str[i];
|
|
2156
2247
|
}
|
|
2157
2248
|
}
|
|
2158
|
-
return
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2249
|
+
return result;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// Split pipe-separated values respecting escaped pipes
|
|
2253
|
+
function splitPipeValues(str) {
|
|
2254
|
+
var values = [];
|
|
2255
|
+
var current = '';
|
|
2256
|
+
for (var i = 0; i < str.length; i++) {
|
|
2257
|
+
if (str[i] === '\\\\' && i + 1 < str.length && str[i + 1] === '|') {
|
|
2258
|
+
current += '|';
|
|
2259
|
+
i++;
|
|
2260
|
+
} else if (str[i] === '|') {
|
|
2261
|
+
values.push(unescapeValue(current));
|
|
2262
|
+
current = '';
|
|
2263
|
+
} else {
|
|
2264
|
+
current += str[i];
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
if (current.length > 0) {
|
|
2268
|
+
values.push(unescapeValue(current));
|
|
2269
|
+
}
|
|
2270
|
+
return values;
|
|
2161
2271
|
}
|
|
2162
2272
|
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
var quoted = '"';
|
|
2273
|
+
// Build SQL IN clause from pipe-separated values
|
|
2274
|
+
function buildInClause(column, rawValue) {
|
|
2275
|
+
var values = splitPipeValues(rawValue);
|
|
2276
|
+
var literals = values.map(function(v) { return formatValue(v); });
|
|
2277
|
+
return column + ' IN (' + literals.join(', ') + ')';
|
|
2278
|
+
}
|
|
2171
2279
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
quoted += c;
|
|
2280
|
+
// Check if a value is numeric and format appropriately
|
|
2281
|
+
function formatValue(value) {
|
|
2282
|
+
// Check if the value is a valid number (integer or decimal)
|
|
2283
|
+
if (/^-?\\d+(\\.\\d+)?$/.test(value)) {
|
|
2284
|
+
return value; // Return as-is without quotes
|
|
2178
2285
|
}
|
|
2286
|
+
return format.literal(value);
|
|
2179
2287
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
};
|
|
2288
|
+
`;
|
|
2289
|
+
var entryGrammar = `
|
|
2290
|
+
{
|
|
2291
|
+
${initializers}
|
|
2185
2292
|
}
|
|
2186
2293
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2294
|
+
Start
|
|
2295
|
+
= sql:StartOld { return { sql: sql, usedOldSyntax: true }; }
|
|
2296
|
+
/ sql:StartNew { return { sql: sql, usedOldSyntax: false }; }
|
|
2297
|
+
`;
|
|
2298
|
+
var oldGrammar = `
|
|
2299
|
+
StartOld
|
|
2300
|
+
= OldExpressionList
|
|
2301
|
+
_
|
|
2302
|
+
= [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
|
|
2303
|
+
|
|
2304
|
+
OldExpressionList
|
|
2305
|
+
= leftExpression:OldExpression _ operator:OldOperator _ rightExpression:OldExpressionList
|
|
2306
|
+
{ return \`\${leftExpression} \${operator} \${rightExpression}\`;}
|
|
2307
|
+
/ OldExpression
|
|
2308
|
+
|
|
2309
|
+
OldExpression
|
|
2310
|
+
= negate:OldNegate? _ "(" _ "column" _ ":" column:OldColumn _ ","? _ value:OldValue? ","? _ type:OldType? _ ")"_
|
|
2311
|
+
{return \`\${negate? " NOT " : ""}(\${type ? type(column, value) : (value == null ? \`\${column} IS NULL\` : \`\${column} = \${formatValue(value)}\`)})\`;}
|
|
2199
2312
|
/
|
|
2200
|
-
negate:
|
|
2313
|
+
negate:OldNegate?"("expression:OldExpressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
|
|
2201
2314
|
|
|
2202
|
-
|
|
2315
|
+
OldNegate
|
|
2316
|
+
= "!"
|
|
2203
2317
|
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2318
|
+
OldOperator
|
|
2319
|
+
= "and"i / "or"i
|
|
2320
|
+
|
|
2321
|
+
OldColumn
|
|
2322
|
+
= first:OldText rest:("." OldText)* {
|
|
2323
|
+
const partsArray = [first];
|
|
2324
|
+
if (rest && rest.length > 0) {
|
|
2325
|
+
partsArray.push(...rest.map(item => item[1]));
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
if (partsArray.length > 3) {
|
|
2329
|
+
throw new SyntaxError('Column path cannot have more than 3 parts (table.column.jsonField)');
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
if (partsArray.length === 1) {
|
|
2333
|
+
return quoteSqlIdentity(partsArray[0]);
|
|
2334
|
+
}
|
|
2335
|
+
const tableName = quoteSqlIdentity(partsArray[0]);
|
|
2336
|
+
|
|
2337
|
+
// If we only have two parts (table.column), use regular dot notation
|
|
2338
|
+
if (partsArray.length === 2) {
|
|
2339
|
+
return tableName + "." + quoteSqlIdentity(partsArray[1]);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// For JSON paths (more than 2 parts), first part is a column, last part uses ->>
|
|
2343
|
+
const jsonColumn = quoteSqlIdentity(partsArray[1]);
|
|
2344
|
+
const lastPart = partsArray[partsArray.length - 1];
|
|
2345
|
+
const escapedLast = lastPart.replace(/'/g, "''");
|
|
2346
|
+
const result = tableName + "." + jsonColumn + "->>'" + escapedLast + "'";
|
|
2347
|
+
return result;
|
|
2225
2348
|
}
|
|
2226
|
-
|
|
2227
|
-
// For JSON paths (more than 2 parts), first part is a column, last part uses ->>
|
|
2228
|
-
const jsonColumn = quoteSqlIdentity(partsArray[1]);
|
|
2229
|
-
const lastPart = partsArray[partsArray.length - 1];
|
|
2230
|
-
const result = tableName + "." + jsonColumn + "->>'" + lastPart + "'";
|
|
2231
|
-
return result;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
|
|
2235
2349
|
|
|
2350
|
+
OldText
|
|
2351
|
+
= text:[a-z0-9 \\t\\r\\n\\-_:@']i+ {
|
|
2352
|
+
return text.join("");
|
|
2353
|
+
}
|
|
2236
2354
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
text:"exact" { return function(column, value) { return \`\${column} = '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2242
|
-
text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2243
|
-
text:"greaterThan" { return function(column, value) { return \`\${column} > '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2244
|
-
text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2245
|
-
text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2246
|
-
text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
|
|
2247
|
-
|
|
2248
|
-
value = "value" _ ":" value:text { return value; }
|
|
2355
|
+
OldType
|
|
2356
|
+
= "type" _ ":" _ type:OldTypeString {
|
|
2357
|
+
return type;
|
|
2358
|
+
}
|
|
2249
2359
|
|
|
2360
|
+
OldTypeString
|
|
2361
|
+
= text:"startsWith" { return function(column, value) { return \`\${column}::text ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } }
|
|
2362
|
+
/ text:"endsWith" { return function(column, value) { return \`\${column}::text ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } }
|
|
2363
|
+
/ text:"contains" { return function(column, value) { return \`\${column}::text ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } }
|
|
2364
|
+
/ text:"exact" { return function(column, value) { return \`\${column} = \${formatValue(value)}\`; } }
|
|
2365
|
+
/ text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= \${formatValue(value)}\`; } }
|
|
2366
|
+
/ text:"greaterThan" { return function(column, value) { return \`\${column} > \${formatValue(value)}\`; } }
|
|
2367
|
+
/ text:"lessThanEqual" { return function(column, value) { return \`\${column} <= \${formatValue(value)}\`; } }
|
|
2368
|
+
/ text:"lessThan" { return function(column, value) { return \`\${column} < \${formatValue(value)}\`; } }
|
|
2369
|
+
/ text:"isNull" { return function(column, value) { return \`\${column} IS NULL\`; } }
|
|
2250
2370
|
|
|
2371
|
+
OldValue
|
|
2372
|
+
= "value" _ ":" value:OldText {
|
|
2373
|
+
return value;
|
|
2374
|
+
}
|
|
2251
2375
|
`;
|
|
2252
|
-
var
|
|
2376
|
+
var newGrammar = `
|
|
2377
|
+
StartNew
|
|
2378
|
+
= ExpressionList
|
|
2379
|
+
|
|
2380
|
+
ExpressionList
|
|
2381
|
+
= left:Expression _ op:("and"i / "or"i) _ right:ExpressionList
|
|
2382
|
+
{ return left + ' ' + op.toUpperCase() + ' ' + right; }
|
|
2383
|
+
/ Expression
|
|
2384
|
+
|
|
2385
|
+
Expression
|
|
2386
|
+
= negate:"!"? _ "(" _ inner:SimpleExprList _ ")" _
|
|
2387
|
+
{ return (negate ? 'NOT ' : '') + '(' + inner + ')'; }
|
|
2388
|
+
/ SimpleExpr
|
|
2389
|
+
|
|
2390
|
+
SimpleExprList
|
|
2391
|
+
= left:SimpleExpr _ op:("and"i / "or"i) _ right:SimpleExprList
|
|
2392
|
+
{ return left + ' ' + op.toUpperCase() + ' ' + right; }
|
|
2393
|
+
/ SimpleExpr
|
|
2394
|
+
|
|
2395
|
+
SimpleExpr
|
|
2396
|
+
= negate:"!"? _ "(" _ col:Column _ "," _ op:OperatorWithValue _ ")" _
|
|
2397
|
+
{ return (negate ? 'NOT ' : '') + '(' + op(col) + ')'; }
|
|
2398
|
+
/ negate:"!"? _ "(" _ col:Column _ "," _ op:NullOperator _ ")" _
|
|
2399
|
+
{ return (negate ? 'NOT ' : '') + '(' + op(col) + ')'; }
|
|
2400
|
+
/ negate:"!"? _ "(" _ col:Column _ "," _ val:Value _ ")" _
|
|
2401
|
+
{ return (negate ? 'NOT ' : '') + '(' + col + ' = ' + formatValue(unescapeValue(val)) + ')'; }
|
|
2402
|
+
|
|
2403
|
+
Column
|
|
2404
|
+
= first:ColPart rest:("." ColPart)* {
|
|
2405
|
+
const partsArray = [first];
|
|
2406
|
+
if (rest && rest.length > 0) {
|
|
2407
|
+
partsArray.push(...rest.map(item => item[1]));
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
if (partsArray.length > 3) {
|
|
2411
|
+
throw new SyntaxError('Column path cannot have more than 3 parts (table.column.jsonField)');
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
if (partsArray.length === 1) {
|
|
2415
|
+
return quoteSqlIdentity(partsArray[0]);
|
|
2416
|
+
}
|
|
2417
|
+
const tableName = quoteSqlIdentity(partsArray[0]);
|
|
2418
|
+
|
|
2419
|
+
if (partsArray.length === 2) {
|
|
2420
|
+
return tableName + '.' + quoteSqlIdentity(partsArray[1]);
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
const jsonColumn = quoteSqlIdentity(partsArray[1]);
|
|
2424
|
+
const lastPart = partsArray[partsArray.length - 1];
|
|
2425
|
+
const escapedLast = lastPart.replace(/'/g, "''");
|
|
2426
|
+
return tableName + '.' + jsonColumn + "->>'" + escapedLast + "'";
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
ColPart
|
|
2430
|
+
= chars:[a-zA-Z0-9_]+ { return chars.join(''); }
|
|
2431
|
+
|
|
2432
|
+
NullOperator
|
|
2433
|
+
= "notnull"i { return function(col) { return col + ' IS NOT NULL'; }; }
|
|
2434
|
+
/ "null"i { return function(col) { return col + ' IS NULL'; }; }
|
|
2435
|
+
|
|
2436
|
+
OperatorWithValue
|
|
2437
|
+
= "in"i _ "," _ val:ValueWithPipes { return function(col) { return buildInClause(col, val); }; }
|
|
2438
|
+
/ "ne"i _ "," _ val:Value { return function(col) { return col + ' <> ' + formatValue(unescapeValue(val)); }; }
|
|
2439
|
+
/ "gte"i _ "," _ val:Value { return function(col) { return col + ' >= ' + formatValue(unescapeValue(val)); }; }
|
|
2440
|
+
/ "gt"i _ "," _ val:Value { return function(col) { return col + ' > ' + formatValue(unescapeValue(val)); }; }
|
|
2441
|
+
/ "lte"i _ "," _ val:Value { return function(col) { return col + ' <= ' + formatValue(unescapeValue(val)); }; }
|
|
2442
|
+
/ "lt"i _ "," _ val:Value { return function(col) { return col + ' < ' + formatValue(unescapeValue(val)); }; }
|
|
2443
|
+
/ "has"i _ "," _ val:Value { return function(col) { return col + '::text ILIKE ' + format.literal('%' + unescapeValue(val) + '%'); }; }
|
|
2444
|
+
/ "sw"i _ "," _ val:Value { return function(col) { return col + '::text ILIKE ' + format.literal(unescapeValue(val) + '%'); }; }
|
|
2445
|
+
/ "ew"i _ "," _ val:Value { return function(col) { return col + '::text ILIKE ' + format.literal('%' + unescapeValue(val)); }; }
|
|
2446
|
+
|
|
2447
|
+
Value
|
|
2448
|
+
= chars:ValueChar+ { return chars.join(''); }
|
|
2449
|
+
|
|
2450
|
+
ValueChar
|
|
2451
|
+
= "\\\\\\\\" { return '\\\\\\\\'; }
|
|
2452
|
+
/ "\\\\," { return '\\\\,'; }
|
|
2453
|
+
/ "\\\\|" { return '\\\\|'; }
|
|
2454
|
+
/ [^,()\\\\|]
|
|
2455
|
+
|
|
2456
|
+
ValueWithPipes
|
|
2457
|
+
= chars:ValueWithPipesChar+ { return chars.join(''); }
|
|
2458
|
+
|
|
2459
|
+
ValueWithPipesChar
|
|
2460
|
+
= "\\\\\\\\" { return '\\\\\\\\'; }
|
|
2461
|
+
/ "\\\\," { return '\\\\,'; }
|
|
2462
|
+
/ "\\\\|" { return '\\\\|'; }
|
|
2463
|
+
/ [^,()\\\\]
|
|
2464
|
+
`;
|
|
2465
|
+
var fullGrammar = entryGrammar + oldGrammar + newGrammar;
|
|
2466
|
+
var filterPsqlParser = peg.generate(fullGrammar, {
|
|
2253
2467
|
format: "commonjs",
|
|
2254
2468
|
dependencies: { format: "pg-format" }
|
|
2255
2469
|
});
|
|
@@ -2410,11 +2624,11 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
2410
2624
|
if (!index.isPrimaryKey) {
|
|
2411
2625
|
let unique = " ";
|
|
2412
2626
|
if (index.isUnique) unique = "UNIQUE ";
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
);
|
|
2627
|
+
let indexSQL = ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}"`;
|
|
2628
|
+
indexSQL += ` (${index.columns.map((item) => `"${item}" ${index.order}`).join(", ")})`;
|
|
2629
|
+
indexSQL += index.where ? ` WHERE ${index.where}` : "";
|
|
2630
|
+
indexSQL += ";";
|
|
2631
|
+
indexes.push(indexSQL);
|
|
2418
2632
|
}
|
|
2419
2633
|
}
|
|
2420
2634
|
sql += "\n);";
|
|
@@ -2856,7 +3070,13 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
|
2856
3070
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
2857
3071
|
return data[requestParam.name]?.toString() || "";
|
|
2858
3072
|
});
|
|
2859
|
-
|
|
3073
|
+
const parseResult = filterPsqlParser_default.parse(statement);
|
|
3074
|
+
if (parseResult.usedOldSyntax) {
|
|
3075
|
+
logger.warn(
|
|
3076
|
+
`Deprecated filter syntax detected in route "${routeData.name}" (${routeData.path}). Please migrate to the new filter syntax.`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
statement = parseResult.sql;
|
|
2860
3080
|
if (whereClause.startsWith("WHERE")) {
|
|
2861
3081
|
whereClause += ` AND (${statement})
|
|
2862
3082
|
`;
|
|
@@ -3290,6 +3510,8 @@ var ResturaEngine = class {
|
|
|
3290
3510
|
this.resturaRouter[route.method.toLowerCase()](
|
|
3291
3511
|
route.path,
|
|
3292
3512
|
// <-- Notice we only use path here since the baseUrl is already added to the router.
|
|
3513
|
+
this.attachRouteData,
|
|
3514
|
+
addDeprecationResponse,
|
|
3293
3515
|
this.executeRouteLogic
|
|
3294
3516
|
);
|
|
3295
3517
|
routeCount++;
|
|
@@ -3367,9 +3589,17 @@ var ResturaEngine = class {
|
|
|
3367
3589
|
});
|
|
3368
3590
|
});
|
|
3369
3591
|
}
|
|
3592
|
+
attachRouteData(req, _res, next) {
|
|
3593
|
+
try {
|
|
3594
|
+
req.routeData = this.getRouteData(req.method, req.baseUrl, req.path);
|
|
3595
|
+
next();
|
|
3596
|
+
} catch (e) {
|
|
3597
|
+
next(e);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3370
3600
|
async executeRouteLogic(req, res, next) {
|
|
3371
3601
|
try {
|
|
3372
|
-
const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
|
|
3602
|
+
const routeData = req.routeData ?? this.getRouteData(req.method, req.baseUrl, req.path);
|
|
3373
3603
|
this.validateAuthorization(req, routeData);
|
|
3374
3604
|
await this.getMulterFilesIfAny(req, res, routeData);
|
|
3375
3605
|
requestValidator(
|
|
@@ -3491,6 +3721,9 @@ __decorateClass([
|
|
|
3491
3721
|
__decorateClass([
|
|
3492
3722
|
boundMethod
|
|
3493
3723
|
], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
|
|
3724
|
+
__decorateClass([
|
|
3725
|
+
boundMethod
|
|
3726
|
+
], ResturaEngine.prototype, "attachRouteData", 1);
|
|
3494
3727
|
__decorateClass([
|
|
3495
3728
|
boundMethod
|
|
3496
3729
|
], ResturaEngine.prototype, "executeRouteLogic", 1);
|
|
@@ -3561,6 +3794,7 @@ export {
|
|
|
3561
3794
|
restura,
|
|
3562
3795
|
resturaGlobalTypesGenerator,
|
|
3563
3796
|
resturaSchema,
|
|
3797
|
+
toSqlLiteral,
|
|
3564
3798
|
updateObjectQuery
|
|
3565
3799
|
};
|
|
3566
3800
|
//# sourceMappingURL=index.js.map
|