Skip to content

Commit d96d074

Browse files
authored
Merge branch 'main' into ops-3126
2 parents 30c6c3e + 07c88c4 commit d96d074

7 files changed

Lines changed: 209 additions & 2 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ServerContext } from '@openops/blocks-framework';
2+
import { AppSystemProp, encryptUtils, system } from '@openops/server-shared';
3+
import { authenticateDefaultUserInOpenOpsTables } from './auth-user';
4+
5+
export function shouldUseDatabaseToken(): boolean {
6+
return system.getBoolean(AppSystemProp.ENABLE_TABLES_DATABASE_TOKEN) ?? false;
7+
}
8+
9+
export type TokenOrResolver = string | { getToken: () => string };
10+
export type TablesServerContext = Pick<
11+
ServerContext,
12+
'tablesDatabaseId' | 'tablesDatabaseToken'
13+
>;
14+
15+
export async function resolveTokenProvider(
16+
serverContext: TablesServerContext,
17+
): Promise<TokenOrResolver> {
18+
if (shouldUseDatabaseToken()) {
19+
return {
20+
getToken: () => {
21+
const { tablesDatabaseToken } = serverContext;
22+
return encryptUtils.decryptString(tablesDatabaseToken);
23+
},
24+
};
25+
}
26+
27+
const { token } = await authenticateDefaultUserInOpenOpsTables();
28+
return token;
29+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { AxiosHeaders } from 'axios';
2+
import { shouldUseDatabaseToken, TokenOrResolver } from './context-helpers';
3+
4+
export enum AuthType {
5+
JWT = 'JWT',
6+
Token = 'Token',
7+
}
8+
9+
function getToken(tokenOrResolver: TokenOrResolver): string {
10+
return typeof tokenOrResolver === 'string'
11+
? tokenOrResolver
12+
: tokenOrResolver.getToken();
13+
}
14+
15+
function getAuthPrefix(
16+
useJwtOverride: boolean,
17+
shouldUseDatabaseTokenConfig: boolean,
18+
): AuthType {
19+
const useJwt = useJwtOverride || !shouldUseDatabaseTokenConfig;
20+
return useJwt ? AuthType.JWT : AuthType.Token;
21+
}
22+
23+
export const createAxiosHeaders = (
24+
tokenOrResolver: TokenOrResolver,
25+
): AxiosHeaders => {
26+
const useJwtOverride = typeof tokenOrResolver === 'string';
27+
const token = getToken(tokenOrResolver);
28+
29+
const prefix = getAuthPrefix(useJwtOverride, shouldUseDatabaseToken());
30+
31+
return new AxiosHeaders({
32+
'Content-Type': 'application/json',
33+
Authorization: `${prefix} ${token}`,
34+
});
35+
};

packages/server/api/src/app/flags/flag.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const flagController: FastifyPluginAsyncTypebox = async (app) => {
1515
{
1616
config: {
1717
allowedPrincipals: ALL_PRINCIPAL_TYPES,
18+
skipAuth: true,
1819
},
1920
logLevel: 'silent',
2021
schema: {

packages/server/api/src/app/helper/pagination/paginator.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,22 @@ export default class Paginator<Entity extends ObjectLiteral> {
9191
public async paginate(
9292
builder: SelectQueryBuilder<Entity>,
9393
): Promise<PagingResult<Entity>> {
94-
const entities = await this.appendPagingQuery(builder).getMany();
95-
const hasMore = entities.length > this.limit;
94+
let entities = await this.appendPagingQuery(builder).getMany();
95+
let hasMore = entities.length > this.limit;
96+
97+
if (
98+
this.hasBeforeCursor() &&
99+
!this.hasAfterCursor() &&
100+
entities.length < this.limit
101+
) {
102+
this.afterCursor = null;
103+
this.beforeCursor = null;
104+
this.nextAfterCursor = null;
105+
this.nextBeforeCursor = null;
106+
107+
entities = await this.appendPagingQuery(builder).getMany();
108+
hasMore = entities.length > this.limit;
109+
}
96110

97111
if (hasMore) {
98112
entities.splice(entities.length - 1, 1);

packages/server/api/test/integration/helper/pagination/paginator.integration.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,33 @@ const TestFlowVersionEntity = new EntitySchema({
7373
},
7474
});
7575

76+
const FOUR_RUNS_TEST_DATA = [
77+
{
78+
id: 'run1',
79+
created: '2025-01-01 08:51:00.880',
80+
projectId: 'proj1',
81+
status: 'SUCCEEDED',
82+
},
83+
{
84+
id: 'run2',
85+
created: '2025-01-01 08:51:00.852',
86+
projectId: 'proj1',
87+
status: 'RUNNING',
88+
},
89+
{
90+
id: 'run3',
91+
created: '2025-01-01 08:51:00.123',
92+
projectId: 'proj1',
93+
status: 'SUCCEEDED',
94+
},
95+
{
96+
id: 'run4',
97+
created: '2025-01-01 08:50:59.999',
98+
projectId: 'proj1',
99+
status: 'FAILED',
100+
},
101+
];
102+
76103
describe('Paginator Integration Tests', () => {
77104
let dataSource: DataSource;
78105

@@ -284,6 +311,105 @@ describe('Paginator Integration Tests', () => {
284311
});
285312

286313
describe('Edge Cases', () => {
314+
describe('refetch when backward result is shorter than limit', () => {
315+
test.each([3, 4])(
316+
'returns correct forward window with limit %i',
317+
async (limit) => {
318+
const testData = FOUR_RUNS_TEST_DATA;
319+
320+
for (const data of testData) {
321+
await dataSource
322+
.createQueryBuilder()
323+
.insert()
324+
.into('test_flow_runs')
325+
.values(data)
326+
.execute();
327+
}
328+
329+
const queryBase = () =>
330+
dataSource
331+
.createQueryBuilder(TestFlowRunEntity, 'fr')
332+
.where('fr.projectId = :projectId', { projectId: 'proj1' });
333+
334+
const paginator1 = new Paginator(TestFlowRunEntity);
335+
paginator1.setAlias('fr');
336+
paginator1.setOrder(Order.DESC);
337+
paginator1.setLimit(2);
338+
const page1 = await paginator1.paginate(queryBase());
339+
340+
const paginator2 = new Paginator(TestFlowRunEntity);
341+
paginator2.setAlias('fr');
342+
paginator2.setOrder(Order.DESC);
343+
paginator2.setLimit(2);
344+
paginator2.setAfterCursor(page1.cursor.afterCursor!);
345+
const page2 = await paginator2.paginate(queryBase());
346+
347+
const paginatorBack = new Paginator(TestFlowRunEntity);
348+
paginatorBack.setAlias('fr');
349+
paginatorBack.setOrder(Order.DESC);
350+
paginatorBack.setLimit(limit);
351+
paginatorBack.setBeforeCursor(page2.cursor.beforeCursor!);
352+
353+
const backPage = await paginatorBack.paginate(queryBase());
354+
355+
const allIds = ['run1', 'run2', 'run3', 'run4'];
356+
const expectedIds = allIds.slice(0, Math.min(limit, allIds.length));
357+
358+
expect(backPage.data.map((d) => d.id)).toEqual(expectedIds);
359+
},
360+
);
361+
362+
test.each([
363+
{ limit: 3, expectAfterDefined: true },
364+
{ limit: 4, expectAfterDefined: false },
365+
])(
366+
'sets expected cursors with limit $limit',
367+
async ({ limit, expectAfterDefined }) => {
368+
const testData = FOUR_RUNS_TEST_DATA;
369+
370+
for (const data of testData) {
371+
await dataSource
372+
.createQueryBuilder()
373+
.insert()
374+
.into('test_flow_runs')
375+
.values(data)
376+
.execute();
377+
}
378+
379+
const queryBase = () =>
380+
dataSource
381+
.createQueryBuilder(TestFlowRunEntity, 'fr')
382+
.where('fr.projectId = :projectId', { projectId: 'proj1' });
383+
384+
const paginator1 = new Paginator(TestFlowRunEntity);
385+
paginator1.setAlias('fr');
386+
paginator1.setOrder(Order.DESC);
387+
paginator1.setLimit(2);
388+
const page1 = await paginator1.paginate(queryBase());
389+
390+
const paginator2 = new Paginator(TestFlowRunEntity);
391+
paginator2.setAlias('fr');
392+
paginator2.setOrder(Order.DESC);
393+
paginator2.setLimit(2);
394+
paginator2.setAfterCursor(page1.cursor.afterCursor!);
395+
const page2 = await paginator2.paginate(queryBase());
396+
397+
const paginatorBack = new Paginator(TestFlowRunEntity);
398+
paginatorBack.setAlias('fr');
399+
paginatorBack.setOrder(Order.DESC);
400+
paginatorBack.setLimit(limit);
401+
paginatorBack.setBeforeCursor(page2.cursor.beforeCursor!);
402+
const backPage = await paginatorBack.paginate(queryBase());
403+
404+
if (expectAfterDefined) {
405+
expect(backPage.cursor.afterCursor).toBeDefined();
406+
} else {
407+
expect(backPage.cursor.afterCursor).toBeNull();
408+
}
409+
expect(backPage.cursor.beforeCursor).toBeNull();
410+
},
411+
);
412+
});
287413
test('should handle empty result set', async () => {
288414
const paginator = new Paginator(TestFlowRunEntity);
289415
paginator.setAlias('fr');

packages/server/shared/src/lib/system/system-prop.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export enum AppSystemProp {
110110

111111
MAX_LLM_CALLS_WITHOUT_INTERACTION = 'MAX_LLM_CALLS_WITHOUT_INTERACTION',
112112
LLM_CHAT_EXPIRE_TIME_SECONDS = 'LLM_CHAT_EXPIRE_TIME_SECONDS',
113+
ENABLE_TABLES_DATABASE_TOKEN = 'ENABLE_TABLES_DATABASE_TOKEN',
113114
}
114115

115116
export enum SharedSystemProp {

packages/server/shared/src/lib/system/system.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ const systemPropDefaultValues: Partial<Record<SystemProp, string>> = {
9898
[AppSystemProp.LLM_CHAT_EXPIRE_TIME_SECONDS]: '86400', // 24 hours
9999
[AppSystemProp.TELEMETRY_MODE]: 'COLLECTOR',
100100
[AppSystemProp.TELEMETRY_COLLECTOR_URL]: 'https://telemetry.openops.com/save',
101+
[AppSystemProp.ENABLE_TABLES_DATABASE_TOKEN]: 'false',
101102
[SharedSystemProp.ENABLE_HOST_VALIDATION]: 'true',
102103
};
103104

0 commit comments

Comments
 (0)