Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"db:generate": "pnpm dlx prisma generate",
"db:migrate": "pnpm dlx prisma migrate dev --name init",
"db:reset": "pnpm dlx prisma migrate reset",
"db:seed": "pnpm build && node dist/database/seeders/seed.js"
"db:seed": "pnpm build && node dist/src/database/seeders/seed.js"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
Expand Down
2 changes: 1 addition & 1 deletion server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import rateLimit from 'express-rate-limit';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AcademicYearContextModule } from './common/academic-year/academic-year-context.module.js';
import { AcademicYearContextModule } from './common/academic-year-context/academic-year-context.module';
import { auth } from './common/utils/auth';
import { PrismaModule } from './database/prisma/prisma.module';
import { AcademicYearsModule } from './modules/academic-years/academic-years.module.js';
Expand Down Expand Up @@ -76,7 +76,7 @@
urlencoded: { enabled: true, limit: '2mb', extended: true },
rawBody: true,
},
middleware: (req, res, next) => rateLimiter(req, res, next),

Check warning on line 79 in server/src/app.module.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `Response<any, Record<string, any>, number>`

Check warning on line 79 in server/src/app.module.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>`
}),
LocalAuthModule,
],
Expand Down
14 changes: 12 additions & 2 deletions server/src/database/seeders/seed-academic-years.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@ export async function seedAcademicYears(appContext: INestApplicationContext) {
return;
}

await prismaService.academicYear.create({
data: {
const academicYearToCreate = [
{
name: '2023-2024',
startDate: new Date('2023-09-01'),
endDate: new Date('2024-06-30'),
isCurrent: true,
},
{
name: '2024-2025',
startDate: new Date('2024-09-01'),
endDate: new Date('2025-06-30'),
isCurrent: false,
},
];

await prismaService.academicYear.createMany({
data: academicYearToCreate,
});
console.log('Academic years seeded successfully!');
} catch (error) {
Expand Down
15 changes: 3 additions & 12 deletions server/src/database/seeders/seed-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,10 @@ export async function seedClasses(appContext: INestApplicationContext) {
return;
}

const academicYear = await prismaService.academicYear.findFirst({
where: { isCurrent: true },
});

if (!academicYear) {
console.log('No current academic year found. Skipping class seeding.');
return;
}

const classesToCreate = [
{ name: 'Grade 1A', academicYearId: academicYear.id },
{ name: 'Grade 1B', academicYearId: academicYear.id },
{ name: 'Grade 2A', academicYearId: academicYear.id },
{ name: 'Grade 1A' },
{ name: 'Grade 1B' },
{ name: 'Grade 2A' },
];

await prismaService.class.createMany({
Expand Down
4 changes: 2 additions & 2 deletions server/src/database/seeders/seed-exams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function seedExams(appContext: INestApplicationContext) {
startTime: new Date('2024-03-15T09:00:00Z'),
endTime: new Date('2024-03-15T11:00:00Z'),
teachingAllocationId: teachingAllocations[0].id,
academicYearId: academicYear.id,
// academicYearId: academicYear.id,
},
{
title: 'Mid-term Science',
Expand All @@ -44,7 +44,7 @@ export async function seedExams(appContext: INestApplicationContext) {
startTime: new Date('2024-03-16T09:00:00Z'),
endTime: new Date('2024-03-16T11:00:00Z'),
teachingAllocationId: teachingAllocations[1].id,
academicYearId: academicYear.id,
// academicYearId: academicYear.id,
},
];

Expand Down
8 changes: 6 additions & 2 deletions server/src/modules/academic-years/academic-years.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '../../../prisma/generated/prisma/client.js';
import { AcademicYearContextService } from '../../common/academic-year-context/academic-year-context.service.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the test file mocks AcademicYearContextService
rg -n 'AcademicYearContextService' server/src/modules/academic-years/academic-years.service.spec.ts

Repository: thureinhtet99/student-os

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== service outline ==\n'
ast-grep outline server/src/modules/academic-years/academic-years.service.ts --view expanded

printf '\n== service file ==\n'
cat -n server/src/modules/academic-years/academic-years.service.ts | sed -n '1,220p'

printf '\n== context service outline ==\n'
ast-grep outline server/src/common/academic-year-context/academic-year-context.service.ts --view expanded

printf '\n== context service file ==\n'
cat -n server/src/common/academic-year-context/academic-year-context.service.ts | sed -n '1,220p'

printf '\n== spec file ==\n'
cat -n server/src/modules/academic-years/academic-years.service.spec.ts | sed -n '1,260p'

Repository: thureinhtet99/student-os

Length of output: 12270


🌐 Web query:

NestJS request-scoped provider injected into singleton service makes consumer request-scoped

💡 Result:

Yes, in NestJS, injecting a request-scoped provider into a singleton service causes the singleton service to become request-scoped as well [1][2][3]. This behavior is known as scope bubbling [1][2][4]. When a provider is marked as request-scoped (using Scope.REQUEST), NestJS must create a new instance of that provider for every incoming request [1][2][3]. Because a singleton service is expected to be shared across the entire application lifetime, NestJS cannot safely inject a volatile, request-specific instance into it [2][3]. To maintain the integrity of the dependency injection container, NestJS automatically promotes the singleton service to request scope [1][2]. This promotion behavior has several implications: 1. Scope Bubbling: The promotion propagates up the dependency graph [1][2][3]. Any controller or service that depends on that promoted singleton will also be forced into request scope [1][2]. 2. Performance Impact: Because the promoted services are now instantiated per request rather than being cached as singletons, this can increase the overhead of handling requests, potentially slowing down application performance [1][2][4]. 3. Background Contexts: If a service that has been promoted to request scope is later injected into a context where no request exists (such as a background worker, cron job, or application bootstrap), the application may fail to resolve the dependency because there is no request-specific DI container available [3]. It is generally recommended to use the default singleton scope whenever possible and only use request scope when absolutely necessary [1][4]. For cases involving request-specific metadata (like user context), developers often use AsyncLocalStorage or pass request metadata explicitly through method arguments to avoid forcing services into request scope [3].

Citations:


Remove the unused AcademicYearContextService injection, or use it in a request-aware method.
It only promotes AcademicYearsService to request scope, adding per-request DI overhead with no benefit. If it stays, server/src/modules/academic-years/academic-years.service.spec.ts also needs a mock provider for AcademicYearContextService.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/academic-years/academic-years.service.ts` at line 3,
Remove the unused AcademicYearContextService import and injection from
AcademicYearsService so it remains singleton-scoped; alternatively, use the
service in a request-aware method and add a corresponding mock provider in
academic-years.service.spec.ts.

