Skip to content

Commit

Permalink
Bugfixes and README corrections (#7)
Browse files Browse the repository at this point in the history
Bugfixes and README corrections
chore: bump cross-spawn version

---------

Co-authored-by: Jens Krefeldt <[email protected]>
Co-authored-by: markford <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2024
1 parent c1b21ce commit edfdae1
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ tmp
dist
*.tgz
coverage/
.vscode/
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ __tests__
*.test.js
coverage
Makefile
.vscode
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## v1.3.1

**Bugfixes:**

- Fix [No wildcard(s) support for `!=` operator](https://github.com/massfords/ts-rsql-query/issues/6).
- Fixed a bug where `RsqlOperatorPluginToSqlOptions.keywordsLowerCase` was not passed to the options when it would be configured to `true`.
- Corrected implementation examples for plugin section in (./README.md#plugins).

**Internals:**

- Added some bugfix related (non-)equality tests to live-DB.
- Git- and npm-ignored `.vscode/` folder.
- Fixed internal function `toSqlOperator` to return SQL `=` for RSQL `==` operator (actually, case was never used before, but now it is used and fixed therefore).
- Added some bugfix related (non-)equality tests to live-DB.

## v1.3.0

**New feature implementations:**
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ const sql = assembleFullQuery(
sort: parsedSorts,
keyset,
},
context
context,
);
if (sql.isValid) {
const rows = await db.manyOrNone(sql.sql, context.values);
Expand Down Expand Up @@ -242,7 +242,7 @@ const context: SqlContext = {

### Plugin implementation

The following codes shows an example of how to implement a plugin by the predefined plugin `MapInToEqualsAnyPlugin`:
The following codes shows an example of how to implement a plugin by the predefined plugin `IsNullPlugin`:

```typescript
import { CustomOperator, formatKeyword, isBooleanValueInvariant, RsqlOperatorPlugin, RsqlOperatorPluginToSqlOptions } from "ts-rsql-query";
Expand All @@ -259,10 +259,11 @@ export const IsNullPlugin: RsqlOperatorPlugin = {
invariant: isBooleanValueInvariant,
toSql: (options: RsqlOperatorPluginToSqlOptions): string => {
const {
keywordsLowerCase,
selector,
ast: { operands },
} = options;
return `${selector} ${formatKeyword("IS", options)}${operands?.[0] === "false" ? ` ${formatKeyword("NOT", options)}` : ""} null`;
return `${selector} ${formatKeyword("IS", options)}${operands?.[0] === "false" ? ` ${formatKeyword("NOT", keywordsLowerCase)}` : ""} null`;
},
};
```
Expand Down Expand Up @@ -292,9 +293,9 @@ export const MapInToEqualsAnyPlugin: RsqlOperatorPlugin = {
invariant(ast.operands);
},
toSql: (options: RsqlOperatorPluginToSqlOptions): string => {
const { ast, selector, values, config } = options;
const { ast, keywordsLowerCase, selector, values, config } = options;
values.push(formatValue({ ast, allowArray: true }, config));
return `${selector} = ${formatKeyword("ANY", options)}($${values.length})`;
return `${selector} = ${formatKeyword("ANY", keywordsLowerCase)}($${values.length})`;
},
};
```
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export type RsqlOperatorPluginToSqlOptions = {
*
* @default false
*/
readonly keywordsLowerCase?: boolean;
readonly keywordsLowerCase?: true | undefined;
/**
* The static query configuration.
*/
Expand Down
4 changes: 2 additions & 2 deletions src/llb/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type KnownOperator = SymbolicOperator | NamedOperator;
*
* > NOTE:
* > - Option `keywordsLowerCase` has no effect for:
* > - `==`
* > - `!=`
* > - `<`
* > - `=lt=`
Expand All @@ -31,7 +32,6 @@ export type KnownOperator = SymbolicOperator | NamedOperator;
* > - `>=`
* > - `=ge=`
* > - Option `detachedOperators` has no effect for:
* > - `==`
* > - `=in=`
* > - `=out=`
*
Expand All @@ -48,7 +48,7 @@ export const toSqlOperator = (
const space = detachedOperators ? " " : "";
switch (operator) {
case "==":
return formatKeyword("LIKE", keywordsLowerCase);
return `${space}=${space}`;
case "!=":
return `${space}<>${space}`;
case "<":
Expand Down
70 changes: 46 additions & 24 deletions src/llb/to-sql.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ASTNode, ComparisonNode, Operand, parseRsql } from "ts-rsql";
import invariant from "tiny-invariant";
import { isAstNode, isComparisonNode } from "./ast";
import { ASTNode, ComparisonNode, Operand, parseRsql } from "ts-rsql";
import type {
SelectorConfig,
SqlContext,
StaticQueryConfig,
Value,
} from "../context";
import { maybeExecuteRsqlOperatorPlugin } from "../plugin";
import { isAstNode, isComparisonNode } from "./ast";
import { KnownOperator, toSqlOperator } from "./operators";
import { validate } from "./validate";
import { maybeExecuteRsqlOperatorPlugin } from "../plugin";

/**
* Formats a keyword according to configuration (either fully upper- or lower-case).
Expand Down Expand Up @@ -126,6 +126,46 @@ export const toSql = (
return { isValid: true, sql: _toSql(ast, context) };
};

/**
* Creates the SQL string from the `==` or `!=` RSQL operator including any wildcard(s) occurrence in its operand.
*
* @param ast - The comparison node.
* @param context - The SQL context.
* @param operator - The `==` or `!=` RSQL operator.
* @param selector - The current selector.
* @returns The proper SQL string for `==` or `!=` RSQL operator.
*/
const _handleEqualOrNotEqualOperator = (
ast: ComparisonNode,
context: SqlContext,
operator: "==" | "!=",
selector: string,
): string => {
const { detachedOperators, keywordsLowerCase, values } = context;
invariant(ast.operands);
invariant(ast.operands[0]);
const operand = ast.operands[0];
const leadingWildcard = operand.startsWith("*");
const trailingWildcard = operand.endsWith("*");
if (leadingWildcard && trailingWildcard) {
values.push(`%${operand.substring(1, operand.length - 1)}%`);
} else if (leadingWildcard) {
values.push(`%${operand.substring(1)}`);
} else if (trailingWildcard) {
values.push(`${operand.substring(0, operand.length - 1)}%`);
} else {
values.push(formatValue({ ast }, context));
}
return `${selector}${
leadingWildcard || trailingWildcard
? ` ${formatKeyword(
`${operator === "!=" ? "NOT " : ""}ILIKE`,
keywordsLowerCase,
)} `
: `${toSqlOperator(operator, keywordsLowerCase, detachedOperators)}`
}$${values.length}`;
};

const _toSql = (ast: ASTNode | null | Operand, context: SqlContext): string => {
if (!isAstNode(ast)) {
// We would have reported this as an error in validate.
Expand Down Expand Up @@ -166,28 +206,10 @@ const _toSql = (ast: ASTNode | null | Operand, context: SqlContext): string => {
}

switch (op) {
case "==": {
invariant(ast.operands);
invariant(ast.operands[0]);
const operand = ast.operands[0];
const leadingWildcard = operand.startsWith("*");
const trailingWildcard = operand.endsWith("*");
if (leadingWildcard && trailingWildcard) {
values.push(`%${operand.substring(1, operand.length - 1)}%`);
} else if (leadingWildcard) {
values.push(`%${operand.substring(1)}`);
} else if (trailingWildcard) {
values.push(`${operand.substring(0, operand.length - 1)}%`);
} else {
values.push(formatValue({ ast }, config));
}
return `${selector}${
leadingWildcard || trailingWildcard
? ` ${formatKeyword("ILIKE", keywordsLowerCase)} `
: `${detachedOperators ? " " : ""}=${detachedOperators ? " " : ""}`
}$${values.length}`;
case "==":
case "!=": {
return _handleEqualOrNotEqualOperator(ast, context, op, selector);
}
case "!=":
case "<":
case "<=":
case ">":
Expand Down
3 changes: 2 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const maybeExecuteRsqlOperatorPlugin = (
ast: ComparisonNode,
formattedSelector: string,
): string | undefined => {
const { plugins, values } = context;
const { keywordsLowerCase, plugins, values } = context;
/* Check for plugin (custom operator or overwrite of known operator). */
const plugin = plugins?.length
? plugins.find((plugin) => plugin.operator.toLowerCase() === ast.operator)
Expand All @@ -60,6 +60,7 @@ export const maybeExecuteRsqlOperatorPlugin = (
selector: formattedSelector,
ast,
values,
keywordsLowerCase,
config: context,
});
}
Expand Down
48 changes: 38 additions & 10 deletions src/tests/live-db.it.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import {
PostgreSqlContainer,
StartedPostgreSqlContainer,
} from "testcontainers";
import invariant from "tiny-invariant";
import { parseSort, SortNode } from "ts-rsql";
import type { SqlContext } from "../context";
import { lastRowToKeySet, toKeySet } from "../keyset";
import { assembleFullQuery } from "../query";
import { TestQueryConfig, TestQueryConfigWithPlugins } from "./fixture";
import {
db,
destroyDb,
idForTestRecord,
initDb,
UserRecord,
} from "./fixture-db";
import invariant from "tiny-invariant";
import { TestQueryConfig, TestQueryConfigWithPlugins } from "./fixture";
import type { SqlContext } from "../context";
import { assembleFullQuery } from "../query";
import { lastRowToKeySet, toKeySet } from "../keyset";
import { parseSort, SortNode } from "ts-rsql";
import {
PostgreSqlContainer,
StartedPostgreSqlContainer,
} from "testcontainers";

describe("runs the sql with a real db connection", () => {
let startedContainer: StartedPostgreSqlContainer | null = null;
Expand All @@ -40,6 +40,34 @@ describe("runs the sql with a real db connection", () => {
filter: "firstName==Alice",
rows: 1,
},
{
filter: "firstName==*Alice",
rows: 1,
},
{
filter: "firstName==Alice*",
rows: 1,
},
{
filter: "firstName==*Alice*",
rows: 1,
},
{
filter: "firstName!=Alice",
rows: 2,
},
{
filter: "firstName!=*Alice",
rows: 2,
},
{
filter: "firstName!=Alice*",
rows: 2,
},
{
filter: "firstName!=*Alice*",
rows: 2,
},
{
filter: "firstName==Alice,firstName==Bo*",
rows: 2,
Expand Down
4 changes: 2 additions & 2 deletions src/tests/operators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("operators tests", () => {
}> = [
{
rsql: "==",
sql: "LIKE",
sql: "=",
},
{
rsql: "!=",
Expand Down Expand Up @@ -71,7 +71,7 @@ describe("operators tests", () => {
}> = [
{
rsql: "==",
sql: "like",
sql: " = ",
},
{
rsql: "!=",
Expand Down
22 changes: 22 additions & 0 deletions src/tests/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,28 @@ describe("tests for sql generation by plugins", () => {
});
});

it("should pass the keywordsLowerCase configuration from context", () => {
const newContext = {
...context,
keywordsLowerCase: true,
} as unknown as SqlContext;
expect(
maybeExecuteRsqlOperatorPlugin(newContext, ast, formattedSelector),
).toBe(sql);

expect(mockInvariant).toHaveBeenCalledTimes(1);
expect(mockInvariant).toHaveBeenCalledWith(ast);

expect(mockToSql).toHaveBeenCalledTimes(1);
expect(mockToSql).toHaveBeenCalledWith({
selector: formattedSelector,
ast,
values,
keywordsLowerCase: true,
config: newContext,
});
});

it("should return undefined if plugins configuration is empty array", () => {
expect(
maybeExecuteRsqlOperatorPlugin(
Expand Down
Loading

0 comments on commit edfdae1

Please sign in to comment.