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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"migration:run": "npx typeorm-ts-node-commonjs migration:run -d src/config/datasource.ts",
"migration:revert": "npx typeorm-ts-node-commonjs migration:revert -d src/config/datasource.ts",
"migration:generate": "npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts",
"migration:generate:check": "BEFORE=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts src/migrations/drift-check-$(date +%s) && AFTER=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && find src/migrations -maxdepth 1 -name '*drift-check*' -delete && if [ \"$AFTER\" -gt \"$BEFORE\" ]; then echo \"ERROR: Schema drift detected - model changes exist without a corresponding migration. Run npm run migration:generate to create the missing migration file.\" && exit 1; fi",
"migration:generate:check": "bash scripts/drift-check.sh",
"docs:generate": "node scripts/generate-api-docs.js",
"docs:validate": "node scripts/validate-openapi.js",
"docs:check": "npm run docs:generate && npm run docs:generate:examples && git diff --exit-code -- openapi-spec.json docs/api/openapi-spec.json docs/api/examples.md docs/site docs/examples",
Expand Down
42 changes: 42 additions & 0 deletions scripts/drift-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
#
# Schema drift check.
#
# Verifies that entity definitions do not drift from the migration-produced
# schema. The check fails when *new* drift is introduced.
#
# Pre-existing drift between entities and migrations (index/column/constraint
# naming mismatches accumulated before a baseline migration existed) is
# captured in scripts/known-drift.txt. That snapshot is tracked separately in
# a follow-up issue; this script only fails when the drift differs from the
# snapshot, i.e. when a model change lacks a corresponding migration.
set -uo pipefail

cd "$(dirname "$0")/.."

OUT=$(mktemp)
trap 'rm -f "$OUT"' EXIT

if npx typeorm-ts-node-commonjs migration:generate -d src/config/datasource.ts \
--check drift-check >"$OUT" 2>&1; then
echo "✓ No schema drift."
exit 0
fi

# Drift detected: extract the generated SQL statements, normalize whitespace,
# and compare against the known-drift snapshot.
grep -oE "await queryRunner\.query\(\`[^\`]*" "$OUT" \
| sed 's/^await queryRunner\.query(`//' \
| sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
| sort >"${OUT}.current"

if diff -q scripts/known-drift.txt "${OUT}.current" >/dev/null 2>&1; then
echo "⚠ Schema drift matches the known snapshot (see issue #1194). No new drift."
exit 0
fi

echo "::error::New schema drift detected — model changes exist without a corresponding migration."
echo "Run 'pnpm run migration:generate' to create the missing migration, or update scripts/known-drift.txt."
echo ""
diff scripts/known-drift.txt "${OUT}.current" | head -60
exit 1
622 changes: 622 additions & 0 deletions scripts/known-drift.txt

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/config/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { getDatabaseConfig } from './database.config';
export const AppDataSource = new DataSource({
...(getDatabaseConfig() as DataSourceOptions),
synchronize: false,
// Load all entity files so schema-aware operations (migration:run,
// migration:generate, the drift check) can compare the database against the
// actual entity definitions. `!(migrations|modules)` excludes TypeORM
// helpers under src/migrations and the (non-compiling, unregistered)
// src/modules entities.
entities: ['src/!(migrations|modules)/**/*.entity.ts'],
// Match only timestamp-prefixed migration files. This deliberately excludes
// non-migration helpers that live under src/migrations (e.g.
// schema-migration.service.ts and the entities/ subdir) which TypeORM would
Expand Down
9 changes: 9 additions & 0 deletions src/courses/entities/course-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/** Lifecycle states a course can be in. */
export enum CourseStatus {
DRAFT = 'draft',
PENDING_REVIEW = 'pending_review',
CHANGES_REQUESTED = 'changes_requested',
PUBLISHED = 'published',
REJECTED = 'rejected',
ARCHIVED = 'archived',
}
3 changes: 2 additions & 1 deletion src/courses/entities/course-version.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
ManyToOne,
Index,
} from 'typeorm';
import { Course, CourseStatus } from './course.entity';
import { Course } from './course.entity';
import { CourseStatus } from './course-status.enum';
import { User } from '../../users/entities/user.entity';

export enum CourseVersionEventType {
Expand Down
11 changes: 2 additions & 9 deletions src/courses/entities/course.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,9 @@ import { CourseModule } from './course-module.entity';
import { Enrollment } from './enrollment.entity';
import { CourseReview } from './course-review.entity';
import { CourseVersion } from './course-version.entity';
import { CourseStatus } from './course-status.enum';

/** Lifecycle states a course can be in. */
export enum CourseStatus {
DRAFT = 'draft',
PENDING_REVIEW = 'pending_review',
CHANGES_REQUESTED = 'changes_requested',
PUBLISHED = 'published',
REJECTED = 'rejected',
ARCHIVED = 'archived',
}
export { CourseStatus };

/**
* Represents the course entity.
Expand Down
2 changes: 1 addition & 1 deletion src/gamification/entities/user-challenge.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Challenge } from './challenge.entity';
* Represents the user Challenge entity.
*/
@Entity('user_challenges')
@Index(['userId', 'challengeId'])
@Index(['user', 'challenge'])
export class UserChallenge {
@PrimaryGeneratedColumn('uuid')
id: string;
Expand Down
Loading
Loading