@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.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
- callback(insertData, data.queryMetadata);
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
- callback(deleteData, data.queryMetadata);
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
- callback(columnChangeData, data.queryMetadata);
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 * as TJS from "typescript-json-schema";
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
- return {
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
- fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes.join("\n"));
1219
- const compilerOptions = {
1220
- strictNullChecks: true,
1221
- skipLibCheck: true
1222
- // Needed if we are processing ES modules
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 program = TJS.getProgramFromFiles(
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
- const ddlSchema = TJS.generateSchema(program, item, {
1237
- required: true
1238
- });
1239
- schemaObject[item] = ddlSchema || {};
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
- type: "object",
1263
- properties: {},
1264
- additionalProperties: false
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
- let schemaForCoercion;
1663
- if (routeData.type === "ONE" || routeData.type === "ARRAY" || routeData.type === "PAGED") {
1664
- const routeKey = `${routeData.method}:${routeData.path}`;
1665
- schemaForCoercion = standardValidationSchema[routeKey];
1666
- if (!schemaForCoercion) {
1667
- throw new RsError("BAD_REQUEST", `No schema found for standard request route: ${routeKey}.`);
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
- const executeValidation = validator.validate(req.data, schemaForCoercion);
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
- let sqlStatement = "";
1959
- if (options.length === 0) {
1960
- sqlStatement = query;
1961
- } else {
1962
- sqlStatement = query.replace(/\$\d+/g, (match) => {
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 filterSqlGrammar = `
2152
- {
2153
- // ported from pg-format but intentionally will add double quotes to every column
2154
- function quoteSqlIdentity(value) {
2155
- if (value === undefined || value === null) {
2156
- throw new Error('SQL identifier cannot be null or undefined');
2157
- } else if (value === false) {
2158
- return '"f"';
2159
- } else if (value === true) {
2160
- return '"t"';
2161
- } else if (value instanceof Date) {
2162
- // return '"' + formatDate(value.toISOString()) + '"';
2163
- } else if (value instanceof Buffer) {
2164
- throw new Error('SQL identifier cannot be a buffer');
2165
- } else if (Array.isArray(value) === true) {
2166
- var temp = [];
2167
- for (var i = 0; i < value.length; i++) {
2168
- if (Array.isArray(value[i]) === true) {
2169
- 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
+ }
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
- // temp.push(quoteIdent(value[i]));
2264
+ current += str[i];
2172
2265
  }
2173
2266
  }
2174
- return temp.toString();
2175
- } else if (value === Object(value)) {
2176
- throw new Error('SQL identifier cannot be an object');
2267
+ if (current.length > 0) {
2268
+ values.push(unescapeValue(current));
2269
+ }
2270
+ return values;
2177
2271
  }
2178
2272
 
2179
- var ident = value.toString().slice(0); // create copy
2180
-
2181
- // do not quote a valid, unquoted identifier
2182
- // if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
2183
- // return ident;
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
- for (var i = 0; i < ident.length; i++) {
2189
- var c = ident[i];
2190
- if (c === '"') {
2191
- quoted += c + c;
2192
- } else {
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
- quoted += '"';
2198
-
2199
- return quoted;
2200
- };
2288
+ `;
2289
+ var entryGrammar = `
2290
+ {
2291
+ ${initializers}
2201
2292
  }
2202
2293
 
2203
- start = expressionList
2204
-
2205
- _ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
2206
-
2207
- expressionList =
2208
- leftExpression:expression _ operator:operator _ rightExpression:expressionList
2209
- { return \`\${leftExpression} \${operator} \${rightExpression}\`;}
2210
- / expression
2211
-
2212
- expression =
2213
- negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
2214
- {return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
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:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
2313
+ negate:OldNegate?"("expression:OldExpressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
2217
2314
 
2218
- negate = "!"
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
- if (partsArray.length > 3) {
2230
- throw new SyntaxError('Column path cannot have more than 3 parts (table.column.jsonField)');
2231
- }
2232
-
2233
- if (partsArray.length === 1) {
2234
- return quoteSqlIdentity(partsArray[0]);
2235
- }
2236
- const tableName = quoteSqlIdentity(partsArray[0]);
2237
-
2238
- // If we only have two parts (table.column), use regular dot notation
2239
- if (partsArray.length === 2) {
2240
- return tableName + "." + quoteSqlIdentity(partsArray[1]);
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
- type = "type" _ ":" _ type:typeString { return type; }
2254
- typeString = text:"startsWith" { return function(column, value) { return \`\${column}::text ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
2255
- text:"endsWith" { return function(column, value) { return \`\${column}::text ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
2256
- text:"contains" { return function(column, value) { return \`\${column}::text ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
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 filterPsqlParser = peg.generate(filterSqlGrammar, {
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
- statement = filterPsqlParser_default.parse(statement);
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