Skip to content

Commit

Permalink
Support WHERE col IN (...) in context.db.table.select and delete (#606)
Browse files Browse the repository at this point in the history
Add support for `context.db.Table.select({column_name:
['a.near', 'b.near']})`. The same support is added for `delete`. Frontend support is added. I also improved parameter naming to reflect SQL statements like `where`, `values` and `set`.
  • Loading branch information
pkudinov authored Mar 19, 2024
1 parent 10914d7 commit d814879
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 37 deletions.
43 changes: 24 additions & 19 deletions frontend/src/utils/pgSchemaTypeGen.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class PgSchemaTypeGen {
if (columnSpec.hasOwnProperty("column") && columnSpec.hasOwnProperty("definition")) {
// New Column
this.addColumn(columnSpec, columns);
} else if (columnSpec.hasOwnProperty("constraint") && columnSpec.constraint_type == "primary key") {
} else if (columnSpec.hasOwnProperty("constraint") && columnSpec.constraint_type === "primary key") {
// Constraint on existing column
for (const foreignKeyDef of columnSpec.definition) {
columns[foreignKeyDef.column].nullable = false;
Expand All @@ -90,7 +90,7 @@ export class PgSchemaTypeGen {
break;
case "constraint": // Add constraint to column(s) (Only PRIMARY KEY constraint impacts output types)
const newConstraint = alterSpec.create_definitions;
if (newConstraint.constraint_type == "primary key") {
if (newConstraint.constraint_type === "primary key") {
for (const foreignKeyDef of newConstraint.definition) {
dbSchema[tableName][foreignKeyDef.column].nullable = false;
}
Expand Down Expand Up @@ -130,10 +130,10 @@ export class PgSchemaTypeGen {
getNullableStatus(columnDef) {
const isPrimaryKey =
columnDef.hasOwnProperty("unique_or_primary") &&
columnDef.unique_or_primary == "primary key";
columnDef.unique_or_primary === "primary key";
const isNullable =
columnDef.hasOwnProperty("nullable") &&
columnDef.nullable.value == "not null";
columnDef.nullable.value === "not null";
return isPrimaryKey || isNullable ? false : true;
}

Expand All @@ -159,38 +159,43 @@ export class PgSchemaTypeGen {
// Process each table
for (const [tableName, columns] of Object.entries(schema)) {
let itemDefinition = "";
let inputDefinition = "";
let partialDefinition = "";
let filterDefinition = "";
const sanitizedTableName = this.sanitizeTableName(tableName);
if (tableList.has(sanitizedTableName)) {
throw new Error(`Table '${tableName}' has the same name as another table in the generated types. Special characters are removed to generate context.db methods. Please rename the table.`);
}
tableList.add(sanitizedTableName);
// Create interfaces for strongly typed input and row item
partialDefinition += `declare interface ${sanitizedTableName}Partial {\n`;
itemDefinition += `declare interface ${sanitizedTableName}Item {\n`;
inputDefinition += `declare interface ${sanitizedTableName}Input {\n`;
for (const [columnName, columnDetails] of Object.entries(columns)) {
let tsType = columnDetails.nullable ? columnDetails.type + " | null" : columnDetails.type;
const optional = columnDetails.required ? "" : "?";
itemDefinition += ` ${columnName}?: ${tsType};\n`; // Item fields are always optional
inputDefinition += ` ${columnName}${optional}: ${tsType};\n`;
filterDefinition += `declare interface ${sanitizedTableName}Filter {\n`;
for (const [colName, col] of Object.entries(columns)) {
const tsType = col.nullable ? `${col.type} | null` : `${col.type}`
partialDefinition += ` ${colName}?: ${tsType};\n`;
const optional = col.required ? "" : "?";
itemDefinition += ` ${colName}${optional}: ${tsType};\n`;
const conditionType = `${tsType} | ${col.type}[]`;
filterDefinition += ` ${colName}?: ${conditionType};\n`;
}
itemDefinition += "}\n\n";
inputDefinition += "}\n\n";
partialDefinition += "}\n\n";
filterDefinition += "}\n\n";

// Create type containing column names to be used as a replacement for string[].
const columnNamesDef = `type ${sanitizedTableName}Columns = "${Object.keys(columns).join('" | "')}";\n\n`;

// Add generated types to definitions
tsDefinitions += itemDefinition + inputDefinition + columnNamesDef;
tsDefinitions += itemDefinition + partialDefinition + filterDefinition + columnNamesDef;

// Create context object with correctly formatted methods. Name, input, and output should match actual implementation
contextObject += `
${sanitizedTableName}: {
insert: (objectsToInsert: ${sanitizedTableName}Input | ${sanitizedTableName}Input[]) => Promise<${sanitizedTableName}Item[]>;
select: (filterObj: ${sanitizedTableName}Item, limit = null) => Promise<${sanitizedTableName}Item[]>;
update: (filterObj: ${sanitizedTableName}Item, updateObj: ${sanitizedTableName}Item) => Promise<${sanitizedTableName}Item[]>;
upsert: (objectsToInsert: ${sanitizedTableName}Input | ${sanitizedTableName}Input[], conflictColumns: ${sanitizedTableName}Columns[], updateColumns: ${sanitizedTableName}Columns[]) => Promise<${sanitizedTableName}Item[]>;
delete: (filterObj: ${sanitizedTableName}Item) => Promise<${sanitizedTableName}Item[]>;
insert: (values: ${sanitizedTableName}Item | ${sanitizedTableName}Item[]) => Promise<${sanitizedTableName}Item[]>;
select: (where: ${sanitizedTableName}Filter, limit = null) => Promise<${sanitizedTableName}Item[]>;
update: (where: ${sanitizedTableName}Partial, set: ${sanitizedTableName}Partial) => Promise<${sanitizedTableName}Item[]>;
upsert: (values: ${sanitizedTableName}Item | ${sanitizedTableName}Item[], conflictColumns: ${sanitizedTableName}Columns[], updateColumns: ${sanitizedTableName}Columns[]) => Promise<${sanitizedTableName}Item[]>;
delete: (where: ${sanitizedTableName}Filter) => Promise<${sanitizedTableName}Item[]>;
},`;
}

Expand Down Expand Up @@ -266,4 +271,4 @@ export class PgSchemaTypeGen {
return "any";
}
}
}
}
38 changes: 33 additions & 5 deletions runner/src/dml-handler/dml-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pgFormat from 'pg-format';
import DmlHandler from './dml-handler';
import PgClient from '../pg-client';
import type PgClient from '../pg-client';

describe('DML Handler tests', () => {
const getDbConnectionParameters = {
Expand All @@ -19,7 +19,7 @@ describe('DML Handler tests', () => {
beforeEach(() => {
query = jest.fn().mockReturnValue({ rows: [] });
pgClient = {
query: query,
query,
format: pgFormat
} as unknown as PgClient;
});
Expand Down Expand Up @@ -76,6 +76,34 @@ describe('DML Handler tests', () => {
]);
});

test('Test valid select with a single column condition and multiple column conditions', async () => {
const inputObj = {
account_id: ['test_acc_near1', 'test_acc_near2'],
block_height: 999,
};

const dmlHandler = DmlHandler.create(getDbConnectionParameters, pgClient);

await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj);
expect(query.mock.calls).toEqual([
['SELECT * FROM test_schema."test_table" WHERE account_id IN ($1,$2) AND block_height=$3', [...inputObj.account_id, inputObj.block_height]]
]);
});

test('Test valid select with two multiple column conditions', async () => {
const inputObj = {
account_id: ['test_acc_near1', 'test_acc_near2'],
block_height: [998, 999],
};

const dmlHandler = DmlHandler.create(getDbConnectionParameters, pgClient);

await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj);
expect(query.mock.calls).toEqual([
['SELECT * FROM test_schema."test_table" WHERE account_id IN ($1,$2) AND block_height IN ($3,$4)', [...inputObj.account_id, ...inputObj.block_height]]
]);
});

test('Test valid select on two fields with limit', async () => {
const inputObj = {
account_id: 'test_acc_near',
Expand Down Expand Up @@ -132,17 +160,17 @@ describe('DML Handler tests', () => {
]);
});

test('Test valid delete on two fields', async () => {
test('Test valid delete with a single column condition and multiple column conditions', async () => {
const inputObj = {
account_id: 'test_acc_near',
block_height: 999,
block_height: [998, 999],
};

const dmlHandler = DmlHandler.create(getDbConnectionParameters, pgClient);

await dmlHandler.delete(SCHEMA, TABLE_NAME, inputObj);
expect(query.mock.calls).toEqual([
['DELETE FROM test_schema."test_table" WHERE account_id=$1 AND block_height=$2 RETURNING *', Object.values(inputObj)]
['DELETE FROM test_schema."test_table" WHERE account_id=$1 AND block_height IN ($2,$3) RETURNING *', [inputObj.account_id, ...inputObj.block_height]]
]);
});
});
43 changes: 30 additions & 13 deletions runner/src/dml-handler/dml-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { wrapError } from '../utility';
import PgClient from '../pg-client';
import { type DatabaseConnectionParameters } from '../provisioner/provisioner';

type WhereClauseMulti = Record<string, (string | number | Array<string | number>)>;
type WhereClauseSingle = Record<string, (string | number)>;

export default class DmlHandler {
validTableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

Expand All @@ -23,6 +26,24 @@ export default class DmlHandler {
return new DmlHandler(pgClient);
}

private getWhereClause(whereObject: WhereClauseMulti) {
const columns = Object.keys(whereObject);
const queryVars: Array<string | number> = [];
const whereClause = columns.map((colName) => {
const colCondition = whereObject[colName];
if (colCondition instanceof Array) {
const inVals: Array<string | number> = colCondition;
const inStr = Array.from({length: inVals.length}, (_, idx) => `$${queryVars.length + idx + 1}`).join(',');
queryVars.push(...inVals);
return `${colName} IN (${inStr})`;
} else {
queryVars.push(colCondition);
return `${colName}=$${queryVars.length}`;
}
}).join(' AND ');
return {queryVars, whereClause};
}

async insert (schemaName: string, tableName: string, objects: any[]): Promise<any[]> {
if (!objects?.length) {
return [];
Expand All @@ -37,20 +58,18 @@ export default class DmlHandler {
return result.rows;
}

async select (schemaName: string, tableName: string, object: any, limit: number | null = null): Promise<any[]> {
const keys = Object.keys(object);
const values = Object.values(object);
const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND ');
let query = `SELECT * FROM ${schemaName}."${tableName}" WHERE ${param}`;
async select (schemaName: string, tableName: string, whereObject: WhereClauseMulti, limit: number | null = null): Promise<any[]> {
const {queryVars, whereClause} = this.getWhereClause(whereObject);
let query = `SELECT * FROM ${schemaName}."${tableName}" WHERE ${whereClause}`;
if (limit !== null) {
query = query.concat(' LIMIT ', Math.round(limit).toString());
}

const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}."${tableName}".`);
const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), queryVars), `Failed to execute '${query}' on ${schemaName}."${tableName}".`);
return result.rows;
}

async update (schemaName: string, tableName: string, whereObject: any, updateObject: any): Promise<any[]> {
async update (schemaName: string, tableName: string, whereObject: WhereClauseSingle, updateObject: any): Promise<any[]> {
const updateKeys = Object.keys(updateObject);
const updateParam = Array.from({ length: updateKeys.length }, (_, index) => `${updateKeys[index]}=$${index + 1}`).join(', ');
const whereKeys = Object.keys(whereObject);
Expand Down Expand Up @@ -78,13 +97,11 @@ export default class DmlHandler {
return result.rows;
}

async delete (schemaName: string, tableName: string, object: any): Promise<any[]> {
const keys = Object.keys(object);
const values = Object.values(object);
const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND ');
const query = `DELETE FROM ${schemaName}."${tableName}" WHERE ${param} RETURNING *`;
async delete (schemaName: string, tableName: string, whereObject: WhereClauseMulti): Promise<any[]> {
const {queryVars, whereClause} = this.getWhereClause(whereObject);
const query = `DELETE FROM ${schemaName}."${tableName}" WHERE ${whereClause} RETURNING *`;

const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}."${tableName}".`);
const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), queryVars), `Failed to execute '${query}' on ${schemaName}."${tableName}".`);
return result.rows;
}
}

0 comments on commit d814879

Please sign in to comment.