unconventional 1.15.0 → 1.17.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.
@@ -35,66 +35,138 @@ function getExpand(model, expand) {
35
35
  return expansions;
36
36
  }
37
37
  exports.getExpand = getExpand;
38
+ /** Parses a single filter condition into a SqlWhere object */
39
+ function parseSingleFilter(model, filter, andOr) {
40
+ const propSummary = model.schema.props;
41
+ const singleQuoteIndex = filter.indexOf("'");
42
+ const matchingOperators = singleQuoteIndex === -1 ?
43
+ db_types_1.ValidSqlOperators.filter(op => filter.includes(` ${op}`)) :
44
+ db_types_1.ValidSqlOperators.filter(op => filter.includes(` ${op}`) && filter.indexOf(` ${op}`) < singleQuoteIndex);
45
+ if (!matchingOperators.length)
46
+ throw api_error_1.APIError.errInvalidQueryParameter(`No valid operator in filter: '${filter}'`);
47
+ // Handle when multiple operators are present in the filter (e.g. LIKE and NOT LIKE)
48
+ let operator = matchingOperators.sort((a, b) => b.length - a.length)[0];
49
+ let field = filter.substring(0, filter.indexOf(operator)).trim();
50
+ let value = filter.substring(filter.indexOf(operator) + operator.length).trim();
51
+ let jsonPath = [];
52
+ let relationPath = undefined;
53
+ // Handle IS NULL and IS NOT NULL, which don't have values
54
+ if (operator.startsWith('IS')) {
55
+ value = null;
56
+ }
57
+ // Handle JSON path
58
+ if (field.includes('.')) {
59
+ jsonPath = field.split('.');
60
+ const target = jsonPath.shift();
61
+ // Split relational fields into relation and field. e.g. 'user.name' -> 'user' and 'name'
62
+ // Otherwise, assume json
63
+ const relation = propSummary[target]?.relation;
64
+ if (relation) {
65
+ if (jsonPath.length !== 1)
66
+ throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'. Relation filters must be in the format: 'relation.field'`);
67
+ const relationModel = (0, api_utils_1.getRelationModel)(relation);
68
+ if (!relationModel.schema.props[jsonPath[0]])
69
+ throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'.`);
70
+ relationPath = target;
71
+ field = jsonPath.shift();
72
+ }
73
+ else {
74
+ field = target;
75
+ }
76
+ }
77
+ if (!relationPath && !propSummary[field])
78
+ throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'`);
79
+ if (!value && !db_types_1.NullSqlOperators.includes(operator))
80
+ throw api_error_1.APIError.errInvalidQueryParameter(`Invalid value in filter: '${value}'`);
81
+ return { field, jsonPath, relationPath, operator: operator, value, andOr };
82
+ }
83
+ /** Extracts content within parentheses and returns it along with the position after the closing paren */
84
+ function extractParenthesesContent(str, startIndex) {
85
+ let depth = 1;
86
+ let i = startIndex + 1;
87
+ while (i < str.length && depth > 0) {
88
+ if (str[i] === '(')
89
+ depth++;
90
+ else if (str[i] === ')')
91
+ depth--;
92
+ i++;
93
+ }
94
+ if (depth !== 0)
95
+ throw api_error_1.APIError.errInvalidQueryParameter(`Unmatched parentheses in filter`);
96
+ return {
97
+ content: str.substring(startIndex + 1, i - 1),
98
+ endIndex: i
99
+ };
100
+ }
101
+ /** Checks if a parenthesis is part of an operator value (e.g., IN (...)) or a compound clause */
102
+ function isOperatorValueParen(str, parenIndex) {
103
+ // Look backwards to see if there's an operator that uses parentheses for values
104
+ const beforeParen = str.substring(0, parenIndex).trimEnd();
105
+ // Check for operators that use parentheses: IN, NOT IN
106
+ return beforeParen.endsWith(' IN') || beforeParen.endsWith(' NOT IN');
107
+ }
38
108
  /** Converts a string of filters to an array of 'where' clause outlines for a SQL environment */