import { PaginatedResponseDto } from '../../common/dto/paginated-response.dto.js';
import { PrismaService } from '../../database/prisma/prisma.service.js';
import { AcademicYearResponseDto } from './dto/academic-year-response.dto.js';
Expand All @@ -9,7 +10,10 @@ import { UpdateAcademicYearDto } from './dto/update-academic-year.dto.js';

@Injectable()
export class AcademicYearsService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly academicYearContext: AcademicYearContextService,
) {}

async create(
createAcademicYearDto: CreateAcademicYearDto,
Expand Down Expand Up @@ -112,13 +116,13 @@ export class AcademicYearsService {
return await this.prisma.academicYear.update({
where: { id },
data: {
name: updateAcademicYearDto.name,
startDate: updateAcademicYearDto.startDate
? new Date(updateAcademicYearDto.startDate)
: undefined,
endDate: updateAcademicYearDto.endDate
? new Date(updateAcademicYearDto.endDate)
: undefined,
isCurrent: updateAcademicYearDto.isCurrent,
},
});
Comment on lines 116 to 127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files 'server/src/modules/academic-years/*' 'server/src/modules/*academic*year*/*' | sed 's#^`#-` #'

printf '\n== Service outline ==\n'
ast-grep outline server/src/modules/academic-years/academic-years.service.ts --view expanded || true

printf '\n== DTO candidates ==\n'
fd -a '.*dto.*' server/src/modules/academic-years server/src/modules | sed 's#^`#-` #'

printf '\n== Search for academic year DTOs and decorators ==\n'
rg -n "class .*AcademicYear|PartialType|IsNotEmpty|IsOptional|trim\\(|new Date\\(" server/src/modules/academic-years server/src/modules -g '*.ts' -A 3 -B 3 || true

Repository: thureinhtet99/student-os

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== update() method ==\n'
sed -n '105,130p' server/src/modules/academic-years/academic-years.service.ts

printf '\n== academic-years service tests around update ==\n'
rg -n "update\\(|trim|name|startDate|endDate" server/src/modules/academic-years/academic-years.service.spec.ts -A 4 -B 4 || true

Repository: thureinhtet99/student-os

Length of output: 1020


Trim name in update() too. create() normalizes the academic-year label, but this path writes raw input, so whitespace-padded updates can still create duplicate-looking values and trip the unique constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/modules/academic-years/academic-years.service.ts` around lines 116
- 127, Normalize the academic-year name in the update method to match create
behavior. In the Prisma update data within the academic-year service’s update()
method, trim updateAcademicYearDto.name before persisting it, while preserving
the existing handling of startDate and endDate.

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class CreateAcademicYearDto {
@IsNotEmpty()
endDate!: string;

@ApiProperty({ example: false, nullable: true })
@ApiProperty({ example: false })
@IsBoolean()
@IsOptional()
isCurrent!: boolean;
Expand Down
23 changes: 20 additions & 3 deletions server/src/modules/academic-years/dto/update-academic-year.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { PartialType } from '@nestjs/mapped-types';
import { OmitType } from '@nestjs/swagger';
import { ApiProperty, OmitType } from '@nestjs/swagger';
import { IsDateString, IsNotEmpty, IsString, MaxLength } from 'class-validator';
import { CreateAcademicYearDto } from './create-academic-year.dto.js';

export class UpdateAcademicYearDto extends PartialType(
OmitType(CreateAcademicYearDto, ['name'] as const),
) {}
OmitType(CreateAcademicYearDto, ['isCurrent'] as const),
) {
@ApiProperty({ type: String, example: '2025-2026' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
name!: string;

@ApiProperty({ type: String, example: '2025-06-01T00:00:00.000Z' })
@IsDateString()
@IsNotEmpty()
startDate!: string;

@ApiProperty({ type: String, example: '2026-05-31T23:59:59.000Z' })
@IsDateString()
@IsNotEmpty()
endDate!: string;
}
2 changes: 1 addition & 1 deletion server/src/modules/classes/classes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
NotFoundException,
} from '@nestjs/common';
import { Prisma } from '../../../prisma/generated/prisma/client.js';
import { AcademicYearContextService } from '../../common/academic-year/academic-year-context.service.js';
import { AcademicYearContextService } from '../../common/academic-year-context/academic-year-context.service.js';
import { PaginatedResponseDto } from '../../common/dto/paginated-response.dto.js';
import { formatClass } from '../../common/formatters/class.formatter.js';
import { PrismaService } from '../../database/prisma/prisma.service.js';
Expand Down
Loading