Skip to content

Commit

Permalink
fix: handle typescript enums correctly in SQL checks (#297)
Browse files Browse the repository at this point in the history
Fixed an issue where TypeScript enums were not processed properly in some cases when comparing with PostgreSQL enums.
  • Loading branch information
Newbie012 authored Dec 7, 2024
1 parent 7e9b259 commit f4c9106
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-oranges-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ts-safeql/eslint-plugin": patch
---

fixed an issue where typescript enums weren't processed properly in some cases
31 changes: 30 additions & 1 deletion packages/eslint-plugin/src/rules/check-sql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,17 @@ RuleTester.describe("check-sql", () => {
\`);
`,
},
{
name: "compare typescript enum with postgres enum",
filename,
options: withConnection(connections.withTag),
code: `
enum Certification { HHA = "HHA", RN = "RN" }
function foo(cert: Certification) {
sql\`select from caregiver where certification = \${cert}\`
}
`,
},
{
name: "select from table with inner joins",
filename,
Expand Down Expand Up @@ -834,6 +845,25 @@ RuleTester.describe("check-sql", () => {
},
],
},
{
name: "incorrect comparison of typescript <> postgres enum",
filename,
options: withConnection(connections.withTag),
code: normalizeIndent`
enum Certification { HHA = "HHA", RN = "RM" }
function foo(cert: Certification) {
sql\`select from caregiver where certification = \${cert}\`
}
`,
errors: [
{
messageId: "invalidQuery",
data: {
error: 'invalid input value for enum certification: "RM"',
},
},
],
},
],
});

Expand Down Expand Up @@ -1105,7 +1135,6 @@ RuleTester.describe("check-sql", () => {
interface Parameter<T> { value: T; }
class LocalDate {}
function run(simple: LocalDate, parameterized: Parameter<LocalDate>) {
sql<{ date_col: LocalDate }>\`select date_col from test_date_column WHERE date_col = \${simple}\`
sql<{ date_col: LocalDate }>\`select date_col from test_date_column WHERE date_col = \${parameterized}\`
}
`,
Expand Down
44 changes: 41 additions & 3 deletions packages/eslint-plugin/src/utils/ts-pg.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,6 @@ function getPgTypeFromTsType(params: {
}): E.Either<string, PgTypeStrategy | null> {
const { checker, node, type, options } = params;

// Utility function to get PostgreSQL type from flags
const getPgTypeFromFlags = (flags: ts.TypeFlags) => tsFlagToPgTypeMap[flags];

// Check for conditional expression
if (node.kind === ts.SyntaxKind.ConditionalExpression) {
const whenTrueType = getPgTypeFromFlags(checker.getTypeAtLocation(node.whenTrue).flags);
Expand Down Expand Up @@ -261,6 +258,19 @@ function getPgTypeFromTsType(params: {
)
: E.left("Invalid array union type");
}

if (
checker.isArrayType(type) &&
TSUtils.isTsTypeReference(type) &&
type.typeArguments?.length === 1
) {
const typeArgument = type.typeArguments?.[0];

return pipe(
checkType({ checker, type: typeArgument, options }),
E.map((pgType): PgTypeStrategy => ({ kind: "cast", cast: `${pgType.cast}[]` })),
);
}
}

if (node.kind === ts.SyntaxKind.StringLiteral) {
Expand Down Expand Up @@ -290,6 +300,20 @@ function getPgTypeFromTsType(params: {
: E.left("Unsupported union type");
}

return checkType({ checker, type, options });
}

function getPgTypeFromFlags(flags: ts.TypeFlags) {
return tsFlagToPgTypeMap[flags];
}

function checkType(params: {
checker: TypeChecker;
type: ts.Type;
options: RuleOptionConnection;
}): E.Either<string, PgTypeStrategy> {
const { checker, type, options } = params;

// Handle array types
const typeStr = checker.typeToString(type);
const singularType = typeStr.replace(/\[\]$/, "");
Expand All @@ -310,6 +334,20 @@ function getPgTypeFromTsType(params: {
}
}

const enumType = TSUtils.getEnumKind(type);

if (enumType) {
switch (enumType.kind) {
case "Const":
case "Numeric":
return E.right({ kind: "cast", cast: "int" });
case "String":
return E.right({ kind: "one-of", types: enumType.values, cast: "text" });
case "Heterogeneous":
return E.left("Heterogeneous enums are not supported");
}
}

// Handle overrides
const typesWithOverrides = { ...defaultTypeMapping, ...options.overrides?.types };
const override = Object.entries(typesWithOverrides).find(([, tsType]) =>
Expand Down
58 changes: 58 additions & 0 deletions packages/eslint-plugin/src/utils/ts.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import ts from "typescript";

type EnumKind =
| { kind: "Const" }
| { kind: "Numeric" }
| { kind: "String"; values: string[] }
| { kind: "Heterogeneous" };

export const TSUtils = {
isTypeUnion: (typeNode: ts.TypeNode | undefined): typeNode is ts.UnionTypeNode => {
return typeNode?.kind === ts.SyntaxKind.UnionType;
Expand Down Expand Up @@ -32,4 +38,56 @@ export const TSUtils = {
isTsObjectType(type: ts.Type): type is ts.ObjectType {
return type.flags === ts.TypeFlags.Object;
},
getEnumKind(type: ts.Type): EnumKind | undefined {
const symbol = type.getSymbol();
if (!symbol || !(symbol.flags & ts.SymbolFlags.Enum)) {
return undefined; // Not an enum
}

const declarations = symbol.getDeclarations();
if (!declarations) {
return undefined;
}

let hasString = false;
let hasNumeric = false;
const stringValues: string[] = [];

for (const declaration of declarations) {
if (ts.isEnumDeclaration(declaration)) {
for (const member of declaration.members) {
const initializer = member.initializer;

if (initializer) {
if (ts.isStringLiteralLike(initializer)) {
hasString = true;
stringValues.push(initializer.text);
}

if (initializer.kind === ts.SyntaxKind.NumericLiteral) {
hasNumeric = true;
}
} else {
// Members without initializers are numeric by default
hasNumeric = true;
}
}
}
}

// Determine the kind of enum
if (symbol.flags & ts.SymbolFlags.ConstEnum) {
return { kind: "Const" };
}

if (hasString && hasNumeric) {
return { kind: "Heterogeneous" };
}

if (hasString) {
return { kind: "String", values: stringValues };
}

return { kind: "Numeric" };
},
};

0 comments on commit f4c9106

Please sign in to comment.