Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AcademicYearContextService } from '../../common/academic-year/academic-year-context.service.js';
import { AcademicYearContextService } from '../../common/academic-year-context/academic-year-context.service.js';
import { PrismaService } from '../../database/prisma/prisma.service.js';
import { ClassesService } from './classes.service';

Expand Down
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