Skip to content

Commit 7f444d1

Browse files
Complete TablesServerContext integration in table lookup functions (#1632)
Fixes OPS-3021, OPS-3022, OPS-3023, OPS-3024, OPS-3025, OPS-3026. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Table-related APIs now operate with explicit server/project context and authentication, improving correct scoping and access to project-specific tables. * Table lookup now consistently resolves duplicates by stable selection and returns normalized table descriptors. * **Chores** * Seeding and startup processes now use the default project DB token when locating project tables. * **Tests** * Tests updated to support and mock the new context-aware table flows. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Marcelo Gonçalves <[email protected]>
1 parent 52c4f37 commit 7f444d1

File tree

10 files changed

+72
-33
lines changed

10 files changed

+72
-33
lines changed

packages/openops/src/lib/openops-tables/openops-tables-common-properties.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ export function openopsTablesDropdownProperty(): any {
1616
displayName: 'Table',
1717
refreshers: [],
1818
required: true,
19-
options: async () => {
20-
const tables = await getTableNames();
19+
options: async (_, { server }) => {
20+
const tables = await getTableNames(server);
2121

2222
return {
2323
disabled: false,

packages/openops/src/lib/openops-tables/tables.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
1+
import { AxiosHeaders } from 'axios';
12
import { makeOpenOpsTablesGet } from '../openops-tables/requests-helpers';
2-
import { getDefaultDatabaseId } from './applications-service';
3-
import { authenticateDefaultUserInOpenOpsTables } from './auth-user';
4-
import { TablesServerContext } from './context-helpers';
3+
import { TablesServerContext, resolveTokenProvider } from './context-helpers';
54
import { createAxiosHeaders } from './create-axios-headers';
65

76
async function getTables(
8-
token: string,
97
databaseId: number,
8+
authenticationHeader: AxiosHeaders,
109
): Promise<OpenOpsTable[]> {
11-
const authenticationHeader = createAxiosHeaders(token);
1210
const getTablesResult = await makeOpenOpsTablesGet<OpenOpsTable[]>(
1311
`api/database/tables/database/${databaseId}/`,
1412
authenticationHeader,
1513
);
16-
return getTablesResult.flatMap((item) => item);
14+
return getTablesResult.flat();
1715
}
1816

1917
export async function getTableIdByTableName(
2018
tableName: string,
21-
_tablesServerContext: TablesServerContext,
19+
tablesServerContext: TablesServerContext,
2220
): Promise<number> {
23-
const table = await getTableByName(tableName);
21+
const table = await getTableByName(tableName, tablesServerContext);
2422

2523
if (!table) {
2624
throw new Error(`Table '${tableName}' not found`);
@@ -31,26 +29,34 @@ export async function getTableIdByTableName(
3129

3230
export async function getTableByName(
3331
tableName: string,
32+
tablesServerContext: TablesServerContext,
3433
): Promise<OpenOpsTable | undefined> {
35-
const tables = await getAvailableTablesInOpenopsTables();
34+
const tables = await getAvailableTablesInOpenopsTables(tablesServerContext);
3635

3736
const table = tables.find((t) => t.name === tableName);
3837

3938
return table;
4039
}
4140

42-
export async function getTableNames(): Promise<string[]> {
43-
const tables = await getAvailableTablesInOpenopsTables();
41+
export async function getTableNames(
42+
tablesServerContext: TablesServerContext,
43+
): Promise<string[]> {
44+
const tables = await getAvailableTablesInOpenopsTables(tablesServerContext);
4445

4546
return tables.map((t) => t.name);
4647
}
4748

48-
async function getAvailableTablesInOpenopsTables(): Promise<OpenOpsTable[]> {
49-
const { token } = await authenticateDefaultUserInOpenOpsTables();
49+
async function getAvailableTablesInOpenopsTables(
50+
serverContext: TablesServerContext,
51+
): Promise<OpenOpsTable[]> {
52+
const tokenOrResolver = await resolveTokenProvider(serverContext);
5053

51-
const tablesDatabaseId = await getDefaultDatabaseId(token);
54+
const authenticationHeader = createAxiosHeaders(tokenOrResolver);
5255

53-
const tables = await getTables(token, tablesDatabaseId);
56+
const tables = await getTables(
57+
serverContext.tablesDatabaseId,
58+
authenticationHeader,
59+
);
5460

5561
return getDistinctTableNames(tables);
5662
}

packages/openops/test/openops-tables/tables.test.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ jest.mock('../../src/lib/openops-tables/auth-user', () => ({
1515
authenticateDefaultUserInOpenOpsTablesMock,
1616
}));
1717

18-
jest.mock('../../src/lib/openops-tables/applications-service', () => ({
19-
getDefaultDatabaseId: jest.fn().mockResolvedValue(1),
20-
}));
21-
2218
jest.mock('@openops/server-shared', () => ({
2319
...jest.requireActual('@openops/server-shared'),
2420
system: {
@@ -58,7 +54,7 @@ describe('get table names', () => {
5854
]);
5955
createAxiosHeadersMock.mockReturnValue('some header');
6056

61-
const result = await getTableNames();
57+
const result = await getTableNames(mockTablesServerContext);
6258

6359
expect(result[0]).toBe('table name 1');
6460
expect(result[1]).toBe('table name 2');
@@ -81,7 +77,7 @@ describe('get table names', () => {
8177
]);
8278
createAxiosHeadersMock.mockReturnValue('some header');
8379

84-
const result = await getTableNames();
80+
const result = await getTableNames(mockTablesServerContext);
8581

8682
expect(result).toStrictEqual(['table name', 'table name2', 'table name3']);
8783
expect(makeOpenOpsTablesGetMock).toBeCalledTimes(1);
@@ -104,7 +100,7 @@ describe('get table names', () => {
104100
]);
105101
createAxiosHeadersMock.mockReturnValue('some header');
106102

107-
const result = await getTableNames();
103+
const result = await getTableNames(mockTablesServerContext);
108104

109105
expect(result).toStrictEqual(['table name', 'table name3', 'Table Name']);
110106
expect(makeOpenOpsTablesGetMock).toBeCalledTimes(1);
@@ -203,7 +199,7 @@ describe('get table by table name', () => {
203199
]);
204200
createAxiosHeadersMock.mockReturnValue('some header');
205201

206-
const result = await getTableByName('table name');
202+
const result = await getTableByName('table name', mockTablesServerContext);
207203

208204
expect(result).toStrictEqual({ id: 1, name: 'table name' });
209205
expect(makeOpenOpsTablesGetMock).toBeCalledTimes(1);
@@ -219,7 +215,7 @@ describe('get table by table name', () => {
219215
makeOpenOpsTablesGetMock.mockResolvedValue([{ id: 1, name: 'table name' }]);
220216
createAxiosHeadersMock.mockReturnValue('some header');
221217

222-
const result = await getTableByName('table name1');
218+
const result = await getTableByName('table name1', mockTablesServerContext);
223219

224220
expect(result).toBe(undefined);
225221
expect(makeOpenOpsTablesGetMock).toBeCalledTimes(1);
@@ -239,7 +235,7 @@ describe('get table by table name', () => {
239235
]);
240236
createAxiosHeadersMock.mockReturnValue('some header');
241237

242-
const result = await getTableByName('table name');
238+
const result = await getTableByName('table name', mockTablesServerContext);
243239

244240
expect(result).toStrictEqual({ id: 1, name: 'table name' });
245241
expect(makeOpenOpsTablesGetMock).toBeCalledTimes(1);

packages/server/api/src/app/ai/mcp/llm-query-router.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { logger } from '@openops/server-shared';
33
import { AiConfigParsed, ChatFlowContext } from '@openops/shared';
44
import { generateObject, LanguageModel, ModelMessage, ToolSet } from 'ai';
55
import { z } from 'zod';
6+
import { projectService } from '../../project/project-service';
67
import { getChatTools } from '../chat/ai-chat.service';
78
import { buildUIContextSection } from '../chat/prompts.service';
89
import { getAdditionalQueryClassificationDescriptions } from './extensions';
@@ -107,7 +108,7 @@ export async function routeQuery({
107108
}));
108109

109110
try {
110-
const openopsTablesNames = await getOpenOpsTablesNames();
111+
const openopsTablesNames = await getOpenOpsTablesNames(projectId);
111112

112113
const { object: selectionResult } = await generateObject({
113114
model: languageModel,
@@ -178,9 +179,13 @@ const getPreviousToolsForChat = async (
178179
}
179180
};
180181

181-
const getOpenOpsTablesNames = async (): Promise<string[]> => {
182+
const getOpenOpsTablesNames = async (projectId: string): Promise<string[]> => {
182183
try {
183-
return await getTableNames();
184+
const project = await projectService.getOneOrThrow(projectId);
185+
return await getTableNames({
186+
tablesDatabaseId: project.tablesDatabaseId,
187+
tablesDatabaseToken: project.tablesDatabaseToken,
188+
});
184189
} catch (error) {
185190
logger.error('Error getting OpenOps table names for the LLM query router', {
186191
error,

packages/server/api/src/app/database/seeds/auto-instances-shutdown-table-seed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FlagEntity } from '../../flags/flag.entity';
44
import { SEED_OPENOPS_AUTO_INSTANCES_SHUTDOWN_TABLE_NAME } from '../../openops-tables/template-tables/create-auto-instances-shutdown-table';
55
import { seedTemplateTablesService } from '../../openops-tables/template-tables/seed-tables-for-templates';
66
import { databaseConnection } from '../database-connection';
7+
import { getDefaultProjectTablesDatabaseToken } from '../get-default-user-db-token';
78

89
const AUTO_INSTANCES_SHUTDOWN_TABLE_SEED = 'AUTOINSTANCESSHUTDOWN';
910

@@ -34,6 +35,7 @@ export const seedAutoInstancesShutdownTable = async (): Promise<void> => {
3435

3536
const table = await getTableByName(
3637
SEED_OPENOPS_AUTO_INSTANCES_SHUTDOWN_TABLE_NAME,
38+
await getDefaultProjectTablesDatabaseToken(),
3739
);
3840

3941
if (!table) {

packages/server/api/src/app/database/seeds/openops-aggregated-costs-seed.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FlagEntity } from '../../flags/flag.entity';
44
import { SEED_TABLE_NAME } from '../../openops-tables/template-tables/create-aggregated-costs-table';
55
import { seedTemplateTablesService } from '../../openops-tables/template-tables/seed-tables-for-templates';
66
import { databaseConnection } from '../database-connection';
7+
import { getDefaultProjectTablesDatabaseToken } from '../get-default-user-db-token';
78

89
const AGGREGATED_TABLE_SEED = 'AGGREGATEDCOSTS';
910

@@ -31,7 +32,10 @@ export const seedFocusDataAggregationTemplateTable =
3132
return;
3233
}
3334

34-
const table = await getTableByName(SEED_TABLE_NAME);
35+
const table = await getTableByName(
36+
SEED_TABLE_NAME,
37+
await getDefaultProjectTablesDatabaseToken(),
38+
);
3539

3640
if (!table) {
3741
await seedTemplateTablesService.createAggregatedCostsTable();

packages/server/api/src/app/database/seeds/openops-delete-old-opportunities-table.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { logger } from '@openops/server-shared';
1010
import { FlagEntity } from '../../flags/flag.entity';
1111
import { SEED_OPENOPS_TABLE_NAME } from '../../openops-tables/template-tables/create-opportunities-table';
1212
import { databaseConnection } from '../database-connection';
13+
import { getDefaultProjectTablesDatabaseToken } from '../get-default-user-db-token';
1314

1415
const OPENOPS_OLD_OPPORTUNITIES_TABLE_DELETED = 'OPPORTUNITYDEL1';
1516

@@ -54,7 +55,10 @@ export const deleteOldOpportunitiesTable = async (): Promise<void> => {
5455
try {
5556
const { token } = await authenticateDefaultUserInOpenOpsTables();
5657

57-
const table = await getTableByName(SEED_OPENOPS_TABLE_NAME);
58+
const table = await getTableByName(
59+
SEED_OPENOPS_TABLE_NAME,
60+
await getDefaultProjectTablesDatabaseToken(),
61+
);
5862
if (!table) {
5963
logger.info('Skip: OpenOps deletion of old opportunities table', {
6064
name: 'deleteOldOpportunitiesTable',

packages/server/api/src/app/database/seeds/openops-knonw-cost-types-by-application-seed.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FlagEntity } from '../../flags/flag.entity';
44
import { SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME } from '../../openops-tables/template-tables/create-known-cost-types-by-application-table';
55
import { seedTemplateTablesService } from '../../openops-tables/template-tables/seed-tables-for-templates';
66
import { databaseConnection } from '../database-connection';
7+
import { getDefaultProjectTablesDatabaseToken } from '../get-default-user-db-token';
78

89
const KNOWN_COST_TYPES_BY_APPLICATION = 'KNOWNCOSTTYPES';
910

@@ -34,6 +35,7 @@ export const seedKnownCostTypesByApplicationTable = async (): Promise<void> => {
3435

3536
const table = await getTableByName(
3637
SEED_OPENOPS_KNOWN_COST_TYPES_BY_APPLICATION_TABLE_NAME,
38+
await getDefaultProjectTablesDatabaseToken(),
3739
);
3840

3941
if (!table) {

packages/server/api/src/app/database/seeds/openops-opportunities-table-seed.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { FlagEntity } from '../../flags/flag.entity';
44
import { SEED_OPENOPS_TABLE_NAME } from '../../openops-tables/template-tables/create-opportunities-table';
55
import { seedTemplateTablesService } from '../../openops-tables/template-tables/seed-tables-for-templates';
66
import { databaseConnection } from '../database-connection';
7+
import { getDefaultProjectTablesDatabaseToken } from '../get-default-user-db-token';
78

89
const OPPORTUNITIES_TABLE_SEED = 'OPPORTUNITIESSEED';
910

@@ -32,7 +33,10 @@ export const seedOpportunitesTemplateTable = async (): Promise<void> => {
3233
return;
3334
}
3435

35-
const table = await getTableByName(SEED_OPENOPS_TABLE_NAME);
36+
const table = await getTableByName(
37+
SEED_OPENOPS_TABLE_NAME,
38+
await getDefaultProjectTablesDatabaseToken(),
39+
);
3640

3741
if (!table) {
3842
await seedTemplateTablesService.createOpportunityTemplateTable();

packages/server/api/test/unit/mcp/llm-query-router.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jest.mock('ai', () => ({
2323
jest.mock('@openops/common', () => ({
2424
isLLMTelemetryEnabled: jest.fn().mockReturnValue(false),
2525
getTableNames: jest.fn().mockResolvedValue(['table1', 'table2', 'table3']),
26+
getDatabaseTableNames: jest
27+
.fn()
28+
.mockResolvedValue(['table1', 'table2', 'table3']),
2629
}));
2730

2831
jest.mock('@openops/server-shared', () => ({
@@ -34,12 +37,25 @@ jest.mock('@openops/server-shared', () => ({
3437
get: jest.fn().mockReturnValue('mock-value'),
3538
getBoolean: jest.fn().mockReturnValue(false),
3639
},
40+
AppSystemProp: {
41+
DB_TYPE: 'DB_TYPE',
42+
ENABLE_TABLES_DATABASE_TOKEN: 'ENABLE_TABLES_DATABASE_TOKEN',
43+
},
3744
}));
3845

3946
jest.mock('../../../src/app/ai/chat/ai-chat.service', () => ({
4047
getChatTools: jest.fn(),
4148
}));
4249

50+
jest.mock('../../../src/app/project/project-service', () => ({
51+
projectService: {
52+
getOneOrThrow: jest.fn().mockResolvedValue({
53+
tablesDatabaseId: 1,
54+
tablesDatabaseToken: 'mock-encrypted-token',
55+
}),
56+
},
57+
}));
58+
4359
const getChatToolsMock = getChatTools as jest.Mock;
4460

4561
describe('selectToolsAndQuery', () => {

0 commit comments

Comments
 (0)