diff --git a/packages/cli/src/modules/data-store/__tests__/data-store.service.test.ts b/packages/cli/src/modules/data-store/__tests__/data-store.service.test.ts index ab916a19621..aafccf487d9 100644 --- a/packages/cli/src/modules/data-store/__tests__/data-store.service.test.ts +++ b/packages/cli/src/modules/data-store/__tests__/data-store.service.test.ts @@ -51,70 +51,7 @@ describe('dataStore', () => { }); describe('createDataStore', () => { - it('should succeed and not create a user table if columns are not provided', async () => { - const name = 'dataStore'; - - // ACT - const result = await dataStoreService.createDataStore(project1.id, { - name, - columns: [], - }); - const { id: dataStoreId } = result; - - // ASSERT - expect(result).toEqual({ - columns: [], - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - createdAt: expect.any(Date), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - id: expect.any(String), - name, - projectId: project1.id, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - project: expect.any(Project), - sizeBytes: 0, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - updatedAt: expect.any(Date), - }); - - const created = await dataStoreRepository.findOneBy({ name, projectId: project1.id }); - expect(created?.id).toBe(dataStoreId); - - const columns = await dataStoreRepository.manager.getRepository('DataStoreColumn').find({ - where: { dataStoreId }, - }); - expect(columns).toEqual([]); - - const userTableName = toTableName(dataStoreId); - await expect( - dataStoreRepository.manager - .createQueryBuilder() - .select() - .from(userTableName, userTableName) - .limit(1) - .getRawMany(), - ).rejects.toThrow(); - }); - - it('should succeed even if the name exists in a different project', async () => { - const name = 'dataStore'; - - await dataStoreService.createDataStore(project2.id, { - name, - columns: [], - }); - - // ACT - const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { - name, - columns: [], - }); - - const created = await dataStoreRepository.findOneBy({ name, projectId: project1.id }); - expect(created?.id).toBe(dataStoreId); - }); - - it('should create the user table and columns entity immediately if columns are provided', async () => { + it('should create a columns table and a user table if columns are provided', async () => { const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { name: 'dataStoreWithColumns', columns: [{ name: 'foo', type: 'string' }], @@ -147,6 +84,48 @@ describe('dataStore', () => { expect(rows).toEqual([]); }); + it('should create a user table and a columns table even if columns are not provided', async () => { + const name = 'dataStore'; + + // ACT + const result = await dataStoreService.createDataStore(project1.id, { + name, + columns: [], + }); + const { id: dataStoreId } = result; + + await expect(dataStoreService.getColumns(dataStoreId, project1.id)).resolves.toEqual([]); + + const userTableName = toTableName(dataStoreId); + const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); + try { + const table = await queryRunner.getTable(userTableName); + const columnNames = table?.columns.map((col) => col.name); + + expect(columnNames).toEqual(['id']); + } finally { + await queryRunner.release(); + } + }); + + it('should succeed even if the name exists in a different project', async () => { + const name = 'dataStore'; + + await dataStoreService.createDataStore(project2.id, { + name, + columns: [], + }); + + // ACT + const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { + name, + columns: [], + }); + + const created = await dataStoreRepository.findOneBy({ name, projectId: project1.id }); + expect(created?.id).toBe(dataStoreId); + }); + it('should populate the project relation when creating a data store', async () => { const name = 'dataStore'; @@ -272,7 +251,7 @@ describe('dataStore', () => { }); describe('addColumn', () => { - it('should succeed with adding columns to a non-empty table', async () => { + it('should succeed with adding columns to a non-empty table as well as to a user table', async () => { const existingColumns: CreateDataStoreColumnDto[] = [{ name: 'myColumn0', type: 'string' }]; const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { @@ -355,33 +334,84 @@ describe('dataStore', () => { updatedAt: expect.any(Date), }, ]); + + const userTableName = toTableName(dataStoreId); + const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); + try { + const table = await queryRunner.getTable(userTableName); + const columnNames = table?.columns.map((col) => col.name); + + expect(columnNames).toEqual( + expect.arrayContaining([ + 'id', + 'myColumn0', + 'myColumn1', + 'myColumn2', + 'myColumn3', + 'myColumn4', + ]), + ); + } finally { + await queryRunner.release(); + } }); - it('should create the user table on first addColumn if it does not exist', async () => { - // ARRANGE + it('should succeed with adding columns to an empty table', async () => { const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { name: 'dataStore', columns: [], }); - // ACT - const result = await dataStoreService.addColumn(dataStoreId, project1.id, { - name: 'foo', - type: 'string', - }); + const columns: AddDataStoreColumnDto[] = [ + { name: 'myColumn0', type: 'string' }, + { name: 'myColumn1', type: 'number' }, + ]; + for (const column of columns) { + // ACT + const result = await dataStoreService.addColumn(dataStoreId, project1.id, column); + // ASSERT + expect(result).toMatchObject(column); + } + const columnResult = await dataStoreService.getColumns(dataStoreId, project1.id); + expect(columnResult.length).toBe(2); - // ASSERT - expect(result).toMatchObject({ name: 'foo', type: 'string' }); + expect(columnResult).toEqual([ + { + index: 0, + dataStoreId, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + id: expect.any(String), + name: 'myColumn0', + type: 'string', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + createdAt: expect.any(Date), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + updatedAt: expect.any(Date), + }, + { + index: 1, + dataStoreId, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + id: expect.any(String), + name: 'myColumn1', + type: 'number', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + createdAt: expect.any(Date), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + updatedAt: expect.any(Date), + }, + ]); const userTableName = toTableName(dataStoreId); - const rows = await dataStoreRepository.manager - .createQueryBuilder() - .select('foo') - .from(userTableName, userTableName) - .limit(1) - .getRawMany(); + const queryRunner = dataStoreRepository.manager.connection.createQueryRunner(); + try { + const table = await queryRunner.getTable(userTableName); + const columnNames = table?.columns.map((col) => col.name); - expect(rows).toEqual([]); + expect(columnNames).toEqual(expect.arrayContaining(['id', 'myColumn0', 'myColumn1'])); + } finally { + await queryRunner.release(); + } }); it('should fail with adding two columns of the same name', async () => { @@ -416,6 +446,60 @@ describe('dataStore', () => { // ASSERT await expect(result).rejects.toThrow(DataStoreNotFoundError); }); + + it('should succeed with adding column to table that already has rows and set null values for existing rows', async () => { + // ARRANGE + const { id: dataStoreId } = await dataStoreService.createDataStore(project1.id, { + name: 'dataStore', + columns: [ + { name: 'name', type: 'string' }, + { name: 'age', type: 'number' }, + ], + }); + + await dataStoreService.insertRows(dataStoreId, project1.id, [ + { name: 'Alice', age: 30 }, + { name: 'Bob', age: 25 }, + { name: 'Charlie', age: 35 }, + ]); + + // ACT + const newColumn = await dataStoreService.addColumn(dataStoreId, project1.id, { + name: 'email', + type: 'string', + }); + + // ASSERT + expect(newColumn).toMatchObject({ name: 'email', type: 'string' }); + + // Verify the column was added to the metadata + const columns = await dataStoreService.getColumns(dataStoreId, project1.id); + expect(columns).toHaveLength(3); + expect(columns.map((c) => c.name)).toEqual(['name', 'age', 'email']); + + // Verify existing rows now have null values for the new column + const updatedData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {}); + expect(updatedData.count).toBe(3); + expect(updatedData.data).toEqual([ + { id: 1, name: 'Alice', age: 30, email: null }, + { id: 2, name: 'Bob', age: 25, email: null }, + { id: 3, name: 'Charlie', age: 35, email: null }, + ]); + + // Verify we can insert new rows with the new column + await dataStoreService.insertRows(dataStoreId, project1.id, [ + { name: 'David', age: 28, email: 'david@example.com' }, + ]); + + const finalData = await dataStoreService.getManyRowsAndCount(dataStoreId, project1.id, {}); + expect(finalData.count).toBe(4); + expect(finalData.data).toEqual([ + { id: 1, name: 'Alice', age: 30, email: null }, + { id: 2, name: 'Bob', age: 25, email: null }, + { id: 3, name: 'Charlie', age: 35, email: null }, + { id: 4, name: 'David', age: 28, email: 'david@example.com' }, + ]); + }); }); describe('deleteColumn', () => { diff --git a/packages/cli/src/modules/data-store/data-store-column.repository.ts b/packages/cli/src/modules/data-store/data-store-column.repository.ts index eef361e6350..fd7b27be392 100644 --- a/packages/cli/src/modules/data-store/data-store-column.repository.ts +++ b/packages/cli/src/modules/data-store/data-store-column.repository.ts @@ -62,7 +62,7 @@ export class DataStoreColumnRepository extends Repository { throw new UnexpectedError('QueryRunner is not available'); } - await this.dataStoreRowsRepository.ensureTableAndAddColumn( + await this.dataStoreRowsRepository.addColumn( dataStoreId, column, queryRunner, diff --git a/packages/cli/src/modules/data-store/data-store-rows.repository.ts b/packages/cli/src/modules/data-store/data-store-rows.repository.ts index 8d4c5f39cf8..7e39c03a4c0 100644 --- a/packages/cli/src/modules/data-store/data-store-rows.repository.ts +++ b/packages/cli/src/modules/data-store/data-store-rows.repository.ts @@ -123,19 +123,14 @@ export class DataStoreRowsRepository { await createTable.execute(queryRunner); } - async ensureTableAndAddColumn( + async addColumn( dataStoreId: string, column: DataStoreColumn, queryRunner: QueryRunner, dbType: DataSourceOptions['type'], ) { const tableName = toTableName(dataStoreId); - const tableExists = await queryRunner.hasTable(tableName); - if (!tableExists) { - await this.createTableWithColumns(tableName, [column], queryRunner); - } else { - await queryRunner.manager.query(addColumnQuery(tableName, column, dbType)); - } + await queryRunner.manager.query(addColumnQuery(tableName, column, dbType)); } async dropColumnFromTable( diff --git a/packages/cli/src/modules/data-store/data-store.repository.ts b/packages/cli/src/modules/data-store/data-store.repository.ts index b94530b921b..e0898a833aa 100644 --- a/packages/cli/src/modules/data-store/data-store.repository.ts +++ b/packages/cli/src/modules/data-store/data-store.repository.ts @@ -38,10 +38,6 @@ export class DataStoreRepository extends Repository { throw new UnexpectedError('QueryRunner is not available'); } - if (columns.length === 0) { - return; - } - // insert columns const columnEntities = columns.map((col, index) => em.create(DataStoreColumn, { @@ -51,9 +47,12 @@ export class DataStoreRepository extends Repository { index: col.index ?? index, }), ); - await em.insert(DataStoreColumn, columnEntities); - // create user table + if (columnEntities.length > 0) { + await em.insert(DataStoreColumn, columnEntities); + } + + // create user table (will create empty table with just id column if no columns) await this.dataStoreRowsRepository.createTableWithColumns( tableName, columnEntities,