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',