Skip to content

Commit 3a1f25e

Browse files
Merge branch 'main' into copilot/add-reddit-messages-folder
2 parents 700ccf3 + 65eabbd commit 3a1f25e

71 files changed

Lines changed: 607 additions & 736 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.

.github/copilot-instructions.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copilot Instructions for codebuilder-api
2+
3+
## Project Overview
4+
5+
This is **codebuilder-api**, a NestJS backend API built with TypeScript. It uses Prisma as the ORM (schema maintained in a Git submodule at `prisma/`), Redis for caching and queues, PostgreSQL for the database, and is deployed via Docker.
6+
7+
## Tech Stack
8+
9+
- **Runtime:** Node.js v22 with pnpm as the package manager
10+
- **Framework:** NestJS 11 with Express adapter
11+
- **Language:** TypeScript (ES2023 target, ESNext modules, bundler resolution)
12+
- **ORM:** Prisma with auto-generated DTOs via `@vegardit/prisma-generator-nestjs-dto`
13+
- **Build:** SWC compiler (configured in `nest-cli.json`)
14+
- **Testing:** Jest with ts-jest; E2E tests use Supertest
15+
- **Linting:** ESLint (flat config) with TypeScript-ESLint and Prettier integration
16+
- **Formatting:** Prettier (140 print width, single quotes, trailing commas ES5, 2-space indent)
17+
18+
## Project Structure
19+
20+
```
21+
src/
22+
├── auth/ # Authentication (JWT, Google OAuth)
23+
├── cloudflare-kv/ # Cloudflare KV integration
24+
├── common/ # Shared infrastructure
25+
│ ├── configs/ # Configuration service
26+
│ ├── database/ # Prisma database module
27+
│ ├── decorators/ # Custom decorators (@Api, @User, @Field)
28+
│ ├── filters/ # Exception filters
29+
│ ├── helpers/ # Utility functions
30+
│ ├── interceptors/# Response interceptors
31+
│ ├── logger/ # Logging service
32+
│ ├── models/ # Common response models
33+
│ ├── pagination/ # Pagination utilities
34+
│ ├── queue/ # BullMQ job queue
35+
│ ├── redis/ # Redis module and providers
36+
│ └── validation/ # Custom validators
37+
├── errors/ # Error reporting
38+
├── events/ # WebSocket events gateway
39+
├── generated/ # Auto-generated Prisma DTOs (do not edit)
40+
├── jobs/ # Job management
41+
├── location/ # Location services
42+
├── notifications/ # Push notifications (Firebase)
43+
├── users/ # User management
44+
└── wss/ # WebSocket support
45+
```
46+
47+
## Coding Conventions
48+
49+
### File Naming
50+
51+
- Controllers: `{name}.controller.ts`
52+
- Services: `{name}.service.ts`
53+
- Modules: `{name}.module.ts`
54+
- DTOs: `{operation}-{entity}.dto.ts` (e.g., `create-job.dto.ts`)
55+
- Filters: `{name}.filter.ts`
56+
- Guards: `{name}.guard.ts`
57+
- Interceptors: `{name}.interceptor.ts`
58+
- Decorators: `{name}.decorator.ts`
59+
60+
### Imports
61+
62+
Use the `@/` path alias for imports from the `src/` directory:
63+
64+
```typescript
65+
import { Api } from '@/common/decorators/api.decorator';
66+
import { NotificationsService } from '@/notifications/notifications.service';
67+
```
68+
69+
### API Patterns
70+
71+
- Use the custom `@Api()` decorator for controller methods to configure Swagger docs, response types, and the response envelope.
72+
- Endpoints using `@Api({ envelope: true })` return `{ "success": true, "data": <payload> }`.
73+
- Paginated endpoints use `paginatedResponseType` and return items with `pageInfo` metadata. Use `buildPaginatedResult()` in services.
74+
- DTO properties use `@Field({ inQuery: true })` or `@Field({ inPath: true })` for automatic Swagger parameter generation.
75+
- Throw standard NestJS `HttpException` subclasses for error handling.
76+
77+
### Validation
78+
79+
- Use `class-validator` and `class-transformer` decorators on DTOs for input validation.
80+
- The global `ValidationPipe` is configured with `whitelist: true` and `transform: true`.
81+
82+
## Important Notes
83+
84+
- **Do not edit files in `src/generated/`** — these are auto-generated from the Prisma schema using `npx prisma generate`.
85+
- **The Prisma schema is a Git submodule** at `prisma/`. Schema changes should be made in the `codebuilderinc/codebuilder-prisma` repository.
86+
- The project uses Prettier for formatting; run `pnpm format` to format code or `pnpm lint` to lint and auto-fix.
87+
88+
## Common Commands
89+
90+
```bash
91+
pnpm install # Install dependencies
92+
pnpm build # Build the project
93+
pnpm dev # Start in development/watch mode (NODE_ENV=local)
94+
pnpm lint # Lint and auto-fix
95+
pnpm format # Format code with Prettier
96+
pnpm test # Run unit tests
97+
pnpm test:e2e # Run end-to-end tests
98+
```

