@restura/core 1.6.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 +17 -4
- package/dist/index.js +373 -168
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
}
|
|
@@ -1658,31 +1688,81 @@ async function isSchemaValid(schemaToCheck) {
|
|
|
1658
1688
|
|
|
1659
1689
|
// src/restura/validators/requestValidator.ts
|
|
1660
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
|
+
}
|
|
1661
1732
|
function requestValidator(req, routeData, customValidationSchema, standardValidationSchema) {
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
} 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) {
|
|
1670
1740
|
if (!routeData.responseType) throw new RsError("BAD_REQUEST", `No response type defined for custom request.`);
|
|
1671
1741
|
if (!routeData.requestType && !routeData.request)
|
|
1672
1742
|
throw new RsError("BAD_REQUEST", `No request type defined for custom request.`);
|
|
1673
|
-
const routeKey = `${routeData.method}:${routeData.path}`;
|
|
1674
|
-
const currentInterface = customValidationSchema[routeData.requestType || routeKey];
|
|
1675
|
-
schemaForCoercion = {
|
|
1676
|
-
...currentInterface,
|
|
1677
|
-
additionalProperties: false
|
|
1678
|
-
};
|
|
1679
|
-
} else {
|
|
1680
|
-
throw new RsError("BAD_REQUEST", `Invalid route type: ${routeData.type}`);
|
|
1681
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;
|
|
1682
1756
|
const requestData = getRequestData(req, schemaForCoercion);
|
|
1683
1757
|
req.data = requestData;
|
|
1684
1758
|
const validator = new jsonschema.Validator();
|
|
1685
|
-
|
|
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);
|
|
1686
1766
|
if (!executeValidation.valid) {
|
|
1687
1767
|
const errorMessages = executeValidation.errors.map((err) => {
|
|
1688
1768
|
const property = err.property.replace("instance.", "");
|
|
@@ -1802,7 +1882,6 @@ import pg from "pg";
|
|
|
1802
1882
|
|
|
1803
1883
|
// src/restura/sql/PsqlConnection.ts
|
|
1804
1884
|
import crypto from "crypto";
|
|
1805
|
-
import format2 from "pg-format";
|
|
1806
1885
|
import { format as sqlFormat } from "sql-formatter";
|
|
1807
1886
|
import { z as z5 } from "zod";
|
|
1808
1887
|
|
|
@@ -1877,6 +1956,13 @@ function SQL(strings, ...values) {
|
|
|
1877
1956
|
});
|
|
1878
1957
|
return query;
|
|
1879
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
|
+
}
|
|
1880
1966
|
|
|
1881
1967
|
// src/restura/sql/PsqlConnection.ts
|
|
1882
1968
|
var PsqlConnection = class {
|
|
@@ -1955,21 +2041,11 @@ var PsqlConnection = class {
|
|
|
1955
2041
|
}
|
|
1956
2042
|
logSqlStatement(query, options, queryMetadata, startTime, prefix = "") {
|
|
1957
2043
|
if (logger.level !== "trace" && logger.level !== "silly") return;
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
const paramIndex = parseInt(match.substring(1)) - 1;
|
|
1964
|
-
if (paramIndex < 0 || paramIndex >= options.length) {
|
|
1965
|
-
return "INVALID_PARAM_INDEX";
|
|
1966
|
-
}
|
|
1967
|
-
const value = options[paramIndex];
|
|
1968
|
-
if (typeof value === "number") return value.toString();
|
|
1969
|
-
if (typeof value === "boolean") return value.toString();
|
|
1970
|
-
return format2.literal(value);
|
|
1971
|
-
});
|
|
1972
|
-
}
|
|
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
|
+
});
|
|
1973
2049
|
const formattedSql = sqlFormat(sqlStatement, {
|
|
1974
2050
|
language: "postgresql",
|
|
1975
2051
|
linesBetweenQueries: 2,
|
|
@@ -2148,124 +2224,246 @@ var SqlEngine = class {
|
|
|
2148
2224
|
|
|
2149
2225
|
// src/restura/sql/filterPsqlParser.ts
|
|
2150
2226
|
import peg from "pegjs";
|
|
2151
|
-
var
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
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
|
+
}
|
|
2245
|
+
} else {
|
|
2246
|
+
result += str[i];
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
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 = '';
|
|
2170
2263
|
} else {
|
|
2171
|
-
|
|
2264
|
+
current += str[i];
|
|
2172
2265
|
}
|
|
2173
2266
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2267
|
+
if (current.length > 0) {
|
|
2268
|
+
values.push(unescapeValue(current));
|
|
2269
|
+
}
|
|
2270
|
+
return values;
|
|
2177
2271
|
}
|
|
2178
2272
|
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
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
|
+
}
|
|
2187
2279
|
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
if (
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
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
|
|
2194
2285
|
}
|
|
2286
|
+
return format.literal(value);
|
|
2195
2287
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
};
|
|
2288
|
+
`;
|
|
2289
|
+
var entryGrammar = `
|
|
2290
|
+
{
|
|
2291
|
+
${initializers}
|
|
2201
2292
|
}
|
|
2202
2293
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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)}\`)})\`;}
|
|
2215
2312
|
/
|
|
2216
|
-
negate:
|
|
2313
|
+
negate:OldNegate?"("expression:OldExpressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
|
|
2217
2314
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
operator = "and"i / "or"i
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
column = first:text rest:("." text)* {
|
|
2224
|
-
const partsArray = [first];
|
|
2225
|
-
if (rest && rest.length > 0) {
|
|
2226
|
-
partsArray.push(...rest.map(item => item[1]));
|
|
2227
|
-
}
|
|
2315
|
+
OldNegate
|
|
2316
|
+
= "!"
|
|
2228
2317
|
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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;
|
|
2241
2348
|
}
|
|
2242
|
-
|
|
2243
|
-
// For JSON paths (more than 2 parts), first part is a column, last part uses ->>
|
|
2244
|
-
const jsonColumn = quoteSqlIdentity(partsArray[1]);
|
|
2245
|
-
const lastPart = partsArray[partsArray.length - 1];
|
|
2246
|
-
const result = tableName + "." + jsonColumn + "->>'" + lastPart + "'";
|
|
2247
|
-
return result;
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
|
|
2251
2349
|
|
|
2350
|
+
OldText
|
|
2351
|
+
= text:[a-z0-9 \\t\\r\\n\\-_:@']i+ {
|
|
2352
|
+
return text.join("");
|
|
2353
|
+
}
|
|
2252
2354
|
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
text:"exact" { return function(column, value) { return \`\${column} = '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2258
|
-
text:"greaterThanEqual" { return function(column, value) { return \`\${column} >= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2259
|
-
text:"greaterThan" { return function(column, value) { return \`\${column} > '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2260
|
-
text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2261
|
-
text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
2262
|
-
text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
|
|
2263
|
-
|
|
2264
|
-
value = "value" _ ":" value:text { return value; }
|
|
2355
|
+
OldType
|
|
2356
|
+
= "type" _ ":" _ type:OldTypeString {
|
|
2357
|
+
return type;
|
|
2358
|
+
}
|
|
2265
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\`; } }
|
|
2266
2370
|
|
|
2371
|
+
OldValue
|
|
2372
|
+
= "value" _ ":" value:OldText {
|
|
2373
|
+
return value;
|
|
2374
|
+
}
|
|
2267
2375
|
`;
|
|
2268
|
-
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, {
|
|
2269
2467
|
format: "commonjs",
|
|
2270
2468
|
dependencies: { format: "pg-format" }
|
|
2271
2469
|
});
|
|
@@ -2872,7 +3070,13 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
|
2872
3070
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
2873
3071
|
return data[requestParam.name]?.toString() || "";
|
|
2874
3072
|
});
|
|
2875
|
-
|
|
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;
|
|
2876
3080
|
if (whereClause.startsWith("WHERE")) {
|
|
2877
3081
|
whereClause += ` AND (${statement})
|
|
2878
3082
|
`;
|
|
@@ -3590,6 +3794,7 @@ export {
|
|
|
3590
3794
|
restura,
|
|
3591
3795
|
resturaGlobalTypesGenerator,
|
|
3592
3796
|
resturaSchema,
|
|
3797
|
+
toSqlLiteral,
|
|
3593
3798
|
updateObjectQuery
|
|
3594
3799
|
};
|
|
3595
3800
|
//# sourceMappingURL=index.js.map
|