Skip to content

Commit f0fb62f

Browse files
committed
chore: merge main into release for new releases
2 parents b0393c3 + 878ac95 commit f0fb62f

66 files changed

Lines changed: 1846 additions & 381 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/api/.env.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ APP_AWS_REGION=
99
APP_AWS_ACCESS_KEY_ID=
1010
APP_AWS_SECRET_ACCESS_KEY=
1111
APP_AWS_ORG_ASSETS_BUCKET=
12+
APP_AWS_ENDPOINT="" # optional for using services like MinIO
1213

1314
DATABASE_URL=
1415

‎apps/api/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@react-email/components": "^0.0.41",
2727
"@trigger.dev/build": "4.0.6",
2828
"@trigger.dev/sdk": "4.0.6",
29-
"@trycompai/db": "^1.3.20",
29+
"@trycompai/db": "1.3.21",
3030
"@trycompai/email": "workspace:*",
3131
"@upstash/redis": "^1.34.2",
3232
"@upstash/vector": "^1.2.2",

‎apps/api/src/app/s3.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const logger = new Logger('S3');
1111
const APP_AWS_REGION = process.env.APP_AWS_REGION;
1212
const APP_AWS_ACCESS_KEY_ID = process.env.APP_AWS_ACCESS_KEY_ID;
1313
const APP_AWS_SECRET_ACCESS_KEY = process.env.APP_AWS_SECRET_ACCESS_KEY;
14+
const APP_AWS_ENDPOINT = process.env.APP_AWS_ENDPOINT;
1415

1516
export const BUCKET_NAME = process.env.APP_AWS_BUCKET_NAME;
1617
export const APP_AWS_QUESTIONNAIRE_UPLOAD_BUCKET =
@@ -37,11 +38,13 @@ try {
3738
}
3839