39
109
  function getWhere(model, filterString) {
40
110
  if (!filterString)
41
111
  return [];
42
- const propSummary = model.schema.props;
43
112
  const wheres = [];
44
- if (filterString) {
45
- filterString.split(db_types_1.andOrPattern).forEach((filter) => {
46
- // Handle logically combined clauses
47
- let andOr = undefined;
48
- if (filter.startsWith(unconventional_pg_queries_1.AndOr.And)) {
49
- filter = filter.substring(unconventional_pg_queries_1.AndOr.And.length + 1);
50
- andOr = unconventional_pg_queries_1.AndOr.And;
51
- }
52
- else if (filter.startsWith(unconventional_pg_queries_1.AndOr.Or)) {
53
- filter = filter.substring(unconventional_pg_queries_1.AndOr.Or.length + 1);
54
- andOr = unconventional_pg_queries_1.AndOr.Or;
55
- }
56
- const singleQuoteIndex = filter.indexOf("'");
57
- const matchingOperators = singleQuoteIndex === -1 ?
58
- db_types_1.ValidSqlOperators.filter(op => filter.includes(` ${op}`)) :
59
- db_types_1.ValidSqlOperators.filter(op => filter.includes(` ${op}`) && filter.indexOf(` ${op}`) < singleQuoteIndex);
60
- if (!matchingOperators.length)
61
- throw api_error_1.APIError.errInvalidQueryParameter(`No valid operator in filter: '${filter}'`);
62
- // Handle when multiple operators are present in the filter (e.g. LIKE and NOT LIKE)
63
- let operator = matchingOperators.sort((a, b) => b.length - a.length)[0];
64
- let field = filter.substring(0, filter.indexOf(operator)).trim();
65
- let value = filter.substring(filter.indexOf(operator) + operator.length).trim();
66
- let jsonPath = [];
67
- let relationPath = undefined;
68
- // Handle IS NULL and IS NOT NULL, which don't have values
69
- if (operator.startsWith('IS')) {
70
- value = null;
113
+ let i = 0;
114
+ let currentFilter = '';
115
+ let currentAndOr = undefined;
116
+ while (i < filterString.length) {
117
+ const char = filterString[i];
118
+ // Check for closing parenthesis without matching opening (error case)
119
+ if (char === ')' && !currentFilter.includes('(')) {
120
+ throw api_error_1.APIError.errInvalidQueryParameter('Unmatched closing parenthesis in filter');
121
+ }
122
+ // Check for opening parenthesis (but distinguish compound clauses from operator values)
123
+ if (char === '(' && !isOperatorValueParen(filterString, i)) {
124
+ const { content, endIndex } = extractParenthesesContent(filterString, i);
125
+ // Recursively parse the compound clause
126
+ const nestedClauses = getWhere(model, content);
127
+ if (nestedClauses.length > 0) {
128
+ // Create a wrapper clause using the first nested clause's properties
129
+ // and add the rest as sub-clauses
130
+ const [firstClause, ...restClauses] = nestedClauses;
131
+ const compoundClause = {
132
+ ...firstClause,
133
+ andOr: currentAndOr,
134
+ clauses: restClauses.length > 0 ? restClauses : undefined
135
+ };
136
+ wheres.push(compoundClause);
71
137
  }
72
- // Handle JSON path
73
- if (field.includes('.')) {
74
- jsonPath = field.split('.');
75
- const target = jsonPath.shift();
76
- // Split relational fields into relation and field. e.g. 'user.name' -> 'user' and 'name'
77
- // Otherwise, assume json
78
- const relation = propSummary[target]?.relation;
79
- if (relation) {
80
- if (jsonPath.length !== 1)
81
- throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'. Relation filters must be in the format: 'relation.field'`);
82
- const relationModel = (0, api_utils_1.getRelationModel)(relation);
83
- if (!relationModel.schema.props[jsonPath[0]])
84
- throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'.`);
85
- relationPath = target;
86
- field = jsonPath.shift();
138
+ i = endIndex;
139
+ currentFilter = '';
140
+ currentAndOr = undefined;
141
+ continue;
142
+ }
143
+ // Check for AND/OR keywords
144
+ if (i === 0 || filterString[i - 1] === ' ' || filterString[i - 1] === ')') {
145
+ if (filterString.substring(i, i + 4) === 'AND ') {
146
+ if (currentFilter.trim()) {
147
+ wheres.push(parseSingleFilter(model, currentFilter.trim(), currentAndOr));
148
+ currentFilter = '';
87
149
  }
88
- else {
89
- field = target;
150
+ currentAndOr = unconventional_pg_queries_1.AndOr.And;
151
+ i += 4;
152
+ continue;
153
+ }
154
+ else if (filterString.substring(i, i + 3) === 'OR ') {
155
+ if (currentFilter.trim()) {
156
+ wheres.push(parseSingleFilter(model, currentFilter.trim(), currentAndOr));
157
+ currentFilter = '';
90
158
  }
159
+ currentAndOr = unconventional_pg_queries_1.AndOr.Or;
160
+ i += 3;
161
+ continue;
91
162
  }
92
- if (!relationPath && !propSummary[field])
93
- throw api_error_1.APIError.errInvalidQueryParameter(`Invalid filter: '${field}'`);
94
- if (!value && !db_types_1.NullSqlOperators.includes(operator))
95
- throw api_error_1.APIError.errInvalidQueryParameter(`Invalid value in filter: '${value}'`);
96
- wheres.push({ field, jsonPath, relationPath, operator: operator, value, andOr });
97
- });
163
+ }
164
+ currentFilter += char;
165
+ i++;
166
+ }
167
+ // Process any remaining filter
168
+ if (currentFilter.trim()) {
169
+ wheres.push(parseSingleFilter(model, currentFilter.trim(), currentAndOr));
98
170
  }
99
171
  return wheres;
100
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unconventional",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "A complete server built on Cloudflare Workers/Pages",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -33,6 +33,7 @@
33
33
  "clean": "tsc --build --clean",
34
34
  "build": "rm -rf dist && tsc --build",
35
35
  "watch": "tsc --watch",
36
+ "test": "for file in src/tests/*.test.ts; do tsx \"$file\" || exit 1; done",
36
37
  "lint": "eslint . --ext js,jsx,ts,tsx",
37
38
  "format": "eslint . --ext js,jsx,ts,tsx --fix",
38
39
  "docs:dev": "vitepress dev docs",
@@ -45,14 +46,15 @@
45
46
  },
46
47
  "peerDependencies": {
47
48
  "hono": "^4",
48
- "unconventional-pg-queries": "^1.4.0"
49
+ "unconventional-pg-queries": "^1.6.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@cloudflare/workers-types": "^4.20231121.0",
52
53
  "@typescript-eslint/eslint-plugin": "^5.62.0",
53
54
  "@typescript-eslint/parser": "^5.62.0",
54
55
  "hono": "^4",
56
+ "tsx": "^4.20.6",
55
57
  "typescript": "^5.3.3",
56
- "unconventional-pg-queries": "^1.4.0"
58
+ "unconventional-pg-queries": "^1.6.0"
57
59
  }
58
60
  }