From e26e1dd4399d172409697005d32b59bc02249e3d Mon Sep 17 00:00:00 2001 From: DaxServer Date: Mon, 29 Sep 2025 20:22:18 +0200 Subject: [PATCH] feat(uppercase-conversion): Implement lowercase conversion service This commit introduces a new `LowercaseConversionService` that provides functionality to perform lowercase conversion operations on a specified table and column. The key changes are: - Implement the `LowercaseConversionService` class that extends the `ColumnOperationService` base class - Provide the `performOperation` method to execute the lowercase conversion on the specified table and column - Implement the `buildParameterizedUpdateQuery` method to construct a parameterized UPDATE query for the lowercase conversion - Implement the `countAffectedRows` method to count the number of rows that would be affected by the lowercase conversion These changes enable the application to perform efficient and safe lowercase conversion operations on database columns. --- backend/src/api/project/index.ts | 63 ++++- .../services/lowercase-conversion.service.ts | 43 +++ .../services/uppercase-conversion.service.ts | 2 +- backend/tests/api/project/lowercase.test.ts | 234 ++++++++++++++++ backend/tests/api/project/uppercase.test.ts | 15 +- .../lowercase-conversion.service.test.ts | 260 ++++++++++++++++++ .../uppercase-conversion.service.test.ts | 72 ++--- .../components/ColumnHeaderMenu.vue | 22 +- 8 files changed, 665 insertions(+), 46 deletions(-) create mode 100644 backend/src/services/lowercase-conversion.service.ts create mode 100644 backend/tests/api/project/lowercase.test.ts create mode 100644 backend/tests/services/lowercase-conversion.service.test.ts diff --git a/backend/src/api/project/index.ts b/backend/src/api/project/index.ts index 74ff56a..f231827 100644 --- a/backend/src/api/project/index.ts +++ b/backend/src/api/project/index.ts @@ -11,6 +11,7 @@ import { } from '@backend/api/project/schemas' import { databasePlugin } from '@backend/plugins/database' import { errorHandlerPlugin } from '@backend/plugins/error-handler' +import { LowercaseConversionService } from '@backend/services/lowercase-conversion.service' import { ReplaceOperationService } from '@backend/services/replace-operation.service' import { TrimWhitespaceService } from '@backend/services/trim-whitespace.service' import { UppercaseConversionService } from '@backend/services/uppercase-conversion.service' @@ -714,8 +715,66 @@ export const projectRoutes = new Elysia({ prefix: '/api/project' }) }, detail: { summary: 'Convert text to uppercase in a column', - description: - 'Convert all text values in a specific column to uppercase', + description: 'Convert all text values in a specific column to uppercase', + tags, + }, + }, + ) + + .post( + '/:projectId/lowercase', + async ({ db, params: { projectId }, body: { column }, status }) => { + const table = `project_${projectId}` + + // Check if column exists + const columnExistsReader = await db().runAndReadAll( + 'SELECT 1 FROM information_schema.columns WHERE table_name = ? AND column_name = ?', + [table, column], + ) + + if (columnExistsReader.getRows().length === 0) { + return status( + 400, + ApiErrorHandler.validationErrorWithData('Column not found', [ + `Column '${column}' does not exist in table '${table}'`, + ]), + ) + } + + const lowercaseConversionService = new LowercaseConversionService(db()) + + try { + const affectedRows = await lowercaseConversionService.performOperation({ + table, + column, + }) + + return { + affectedRows, + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + return status( + 500, + ApiErrorHandler.internalServerErrorWithData( + 'Failed to perform lowercase conversion operation', + [errorMessage], + ), + ) + } + }, + { + body: ColumnNameSchema, + response: { + 200: AffectedRowsSchema, + 400: ApiErrors, + 404: ApiErrors, + 422: ApiErrors, + 500: ApiErrors, + }, + detail: { + summary: 'Convert text to lowercase in a column', + description: 'Convert all text values in a specific column to lowercase', tags, }, }, diff --git a/backend/src/services/lowercase-conversion.service.ts b/backend/src/services/lowercase-conversion.service.ts new file mode 100644 index 0000000..1a342c5 --- /dev/null +++ b/backend/src/services/lowercase-conversion.service.ts @@ -0,0 +1,43 @@ +import type { ColumnOperationParams } from '@backend/services/column-operation.service' +import { ColumnOperationService } from '@backend/services/column-operation.service' + +export class LowercaseConversionService extends ColumnOperationService { + public async performOperation(params: ColumnOperationParams): Promise { + const { table, column } = params + + return this.executeColumnOperation( + table, + column, + () => this.buildParameterizedUpdateQuery(table, column), + () => this.countAffectedRows(table, column), + ) + } + + /** + * Builds a parameterized UPDATE query to safely perform lowercase conversion operations + */ + private buildParameterizedUpdateQuery(table: string, column: string) { + const query = ` + UPDATE "${table}" + SET "${column}" = LOWER("${column}") + WHERE "${column}" IS NOT NULL + AND "${column}" != LOWER("${column}") + ` + + return { query, params: [] } + } + + /** + * Counts the number of rows that would be affected by the lowercase conversion operation + */ + private countAffectedRows(table: string, column: string): Promise { + const query = ` + SELECT COUNT(*) as count + FROM "${table}" + WHERE "${column}" IS NOT NULL + AND "${column}" != LOWER("${column}") + ` + + return this.getCount(query, []) + } +} diff --git a/backend/src/services/uppercase-conversion.service.ts b/backend/src/services/uppercase-conversion.service.ts index 38fc3eb..44385df 100644 --- a/backend/src/services/uppercase-conversion.service.ts +++ b/backend/src/services/uppercase-conversion.service.ts @@ -40,4 +40,4 @@ export class UppercaseConversionService extends ColumnOperationService { return this.getCount(query, []) } -} \ No newline at end of file +} diff --git a/backend/tests/api/project/lowercase.test.ts b/backend/tests/api/project/lowercase.test.ts new file mode 100644 index 0000000..3755e64 --- /dev/null +++ b/backend/tests/api/project/lowercase.test.ts @@ -0,0 +1,234 @@ +import { projectRoutes } from '@backend/api/project' +import { closeDb, initializeDb } from '@backend/plugins/database' +import { treaty } from '@elysiajs/eden' +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { Elysia } from 'elysia' +import { tmpdir } from 'node:os' + +interface TestData { + name: string + email: string + city: string +} + +const TEST_DATA: TestData[] = [ + { name: 'John Doe', email: 'john@example.com', city: 'New York' }, + { name: 'Jane Smith', email: 'jane@example.com', city: 'Los Angeles' }, + { name: 'Bob Johnson', email: 'bob@example.com', city: 'New York' }, + { name: 'Alice Brown', email: 'alice@test.com', city: 'Chicago' }, + { name: 'Charlie Davis', email: 'charlie@example.com', city: 'New York' }, +] + +const createTestApi = () => { + return treaty(new Elysia().use(projectRoutes)).api +} + +const tempFilePath = tmpdir() + '/test-data.json' + +describe('Project API - Lowercase Conversion', () => { + let api: ReturnType + let projectId: string + + const importTestData = async () => { + await Bun.write(tempFilePath, JSON.stringify(TEST_DATA)) + + const { status, error } = await api.project({ projectId }).import.post({ + filePath: tempFilePath, + }) + + expect(error).toBeNull() + expect(status).toBe(201) + } + + beforeEach(async () => { + await initializeDb(':memory:') + api = createTestApi() + + const { data, status, error } = await api.project.post({ + name: 'Test Project for lowercase', + }) + expect(error).toBeNull() + expect(status).toBe(201) + projectId = (data as any)!.data!.id as string + + await importTestData() + }) + + afterEach(async () => { + await closeDb() + }) + + test('should perform basic lowercase conversion', async () => { + const { data, status, error } = await api.project({ projectId }).lowercase.post({ + column: 'name', + }) + + expect(status).toBe(200) + expect(error).toBeNull() + expect(data).toEqual({ + affectedRows: 5, + }) + + // Verify the data was actually changed + const { data: projectData } = await api.project({ projectId }).get({ + query: { offset: 0, limit: 25 }, + }) + + expect(projectData).toHaveProperty( + 'data', + expect.arrayContaining([ + expect.objectContaining({ name: 'john doe' }), + expect.objectContaining({ name: 'jane smith' }), + expect.objectContaining({ name: 'bob johnson' }), + expect.objectContaining({ name: 'alice brown' }), + expect.objectContaining({ name: 'charlie davis' }), + ]), + ) + }) + + test('should return 400 for non-existent column', async () => { + const { data, status, error } = await api.project({ projectId }).lowercase.post({ + column: 'nonexistent_column', + }) + + expect(status).toBe(400) + expect(data).toBeNull() + expect(error).toHaveProperty('status', 400) + expect(error).toHaveProperty('value', [ + { + code: 'VALIDATION', + message: 'Column not found', + details: [`Column 'nonexistent_column' does not exist in table 'project_${projectId}'`], + }, + ]) + }) + + test('should return 422 for missing required fields', async () => { + const { data, status, error } = await api.project({ projectId }).lowercase.post({ + column: '', + }) + + expect(status).toBe(422) + expect(data).toBeNull() + expect(error).toHaveProperty('status', 422) + expect(error).toHaveProperty( + 'value', + expect.arrayContaining([ + expect.objectContaining({ + message: 'Expected string length greater or equal to 1', + path: '/column', + }), + ]), + ) + }) + + test('should handle mixed case data with some already lowercase', async () => { + // Create a new project for this test to avoid import conflicts + const { + data: newProjectData, + status: newProjectStatus, + error: newProjectError, + } = await api.project.post({ + name: 'Test Project for lowercase - mixed case data', + }) + expect(newProjectError).toBeNull() + expect(newProjectStatus).toBe(201) + const newProjectId = (newProjectData as any)!.data!.id as string + + // Create test data with mixed case + const mixedData = [ + { name: 'JOHN DOE', email: 'john@example.com', city: 'NEW YORK' }, + { name: 'jane smith', email: 'jane@example.com', city: 'los angeles' }, + { name: 'Bob Johnson', email: 'bob@example.com', city: 'New York' }, + { name: 'alice brown', email: 'alice@test.com', city: 'chicago' }, + { name: 'CHARLIE DAVIS', email: 'charlie@example.com', city: 'CHICAGO' }, + ] + + await Bun.write(tempFilePath, JSON.stringify(mixedData)) + + // Import the mixed data + const { status, error } = await api.project({ projectId: newProjectId }).import.post({ + filePath: tempFilePath, + }) + + expect(error).toBeNull() + expect(status).toBe(201) + + // Perform lowercase conversion + const { + data, + status: lowercaseStatus, + error: lowercaseError, + } = await api.project({ projectId: newProjectId }).lowercase.post({ + column: 'name', + }) + + expect(lowercaseStatus).toBe(200) + expect(lowercaseError).toBeNull() + expect(data).toEqual({ + affectedRows: 3, // JOHN DOE, Bob Johnson, CHARLIE DAVIS should be affected + }) + + // Verify the data was converted correctly + const { data: projectData } = await api.project({ projectId: newProjectId }).get({ + query: { offset: 0, limit: 25 }, + }) + + expect(projectData).toHaveProperty( + 'data', + expect.arrayContaining([ + expect.objectContaining({ name: 'john doe' }), // 'JOHN DOE' -> 'john doe' + expect.objectContaining({ name: 'jane smith' }), // 'jane smith' -> unchanged (already lowercase) + expect.objectContaining({ name: 'bob johnson' }), // 'Bob Johnson' -> 'bob johnson' + expect.objectContaining({ name: 'alice brown' }), // 'alice brown' -> unchanged (already lowercase) + expect.objectContaining({ name: 'charlie davis' }), // 'CHARLIE DAVIS' -> 'charlie davis' + ]), + ) + }) + + test('should return 0 when no rows need conversion', async () => { + // Create a new project for this test to avoid import conflicts + const { + data: newProjectData, + status: newProjectStatus, + error: newProjectError, + } = await api.project.post({ + name: 'Test Project for lowercase - already lowercase data', + }) + expect(newProjectError).toBeNull() + expect(newProjectStatus).toBe(201) + const newProjectId = (newProjectData as any)!.data!.id as string + + // Create test data that's already lowercase + const lowercaseData = [ + { name: 'john doe', email: 'john@example.com', city: 'new york' }, + { name: 'jane smith', email: 'jane@example.com', city: 'los angeles' }, + { name: 'bob johnson', email: 'bob@example.com', city: 'new york' }, + ] + + await Bun.write(tempFilePath, JSON.stringify(lowercaseData)) + + // Import the lowercase data + const { status, error } = await api.project({ projectId: newProjectId }).import.post({ + filePath: tempFilePath, + }) + + expect(error).toBeNull() + expect(status).toBe(201) + + // Perform lowercase conversion + const { + data, + status: lowercaseStatus, + error: lowercaseError, + } = await api.project({ projectId: newProjectId }).lowercase.post({ + column: 'name', + }) + + expect(lowercaseStatus).toBe(200) + expect(lowercaseError).toBeNull() + expect(data).toEqual({ + affectedRows: 0, // No rows should be affected as all text is already lowercase + }) + }) +}) diff --git a/backend/tests/api/project/uppercase.test.ts b/backend/tests/api/project/uppercase.test.ts index 0d5b5bd..8d6a3b9 100644 --- a/backend/tests/api/project/uppercase.test.ts +++ b/backend/tests/api/project/uppercase.test.ts @@ -111,11 +111,14 @@ describe('Project API - Uppercase Conversion', () => { expect(status).toBe(422) expect(data).toBeNull() expect(error).toHaveProperty('status', 422) - expect(error).toHaveProperty('value', expect.arrayContaining([ - expect.objectContaining({ - message: 'Expected string length greater or equal to 1', - path: '/column', - }), - ])) + expect(error).toHaveProperty( + 'value', + expect.arrayContaining([ + expect.objectContaining({ + message: 'Expected string length greater or equal to 1', + path: '/column', + }), + ]), + ) }) }) diff --git a/backend/tests/services/lowercase-conversion.service.test.ts b/backend/tests/services/lowercase-conversion.service.test.ts new file mode 100644 index 0000000..ed7aacd --- /dev/null +++ b/backend/tests/services/lowercase-conversion.service.test.ts @@ -0,0 +1,260 @@ +import { closeDb, getDb, initializeDb } from '@backend/plugins/database' +import { LowercaseConversionService } from '@backend/services/lowercase-conversion.service' +import type { DuckDBConnection } from '@duckdb/node-api' +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' + +describe('LowercaseConversionService', () => { + let service: LowercaseConversionService + let db: DuckDBConnection + + beforeEach(async () => { + await initializeDb(':memory:') + db = getDb() + service = new LowercaseConversionService(db) + }) + + afterEach(async () => { + await closeDb() + }) + + describe('performOperation', () => { + test('should convert mixed case text to lowercase', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + name VARCHAR, + email VARCHAR + ) + `) + + // Insert test data with mixed case + await db.run(` + INSERT INTO test (id, name, email) VALUES + (1, 'John Doe', 'john@example.com'), + (2, 'Jane Smith', 'JANE@EXAMPLE.COM'), + (3, 'bob johnson', 'Bob@Example.com'), + (4, 'ALICE BROWN', 'alice@example.com'), + (5, 'Charlie Green', 'charlie@EXAMPLE.com') + `) + + // Perform lowercase conversion on name column + expect( + service.performOperation({ + table: 'test', + column: 'name', + }), + ).resolves.toBe(4) // John Doe, Jane Smith, ALICE BROWN, Charlie Green should be affected + + // Verify the data was actually changed + const selectResult = await db.runAndReadAll(`SELECT name FROM test ORDER BY id`) + const rows = selectResult.getRowObjectsJson() + + expect(rows[0]!.name).toBe('john doe') // 'John Doe' -> 'john doe' + expect(rows[1]!.name).toBe('jane smith') // 'Jane Smith' -> 'jane smith' + expect(rows[2]!.name).toBe('bob johnson') // 'bob johnson' -> unchanged (already lowercase) + expect(rows[3]!.name).toBe('alice brown') // 'ALICE BROWN' -> 'alice brown' + expect(rows[4]!.name).toBe('charlie green') // 'Charlie Green' -> 'charlie green' + }) + + test('should handle empty strings', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + text_col VARCHAR + ) + `) + + // Insert test data with empty strings + await db.run(` + INSERT INTO test (id, text_col) VALUES + (1, ''), + (2, 'SOME TEXT'), + (3, ''), + (4, 'another text') + `) + + // Perform lowercase conversion + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(1) // Only 'SOME TEXT' should be affected (another text is already lowercase) + + // Verify the data + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) + const rows = selectResult.getRowObjectsJson() + + expect(rows[0]!.text_col).toBe('') // Empty string remains empty + expect(rows[1]!.text_col).toBe('some text') // 'SOME TEXT' -> 'some text' + expect(rows[2]!.text_col).toBe('') // Empty string remains empty + expect(rows[3]!.text_col).toBe('another text') // 'another text' -> unchanged (already lowercase) + }) + + test('should handle NULL values', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + text_col VARCHAR + ) + `) + + // Insert test data with NULL values + await db.run(` + INSERT INTO test (id, text_col) VALUES + (1, NULL), + (2, 'SOME TEXT'), + (3, NULL), + (4, 'another text') + `) + + // Perform lowercase conversion + const affectedRows = await service.performOperation({ + table: 'test', + column: 'text_col', + }) + + expect(affectedRows).toBe(1) // Only 'SOME TEXT' should be affected (another text is already lowercase) + + // Verify the data + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) + const rows = selectResult.getRowObjectsJson() + + expect(rows[0]!.text_col).toBeNull() // NULL remains NULL + expect(rows[1]!.text_col).toBe('some text') // 'SOME TEXT' -> 'some text' + expect(rows[2]!.text_col).toBeNull() // NULL remains NULL + expect(rows[3]!.text_col).toBe('another text') // 'another text' -> unchanged (already lowercase) + }) + + test('should handle non-existent column', async () => { + // Create test table first + await db.run(` + CREATE TABLE test ( + id INTEGER, + name VARCHAR + ) + `) + + expect( + service.performOperation({ table: 'test', column: 'nonexistent_col' }), + ).rejects.toThrowError(`Column 'nonexistent_col' not found in table 'test'`) + }) + + test('should handle non-existent table', () => { + expect( + service.performOperation({ table: 'nonexistent_table', column: 'text_col' }), + ).rejects.toThrow() + }) + + test('should return 0 when no rows need conversion', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + text_col VARCHAR + ) + `) + + // Insert test data that's already lowercase + await db.run(` + INSERT INTO test (id, text_col) VALUES + (1, 'lowercase text'), + (2, 'all lowercase'), + (3, 'another lowercase'), + (4, NULL), + (5, '') + `) + + // Perform lowercase conversion + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(0) // No rows should be affected as all text is already lowercase + }) + + test('should handle special characters and numbers', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + text_col VARCHAR + ) + `) + + // Insert test data with special characters and numbers + await db.run(` + INSERT INTO test (id, text_col) VALUES + (1, 'Hello World!'), + (2, 'TEST@EMAIL.COM'), + (3, 'USER123NAME'), + (4, 'CamelCaseText'), + (5, 'SNAKE_CASE_TEXT'), + (6, 'KEBAB-CASE-TEXT'), + (7, 'MIXED123Case456Text') + `) + + // Perform lowercase conversion + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(7) // All rows should be affected + + // Verify the data was converted correctly + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) + const rows = selectResult.getRowObjectsJson() + + expect(rows[0]!.text_col).toBe('hello world!') // 'Hello World!' -> 'hello world!' + expect(rows[1]!.text_col).toBe('test@email.com') // 'TEST@EMAIL.COM' -> 'test@email.com' + expect(rows[2]!.text_col).toBe('user123name') // 'USER123NAME' -> 'user123name' + expect(rows[3]!.text_col).toBe('camelcasetext') // 'CamelCaseText' -> 'camelcasetext' + expect(rows[4]!.text_col).toBe('snake_case_text') // 'SNAKE_CASE_TEXT' -> 'snake_case_text' + expect(rows[5]!.text_col).toBe('kebab-case-text') // 'KEBAB-CASE-TEXT' -> 'kebab-case-text' + expect(rows[6]!.text_col).toBe('mixed123case456text') // 'MIXED123Case456Text' -> 'mixed123case456text' + }) + + test('should handle unicode characters', async () => { + // Create test table + await db.run(` + CREATE TABLE test ( + id INTEGER, + text_col VARCHAR + ) + `) + + // Insert test data with unicode characters + await db.run(` + INSERT INTO test (id, text_col) VALUES + (1, 'CAFÉ'), + (2, 'NAÏVE'), + (3, 'RÉSUMÉ'), + (4, 'ÜBER'), + (5, 'SEÑOR') + `) + + // Perform lowercase conversion + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(5) // All rows should be affected + + // Verify the data was converted correctly + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) + const rows = selectResult.getRowObjectsJson() + + expect(rows[0]!.text_col).toBe('café') // 'CAFÉ' -> 'café' + expect(rows[1]!.text_col).toBe('naïve') // 'NAÏVE' -> 'naïve' + expect(rows[2]!.text_col).toBe('résumé') // 'RÉSUMÉ' -> 'résumé' + expect(rows[3]!.text_col).toBe('über') // 'ÜBER' -> 'über' + expect(rows[4]!.text_col).toBe('señor') // 'SEÑOR' -> 'señor' + }) + }) +}) diff --git a/backend/tests/services/uppercase-conversion.service.test.ts b/backend/tests/services/uppercase-conversion.service.test.ts index 8b4462e..8966c46 100644 --- a/backend/tests/services/uppercase-conversion.service.test.ts +++ b/backend/tests/services/uppercase-conversion.service.test.ts @@ -1,4 +1,4 @@ -import { getDb, initializeDb, closeDb } from '@backend/plugins/database' +import { closeDb, getDb, initializeDb } from '@backend/plugins/database' import { UppercaseConversionService } from '@backend/services/uppercase-conversion.service' import type { DuckDBConnection } from '@duckdb/node-api' import { afterEach, beforeEach, describe, expect, test } from 'bun:test' @@ -39,15 +39,15 @@ describe('UppercaseConversionService', () => { `) // Perform uppercase conversion on name column - expect(service.performOperation({ - table: 'test', - column: 'name', - })).resolves.toBe(4) // John Doe, Jane Smith, bob johnson, Charlie Green should be affected + expect( + service.performOperation({ + table: 'test', + column: 'name', + }), + ).resolves.toBe(4) // John Doe, Jane Smith, bob johnson, Charlie Green should be affected // Verify the data was actually changed - const selectResult = await db.runAndReadAll( - `SELECT name FROM test ORDER BY id`, - ) + const selectResult = await db.runAndReadAll(`SELECT name FROM test ORDER BY id`) const rows = selectResult.getRowObjectsJson() expect(rows[0]!.name).toBe('JOHN DOE') // 'John Doe' -> 'JOHN DOE' @@ -76,15 +76,15 @@ describe('UppercaseConversionService', () => { `) // Perform uppercase conversion - expect(service.performOperation({ - table: 'test', - column: 'text_col', - })).resolves.toBe(1) // Only 'some text' should be affected (ANOTHER TEXT is already uppercase) + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(1) // Only 'some text' should be affected (ANOTHER TEXT is already uppercase) // Verify the data - const selectResult = await db.runAndReadAll( - `SELECT text_col FROM test ORDER BY id`, - ) + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) const rows = selectResult.getRowObjectsJson() expect(rows[0]!.text_col).toBe('') // Empty string remains empty @@ -120,9 +120,7 @@ describe('UppercaseConversionService', () => { expect(affectedRows).toBe(1) // Only 'some text' should be affected (ANOTHER TEXT is already uppercase) // Verify the data - const selectResult = await db.runAndReadAll( - `SELECT text_col FROM test ORDER BY id`, - ) + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) const rows = selectResult.getRowObjectsJson() expect(rows[0]!.text_col).toBeNull() // NULL remains NULL @@ -171,10 +169,12 @@ describe('UppercaseConversionService', () => { `) // Perform uppercase conversion - expect(service.performOperation({ - table: 'test', - column: 'text_col', - })).resolves.toBe(0) // No rows should be affected as all text is already uppercase + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(0) // No rows should be affected as all text is already uppercase }) test('should handle special characters and numbers', async () => { @@ -199,15 +199,15 @@ describe('UppercaseConversionService', () => { `) // Perform uppercase conversion - expect(service.performOperation({ - table: 'test', - column: 'text_col', - })).resolves.toBe(7) // All rows should be affected + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(7) // All rows should be affected // Verify the data was converted correctly - const selectResult = await db.runAndReadAll( - `SELECT text_col FROM test ORDER BY id`, - ) + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) const rows = selectResult.getRowObjectsJson() expect(rows[0]!.text_col).toBe('HELLO WORLD!') // 'Hello World!' -> 'HELLO WORLD!' @@ -239,15 +239,15 @@ describe('UppercaseConversionService', () => { `) // Perform uppercase conversion - expect(service.performOperation({ - table: 'test', - column: 'text_col', - })).resolves.toBe(5) // All rows should be affected + expect( + service.performOperation({ + table: 'test', + column: 'text_col', + }), + ).resolves.toBe(5) // All rows should be affected // Verify the data was converted correctly - const selectResult = await db.runAndReadAll( - `SELECT text_col FROM test ORDER BY id`, - ) + const selectResult = await db.runAndReadAll(`SELECT text_col FROM test ORDER BY id`) const rows = selectResult.getRowObjectsJson() expect(rows[0]!.text_col).toBe('CAFÉ') // 'café' -> 'CAFÉ' diff --git a/frontend/src/features/data-processing/components/ColumnHeaderMenu.vue b/frontend/src/features/data-processing/components/ColumnHeaderMenu.vue index 9bb88b5..adaff45 100644 --- a/frontend/src/features/data-processing/components/ColumnHeaderMenu.vue +++ b/frontend/src/features/data-processing/components/ColumnHeaderMenu.vue @@ -64,6 +64,26 @@ const handleUpperCase = async () => { } } +const handleLowerCase = async () => { + const { data, error } = await api.project({ projectId: projectId.value }).lowercase.post({ + column: props.columnField, + }) + + if (error?.value) { + showError(error.value as ExtendedError[]) + return + } + + const affectedRows = data?.affectedRows || 0 + + if (affectedRows === 0) { + showWarning('To lowercase completed: No rows were affected') + } else { + showSuccess(`To lowercase completed: ${affectedRows} rows affected`) + emit('replaceCompleted') + } +} + const menuItems = ref([ { label: 'Sort', @@ -122,7 +142,7 @@ const menuItems = ref([ }, { label: 'To lowercase', - command: () => console.log(`Transform ${props.columnHeader} to Lowercase`), + command: handleLowerCase, }, { label: 'To titlecase',