3940
s3ClientInstance = new S3Client({
41+
endpoint: APP_AWS_ENDPOINT || undefined,
4042
region: APP_AWS_REGION,
4143
credentials: {
4244
accessKeyId: APP_AWS_ACCESS_KEY_ID,
4345
secretAccessKey: APP_AWS_SECRET_ACCESS_KEY,
4446
},
47+
forcePathStyle: !!APP_AWS_ENDPOINT,
4548
});
4649
} catch (error) {
4750
logger.error(

‎apps/api/src/assistant-chat/assistant-chat.controller.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import type { AssistantChatMessage } from './assistant-chat.types';
3434
export class AssistantChatController {
3535
constructor(private readonly assistantChatService: AssistantChatService) {}
3636

37-
private getUserScopedContext(auth: AuthContextType): { organizationId: string; userId: string } {
37+
private getUserScopedContext(auth: AuthContextType): {
38+
organizationId: string;
39+
userId: string;
40+
} {
3841
// Defensive checks (should already be guaranteed by HybridAuthGuard + AuthContext decorator)
3942
if (!auth.organizationId) {
4043
throw new BadRequestException('Organization ID is required');
@@ -69,7 +72,9 @@ export class AssistantChatController {
6972
},
7073
},
7174
})
72-
async getHistory(@AuthContext() auth: AuthContextType): Promise<{ messages: AssistantChatMessage[] }> {
75+
async getHistory(
76+
@AuthContext() auth: AuthContextType,
77+
): Promise<{ messages: AssistantChatMessage[] }> {
7378
const { organizationId, userId } = this.getUserScopedContext(auth);
7479

7580
const messages = await this.assistantChatService.getHistory({
@@ -105,7 +110,9 @@ export class AssistantChatController {
105110
summary: 'Clear assistant chat history',
106111
description: 'Deletes the current user-scoped assistant chat history.',
107112
})
108-
async clearHistory(@AuthContext() auth: AuthContextType): Promise<{ success: true }> {
113+
async clearHistory(
114+
@AuthContext() auth: AuthContextType,
115+
): Promise<{ success: true }> {
109116
const { organizationId, userId } = this.getUserScopedContext(auth);
110117

111118
await this.assistantChatService.clearHistory({
@@ -116,5 +123,3 @@ export class AssistantChatController {
116123
return { success: true };
117124
}
118125
}
119-
120-

‎apps/api/src/assistant-chat/assistant-chat.dto.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsArray, IsIn, IsNumber, IsString, ValidateNested } from 'class-validator';
2+
import {
3+
IsArray,
4+
IsIn,
5+
IsNumber,
6+
IsString,
7+
ValidateNested,
8+
} from 'class-validator';
39
import { Type } from 'class-transformer';
410

511
export class AssistantChatMessageDto {
@@ -27,5 +33,3 @@ export class SaveAssistantChatHistoryDto {
2733
@Type(() => AssistantChatMessageDto)
2834
messages!: AssistantChatMessageDto[];
2935
}
30-
31-

‎apps/api/src/assistant-chat/assistant-chat.module.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,3 @@ import { AssistantChatService } from './assistant-chat.service';
99
providers: [AssistantChatService],
1010
})
1111
export class AssistantChatModule {}
12-
13-

‎apps/api/src/assistant-chat/assistant-chat.service.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ type GetAssistantChatKeyParams = {
1717
userId: string;
1818
};
1919

20-
const getAssistantChatKey = ({ organizationId, userId }: GetAssistantChatKeyParams): string => {
20+
const getAssistantChatKey = ({
21+
organizationId,
22+
userId,
23+
}: GetAssistantChatKeyParams): string => {
2124
return `assistant-chat:v1:${organizationId}:${userId}`;
2225
};
2326

@@ -27,17 +30,24 @@ export class AssistantChatService {
2730
* Default TTL is 7 days. This is intended to behave like "session context"
2831
* rather than a long-term, searchable archive.
2932
*/
30-
private readonly ttlSeconds = Number(process.env.ASSISTANT_CHAT_TTL_SECONDS ?? 60 * 60 * 24 * 7);
33+
private readonly ttlSeconds = Number(
34+
process.env.ASSISTANT_CHAT_TTL_SECONDS ?? 60 * 60 * 24 * 7,
35+
);
3136

32-
async getHistory(params: GetAssistantChatKeyParams): Promise<AssistantChatMessage[]> {
37+
async getHistory(
38+
params: GetAssistantChatKeyParams,
39+
): Promise<AssistantChatMessage[]> {
3340
const key = getAssistantChatKey(params);
3441
const raw = await assistantChatRedisClient.get<unknown>(key);
3542
const parsed = StoredMessagesSchema.safeParse(raw);
3643
if (!parsed.success) return [];
3744
return parsed.data;
3845
}
3946

40-
async saveHistory(params: GetAssistantChatKeyParams, messages: AssistantChatMessage[]): Promise<void> {
47+
async saveHistory(
48+
params: GetAssistantChatKeyParams,
49+
messages: AssistantChatMessage[],
50+
): Promise<void> {
4151
const key = getAssistantChatKey(params);
4252
// Always validate before writing to keep the cache shape stable.
4353
const validated = StoredMessagesSchema.parse(messages);
@@ -49,5 +59,3 @@ export class AssistantChatService {
4959
await assistantChatRedisClient.del(key);
5060
}
5161
}
52-
53-

‎apps/api/src/assistant-chat/assistant-chat.types.ts‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,3 @@ export type AssistantChatMessage = {
44
text: string;
55
createdAt: number;
66
};
7-
8-

‎apps/api/src/assistant-chat/upstash-redis.client.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ class InMemoryRedis {
1919
return record.value as T;
2020
}
2121

22-
async set(key: string, value: unknown, options?: { ex?: number }): Promise<'OK'> {
22+
async set(
23+
key: string,
24+
value: unknown,
25+
options?: { ex?: number },
26+
): Promise<'OK'> {
2327
const expiresAt = options?.ex ? Date.now() + options.ex * 1000 : undefined;
2428
this.storage.set(key, { value, expiresAt });
2529
return 'OK';
@@ -32,7 +36,8 @@ class InMemoryRedis {
3236
}
3337

3438
const hasUpstashConfig =
35-
!!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN;
39+
!!process.env.UPSTASH_REDIS_REST_URL &&
40+
!!process.env.UPSTASH_REDIS_REST_TOKEN;
3641

3742
export const assistantChatRedisClient: Pick<Redis, 'get' | 'set' | 'del'> =
3843
hasUpstashConfig
@@ -41,5 +46,3 @@ export const assistantChatRedisClient: Pick<Redis, 'get' | 'set' | 'del'> =
4146
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
4247
})
4348
: (new InMemoryRedis() as unknown as Pick<Redis, 'get' | 'set' | 'del'>);
44-
45-

‎apps/api/src/attachments/attachments.service.ts‎

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { randomBytes } from 'crypto';
1515
import { AttachmentResponseDto } from '../tasks/dto/task-responses.dto';
1616
import { UploadAttachmentDto } from './upload-attachment.dto';
17+
import { s3Client } from '@/app/s3';
1718

1819
@Injectable()
1920
export class AttachmentsService {
@@ -27,20 +28,14 @@ export class AttachmentsService {
2728
// Safe to access environment variables directly since they're validated
2829
this.bucketName = process.env.APP_AWS_BUCKET_NAME!;
2930

30-
if (
31-
!process.env.APP_AWS_ACCESS_KEY_ID ||
32-
!process.env.APP_AWS_SECRET_ACCESS_KEY
33-
) {
34-
console.warn('AWS credentials are missing, S3 client may fail');
31+
if (!s3Client) {
32+
console.error('S3 Client is not initialized. Check AWS S3 configuration.');
33+
throw new Error(
34+
'S3 Client is not initialized. Check AWS S3 configuration.',
35+
);
3536
}
3637

37-
this.s3Client = new S3Client({
38-
region: process.env.APP_AWS_REGION || 'us-east-1',
39-
credentials: {
40-
accessKeyId: process.env.APP_AWS_ACCESS_KEY_ID!,
41-
secretAccessKey: process.env.APP_AWS_SECRET_ACCESS_KEY!,
42-
},
43-
});
38+
this.s3Client = s3Client;
4439
}
4540

4641
/**
@@ -385,6 +380,25 @@ export class AttachmentsService {
385380
return this.generateSignedUrl(s3Key);
386381
}
387382

383+
/**
384+
* Generate presigned download URL with a custom download filename
385+
*/
386+
async getPresignedDownloadUrlWithFilename(
387+
s3Key: string,
388+
downloadFilename: string,
389+
): Promise<string> {
390+
const sanitizedFilename = this.sanitizeHeaderValue(downloadFilename);
391+
const getCommand = new GetObjectCommand({
392+
Bucket: this.bucketName,
393+
Key: s3Key,
394+
ResponseContentDisposition: `attachment; filename="${sanitizedFilename}"`,
395+
});
396+
397+
return getSignedUrl(this.s3Client, getCommand, {
398+
expiresIn: this.SIGNED_URL_EXPIRY,
399+
});
400+
}
401+
388402
async getObjectBuffer(s3Key: string): Promise<Buffer> {
389403
const getCommand = new GetObjectCommand({
390404
Bucket: this.bucketName,

0 commit comments

Comments
 (0)