src/auth/auth.service.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { PrismaService } from 'nestjs-prisma';
22
import { Prisma, User } from '@prisma/client';
3-
import {
4-
Injectable,
5-
NotFoundException,
6-
BadRequestException,
7-
ConflictException,
8-
UnauthorizedException,
9-
} from '@nestjs/common';
3+
import { Injectable, NotFoundException, BadRequestException, ConflictException, UnauthorizedException } from '@nestjs/common';
104
import { JwtService } from '@nestjs/jwt';
115
import { PasswordService } from './password.service';
126
import { SignupInput } from './dto/signup.input';
@@ -202,8 +196,7 @@ export class AuthService {
202196
async googleAuth(idToken: string, buildType?: string): Promise<Token> {
203197
try {
204198
// Determine which client ID to use based on build type
205-
const clientId =
206-
buildType === 'development' ? process.env.GOOGLE_WEB_CLIENT_ID_DEV : process.env.GOOGLE_WEB_CLIENT_ID;
199+
const clientId = buildType === 'development' ? process.env.GOOGLE_WEB_CLIENT_ID_DEV : process.env.GOOGLE_WEB_CLIENT_ID;
207200

208201
if (!clientId) {
209202
console.error(`Missing Google OAuth client ID for build type: ${buildType}`);

src/common/database/database.service.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ export class DatabaseService extends PrismaClient implements OnModuleInit, OnMod
3535
// },
3636
// ]
3737
});
38-
this.logger.info(
39-
`Using Prisma v${Prisma.prismaVersion.client} ${configService.get('DATABASE_URL').replace(/:[^:]*@/, ':***@')}`
40-
);
38+
this.logger.info(`Using Prisma v${Prisma.prismaVersion.client} ${configService.get('DATABASE_URL').replace(/:[^:]*@/, ':***@')}`);
4139

4240
/* this.meter = meterProvider.getMeter('example-exporter-collector');
4341

src/common/decorators/api.decorator.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {applyDecorators, Type as NestType} from '@nestjs/common';
1+
import { applyDecorators, Type as NestType } from '@nestjs/common';
22
import {
33
ApiOperation,
44
ApiUnauthorizedResponse,
@@ -241,7 +241,7 @@ export function Api(options: ApiOptions): MethodDecorator {
241241

242242
// Add request body documentation if bodyType is specified
243243
if (options.bodyType) {
244-
decorators.push(ApiBody({type: options.bodyType}));
244+
decorators.push(ApiBody({ type: options.bodyType }));
245245
}
246246

247247
// ============================================================================
@@ -348,22 +348,22 @@ export function Api(options: ApiOptions): MethodDecorator {
348348
schema: {
349349
type: 'object',
350350
properties: {
351-
success: {type: 'boolean', example: true},
352-
data: {$ref: getSchemaPath(options.responseType)},
351+
success: { type: 'boolean', example: true },
352+
data: { $ref: getSchemaPath(options.responseType) },
353353
},
354354
},
355355
})
356356
);
357357
} else {
358358
// Direct response without envelope
359-
decorators.push(ApiResponse({status: 200, description: 'Successful response', type: options.responseType}));
359+
decorators.push(ApiResponse({ status: 200, description: 'Successful response', type: options.responseType }));
360360
}
361361
}
362362
// Array response
363363
else if (options.responseArrayType) {
364364
const arraySchema = {
365365
type: 'array',
366-
items: {$ref: getSchemaPath(options.responseArrayType)},
366+
items: { $ref: getSchemaPath(options.responseArrayType) },
367367
};
368368
if (options.envelope) {
369369
// Wrapped in envelope: { success: true, data: [...] }
@@ -374,7 +374,7 @@ export function Api(options: ApiOptions): MethodDecorator {
374374
schema: {
375375
type: 'object',
376376
properties: {
377-
success: {type: 'boolean', example: true},
377+
success: { type: 'boolean', example: true },
378378
data: arraySchema,
379379
},
380380
},
@@ -398,18 +398,18 @@ export function Api(options: ApiOptions): MethodDecorator {
398398
properties: {
399399
items: {
400400
type: 'array',
401-
items: {$ref: getSchemaPath(options.paginatedResponseType)},
401+
items: { $ref: getSchemaPath(options.paginatedResponseType) },
402402
},
403403
pageInfo: {
404404
type: 'object',
405405
properties: {
406-
hasNextPage: {type: 'boolean'},
407-
hasPreviousPage: {type: 'boolean'},
408-
startCursor: {type: 'string', nullable: true},
409-
endCursor: {type: 'string', nullable: true},
406+
hasNextPage: { type: 'boolean' },
407+
hasPreviousPage: { type: 'boolean' },
408+
startCursor: { type: 'string', nullable: true },
409+
endCursor: { type: 'string', nullable: true },
410410
},
411411
},
412-
totalCount: {type: 'number'},
412+
totalCount: { type: 'number' },
413413
meta: {
414414
type: 'object',
415415
additionalProperties: true,
@@ -427,7 +427,7 @@ export function Api(options: ApiOptions): MethodDecorator {
427427
schema: {
428428
type: 'object',
429429
properties: {
430-
success: {type: 'boolean', example: true},
430+
success: { type: 'boolean', example: true },
431431
data: basePaginated,
432432
},
433433
},
@@ -451,14 +451,14 @@ export function Api(options: ApiOptions): MethodDecorator {
451451
// ============================================================================
452452
// Add default error responses (401, 201, 403) if user hasn't provided custom responses
453453
if (addDefaultSet) {
454-
decorators.push(ApiUnauthorizedResponse({description: 'Unauthorized'}));
455-
decorators.push(ApiCreatedResponse({description: 'The record has been successfully created.'}));
456-
decorators.push(ApiForbiddenResponse({description: 'Forbidden.'}));
454+
decorators.push(ApiUnauthorizedResponse({ description: 'Unauthorized' }));
455+
decorators.push(ApiCreatedResponse({ description: 'The record has been successfully created.' }));
456+
decorators.push(ApiForbiddenResponse({ description: 'Forbidden.' }));
457457
} else {
458458
// If user provided custom responses, ensure 401 is still added for authenticated endpoints
459459
let has401 = userProvidedResponses.some((r) => r.status === 401);
460460
if (options.authenticationRequired && !has401) {
461-
decorators.push(ApiUnauthorizedResponse({description: 'Unauthorized'}));
461+
decorators.push(ApiUnauthorizedResponse({ description: 'Unauthorized' }));
462462
has401 = true;
463463
}
464464
}

src/common/decorators/field.decorator.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {
1515
MaxLength,
1616
ValidationOptions,
1717
} from 'class-validator';
18-
import {pick} from './../../common/helpers/array.helper';
19-
import {ApiProperty, ApiPropertyOptional} from '@nestjs/swagger';
18+
import { pick } from './../../common/helpers/array.helper';
19+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
2020
import ValidatorJS from 'validator';
2121

2222
type FieldOptions = {
@@ -29,16 +29,16 @@ type FieldOptions = {
2929
nullable?: boolean; // kept for backwards compat; implies optional
3030
minLength?: number;
3131
maxLength?: number;
32-
isEnum?: {entity: object; validationOptions?: ValidationOptions};
32+
isEnum?: { entity: object; validationOptions?: ValidationOptions };
3333
isString?: ValidationOptions;
3434
isEthereumAddress?: ValidationOptions;
35-
isDecimal?: {options?: ValidatorJS.IsDecimalOptions; validationOptions: ValidationOptions};
35+
isDecimal?: { options?: ValidatorJS.IsDecimalOptions; validationOptions: ValidationOptions };
3636
isBoolean?: ValidationOptions;
3737
isInt?: ValidationOptions;
38-
isBigInt?: {message: string};
39-
isHash?: {algorithm: string; validationOptions: ValidationOptions};
38+
isBigInt?: { message: string };
39+
isHash?: { algorithm: string; validationOptions: ValidationOptions };
4040
isUrl?: ValidationOptions;
41-
pattern?: {regex: RegExp; message?: string};
41+
pattern?: { regex: RegExp; message?: string };
4242
isArray?: boolean;
4343
inQuery?: boolean; // mark field as query parameter candidate
4444
inPath?: boolean; // mark field as path parameter candidate
@@ -68,11 +68,11 @@ export function Field(options: FieldOptions) {
6868
if (!required) {
6969
IsOptional()(target, propertyKey);
7070
} else {
71-
IsNotEmpty({message: options.name + ': Value is required.'})(target, propertyKey);
71+
IsNotEmpty({ message: options.name + ': Value is required.' })(target, propertyKey);
7272
}
7373

7474
if (options.isEnum) {
75-
IsEnum(options.isEnum.entity, options.isEnum.validationOptions || {message: options.name + ': Invalid enum value.'})(
75+
IsEnum(options.isEnum.entity, options.isEnum.validationOptions || { message: options.name + ': Invalid enum value.' })(
7676
target,
7777
propertyKey
7878
);
@@ -84,10 +84,10 @@ export function Field(options: FieldOptions) {
8484
if (options.isDecimal) IsDecimal(options.isDecimal.options, options.isDecimal.validationOptions)(target, propertyKey);
8585
if (options.isBoolean) IsBoolean(options.isBoolean)(target, propertyKey);
8686
if (options.isInt) IsInt(options.isInt)(target, propertyKey);
87-
if (options.isBigInt) IsInt({message: options.isBigInt.message})(target, propertyKey);
87+
if (options.isBigInt) IsInt({ message: options.isBigInt.message })(target, propertyKey);
8888
if (options.isHash) IsHash(options.isHash.algorithm, options.isHash.validationOptions)(target, propertyKey);
8989
if (options.isUrl) IsUrl(undefined, options.isUrl)(target, propertyKey);
90-
if (options.pattern) Matches(options.pattern.regex, {message: options.pattern.message})(target, propertyKey);
90+
if (options.pattern) Matches(options.pattern.regex, { message: options.pattern.message })(target, propertyKey);
9191
if (options.isArray) IsArray()(target, propertyKey);
9292

9393
// Persist metadata on the class constructor so @Api decorator can build params/queries automatically

src/common/helpers/array.helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function reduceToObject<T>(array: T[], key: string): { [K: string]: T } {
2323
export function groupBy<T>(array: T[], key: string): { [key: string]: T[] } {
2424
return array.reduce(
2525
(acc, curr) => {
26-
if (!acc.hasOwnProperty(curr[key])) {
26+
if (!Object.prototype.hasOwnProperty.call(acc, curr[key])) {
2727
acc[curr[key]] = [];
2828
}
2929

src/common/helpers/req.helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function getOrigin(req: Request) {
5555
const origin = req.headers.origin;
5656

5757
if (!origin || typeof origin === 'string') {
58-
return origin as string;
58+
return origin;
5959
}
6060

6161
return origin[0];

src/common/validation/is-generic-type.decorator.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ export const IsGenericType = (
3030
name: 'IS_GENERIC_TYPE',
3131
validator: {
3232
validate: (value: unknown) => {
33-
return validators.some((item) =>
34-
typeof item === 'function' ? item(value) : InnerTypesValidator[item]?.(value)
35-
);
33+
return validators.some((item) => (typeof item === 'function' ? item(value) : InnerTypesValidator[item]?.(value)));
3634
},
3735
defaultMessage: (validationArguments?: ValidationArguments) => {
3836
return `${validationArguments?.property}: Data type mismatch`;

src/common/validation/is-unique-constraint.decorator.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ export class IsUniqueConstraint implements ValidatorConstraintInterface {
2121
async validate(value: string, args: ValidationArguments) {
2222
const [entity, column] = args.constraints;
2323

24-
let result = await this.prisma.$queryRawUnsafe(
25-
`SELECT * FROM $1 WHERE $2 = $3 WHERE LIMIT 1`,
26-
entity,
27-
column,
28-
value
29-
);
24+
const result = await this.prisma.$queryRawUnsafe(`SELECT * FROM $1 WHERE $2 = $3 WHERE LIMIT 1`, entity, column, value);
3025

3126
if (result) {
3227
return false;
@@ -40,7 +35,7 @@ export class IsUniqueConstraint implements ValidatorConstraintInterface {
4035
}
4136
}
4237

43-
export function IsUnique(entity: Function, column: string, validationOptions?: ValidationOptions) {
38+
export function IsUnique(entity: { new (): any }, column: string, validationOptions?: ValidationOptions) {
4439
return (object: object, propertyName: string) => {
4540
registerDecorator({
4641
target: object.constructor,
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
2-
import {Job} from './job.entity'
3-
1+
import { Job } from './job.entity';
42

53
export class Company {
6-
id: number ;
7-
name: string ;
8-
jobs?: Job[] ;
9-
createdAt: Date ;
4+
id: number;
5+
name: string;
6+
jobs?: Job[];
7+
createdAt: Date;
108
}

0 commit comments

Comments
 (0)