Add lessons domain Phase 1: instructor and lesson type entities
Foundation tables for the lessons module with full CRUD, pagination, search, and sorting. Includes migration, Drizzle schema, Zod validation, services, routes, and 23 integration tests.
This commit is contained in:
154
packages/backend/src/services/lesson.service.ts
Normal file
154
packages/backend/src/services/lesson.service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { eq, and, count, type Column } from 'drizzle-orm'
|
||||
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
||||
import { instructors, lessonTypes } from '../db/schema/lessons.js'
|
||||
import type {
|
||||
InstructorCreateInput,
|
||||
InstructorUpdateInput,
|
||||
LessonTypeCreateInput,
|
||||
LessonTypeUpdateInput,
|
||||
PaginationInput,
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import {
|
||||
withPagination,
|
||||
withSort,
|
||||
buildSearchCondition,
|
||||
paginatedResponse,
|
||||
} from '../utils/pagination.js'
|
||||
|
||||
export const InstructorService = {
|
||||
async create(db: PostgresJsDatabase<any>, input: InstructorCreateInput) {
|
||||
const [instructor] = await db
|
||||
.insert(instructors)
|
||||
.values({
|
||||
userId: input.userId,
|
||||
displayName: input.displayName,
|
||||
bio: input.bio,
|
||||
instruments: input.instruments,
|
||||
})
|
||||
.returning()
|
||||
return instructor
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [instructor] = await db
|
||||
.select()
|
||||
.from(instructors)
|
||||
.where(eq(instructors.id, id))
|
||||
.limit(1)
|
||||
return instructor ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(instructors.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [instructors.displayName])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
display_name: instructors.displayName,
|
||||
created_at: instructors.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(instructors).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, instructors.displayName)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(instructors).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: InstructorUpdateInput) {
|
||||
const [instructor] = await db
|
||||
.update(instructors)
|
||||
.set({ ...input, updatedAt: new Date() })
|
||||
.where(eq(instructors.id, id))
|
||||
.returning()
|
||||
return instructor ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [instructor] = await db
|
||||
.update(instructors)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(eq(instructors.id, id))
|
||||
.returning()
|
||||
return instructor ?? null
|
||||
},
|
||||
}
|
||||
|
||||
export const LessonTypeService = {
|
||||
async create(db: PostgresJsDatabase<any>, input: LessonTypeCreateInput) {
|
||||
const [lessonType] = await db
|
||||
.insert(lessonTypes)
|
||||
.values({
|
||||
name: input.name,
|
||||
instrument: input.instrument,
|
||||
durationMinutes: input.durationMinutes,
|
||||
lessonFormat: input.lessonFormat,
|
||||
baseRateMonthly: input.baseRateMonthly?.toString(),
|
||||
})
|
||||
.returning()
|
||||
return lessonType
|
||||
},
|
||||
|
||||
async getById(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [lessonType] = await db
|
||||
.select()
|
||||
.from(lessonTypes)
|
||||
.where(eq(lessonTypes.id, id))
|
||||
.limit(1)
|
||||
return lessonType ?? null
|
||||
},
|
||||
|
||||
async list(db: PostgresJsDatabase<any>, params: PaginationInput) {
|
||||
const baseWhere = eq(lessonTypes.isActive, true)
|
||||
const searchCondition = params.q
|
||||
? buildSearchCondition(params.q, [lessonTypes.name, lessonTypes.instrument])
|
||||
: undefined
|
||||
const where = searchCondition ? and(baseWhere, searchCondition) : baseWhere
|
||||
|
||||
const sortableColumns: Record<string, Column> = {
|
||||
name: lessonTypes.name,
|
||||
instrument: lessonTypes.instrument,
|
||||
duration_minutes: lessonTypes.durationMinutes,
|
||||
created_at: lessonTypes.createdAt,
|
||||
}
|
||||
|
||||
let query = db.select().from(lessonTypes).where(where).$dynamic()
|
||||
query = withSort(query, params.sort, params.order, sortableColumns, lessonTypes.name)
|
||||
query = withPagination(query, params.page, params.limit)
|
||||
|
||||
const [data, [{ total }]] = await Promise.all([
|
||||
query,
|
||||
db.select({ total: count() }).from(lessonTypes).where(where),
|
||||
])
|
||||
|
||||
return paginatedResponse(data, total, params.page, params.limit)
|
||||
},
|
||||
|
||||
async update(db: PostgresJsDatabase<any>, id: string, input: LessonTypeUpdateInput) {
|
||||
const values: Record<string, unknown> = { ...input, updatedAt: new Date() }
|
||||
if (input.baseRateMonthly !== undefined) values.baseRateMonthly = input.baseRateMonthly.toString()
|
||||
|
||||
const [lessonType] = await db
|
||||
.update(lessonTypes)
|
||||
.set(values)
|
||||
.where(eq(lessonTypes.id, id))
|
||||
.returning()
|
||||
return lessonType ?? null
|
||||
},
|
||||
|
||||
async delete(db: PostgresJsDatabase<any>, id: string) {
|
||||
const [lessonType] = await db
|
||||
.update(lessonTypes)
|
||||
.set({ isActive: false, updatedAt: new Date() })
|
||||
.where(eq(lessonTypes.id, id))
|
||||
.returning()
|
||||
return lessonType ?? null
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user