Compare commits
36 Commits
2ff1e89263
...
5b53929d92
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b53929d92 | |||
| dc788c04cb | |||
| 6c672c4b20 | |||
| 9db9c9899a | |||
| 152478a96c | |||
| 9154dec067 | |||
| 5fbca53534 | |||
| 6839d1aa0d | |||
| 72d1a0c3ed | |||
| de95c4d0d2 | |||
| 90baa582be | |||
| bbd919ca12 | |||
| 3e54b5039d | |||
| 24f1aca97a | |||
| ce1b827768 | |||
| 886b62ed68 | |||
| 7ff0b639cf | |||
| c5d3935062 | |||
| 518c3ee751 | |||
| 1026354c24 | |||
| 343ce470cc | |||
| 01914cb81e | |||
| d686bdf8bd | |||
| ea55d9b371 | |||
| 9ee8952d7f | |||
| 1c8d041747 | |||
| a9c6966820 | |||
| 06d86c9098 | |||
| 57f0db269a | |||
| 3889334e3f | |||
| f5e925dc96 | |||
| 83f5541650 | |||
| 542a597f41 | |||
| 9123ff571d | |||
| 0cc813d224 | |||
| a4d1f395da |
|
|
@ -1,4 +1,4 @@
|
||||||
# Yimaru Backend
|
# Yimaru Backend API
|
||||||
|
|
||||||
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
Yimaru Backend is the server-side application that powers the Yimaru online learning system. It manages courses, lessons, quizzes, student progress, instructor content, and administrative operations for institutions and users on the platform.
|
||||||
|
|
||||||
|
|
|
||||||
49
cmd/main.go
49
cmd/main.go
|
|
@ -14,10 +14,15 @@ import (
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
"Yimaru-Backend/internal/services/course_management"
|
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
"Yimaru-Backend/internal/services/messenger"
|
"Yimaru-Backend/internal/services/messenger"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
coursesservice "Yimaru-Backend/internal/services/courses"
|
||||||
|
lessonsservice "Yimaru-Backend/internal/services/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
|
moduleservice "Yimaru-Backend/internal/services/modules"
|
||||||
|
practicesservice "Yimaru-Backend/internal/services/practices"
|
||||||
|
programsservice "Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
|
|
@ -360,24 +365,10 @@ func main() {
|
||||||
logger.Info("Vimeo service disabled (VIMEO_ENABLED not set or missing access token)")
|
logger.Info("Vimeo service disabled (VIMEO_ENABLED not set or missing access token)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Course management service
|
// CloudConvert service for image/video optimization
|
||||||
courseSvc := course_management.NewService(
|
|
||||||
repository.NewUserStore(store),
|
|
||||||
repository.NewCourseStore(store),
|
|
||||||
repository.NewProgressionStore(store),
|
|
||||||
notificationSvc,
|
|
||||||
cfg,
|
|
||||||
)
|
|
||||||
// Wire up Vimeo service to course management
|
|
||||||
if vimeoSvc != nil {
|
|
||||||
courseSvc.SetVimeoService(vimeoSvc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudConvert service for video compression
|
|
||||||
var ccSvc *cloudconvertservice.Service
|
var ccSvc *cloudconvertservice.Service
|
||||||
if cfg.CloudConvert.Enabled && cfg.CloudConvert.APIKey != "" {
|
if cfg.CloudConvert.Enabled && cfg.CloudConvert.APIKey != "" {
|
||||||
ccSvc = cloudconvertservice.NewService(cfg.CloudConvert.APIKey, domain.MongoDBLogger)
|
ccSvc = cloudconvertservice.NewService(cfg.CloudConvert.APIKey, domain.MongoDBLogger)
|
||||||
courseSvc.SetCloudConvertService(ccSvc)
|
|
||||||
logger.Info("CloudConvert service initialized")
|
logger.Info("CloudConvert service initialized")
|
||||||
} else {
|
} else {
|
||||||
logger.Info("CloudConvert service disabled (CLOUDCONVERT_ENABLED not set or missing API key)")
|
logger.Info("CloudConvert service disabled (CLOUDCONVERT_ENABLED not set or missing API key)")
|
||||||
|
|
@ -402,6 +393,23 @@ func main() {
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
|
|
||||||
|
// LMS programs (top-level hierarchy)
|
||||||
|
programSvc := programsservice.NewService(store)
|
||||||
|
|
||||||
|
// LMS courses (under programs)
|
||||||
|
courseSvc := coursesservice.NewService(store, store)
|
||||||
|
|
||||||
|
// LMS modules (under courses)
|
||||||
|
moduleSvc := moduleservice.NewService(store, store)
|
||||||
|
|
||||||
|
// LMS lessons (under modules)
|
||||||
|
lessonSvc := lessonsservice.NewService(store, store)
|
||||||
|
|
||||||
|
lmsProgressSvc := lmsprogress.NewService(store)
|
||||||
|
|
||||||
|
// LMS practices (under course, module, or lesson)
|
||||||
|
practiceSvc := practicesservice.NewService(store, store, store, store, store, store)
|
||||||
|
|
||||||
// Subscriptions service
|
// Subscriptions service
|
||||||
subscriptionsSvc := subscriptions.NewService(store)
|
subscriptionsSvc := subscriptions.NewService(store)
|
||||||
|
|
||||||
|
|
@ -414,7 +422,7 @@ func main() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team management service
|
// Team management service
|
||||||
teamSvc := team.NewService(repository.NewTeamStore(store))
|
teamSvc := team.NewService(repository.NewTeamStore(store), cfg.RefreshExpiry)
|
||||||
|
|
||||||
// santimpayClient := santimpay.NewSantimPayClient(cfg)
|
// santimpayClient := santimpay.NewSantimPayClient(cfg)
|
||||||
|
|
||||||
|
|
@ -442,8 +450,13 @@ func main() {
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
courseSvc,
|
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
|
programSvc,
|
||||||
|
courseSvc,
|
||||||
|
moduleSvc,
|
||||||
|
lessonSvc,
|
||||||
|
lmsProgressSvc,
|
||||||
|
practiceSvc,
|
||||||
subscriptionsSvc,
|
subscriptionsSvc,
|
||||||
arifpaySvc,
|
arifpaySvc,
|
||||||
issueReportingSvc,
|
issueReportingSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
-- Intentionally empty: course hierarchy is not seeded from SQL.
|
|
||||||
-- Use admin/API or migrations to create content.
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
ALTER TABLE IF EXISTS sub_module_lessons
|
DROP INDEX IF EXISTS idx_sub_module_lessons_sub_module_id;
|
||||||
RENAME TO sub_module_practices;
|
|
||||||
|
|
||||||
ALTER INDEX IF EXISTS idx_sub_module_lessons_sub_module_id
|
DROP TABLE IF EXISTS sub_module_lessons;
|
||||||
RENAME TO idx_sub_module_practices_sub_module_id;
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,15 @@
|
||||||
ALTER TABLE IF EXISTS sub_module_practices
|
-- Keep practices as a separate feature and introduce lessons as a new table.
|
||||||
RENAME TO sub_module_lessons;
|
CREATE TABLE IF NOT EXISTS sub_module_lessons (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
intro_video_url TEXT,
|
||||||
|
display_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(question_set_id)
|
||||||
|
);
|
||||||
|
|
||||||
ALTER INDEX IF EXISTS idx_sub_module_practices_sub_module_id
|
CREATE INDEX IF NOT EXISTS idx_sub_module_lessons_sub_module_id
|
||||||
RENAME TO idx_sub_module_lessons_sub_module_id;
|
ON sub_module_lessons(sub_module_id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,24 @@ CREATE TABLE IF NOT EXISTS sub_module_practices (
|
||||||
UNIQUE(question_set_id)
|
UNIQUE(question_set_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- If the table already existed from older unified hierarchy migrations,
|
||||||
|
-- backfill missing columns so practices keep their own richer schema.
|
||||||
|
ALTER TABLE sub_module_practices
|
||||||
|
ADD COLUMN IF NOT EXISTS title VARCHAR(255);
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_practices
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_practices
|
||||||
|
ADD COLUMN IF NOT EXISTS thumbnail TEXT;
|
||||||
|
|
||||||
|
UPDATE sub_module_practices
|
||||||
|
SET title = COALESCE(NULLIF(title, ''), 'Practice')
|
||||||
|
WHERE title IS NULL OR title = '';
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_practices
|
||||||
|
ALTER COLUMN title SET NOT NULL;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id
|
CREATE INDEX IF NOT EXISTS idx_sub_module_practices_sub_module_id
|
||||||
ON sub_module_practices(sub_module_id);
|
ON sub_module_practices(sub_module_id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- Restores legacy lesson columns. Rows will have NULL question_set_id until repopulated.
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons
|
||||||
|
ADD COLUMN IF NOT EXISTS question_set_id BIGINT REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
ADD COLUMN IF NOT EXISTS intro_video_url TEXT;
|
||||||
|
|
||||||
|
UPDATE sub_module_lessons
|
||||||
|
SET intro_video_url = teaching_video_url
|
||||||
|
WHERE teaching_video_url IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons
|
||||||
|
DROP COLUMN IF EXISTS title,
|
||||||
|
DROP COLUMN IF EXISTS description,
|
||||||
|
DROP COLUMN IF EXISTS thumbnail,
|
||||||
|
DROP COLUMN IF EXISTS teaching_text,
|
||||||
|
DROP COLUMN IF EXISTS teaching_image_url,
|
||||||
|
DROP COLUMN IF EXISTS teaching_audio_url,
|
||||||
|
DROP COLUMN IF EXISTS teaching_video_url;
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
-- Lessons are teaching content only (text, images, audio, video, thumbnail).
|
||||||
|
-- Question sets remain linked to practices, not lessons.
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons
|
||||||
|
ADD COLUMN IF NOT EXISTS title VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS thumbnail TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS teaching_text TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS teaching_image_url TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS teaching_audio_url TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS teaching_video_url TEXT;
|
||||||
|
|
||||||
|
UPDATE sub_module_lessons sml
|
||||||
|
SET
|
||||||
|
title = qs.title,
|
||||||
|
description = qs.description
|
||||||
|
FROM question_sets qs
|
||||||
|
WHERE sml.question_set_id IS NOT NULL
|
||||||
|
AND qs.id = sml.question_set_id;
|
||||||
|
|
||||||
|
UPDATE sub_module_lessons
|
||||||
|
SET title = 'Lesson'
|
||||||
|
WHERE title IS NULL OR trim(title) = '';
|
||||||
|
|
||||||
|
UPDATE sub_module_lessons
|
||||||
|
SET teaching_video_url = intro_video_url
|
||||||
|
WHERE intro_video_url IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_fkey;
|
||||||
|
ALTER TABLE sub_module_lessons DROP CONSTRAINT IF EXISTS sub_module_lessons_question_set_id_key;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS question_set_id;
|
||||||
|
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS intro_video_url;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons
|
||||||
|
ALTER COLUMN title SET NOT NULL,
|
||||||
|
ALTER COLUMN title SET DEFAULT 'Lesson';
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE levels
|
||||||
|
DROP COLUMN IF EXISTS title,
|
||||||
|
DROP COLUMN IF EXISTS description,
|
||||||
|
DROP COLUMN IF EXISTS thumbnail;
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
ALTER TABLE levels
|
||||||
|
ADD COLUMN IF NOT EXISTS title VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS thumbnail TEXT;
|
||||||
|
|
||||||
|
UPDATE levels
|
||||||
|
SET title = cefr_level
|
||||||
|
WHERE title IS NULL OR trim(title) = '';
|
||||||
|
|
||||||
|
ALTER TABLE levels
|
||||||
|
ALTER COLUMN title SET NOT NULL;
|
||||||
12
db/migrations/000035_sub_module_capstones.down.sql
Normal file
12
db/migrations/000035_sub_module_capstones.down.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
DROP INDEX IF EXISTS idx_sub_module_capstones_sub_module_id;
|
||||||
|
DROP TABLE IF EXISTS sub_module_capstones;
|
||||||
|
|
||||||
|
ALTER TABLE question_sets DROP CONSTRAINT IF EXISTS question_sets_set_type_check;
|
||||||
|
ALTER TABLE question_sets ADD CONSTRAINT question_sets_set_type_check
|
||||||
|
CHECK (set_type IN (
|
||||||
|
'PRACTICE',
|
||||||
|
'INITIAL_ASSESSMENT',
|
||||||
|
'QUIZ',
|
||||||
|
'EXAM',
|
||||||
|
'SURVEY'
|
||||||
|
));
|
||||||
29
db/migrations/000035_sub_module_capstones.up.sql
Normal file
29
db/migrations/000035_sub_module_capstones.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- Capstone assessments: sub-module scoped, backed by question_sets (type CAPSTONE).
|
||||||
|
|
||||||
|
ALTER TABLE question_sets DROP CONSTRAINT IF EXISTS question_sets_set_type_check;
|
||||||
|
ALTER TABLE question_sets ADD CONSTRAINT question_sets_set_type_check
|
||||||
|
CHECK (set_type IN (
|
||||||
|
'PRACTICE',
|
||||||
|
'INITIAL_ASSESSMENT',
|
||||||
|
'QUIZ',
|
||||||
|
'EXAM',
|
||||||
|
'SURVEY',
|
||||||
|
'CAPSTONE'
|
||||||
|
));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sub_module_capstones (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
sub_module_id BIGINT NOT NULL REFERENCES sub_modules(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
tips TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
display_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (question_set_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sub_module_capstones_sub_module_id
|
||||||
|
ON sub_module_capstones (sub_module_id);
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
DROP INDEX IF EXISTS idx_module_capstones_module_id;
|
||||||
|
DROP TABLE IF EXISTS module_capstones;
|
||||||
|
|
||||||
|
ALTER TABLE modules DROP COLUMN IF EXISTS icon_url;
|
||||||
19
db/migrations/000036_module_icon_and_module_capstones.up.sql
Normal file
19
db/migrations/000036_module_icon_and_module_capstones.up.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
ALTER TABLE modules
|
||||||
|
ADD COLUMN IF NOT EXISTS icon_url TEXT;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS module_capstones (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
module_id BIGINT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
tips TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
display_order INT NOT NULL DEFAULT 0,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (question_set_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_module_capstones_module_id
|
||||||
|
ON module_capstones (module_id);
|
||||||
3
db/migrations/000037_sub_modules_thumbnail_tips.down.sql
Normal file
3
db/migrations/000037_sub_modules_thumbnail_tips.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE sub_modules
|
||||||
|
DROP COLUMN IF EXISTS tips,
|
||||||
|
DROP COLUMN IF EXISTS thumbnail;
|
||||||
3
db/migrations/000037_sub_modules_thumbnail_tips.up.sql
Normal file
3
db/migrations/000037_sub_modules_thumbnail_tips.up.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE sub_modules
|
||||||
|
ADD COLUMN IF NOT EXISTS thumbnail TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS tips TEXT;
|
||||||
7
db/migrations/000038_levels_flexible_cefr_level.down.sql
Normal file
7
db/migrations/000038_levels_flexible_cefr_level.down.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Restores fixed CEFR list; fails if any row has cefr_level outside the old set or longer than 2 characters.
|
||||||
|
ALTER TABLE levels
|
||||||
|
ALTER COLUMN cefr_level TYPE VARCHAR(2);
|
||||||
|
|
||||||
|
ALTER TABLE levels
|
||||||
|
ADD CONSTRAINT levels_cefr_level_check
|
||||||
|
CHECK (cefr_level IN ('A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3'));
|
||||||
20
db/migrations/000038_levels_flexible_cefr_level.up.sql
Normal file
20
db/migrations/000038_levels_flexible_cefr_level.up.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- Allow arbitrary level codes/labels per course (not only fixed CEFR bands).
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
con_name text;
|
||||||
|
BEGIN
|
||||||
|
SELECT c.conname INTO con_name
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_class t ON c.conrelid = t.oid
|
||||||
|
JOIN pg_namespace n ON t.relnamespace = n.oid
|
||||||
|
WHERE n.nspname = current_schema()
|
||||||
|
AND t.relname = 'levels'
|
||||||
|
AND c.contype = 'c'
|
||||||
|
AND pg_get_constraintdef(c.oid) LIKE '%cefr_level%IN (%A1%';
|
||||||
|
IF con_name IS NOT NULL THEN
|
||||||
|
EXECUTE format('ALTER TABLE levels DROP CONSTRAINT %I', con_name);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE levels
|
||||||
|
ALTER COLUMN cefr_level TYPE VARCHAR(64);
|
||||||
2
db/migrations/000039_team_refresh_tokens.down.sql
Normal file
2
db/migrations/000039_team_refresh_tokens.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP INDEX IF EXISTS idx_team_refresh_tokens_team_member_id;
|
||||||
|
DROP TABLE IF EXISTS team_refresh_tokens;
|
||||||
11
db/migrations/000039_team_refresh_tokens.up.sql
Normal file
11
db/migrations/000039_team_refresh_tokens.up.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS team_refresh_tokens (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
team_member_id BIGINT NOT NULL REFERENCES team_members(id) ON DELETE CASCADE,
|
||||||
|
token TEXT NOT NULL UNIQUE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_team_refresh_tokens_team_member_id
|
||||||
|
ON team_refresh_tokens (team_member_id);
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
ALTER TABLE sub_module_lessons DROP COLUMN IF EXISTS inactive_since;
|
||||||
|
ALTER TABLE sub_module_practices DROP COLUMN IF EXISTS inactive_since;
|
||||||
|
ALTER TABLE sub_module_capstones DROP COLUMN IF EXISTS inactive_since;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
-- Track when submodule lessons, practices, and capstones became inactive for retention-based hard delete.
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_lessons
|
||||||
|
ADD COLUMN IF NOT EXISTS inactive_since TIMESTAMPTZ;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_practices
|
||||||
|
ADD COLUMN IF NOT EXISTS inactive_since TIMESTAMPTZ;
|
||||||
|
|
||||||
|
ALTER TABLE sub_module_capstones
|
||||||
|
ADD COLUMN IF NOT EXISTS inactive_since TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Existing inactive rows: start retention window from migration time (conservative).
|
||||||
|
UPDATE sub_module_lessons
|
||||||
|
SET inactive_since = NOW()
|
||||||
|
WHERE is_active = FALSE
|
||||||
|
AND inactive_since IS NULL;
|
||||||
|
|
||||||
|
UPDATE sub_module_practices
|
||||||
|
SET inactive_since = NOW()
|
||||||
|
WHERE is_active = FALSE
|
||||||
|
AND inactive_since IS NULL;
|
||||||
|
|
||||||
|
UPDATE sub_module_capstones
|
||||||
|
SET inactive_since = NOW()
|
||||||
|
WHERE is_active = FALSE
|
||||||
|
AND inactive_since IS NULL;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
-- Restoring the removed course hierarchy is not supported; apply new migrations for the next model.
|
||||||
46
db/migrations/000041_remove_course_management_schema.up.sql
Normal file
46
db/migrations/000041_remove_course_management_schema.up.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
-- Tear down the legacy course / learning-tree schema so a new hierarchy can be introduced.
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Entry-assessment automation on sub_courses (from 000024)
|
||||||
|
DROP TRIGGER IF EXISTS trg_sub_courses_create_entry_assessment ON sub_courses;
|
||||||
|
DROP FUNCTION IF EXISTS create_sub_course_entry_assessment();
|
||||||
|
DROP FUNCTION IF EXISTS clone_default_initial_assessment_items(BIGINT);
|
||||||
|
DROP INDEX IF EXISTS idx_question_sets_unique_subcourse_initial_assessment;
|
||||||
|
|
||||||
|
ALTER TABLE question_sets DROP COLUMN IF EXISTS sub_course_video_id;
|
||||||
|
|
||||||
|
-- Dependent objects first
|
||||||
|
DROP TABLE IF EXISTS user_sub_course_video_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS user_practice_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_course_prerequisites CASCADE;
|
||||||
|
DROP TABLE IF EXISTS user_sub_course_progress CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS sub_module_practices CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_capstones CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_lessons CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_videos CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_modules CASCADE;
|
||||||
|
DROP TABLE IF EXISTS module_capstones CASCADE;
|
||||||
|
DROP TABLE IF EXISTS modules CASCADE;
|
||||||
|
DROP TABLE IF EXISTS levels CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS sub_course_videos CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_courses CASCADE;
|
||||||
|
DROP TABLE IF EXISTS course_sub_categories CASCADE;
|
||||||
|
DROP TABLE IF EXISTS courses CASCADE;
|
||||||
|
DROP TABLE IF EXISTS course_categories CASCADE;
|
||||||
|
|
||||||
|
-- Keep learner practice completion for the questions system (no sub_course column)
|
||||||
|
CREATE TABLE user_practice_progress (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, question_set_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_practice_progress_user_id ON user_practice_progress(user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
1
db/migrations/000042_programs.down.sql
Normal file
1
db/migrations/000042_programs.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS programs;
|
||||||
11
db/migrations/000042_programs.up.sql
Normal file
11
db/migrations/000042_programs.up.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Top-level LMS program (e.g. Beginner / Intermediate / Advanced — labels come from admin config later).
|
||||||
|
CREATE TABLE programs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_programs_created_at ON programs (created_at DESC);
|
||||||
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE (name = 'Beginner' AND description = 'Default program for the beginner level.')
|
||||||
|
OR (name = 'Intermediate' AND description = 'Default program for the intermediate level.')
|
||||||
|
OR (name = 'Advanced' AND description = 'Default program for the advanced level.');
|
||||||
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Default top-level programs (hierarchy: Program → Course → …).
|
||||||
|
INSERT INTO programs (name, description, thumbnail)
|
||||||
|
VALUES
|
||||||
|
('Beginner', 'Default program for the beginner level.', NULL),
|
||||||
|
('Intermediate', 'Default program for the intermediate level.', NULL),
|
||||||
|
('Advanced', 'Default program for the advanced level.', NULL);
|
||||||
1
db/migrations/000044_lms_courses.down.sql
Normal file
1
db/migrations/000044_lms_courses.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS courses;
|
||||||
13
db/migrations/000044_lms_courses.up.sql
Normal file
13
db/migrations/000044_lms_courses.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Courses belong to a Program (CEFR-style labels like A1..C2 will be configured separately).
|
||||||
|
CREATE TABLE courses (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
program_id BIGINT NOT NULL REFERENCES programs (id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_courses_program_id ON courses (program_id);
|
||||||
|
CREATE INDEX idx_courses_program_created ON courses (program_id, created_at DESC);
|
||||||
2
db/migrations/000045_lms_modules.down.sql
Normal file
2
db/migrations/000045_lms_modules.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TABLE IF EXISTS modules;
|
||||||
|
ALTER TABLE courses DROP CONSTRAINT IF EXISTS courses_program_id_id_key;
|
||||||
22
db/migrations/000045_lms_modules.up.sql
Normal file
22
db/migrations/000045_lms_modules.up.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- Modules belong to a Course; program_id is denormalized and enforced with the course by a composite FK.
|
||||||
|
ALTER TABLE courses
|
||||||
|
ADD CONSTRAINT courses_program_id_id_key UNIQUE (program_id, id);
|
||||||
|
|
||||||
|
CREATE TABLE modules (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
program_id BIGINT NOT NULL,
|
||||||
|
course_id BIGINT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
icon TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT modules_course_scope_fkey
|
||||||
|
FOREIGN KEY (program_id, course_id)
|
||||||
|
REFERENCES courses (program_id, id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_modules_course_id ON modules (course_id);
|
||||||
|
CREATE INDEX idx_modules_program_id ON modules (program_id);
|
||||||
|
CREATE INDEX idx_modules_program_course_created ON modules (program_id, course_id, created_at DESC);
|
||||||
1
db/migrations/000046_lms_lessons.down.sql
Normal file
1
db/migrations/000046_lms_lessons.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS lessons;
|
||||||
14
db/migrations/000046_lms_lessons.up.sql
Normal file
14
db/migrations/000046_lms_lessons.up.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Lessons belong to a Module.
|
||||||
|
CREATE TABLE lessons (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
video_url TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lessons_module_id ON lessons (module_id);
|
||||||
|
CREATE INDEX idx_lessons_module_created ON lessons (module_id, created_at DESC);
|
||||||
1
db/migrations/000047_lms_practices.down.sql
Normal file
1
db/migrations/000047_lms_practices.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS lms_practices;
|
||||||
29
db/migrations/000047_lms_practices.up.sql
Normal file
29
db/migrations/000047_lms_practices.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- Practices attach to exactly one of: course, module, or lesson.
|
||||||
|
CREATE TABLE lms_practices (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
course_id BIGINT REFERENCES courses (id) ON DELETE CASCADE,
|
||||||
|
module_id BIGINT REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
lesson_id BIGINT REFERENCES lessons (id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
story_description TEXT,
|
||||||
|
story_image TEXT,
|
||||||
|
persona_id BIGINT REFERENCES users (id) ON DELETE SET NULL,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets (id) ON DELETE RESTRICT,
|
||||||
|
quick_tips TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT lms_practices_one_parent CHECK (
|
||||||
|
(course_id IS NOT NULL)::int
|
||||||
|
+ (module_id IS NOT NULL)::int
|
||||||
|
+ (lesson_id IS NOT NULL)::int
|
||||||
|
= 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_practices_course_id ON lms_practices (course_id);
|
||||||
|
CREATE INDEX idx_lms_practices_module_id ON lms_practices (module_id);
|
||||||
|
CREATE INDEX idx_lms_practices_lesson_id ON lms_practices (lesson_id);
|
||||||
|
CREATE INDEX idx_lms_practices_question_set_id ON lms_practices (question_set_id);
|
||||||
|
CREATE INDEX idx_lms_practices_course_created ON lms_practices (course_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_lms_practices_module_created ON lms_practices (module_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_lms_practices_lesson_created ON lms_practices (lesson_id, created_at DESC);
|
||||||
3
db/migrations/000048_seed_default_courses.down.sql
Normal file
3
db/migrations/000048_seed_default_courses.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE description = 'Default CEFR level course (system seed).'
|
||||||
|
AND name IN ('A1', 'A2', 'B1', 'B2', 'C1', 'C2');
|
||||||
18
db/migrations/000048_seed_default_courses.up.sql
Normal file
18
db/migrations/000048_seed_default_courses.up.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- Default CEFR-style course names per program (custom courses can still be created via the API with any name).
|
||||||
|
-- Matches hierarchy note on courses: CEFR labels A1..C2, plus ad-hoc names allowed.
|
||||||
|
INSERT INTO courses (program_id, name, description, thumbnail)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
v.name,
|
||||||
|
'Default CEFR level course (system seed).',
|
||||||
|
NULL
|
||||||
|
FROM programs AS p
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('A1'),
|
||||||
|
('A2'),
|
||||||
|
('B1'),
|
||||||
|
('B2'),
|
||||||
|
('C1'),
|
||||||
|
('C2')
|
||||||
|
) AS v (name);
|
||||||
18
db/migrations/000049_lms_sequential_learning.down.sql
Normal file
18
db/migrations/000049_lms_sequential_learning.down.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
DROP TABLE IF EXISTS lms_user_program_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_course_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_module_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_lesson_progress;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS uq_lessons_module_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_modules_course_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_courses_program_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_programs_sort_order;
|
||||||
|
|
||||||
|
ALTER TABLE lessons
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE modules
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE courses
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE programs
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
150
db/migrations/000049_lms_sequential_learning.up.sql
Normal file
150
db/migrations/000049_lms_sequential_learning.up.sql
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
-- Sequential order for programs, courses, modules, and lessons (1 = first in each scope).
|
||||||
|
-- Progress tables mark completion; API enforces prerequisites for learners (STUDENT role).
|
||||||
|
|
||||||
|
ALTER TABLE programs
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE courses
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE modules
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE lessons
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Program order (one global sequence): Beginner -> Intermediate -> Advanced; others by id
|
||||||
|
UPDATE programs
|
||||||
|
SET sort_order = v.so
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('Beginner', 1),
|
||||||
|
('Intermediate', 2),
|
||||||
|
('Advanced', 3)
|
||||||
|
) AS v (name, so)
|
||||||
|
WHERE programs.name = v.name;
|
||||||
|
|
||||||
|
UPDATE programs
|
||||||
|
SET sort_order = 1000 + r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
ORDER BY id
|
||||||
|
) AS rn
|
||||||
|
FROM programs
|
||||||
|
WHERE
|
||||||
|
sort_order = 0
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
programs.id = r.id;
|
||||||
|
|
||||||
|
-- CEFR courses: A1..C2; remaining courses in each program: stable order
|
||||||
|
UPDATE courses
|
||||||
|
SET sort_order = CASE name
|
||||||
|
WHEN 'A1' THEN
|
||||||
|
1
|
||||||
|
WHEN 'A2' THEN
|
||||||
|
2
|
||||||
|
WHEN 'B1' THEN
|
||||||
|
3
|
||||||
|
WHEN 'B2' THEN
|
||||||
|
4
|
||||||
|
WHEN 'C1' THEN
|
||||||
|
5
|
||||||
|
WHEN 'C2' THEN
|
||||||
|
6
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
name IN ('A1', 'A2', 'B1', 'B2', 'C1', 'C2');
|
||||||
|
|
||||||
|
UPDATE courses c
|
||||||
|
SET sort_order = 2000 + s.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY program_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM courses
|
||||||
|
WHERE
|
||||||
|
sort_order = 0
|
||||||
|
) AS s
|
||||||
|
WHERE
|
||||||
|
c.id = s.id;
|
||||||
|
|
||||||
|
UPDATE modules m
|
||||||
|
SET sort_order = r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY course_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM modules
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
m.id = r.id;
|
||||||
|
|
||||||
|
UPDATE lessons l
|
||||||
|
SET sort_order = r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY module_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM lessons
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
l.id = r.id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uq_programs_sort_order ON programs (sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_courses_program_sort ON courses (program_id, sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_modules_course_sort ON modules (course_id, sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_lessons_module_sort ON lessons (module_id, sort_order);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_lesson_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
lesson_id BIGINT NOT NULL REFERENCES lessons (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, lesson_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_lesson_progress_user ON lms_user_lesson_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_lesson_progress_lesson ON lms_user_lesson_progress (lesson_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_module_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, module_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_module_progress_user ON lms_user_module_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_module_progress_module ON lms_user_module_progress (module_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_course_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
course_id BIGINT NOT NULL REFERENCES courses (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, course_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_course_progress_user ON lms_user_course_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_course_progress_course ON lms_user_course_progress (course_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_program_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
program_id BIGINT NOT NULL REFERENCES programs (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, program_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_program_progress_user ON lms_user_program_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_program_progress_program ON lms_user_program_progress (program_id);
|
||||||
|
|
@ -163,10 +163,10 @@ ORDER BY d.date;
|
||||||
|
|
||||||
-- name: AnalyticsCourseCounts :one
|
-- name: AnalyticsCourseCounts :one
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
0::bigint AS total_categories,
|
||||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
0::bigint AS total_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
0::bigint AS total_sub_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos;
|
0::bigint AS total_videos;
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
-- Content Analytics
|
-- Content Analytics
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
-- name: CreateCourseCategory :one
|
|
||||||
INSERT INTO course_categories (
|
|
||||||
name,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, COALESCE($2, true))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCourseCategoryByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM course_categories
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetAllCourseCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
FROM course_categories
|
|
||||||
ORDER BY display_order ASC, created_at DESC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateCourseCategory :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET
|
|
||||||
name = COALESCE($1, name),
|
|
||||||
is_active = COALESCE($2, is_active)
|
|
||||||
WHERE id = $3;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteCourseCategory :exec
|
|
||||||
DELETE FROM course_categories
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ReorderCourseCategories :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE course_categories.id = bulk.id;
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
-- name: CreateCourse :one
|
|
||||||
INSERT INTO courses (
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCourseByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCoursesByCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
FROM courses
|
|
||||||
WHERE category_id = $1
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateCourse :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET
|
|
||||||
title = COALESCE($1, title),
|
|
||||||
description = COALESCE($2, description),
|
|
||||||
thumbnail = COALESCE($3, thumbnail),
|
|
||||||
intro_video_url = COALESCE($4, intro_video_url),
|
|
||||||
is_active = COALESCE($5, is_active)
|
|
||||||
WHERE id = $6;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteCourse :exec
|
|
||||||
DELETE FROM courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ReorderCourses :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE courses.id = bulk.id;
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
-- name: GetCoursesWithHierarchy :many
|
|
||||||
SELECT
|
|
||||||
cc.id AS category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.id AS sub_category_id,
|
|
||||||
csc.name AS sub_category_name,
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title
|
|
||||||
FROM course_categories cc
|
|
||||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
|
||||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
|
||||||
WHERE cc.is_active = TRUE
|
|
||||||
ORDER BY cc.id, csc.display_order, csc.id, c.id;
|
|
||||||
|
|
||||||
-- name: GetLevelsByCourseID :many
|
|
||||||
SELECT *
|
|
||||||
FROM levels
|
|
||||||
WHERE course_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetModulesByLevelID :many
|
|
||||||
SELECT *
|
|
||||||
FROM modules
|
|
||||||
WHERE level_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModulesByModuleID :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE module_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleVideos :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_module_videos
|
|
||||||
WHERE sub_module_id = $1
|
|
||||||
AND status != 'ARCHIVED'
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleLessons :many
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
qs.title,
|
|
||||||
qs.description,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_lessons smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.sub_module_id = $1
|
|
||||||
AND smp.is_active = TRUE
|
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModulePractices :many
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.title,
|
|
||||||
smp.description,
|
|
||||||
smp.thumbnail,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_practices smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.sub_module_id = $1
|
|
||||||
AND smp.is_active = TRUE
|
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
|
||||||
|
|
||||||
-- name: GetFullHierarchyByCourseID :many
|
|
||||||
SELECT
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title,
|
|
||||||
l.id AS level_id,
|
|
||||||
l.cefr_level,
|
|
||||||
m.id AS module_id,
|
|
||||||
m.title AS module_title,
|
|
||||||
sm.id AS sub_module_id,
|
|
||||||
sm.title AS sub_module_title
|
|
||||||
FROM courses c
|
|
||||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
|
||||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
|
||||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
|
||||||
WHERE c.id = $1
|
|
||||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id;
|
|
||||||
|
|
||||||
-- name: CreateCourseSubCategory :one
|
|
||||||
INSERT INTO course_sub_categories (
|
|
||||||
category_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateLevel :one
|
|
||||||
INSERT INTO levels (
|
|
||||||
course_id,
|
|
||||||
cefr_level,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateModule :one
|
|
||||||
INSERT INTO modules (
|
|
||||||
level_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModule :one
|
|
||||||
INSERT INTO sub_modules (
|
|
||||||
module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModuleVideo :one
|
|
||||||
INSERT INTO sub_module_videos (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
video_url,
|
|
||||||
duration,
|
|
||||||
resolution,
|
|
||||||
is_published,
|
|
||||||
publish_date,
|
|
||||||
visibility,
|
|
||||||
instructor_id,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
status,
|
|
||||||
vimeo_id,
|
|
||||||
vimeo_embed_url,
|
|
||||||
vimeo_player_html,
|
|
||||||
vimeo_status,
|
|
||||||
video_host_provider
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, $2, $3, $4, $5, $6,
|
|
||||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
|
||||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
|
||||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
|
||||||
)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: AttachQuestionSetLessonToSubModule :one
|
|
||||||
INSERT INTO sub_module_lessons (
|
|
||||||
sub_module_id,
|
|
||||||
question_set_id,
|
|
||||||
intro_video_url,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModulePractice :one
|
|
||||||
INSERT INTO sub_module_practices (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
question_set_id,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
67
db/query/lms_courses.sql
Normal file
67
db/query/lms_courses.sql
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- name: CreateCourse :one
|
||||||
|
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(c.sort_order)
|
||||||
|
FROM courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetCourseByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM courses
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListCourseIDsByProgram :many
|
||||||
|
SELECT
|
||||||
|
c.id
|
||||||
|
FROM
|
||||||
|
courses AS c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.id;
|
||||||
|
|
||||||
|
-- name: ListCoursesByProgramID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.program_id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.sort_order,
|
||||||
|
c.created_at,
|
||||||
|
c.updated_at
|
||||||
|
FROM
|
||||||
|
courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.sort_order ASC,
|
||||||
|
c.id ASC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateCourse :one
|
||||||
|
UPDATE courses
|
||||||
|
SET
|
||||||
|
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||||
|
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = sqlc.arg('id')
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteCourse :exec
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE id = $1;
|
||||||
61
db/query/lms_lessons.sql
Normal file
61
db/query/lms_lessons.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
-- name: CreateLesson :one
|
||||||
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(l.sort_order)
|
||||||
|
FROM lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetLessonByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM lessons
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListLessonsByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.id,
|
||||||
|
l.module_id,
|
||||||
|
l.title,
|
||||||
|
l.video_url,
|
||||||
|
l.thumbnail,
|
||||||
|
l.description,
|
||||||
|
l.sort_order,
|
||||||
|
l.created_at,
|
||||||
|
l.updated_at
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
ORDER BY
|
||||||
|
l.sort_order ASC,
|
||||||
|
l.id ASC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateLesson :one
|
||||||
|
UPDATE lessons
|
||||||
|
SET
|
||||||
|
title = COALESCE(sqlc.narg('title')::varchar, title),
|
||||||
|
video_url = COALESCE(sqlc.narg('video_url')::text, video_url),
|
||||||
|
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = sqlc.arg('id')
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteLesson :exec
|
||||||
|
DELETE FROM lessons
|
||||||
|
WHERE id = $1;
|
||||||
71
db/query/lms_modules.sql
Normal file
71
db/query/lms_modules.sql
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- name: CreateModule :one
|
||||||
|
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(m.sort_order)
|
||||||
|
FROM modules m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $2), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetModuleByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListModuleIDsByCourse :many
|
||||||
|
SELECT
|
||||||
|
m.id
|
||||||
|
FROM
|
||||||
|
modules AS m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
ORDER BY
|
||||||
|
m.id;
|
||||||
|
|
||||||
|
-- name: ListModulesByProgramAndCourse :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.id,
|
||||||
|
m.program_id,
|
||||||
|
m.course_id,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.icon,
|
||||||
|
m.sort_order,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM
|
||||||
|
modules m
|
||||||
|
WHERE
|
||||||
|
m.program_id = $1
|
||||||
|
AND m.course_id = $2
|
||||||
|
ORDER BY
|
||||||
|
m.sort_order ASC,
|
||||||
|
m.id ASC
|
||||||
|
LIMIT $3
|
||||||
|
OFFSET $4;
|
||||||
|
|
||||||
|
-- name: UpdateModule :one
|
||||||
|
UPDATE modules
|
||||||
|
SET
|
||||||
|
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
icon = COALESCE(sqlc.narg('icon')::text, icon),
|
||||||
|
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = sqlc.arg('id')
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteModule :exec
|
||||||
|
DELETE FROM modules
|
||||||
|
WHERE id = $1;
|
||||||
88
db/query/lms_practices.sql
Normal file
88
db/query/lms_practices.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
-- name: CreateLmsPractice :one
|
||||||
|
INSERT INTO lms_practices (
|
||||||
|
course_id, module_id, lesson_id,
|
||||||
|
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetLmsPracticeByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM lms_practices
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByCourseID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.course_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.module_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByLessonID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.lesson_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateLmsPractice :one
|
||||||
|
UPDATE lms_practices
|
||||||
|
SET
|
||||||
|
title = COALESCE(sqlc.narg('title')::varchar, title),
|
||||||
|
story_description = COALESCE(sqlc.narg('story_description')::text, story_description),
|
||||||
|
story_image = COALESCE(sqlc.narg('story_image')::text, story_image),
|
||||||
|
persona_id = COALESCE(sqlc.narg('persona_id')::bigint, persona_id),
|
||||||
|
question_set_id = COALESCE(sqlc.narg('question_set_id')::bigint, question_set_id),
|
||||||
|
quick_tips = COALESCE(sqlc.narg('quick_tips')::text, quick_tips),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = sqlc.arg('id')
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteLmsPractice :exec
|
||||||
|
DELETE FROM lms_practices
|
||||||
|
WHERE id = $1;
|
||||||
248
db/query/lms_progress.sql
Normal file
248
db/query/lms_progress.sql
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
-- name: GetPreviousProgram :one
|
||||||
|
SELECT
|
||||||
|
p2.*
|
||||||
|
FROM
|
||||||
|
programs AS p1
|
||||||
|
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
p1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousCourseInProgram :one
|
||||||
|
SELECT
|
||||||
|
c2.*
|
||||||
|
FROM
|
||||||
|
courses AS c1
|
||||||
|
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
|
||||||
|
AND c2.sort_order = c1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
c1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousModuleInCourse :one
|
||||||
|
SELECT
|
||||||
|
m2.*
|
||||||
|
FROM
|
||||||
|
modules AS m1
|
||||||
|
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
|
||||||
|
AND m2.sort_order = m1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
m1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousLessonInModule :one
|
||||||
|
SELECT
|
||||||
|
l2.*
|
||||||
|
FROM
|
||||||
|
lessons AS l1
|
||||||
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
|
AND l2.sort_order = l1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
l1.id = $1;
|
||||||
|
|
||||||
|
-- name: UserHasProgramProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND program_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasCourseProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND course_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasModuleProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND module_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasLessonProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND lesson_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: InsertUserLessonProgress :exec
|
||||||
|
INSERT INTO lms_user_lesson_progress (user_id, lesson_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, lesson_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserModuleProgress :exec
|
||||||
|
INSERT INTO lms_user_module_progress (user_id, module_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, module_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserCourseProgress :exec
|
||||||
|
INSERT INTO lms_user_course_progress (user_id, course_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, course_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserProgramProgress :exec
|
||||||
|
INSERT INTO lms_user_program_progress (user_id, program_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, program_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: CountLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons
|
||||||
|
WHERE
|
||||||
|
module_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
||||||
|
-- name: CountModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
modules
|
||||||
|
WHERE
|
||||||
|
course_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress ump
|
||||||
|
INNER JOIN modules m ON m.id = ump.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ump.user_id = $2;
|
||||||
|
|
||||||
|
-- name: CountCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
program_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress ucp
|
||||||
|
INNER JOIN courses c ON c.id = ucp.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ucp.user_id = $2;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedLessonIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ulp.lesson_id
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress AS ulp
|
||||||
|
WHERE
|
||||||
|
ulp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ulp.completed_at ASC,
|
||||||
|
ulp.lesson_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedModuleIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ump.module_id
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress AS ump
|
||||||
|
WHERE
|
||||||
|
ump.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ump.completed_at ASC,
|
||||||
|
ump.module_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedCourseIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ucp.course_id
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress AS ucp
|
||||||
|
WHERE
|
||||||
|
ucp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ucp.completed_at ASC,
|
||||||
|
ucp.course_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedProgramIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
upp.program_id
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress AS upp
|
||||||
|
WHERE
|
||||||
|
upp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
upp.completed_at ASC,
|
||||||
|
upp.program_id ASC;
|
||||||
|
|
||||||
|
-- Lesson-based progress within a course (all modules).
|
||||||
|
-- name: CountLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
||||||
|
-- Lesson-based progress within a program (all courses).
|
||||||
|
-- name: CountLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
@ -29,27 +29,16 @@ LIMIT 1;
|
||||||
-- name: MarkPracticeCompleted :execrows
|
-- name: MarkPracticeCompleted :execrows
|
||||||
INSERT INTO user_practice_progress (
|
INSERT INTO user_practice_progress (
|
||||||
user_id,
|
user_id,
|
||||||
sub_course_id,
|
|
||||||
question_set_id,
|
question_set_id,
|
||||||
completed_at,
|
completed_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
SELECT
|
VALUES (
|
||||||
@user_id::BIGINT,
|
@user_id::BIGINT,
|
||||||
CASE
|
@question_set_id::BIGINT,
|
||||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
|
||||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
|
||||||
ELSE NULL
|
|
||||||
END,
|
|
||||||
qs.id,
|
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
FROM question_sets qs
|
)
|
||||||
LEFT JOIN sub_modules sm
|
|
||||||
ON qs.owner_type = 'SUB_MODULE'
|
|
||||||
AND qs.owner_id = sm.id
|
|
||||||
WHERE qs.id = @question_set_id::BIGINT
|
|
||||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||||
SET completed_at = EXCLUDED.completed_at,
|
SET completed_at = EXCLUDED.completed_at,
|
||||||
updated_at = EXCLUDED.updated_at;
|
updated_at = EXCLUDED.updated_at;
|
||||||
|
|
||||||
|
|
|
||||||
56
db/query/programs.sql
Normal file
56
db/query/programs.sql
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
-- name: CreateProgram :one
|
||||||
|
INSERT INTO programs (name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(p.sort_order)
|
||||||
|
FROM programs AS p), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetProgramByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM programs
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListAllProgramIDs :many
|
||||||
|
SELECT
|
||||||
|
p.id
|
||||||
|
FROM
|
||||||
|
programs AS p
|
||||||
|
ORDER BY
|
||||||
|
p.id;
|
||||||
|
|
||||||
|
-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.sort_order,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.sort_order ASC, p.id ASC
|
||||||
|
LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
|
-- name: UpdateProgram :one
|
||||||
|
UPDATE programs
|
||||||
|
SET
|
||||||
|
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||||
|
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = sqlc.arg('id')
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteProgram :exec
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE id = $1;
|
||||||
|
|
@ -11,10 +11,9 @@ INSERT INTO question_sets (
|
||||||
passing_score,
|
passing_score,
|
||||||
shuffle_questions,
|
shuffle_questions,
|
||||||
status,
|
status,
|
||||||
sub_course_video_id,
|
|
||||||
intro_video_url
|
intro_video_url
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetQuestionSetByID :one
|
-- name: GetQuestionSetByID :one
|
||||||
|
|
@ -61,9 +60,8 @@ SET
|
||||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||||
status = COALESCE($8, status),
|
status = COALESCE($8, status),
|
||||||
intro_video_url = COALESCE($9, intro_video_url),
|
intro_video_url = COALESCE($9, intro_video_url),
|
||||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11;
|
WHERE id = $10;
|
||||||
|
|
||||||
-- name: ArchiveQuestionSet :exec
|
-- name: ArchiveQuestionSet :exec
|
||||||
UPDATE question_sets
|
UPDATE question_sets
|
||||||
|
|
@ -82,16 +80,6 @@ WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: GetSubCourseInitialAssessmentSet :one
|
|
||||||
SELECT *
|
|
||||||
FROM question_sets
|
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
|
||||||
AND owner_type = 'SUB_COURSE'
|
|
||||||
AND owner_id = $1
|
|
||||||
AND status = 'PUBLISHED'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: AddUserPersonaToQuestionSet :one
|
-- name: AddUserPersonaToQuestionSet :one
|
||||||
INSERT INTO question_set_personas (
|
INSERT INTO question_set_personas (
|
||||||
question_set_id,
|
question_set_id,
|
||||||
|
|
@ -120,13 +108,6 @@ INNER JOIN question_set_personas qsp ON qsp.user_id = u.id
|
||||||
WHERE qsp.question_set_id = $1
|
WHERE qsp.question_set_id = $1
|
||||||
ORDER BY qsp.display_order ASC;
|
ORDER BY qsp.display_order ASC;
|
||||||
|
|
||||||
-- name: UpdateQuestionSetVideoLink :exec
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
sub_course_video_id = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2;
|
|
||||||
|
|
||||||
-- name: ReorderQuestionSets :exec
|
-- name: ReorderQuestionSets :exec
|
||||||
UPDATE question_sets
|
UPDATE question_sets
|
||||||
SET display_order = bulk.position
|
SET display_order = bulk.position
|
||||||
|
|
|
||||||
19
db/query/team_refresh_tokens.sql
Normal file
19
db/query/team_refresh_tokens.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
-- name: RevokeAllActiveTeamRefreshTokensForMember :exec
|
||||||
|
UPDATE team_refresh_tokens
|
||||||
|
SET revoked = TRUE
|
||||||
|
WHERE team_member_id = $1
|
||||||
|
AND revoked = FALSE;
|
||||||
|
|
||||||
|
-- name: CreateTeamRefreshToken :exec
|
||||||
|
INSERT INTO team_refresh_tokens (team_member_id, token, expires_at, revoked, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5);
|
||||||
|
|
||||||
|
-- name: GetTeamRefreshTokenByToken :one
|
||||||
|
SELECT *
|
||||||
|
FROM team_refresh_tokens
|
||||||
|
WHERE token = $1;
|
||||||
|
|
||||||
|
-- name: RevokeTeamRefreshTokenByToken :exec
|
||||||
|
UPDATE team_refresh_tokens
|
||||||
|
SET revoked = TRUE
|
||||||
|
WHERE token = $1;
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
# Human Language Mobile Integration Guide
|
|
||||||
|
|
||||||
This guide explains how to integrate the new **Human Language** feature in the **Yimaru learner mobile app** (not admin).
|
|
||||||
|
|
||||||
It is designed to keep the existing non-language hierarchy intact while introducing a dedicated CEFR-based flow for language learning.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Scope and Goals
|
|
||||||
|
|
||||||
### What is new
|
|
||||||
|
|
||||||
- A dedicated backend API namespace for Human Language:
|
|
||||||
- `GET /api/v1/course-management/human-language/courses/:courseId/lessons?cefr_level=A1..C3`
|
|
||||||
- `POST /api/v1/course-management/human-language/lessons` (admin/instructor side)
|
|
||||||
- `PATCH /api/v1/course-management/human-language/lessons/:id` (admin/instructor side)
|
|
||||||
- CEFR levels are fixed to:
|
|
||||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
|
||||||
- No custom sub-levels in Human Language flow.
|
|
||||||
|
|
||||||
### What remains unchanged
|
|
||||||
|
|
||||||
- Existing non-human-language content hierarchy and APIs.
|
|
||||||
- Existing course/category/sub-course endpoints for non-language domains (programming, etc.).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2) Target Hierarchy for Human Language
|
|
||||||
|
|
||||||
- Course Category (Human Language)
|
|
||||||
- Course (e.g., English)
|
|
||||||
- CEFR Lesson Unit (A1..C3 only)
|
|
||||||
- Intro/lesson videos
|
|
||||||
- Practices
|
|
||||||
- Audio questions
|
|
||||||
|
|
||||||
Backend implementation stores CEFR lesson units using the existing sub-course model, with CEFR mapped internally and validated strictly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3) Authentication and Access
|
|
||||||
|
|
||||||
All Human Language endpoints are under `/api/v1` and require bearer token auth.
|
|
||||||
|
|
||||||
- Header:
|
|
||||||
- `Authorization: Bearer <access_token>`
|
|
||||||
|
|
||||||
### Permission notes
|
|
||||||
|
|
||||||
- **Learner mobile app** typically needs only the `GET` endpoint.
|
|
||||||
- `POST` and `PATCH` are content-management endpoints and should generally be used by admin/instructor clients, not learner clients.
|
|
||||||
|
|
||||||
If learner roles do not currently have `learning_tree.get`, coordinate RBAC assignment for read-only access to the `GET` endpoint.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4) Endpoint Contracts
|
|
||||||
|
|
||||||
## 4.1 Fetch lessons by CEFR level (learner-facing)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`GET /api/v1/course-management/human-language/courses/{courseId}/lessons?cefr_level={A1|A2|A3|B1|B2|B3|C1|C2|C3}`
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
`GET /api/v1/course-management/human-language/courses/12/lessons?cefr_level=B1`
|
|
||||||
|
|
||||||
### Response (success)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Human-language lessons retrieved successfully",
|
|
||||||
"data": {
|
|
||||||
"course_id": 12,
|
|
||||||
"course_title": "English",
|
|
||||||
"cefr_level": "B1",
|
|
||||||
"lessons": [
|
|
||||||
{
|
|
||||||
"id": 201,
|
|
||||||
"course_id": 12,
|
|
||||||
"title": "B1 Module 1",
|
|
||||||
"description": "Intermediate conversational patterns",
|
|
||||||
"thumbnail": "https://.../thumb.jpg",
|
|
||||||
"display_order": 1,
|
|
||||||
"level": "B1",
|
|
||||||
"video_count": 3,
|
|
||||||
"practice_count": 2,
|
|
||||||
"videos": [
|
|
||||||
{
|
|
||||||
"id": 9001,
|
|
||||||
"title": "B1 Intro",
|
|
||||||
"description": "Lesson intro",
|
|
||||||
"video_url": "https://...",
|
|
||||||
"duration": 420,
|
|
||||||
"resolution": "1080p",
|
|
||||||
"display_order": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"practices": [
|
|
||||||
{
|
|
||||||
"id": 4401,
|
|
||||||
"title": "B1 Speaking Practice",
|
|
||||||
"status": "PUBLISHED",
|
|
||||||
"question_count": 10
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"success": true,
|
|
||||||
"status_code": 200,
|
|
||||||
"metadata": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Validation errors
|
|
||||||
|
|
||||||
- Invalid CEFR level:
|
|
||||||
- `400` with error message: `Use one of: A1,A2,A3,B1,B2,B3,C1,C2,C3`
|
|
||||||
- Course not in human language category:
|
|
||||||
- `400` with error message indicating invalid human-language course.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4.2 Create Human Language lesson unit (admin/instructor)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`POST /api/v1/course-management/human-language/lessons`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"course_id": 12,
|
|
||||||
"title": "A2 Module 1",
|
|
||||||
"description": "A2 speaking fundamentals",
|
|
||||||
"thumbnail": "https://...",
|
|
||||||
"display_order": 1,
|
|
||||||
"cefr_level": "A2"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
Returns created lesson metadata with CEFR-normalized level behavior.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4.3 Update Human Language lesson unit (admin/instructor)
|
|
||||||
|
|
||||||
### Request
|
|
||||||
|
|
||||||
`PATCH /api/v1/course-management/human-language/lessons/{lessonId}`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "A2 Module 1 - Updated",
|
|
||||||
"description": "Updated description",
|
|
||||||
"cefr_level": "A3",
|
|
||||||
"is_active": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5) Recommended Mobile App Integration Flow
|
|
||||||
|
|
||||||
## Step 1: Discover courses under Human Language category
|
|
||||||
|
|
||||||
Use existing endpoints:
|
|
||||||
|
|
||||||
1. `GET /course-management/categories`
|
|
||||||
2. Pick category where name indicates human language.
|
|
||||||
3. `GET /course-management/categories/:categoryId/courses`
|
|
||||||
|
|
||||||
## Step 2: Select CEFR level in UI
|
|
||||||
|
|
||||||
Use static list in app:
|
|
||||||
|
|
||||||
- `A1, A2, A3, B1, B2, B3, C1, C2, C3`
|
|
||||||
|
|
||||||
Do not allow custom levels in mobile UI.
|
|
||||||
|
|
||||||
## Step 3: Fetch lessons by selected CEFR level
|
|
||||||
|
|
||||||
Call:
|
|
||||||
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=<selected>`
|
|
||||||
|
|
||||||
Render grouped lessons with nested videos/practices from response.
|
|
||||||
|
|
||||||
## Step 4: Navigate to learning/practice screens
|
|
||||||
|
|
||||||
- Lesson video playback uses returned `videos[]`.
|
|
||||||
- Practice entry uses returned `practices[]` IDs and existing practice-question endpoints.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6) Mobile UI/UX Recommendations
|
|
||||||
|
|
||||||
- Use CEFR tabs/segmented control (`A1`...`C3`) at top.
|
|
||||||
- Cache last selected level per course.
|
|
||||||
- Show empty state per level:
|
|
||||||
- "No lessons available for this level yet."
|
|
||||||
- Sort by `display_order` and maintain backend order.
|
|
||||||
- Show badges:
|
|
||||||
- video count
|
|
||||||
- practice count
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7) Error Handling and Resilience
|
|
||||||
|
|
||||||
- `400` invalid level: reset selection to previous valid CEFR value and show toast/snackbar.
|
|
||||||
- `401/403`: trigger token refresh / re-login flow.
|
|
||||||
- `5xx`: show retry UI with exponential backoff.
|
|
||||||
- If category/course fetch succeeds but level fetch fails, keep course visible and allow manual retry.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8) Data Model Mapping (Mobile DTO)
|
|
||||||
|
|
||||||
Suggested DTO for learner app:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type CefrLevel = "A1"|"A2"|"A3"|"B1"|"B2"|"B3"|"C1"|"C2"|"C3";
|
|
||||||
|
|
||||||
interface HumanLanguageLessonDTO {
|
|
||||||
id: number;
|
|
||||||
courseId: number;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
thumbnail?: string;
|
|
||||||
order: number;
|
|
||||||
level: CefrLevel;
|
|
||||||
videoCount: number;
|
|
||||||
practiceCount: number;
|
|
||||||
videos: {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
duration: number;
|
|
||||||
order: number;
|
|
||||||
}[];
|
|
||||||
practices: {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
status: string;
|
|
||||||
questionCount: number;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9) Backward Compatibility
|
|
||||||
|
|
||||||
- Existing non-language content (programming and other categories) continues to use current APIs and hierarchy unchanged.
|
|
||||||
- New Human Language endpoint is additive and isolated.
|
|
||||||
- Mobile app can progressively enable the new flow for language categories only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10) QA Checklist for Mobile Team
|
|
||||||
|
|
||||||
- [ ] Category discovery correctly identifies Human Language category.
|
|
||||||
- [ ] CEFR selector only allows A1..C3.
|
|
||||||
- [ ] Fetch by CEFR level returns only matching lessons.
|
|
||||||
- [ ] Video/practice counts match rendered lists.
|
|
||||||
- [ ] Empty-level state works.
|
|
||||||
- [ ] Unauthorized/session-expired flow works.
|
|
||||||
- [ ] Non-language courses still load via existing app flow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11) Example Call Matrix
|
|
||||||
|
|
||||||
- Load language categories/courses:
|
|
||||||
- `GET /course-management/categories`
|
|
||||||
- `GET /course-management/categories/:id/courses`
|
|
||||||
- Load A1 lessons:
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=A1`
|
|
||||||
- Load B2 lessons:
|
|
||||||
- `GET /course-management/human-language/courses/:courseId/lessons?cefr_level=B2`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
For backend ownership questions, refer to:
|
|
||||||
|
|
||||||
- `internal/web_server/handlers/course_management.go`
|
|
||||||
- `internal/web_server/routes.go`
|
|
||||||
|
|
||||||
1900
docs/docs.go
1900
docs/docs.go
File diff suppressed because it is too large
Load Diff
1900
docs/swagger.json
1900
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1267
docs/swagger.yaml
1267
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -12,10 +12,10 @@ import (
|
||||||
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
0::bigint AS total_categories,
|
||||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
0::bigint AS total_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
0::bigint AS total_sub_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos
|
0::bigint AS total_videos
|
||||||
`
|
`
|
||||||
|
|
||||||
type AnalyticsCourseCountsRow struct {
|
type AnalyticsCourseCountsRow struct {
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModuleByIDCompat(ctx context.Context, id int64) (SubModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, `
|
|
||||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE id = $1
|
|
||||||
`, id)
|
|
||||||
var i SubModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LegacySubCourseID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleCompat(ctx context.Context, id int64, title string, description string, isActive bool) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_modules
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
is_active = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_modules WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleVideoCompat(ctx context.Context, id int64, title string, description string, videoURL string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_videos
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
video_url = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, videoURL, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleVideoCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_module_videos WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeCompat(ctx context.Context, id int64, title string, description string, persona string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
persona = NULLIF($3, ''),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, persona, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, '')
|
|
||||||
WHERE question_set_id = $3
|
|
||||||
`, title, description, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeStatusCompat(ctx context.Context, id int64, isActive bool) error {
|
|
||||||
status := "ARCHIVED"
|
|
||||||
if isActive {
|
|
||||||
status = "PUBLISHED"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
status = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`, status, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET is_active = $1
|
|
||||||
WHERE question_set_id = $2
|
|
||||||
`, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeletePracticeCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM question_sets WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: course_catagories.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CreateCourseCategory = `-- name: CreateCourseCategory :one
|
|
||||||
INSERT INTO course_categories (
|
|
||||||
name,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, COALESCE($2, true))
|
|
||||||
RETURNING id, name, is_active, created_at, display_order
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateCourseCategoryParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Column2 interface{} `json:"column_2"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCategoryParams) (CourseCategory, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateCourseCategory, arg.Name, arg.Column2)
|
|
||||||
var i CourseCategory
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteCourseCategory = `-- name: DeleteCourseCategory :exec
|
|
||||||
DELETE FROM course_categories
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourseCategory(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, DeleteCourseCategory, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetAllCourseCategories = `-- name: GetAllCourseCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
FROM course_categories
|
|
||||||
ORDER BY display_order ASC, created_at DESC
|
|
||||||
LIMIT $2::INT
|
|
||||||
OFFSET $1::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetAllCourseCategoriesParams struct {
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAllCourseCategoriesRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCategoriesParams) ([]GetAllCourseCategoriesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetAllCourseCategories, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetAllCourseCategoriesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetAllCourseCategoriesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one
|
|
||||||
SELECT id, name, is_active, created_at, display_order
|
|
||||||
FROM course_categories
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCategory, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetCourseCategoryByID, id)
|
|
||||||
var i CourseCategory
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReorderCourseCategories = `-- name: ReorderCourseCategories :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE course_categories.id = bulk.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type ReorderCourseCategoriesParams struct {
|
|
||||||
Ids []int64 `json:"ids"`
|
|
||||||
Positions []int32 `json:"positions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ReorderCourseCategories(ctx context.Context, arg ReorderCourseCategoriesParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, ReorderCourseCategories, arg.Ids, arg.Positions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET
|
|
||||||
name = COALESCE($1, name),
|
|
||||||
is_active = COALESCE($2, is_active)
|
|
||||||
WHERE id = $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateCourseCategoryParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateCourseCategory(ctx context.Context, arg UpdateCourseCategoryParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateCourseCategory, arg.Name, arg.IsActive, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: courses.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CreateCourse = `-- name: CreateCourse :one
|
|
||||||
INSERT INTO courses (
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
|
||||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateCourseParams struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
Column6 interface{} `json:"column_6"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateCourse,
|
|
||||||
arg.CategoryID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.Column6,
|
|
||||||
)
|
|
||||||
var i Course
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteCourse = `-- name: DeleteCourse :exec
|
|
||||||
DELETE FROM courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, DeleteCourse, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
|
||||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
|
||||||
FROM courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
|
||||||
var i Course
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCoursesByCategory = `-- name: GetCoursesByCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
FROM courses
|
|
||||||
WHERE category_id = $1
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
LIMIT $3::INT
|
|
||||||
OFFSET $2::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetCoursesByCategoryParams struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetCoursesByCategoryRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCategoryParams) ([]GetCoursesByCategoryRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetCoursesByCategory, arg.CategoryID, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetCoursesByCategoryRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetCoursesByCategoryRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE courses.id = bulk.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type ReorderCoursesParams struct {
|
|
||||||
Ids []int64 `json:"ids"`
|
|
||||||
Positions []int32 `json:"positions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ReorderCourses(ctx context.Context, arg ReorderCoursesParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, ReorderCourses, arg.Ids, arg.Positions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateCourse = `-- name: UpdateCourse :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET
|
|
||||||
title = COALESCE($1, title),
|
|
||||||
description = COALESCE($2, description),
|
|
||||||
thumbnail = COALESCE($3, thumbnail),
|
|
||||||
intro_video_url = COALESCE($4, intro_video_url),
|
|
||||||
is_active = COALESCE($5, is_active)
|
|
||||||
WHERE id = $6
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateCourseParams struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateCourse,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.IsActive,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,766 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: hierarchy.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const AttachQuestionSetLessonToSubModule = `-- name: AttachQuestionSetLessonToSubModule :one
|
|
||||||
INSERT INTO sub_module_lessons (
|
|
||||||
sub_module_id,
|
|
||||||
question_set_id,
|
|
||||||
intro_video_url,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING id, sub_module_id, question_set_id, intro_video_url, display_order, is_active, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type AttachQuestionSetLessonToSubModuleParams struct {
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
Column5 interface{} `json:"column_5"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) AttachQuestionSetLessonToSubModule(ctx context.Context, arg AttachQuestionSetLessonToSubModuleParams) (SubModuleLesson, error) {
|
|
||||||
row := q.db.QueryRow(ctx, AttachQuestionSetLessonToSubModule,
|
|
||||||
arg.SubModuleID,
|
|
||||||
arg.QuestionSetID,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.Column4,
|
|
||||||
arg.Column5,
|
|
||||||
)
|
|
||||||
var i SubModuleLesson
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateCourseSubCategory = `-- name: CreateCourseSubCategory :one
|
|
||||||
INSERT INTO course_sub_categories (
|
|
||||||
category_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING id, category_id, name, description, is_active, display_order, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateCourseSubCategoryParams struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
Column5 interface{} `json:"column_5"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourseSubCategory(ctx context.Context, arg CreateCourseSubCategoryParams) (CourseSubCategory, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateCourseSubCategory,
|
|
||||||
arg.CategoryID,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Column4,
|
|
||||||
arg.Column5,
|
|
||||||
)
|
|
||||||
var i CourseSubCategory
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateLevel = `-- name: CreateLevel :one
|
|
||||||
INSERT INTO levels (
|
|
||||||
course_id,
|
|
||||||
cefr_level,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, TRUE))
|
|
||||||
RETURNING id, course_id, cefr_level, display_order, is_active, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateLevelParams struct {
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
CefrLevel string `json:"cefr_level"`
|
|
||||||
Column3 interface{} `json:"column_3"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateLevel,
|
|
||||||
arg.CourseID,
|
|
||||||
arg.CefrLevel,
|
|
||||||
arg.Column3,
|
|
||||||
arg.Column4,
|
|
||||||
)
|
|
||||||
var i Level
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CourseID,
|
|
||||||
&i.CefrLevel,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateModule = `-- name: CreateModule :one
|
|
||||||
INSERT INTO modules (
|
|
||||||
level_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING id, level_id, title, description, display_order, is_active, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateModuleParams struct {
|
|
||||||
LevelID int64 `json:"level_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
Column5 interface{} `json:"column_5"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateModule,
|
|
||||||
arg.LevelID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Column4,
|
|
||||||
arg.Column5,
|
|
||||||
)
|
|
||||||
var i Module
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.LevelID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateSubModule = `-- name: CreateSubModule :one
|
|
||||||
INSERT INTO sub_modules (
|
|
||||||
module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateSubModuleParams struct {
|
|
||||||
ModuleID int64 `json:"module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Column4 interface{} `json:"column_4"`
|
|
||||||
Column5 interface{} `json:"column_5"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateSubModule(ctx context.Context, arg CreateSubModuleParams) (SubModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateSubModule,
|
|
||||||
arg.ModuleID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Column4,
|
|
||||||
arg.Column5,
|
|
||||||
)
|
|
||||||
var i SubModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LegacySubCourseID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateSubModulePractice = `-- name: CreateSubModulePractice :one
|
|
||||||
INSERT INTO sub_module_practices (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
question_set_id,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
|
||||||
RETURNING id, sub_module_id, title, description, thumbnail, intro_video_url, question_set_id, display_order, is_active, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateSubModulePracticeParams struct {
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
Column7 interface{} `json:"column_7"`
|
|
||||||
Column8 interface{} `json:"column_8"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateSubModulePractice(ctx context.Context, arg CreateSubModulePracticeParams) (SubModulePractice, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateSubModulePractice,
|
|
||||||
arg.SubModuleID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.QuestionSetID,
|
|
||||||
arg.Column7,
|
|
||||||
arg.Column8,
|
|
||||||
)
|
|
||||||
var i SubModulePractice
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateSubModuleVideo = `-- name: CreateSubModuleVideo :one
|
|
||||||
INSERT INTO sub_module_videos (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
video_url,
|
|
||||||
duration,
|
|
||||||
resolution,
|
|
||||||
is_published,
|
|
||||||
publish_date,
|
|
||||||
visibility,
|
|
||||||
instructor_id,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
status,
|
|
||||||
vimeo_id,
|
|
||||||
vimeo_embed_url,
|
|
||||||
vimeo_player_html,
|
|
||||||
vimeo_status,
|
|
||||||
video_host_provider
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, $2, $3, $4, $5, $6,
|
|
||||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
|
||||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
|
||||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
|
||||||
)
|
|
||||||
RETURNING id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateSubModuleVideoParams struct {
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration pgtype.Int4 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
Column7 interface{} `json:"column_7"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Column12 interface{} `json:"column_12"`
|
|
||||||
Column13 interface{} `json:"column_13"`
|
|
||||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
|
||||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
|
||||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
|
||||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
|
||||||
Column18 interface{} `json:"column_18"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateSubModuleVideo(ctx context.Context, arg CreateSubModuleVideoParams) (SubModuleVideo, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateSubModuleVideo,
|
|
||||||
arg.SubModuleID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.VideoUrl,
|
|
||||||
arg.Duration,
|
|
||||||
arg.Resolution,
|
|
||||||
arg.Column7,
|
|
||||||
arg.PublishDate,
|
|
||||||
arg.Visibility,
|
|
||||||
arg.InstructorID,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.Column12,
|
|
||||||
arg.Column13,
|
|
||||||
arg.VimeoID,
|
|
||||||
arg.VimeoEmbedUrl,
|
|
||||||
arg.VimeoPlayerHtml,
|
|
||||||
arg.VimeoStatus,
|
|
||||||
arg.Column18,
|
|
||||||
)
|
|
||||||
var i SubModuleVideo
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Duration,
|
|
||||||
&i.Resolution,
|
|
||||||
&i.IsPublished,
|
|
||||||
&i.PublishDate,
|
|
||||||
&i.Visibility,
|
|
||||||
&i.InstructorID,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.Status,
|
|
||||||
&i.VimeoID,
|
|
||||||
&i.VimeoEmbedUrl,
|
|
||||||
&i.VimeoPlayerHtml,
|
|
||||||
&i.VimeoStatus,
|
|
||||||
&i.VideoHostProvider,
|
|
||||||
&i.CreatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCoursesWithHierarchy = `-- name: GetCoursesWithHierarchy :many
|
|
||||||
SELECT
|
|
||||||
cc.id AS category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.id AS sub_category_id,
|
|
||||||
csc.name AS sub_category_name,
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title
|
|
||||||
FROM course_categories cc
|
|
||||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
|
||||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
|
||||||
WHERE cc.is_active = TRUE
|
|
||||||
ORDER BY cc.id, csc.display_order, csc.id, c.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetCoursesWithHierarchyRow struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
SubCategoryName pgtype.Text `json:"sub_category_name"`
|
|
||||||
CourseID pgtype.Int8 `json:"course_id"`
|
|
||||||
CourseTitle pgtype.Text `json:"course_title"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetCoursesWithHierarchy(ctx context.Context) ([]GetCoursesWithHierarchyRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetCoursesWithHierarchy)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetCoursesWithHierarchyRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetCoursesWithHierarchyRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.CategoryName,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
&i.SubCategoryName,
|
|
||||||
&i.CourseID,
|
|
||||||
&i.CourseTitle,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetFullHierarchyByCourseID = `-- name: GetFullHierarchyByCourseID :many
|
|
||||||
SELECT
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title,
|
|
||||||
l.id AS level_id,
|
|
||||||
l.cefr_level,
|
|
||||||
m.id AS module_id,
|
|
||||||
m.title AS module_title,
|
|
||||||
sm.id AS sub_module_id,
|
|
||||||
sm.title AS sub_module_title
|
|
||||||
FROM courses c
|
|
||||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
|
||||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
|
||||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
|
||||||
WHERE c.id = $1
|
|
||||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetFullHierarchyByCourseIDRow struct {
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
CourseTitle string `json:"course_title"`
|
|
||||||
LevelID pgtype.Int8 `json:"level_id"`
|
|
||||||
CefrLevel pgtype.Text `json:"cefr_level"`
|
|
||||||
ModuleID pgtype.Int8 `json:"module_id"`
|
|
||||||
ModuleTitle pgtype.Text `json:"module_title"`
|
|
||||||
SubModuleID pgtype.Int8 `json:"sub_module_id"`
|
|
||||||
SubModuleTitle pgtype.Text `json:"sub_module_title"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetFullHierarchyByCourseID(ctx context.Context, id int64) ([]GetFullHierarchyByCourseIDRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetFullHierarchyByCourseID, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetFullHierarchyByCourseIDRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetFullHierarchyByCourseIDRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.CourseID,
|
|
||||||
&i.CourseTitle,
|
|
||||||
&i.LevelID,
|
|
||||||
&i.CefrLevel,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.ModuleTitle,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.SubModuleTitle,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetLevelsByCourseID = `-- name: GetLevelsByCourseID :many
|
|
||||||
SELECT id, course_id, cefr_level, display_order, is_active, created_at
|
|
||||||
FROM levels
|
|
||||||
WHERE course_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetLevelsByCourseID(ctx context.Context, courseID int64) ([]Level, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetLevelsByCourseID, courseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Level
|
|
||||||
for rows.Next() {
|
|
||||||
var i Level
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CourseID,
|
|
||||||
&i.CefrLevel,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetModulesByLevelID = `-- name: GetModulesByLevelID :many
|
|
||||||
SELECT id, level_id, title, description, display_order, is_active, created_at
|
|
||||||
FROM modules
|
|
||||||
WHERE level_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetModulesByLevelID(ctx context.Context, levelID int64) ([]Module, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetModulesByLevelID, levelID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Module
|
|
||||||
for rows.Next() {
|
|
||||||
var i Module
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.LevelID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetSubModuleLessons = `-- name: GetSubModuleLessons :many
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
qs.title,
|
|
||||||
qs.description,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_lessons smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.sub_module_id = $1
|
|
||||||
AND smp.is_active = TRUE
|
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetSubModuleLessonsRow struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
SetType string `json:"set_type"`
|
|
||||||
QuestionCount int64 `json:"question_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModuleLessons(ctx context.Context, subModuleID int64) ([]GetSubModuleLessonsRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetSubModuleLessons, subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetSubModuleLessonsRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetSubModuleLessonsRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Status,
|
|
||||||
&i.SetType,
|
|
||||||
&i.QuestionCount,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetSubModulePractices = `-- name: GetSubModulePractices :many
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.title,
|
|
||||||
smp.description,
|
|
||||||
smp.thumbnail,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_practices smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.sub_module_id = $1
|
|
||||||
AND smp.is_active = TRUE
|
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetSubModulePracticesRow struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
SetType string `json:"set_type"`
|
|
||||||
QuestionCount int64 `json:"question_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModulePractices(ctx context.Context, subModuleID int64) ([]GetSubModulePracticesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetSubModulePractices, subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetSubModulePracticesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetSubModulePracticesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Status,
|
|
||||||
&i.SetType,
|
|
||||||
&i.QuestionCount,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetSubModuleVideos = `-- name: GetSubModuleVideos :many
|
|
||||||
SELECT id, sub_module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider, created_at
|
|
||||||
FROM sub_module_videos
|
|
||||||
WHERE sub_module_id = $1
|
|
||||||
AND status != 'ARCHIVED'
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModuleVideos(ctx context.Context, subModuleID int64) ([]SubModuleVideo, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetSubModuleVideos, subModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []SubModuleVideo
|
|
||||||
for rows.Next() {
|
|
||||||
var i SubModuleVideo
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.SubModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Duration,
|
|
||||||
&i.Resolution,
|
|
||||||
&i.IsPublished,
|
|
||||||
&i.PublishDate,
|
|
||||||
&i.Visibility,
|
|
||||||
&i.InstructorID,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.Status,
|
|
||||||
&i.VimeoID,
|
|
||||||
&i.VimeoEmbedUrl,
|
|
||||||
&i.VimeoPlayerHtml,
|
|
||||||
&i.VimeoStatus,
|
|
||||||
&i.VideoHostProvider,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetSubModulesByModuleID = `-- name: GetSubModulesByModuleID :many
|
|
||||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE module_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModulesByModuleID(ctx context.Context, moduleID int64) ([]SubModule, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetSubModulesByModuleID, moduleID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []SubModule
|
|
||||||
for rows.Next() {
|
|
||||||
var i SubModule
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LegacySubCourseID,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
233
gen/db/lms_courses.sql.go
Normal file
233
gen/db/lms_courses.sql.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_courses.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateCourse = `-- name: CreateCourse :one
|
||||||
|
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(c.sort_order)
|
||||||
|
FROM courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateCourseParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateCourse,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteCourse = `-- name: DeleteCourse :exec
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteCourse, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetCourseByID = `-- name: GetCourseByID :one
|
||||||
|
SELECT id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
FROM courses
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListCourseIDsByProgram = `-- name: ListCourseIDsByProgram :many
|
||||||
|
SELECT
|
||||||
|
c.id
|
||||||
|
FROM
|
||||||
|
courses AS c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListCourseIDsByProgram, programID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListCoursesByProgramID = `-- name: ListCoursesByProgramID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.program_id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.sort_order,
|
||||||
|
c.created_at,
|
||||||
|
c.updated_at
|
||||||
|
FROM
|
||||||
|
courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.sort_order ASC,
|
||||||
|
c.id ASC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListCoursesByProgramIDParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCoursesByProgramIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListCoursesByProgramID, arg.ProgramID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListCoursesByProgramIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListCoursesByProgramIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.SortOrder,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateCourse = `-- name: UpdateCourse :one
|
||||||
|
UPDATE courses
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::varchar, name),
|
||||||
|
description = COALESCE($2::text, description),
|
||||||
|
thumbnail = COALESCE($3::text, thumbnail),
|
||||||
|
sort_order = coalesce($4::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = $5
|
||||||
|
RETURNING
|
||||||
|
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateCourseParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateCourse,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
215
gen/db/lms_lessons.sql.go
Normal file
215
gen/db/lms_lessons.sql.go
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_lessons.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateLesson = `-- name: CreateLesson :one
|
||||||
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(l.sort_order)
|
||||||
|
FROM lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateLessonParams struct {
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
VideoUrl pgtype.Text `json:"video_url"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateLesson,
|
||||||
|
arg.ModuleID,
|
||||||
|
arg.Title,
|
||||||
|
arg.VideoUrl,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.Description,
|
||||||
|
)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteLesson = `-- name: DeleteLesson :exec
|
||||||
|
DELETE FROM lessons
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteLesson, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetLessonByID = `-- name: GetLessonByID :one
|
||||||
|
SELECT id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
FROM lessons
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLessonByID, id)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLessonsByModuleID = `-- name: ListLessonsByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.id,
|
||||||
|
l.module_id,
|
||||||
|
l.title,
|
||||||
|
l.video_url,
|
||||||
|
l.thumbnail,
|
||||||
|
l.description,
|
||||||
|
l.sort_order,
|
||||||
|
l.created_at,
|
||||||
|
l.updated_at
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
ORDER BY
|
||||||
|
l.sort_order ASC,
|
||||||
|
l.id ASC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLessonsByModuleIDParams struct {
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLessonsByModuleIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
VideoUrl pgtype.Text `json:"video_url"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLessonsByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLessonsByModuleIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLessonsByModuleIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.SortOrder,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateLesson = `-- name: UpdateLesson :one
|
||||||
|
UPDATE lessons
|
||||||
|
SET
|
||||||
|
title = COALESCE($1::varchar, title),
|
||||||
|
video_url = COALESCE($2::text, video_url),
|
||||||
|
thumbnail = COALESCE($3::text, thumbnail),
|
||||||
|
description = COALESCE($4::text, description),
|
||||||
|
sort_order = coalesce($5::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = $6
|
||||||
|
RETURNING
|
||||||
|
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateLessonParams struct {
|
||||||
|
Title pgtype.Text `json:"title"`
|
||||||
|
VideoUrl pgtype.Text `json:"video_url"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateLesson,
|
||||||
|
arg.Title,
|
||||||
|
arg.VideoUrl,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.Description,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
250
gen/db/lms_modules.sql.go
Normal file
250
gen/db/lms_modules.sql.go
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_modules.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateModule = `-- name: CreateModule :one
|
||||||
|
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(m.sort_order)
|
||||||
|
FROM modules m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $2), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateModuleParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Icon pgtype.Text `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateModule,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Icon,
|
||||||
|
)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteModule = `-- name: DeleteModule :exec
|
||||||
|
DELETE FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteModule, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetModuleByID = `-- name: GetModuleByID :one
|
||||||
|
SELECT id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListModuleIDsByCourse = `-- name: ListModuleIDsByCourse :many
|
||||||
|
SELECT
|
||||||
|
m.id
|
||||||
|
FROM
|
||||||
|
modules AS m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
ORDER BY
|
||||||
|
m.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListModuleIDsByCourse, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListModulesByProgramAndCourse = `-- name: ListModulesByProgramAndCourse :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.id,
|
||||||
|
m.program_id,
|
||||||
|
m.course_id,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.icon,
|
||||||
|
m.sort_order,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM
|
||||||
|
modules m
|
||||||
|
WHERE
|
||||||
|
m.program_id = $1
|
||||||
|
AND m.course_id = $2
|
||||||
|
ORDER BY
|
||||||
|
m.sort_order ASC,
|
||||||
|
m.id ASC
|
||||||
|
LIMIT $3
|
||||||
|
OFFSET $4
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListModulesByProgramAndCourseParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListModulesByProgramAndCourseRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Icon pgtype.Text `json:"icon"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListModulesByProgramAndCourse,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.Limit,
|
||||||
|
arg.Offset,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListModulesByProgramAndCourseRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListModulesByProgramAndCourseRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.SortOrder,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateModule = `-- name: UpdateModule :one
|
||||||
|
UPDATE modules
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::varchar, name),
|
||||||
|
description = COALESCE($2::text, description),
|
||||||
|
icon = COALESCE($3::text, icon),
|
||||||
|
sort_order = coalesce($4::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = $5
|
||||||
|
RETURNING
|
||||||
|
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateModuleParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Icon pgtype.Text `json:"icon"`
|
||||||
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateModule,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Icon,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
381
gen/db/lms_practices.sql.go
Normal file
381
gen/db/lms_practices.sql.go
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_practices.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateLmsPractice = `-- name: CreateLmsPractice :one
|
||||||
|
INSERT INTO lms_practices (
|
||||||
|
course_id, module_id, lesson_id,
|
||||||
|
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateLmsPracticeParams struct {
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticeParams) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateLmsPractice,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.ModuleID,
|
||||||
|
arg.LessonID,
|
||||||
|
arg.Title,
|
||||||
|
arg.StoryDescription,
|
||||||
|
arg.StoryImage,
|
||||||
|
arg.PersonaID,
|
||||||
|
arg.QuestionSetID,
|
||||||
|
arg.QuickTips,
|
||||||
|
)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteLmsPractice = `-- name: DeleteLmsPractice :exec
|
||||||
|
DELETE FROM lms_practices
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteLmsPractice(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteLmsPractice, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetLmsPracticeByID = `-- name: GetLmsPracticeByID :one
|
||||||
|
SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
FROM lms_practices
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLmsPracticeByID(ctx context.Context, id int64) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLmsPracticeByID, id)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLmsPracticesByCourseID = `-- name: ListLmsPracticesByCourseID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.course_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByCourseIDParams struct {
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByCourseIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPracticesByCourseIDParams) ([]ListLmsPracticesByCourseIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByCourseID, arg.CourseID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByCourseIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByCourseIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLmsPracticesByLessonID = `-- name: ListLmsPracticesByLessonID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.lesson_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByLessonIDParams struct {
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByLessonIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPracticesByLessonIDParams) ([]ListLmsPracticesByLessonIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByLessonID, arg.LessonID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByLessonIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByLessonIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLmsPracticesByModuleID = `-- name: ListLmsPracticesByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.lesson_id,
|
||||||
|
p.title,
|
||||||
|
p.story_description,
|
||||||
|
p.story_image,
|
||||||
|
p.persona_id,
|
||||||
|
p.question_set_id,
|
||||||
|
p.quick_tips,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM lms_practices p
|
||||||
|
WHERE p.module_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByModuleIDParams struct {
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByModuleIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPracticesByModuleIDParams) ([]ListLmsPracticesByModuleIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByModuleIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByModuleIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateLmsPractice = `-- name: UpdateLmsPractice :one
|
||||||
|
UPDATE lms_practices
|
||||||
|
SET
|
||||||
|
title = COALESCE($1::varchar, title),
|
||||||
|
story_description = COALESCE($2::text, story_description),
|
||||||
|
story_image = COALESCE($3::text, story_image),
|
||||||
|
persona_id = COALESCE($4::bigint, persona_id),
|
||||||
|
question_set_id = COALESCE($5::bigint, question_set_id),
|
||||||
|
quick_tips = COALESCE($6::text, quick_tips),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $7
|
||||||
|
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateLmsPracticeParams struct {
|
||||||
|
Title pgtype.Text `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID pgtype.Int8 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticeParams) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateLmsPractice,
|
||||||
|
arg.Title,
|
||||||
|
arg.StoryDescription,
|
||||||
|
arg.StoryImage,
|
||||||
|
arg.PersonaID,
|
||||||
|
arg.QuestionSetID,
|
||||||
|
arg.QuickTips,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
613
gen/db/lms_progress.sql.go
Normal file
613
gen/db/lms_progress.sql.go
Normal file
|
|
@ -0,0 +1,613 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_progress.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CountCoursesInProgram = `-- name: CountCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
program_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountCoursesInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountCoursesInProgram, programID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInCourse = `-- name: CountLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Lesson-based progress within a course (all modules).
|
||||||
|
func (q *Queries) CountLessonsInCourse(ctx context.Context, courseID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInCourse, courseID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInModule = `-- name: CountLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons
|
||||||
|
WHERE
|
||||||
|
module_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountLessonsInModule(ctx context.Context, moduleID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInModule, moduleID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInProgram = `-- name: CountLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Lesson-based progress within a program (all courses).
|
||||||
|
func (q *Queries) CountLessonsInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInProgram, programID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountModulesInCourse = `-- name: CountModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
modules
|
||||||
|
WHERE
|
||||||
|
course_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountModulesInCourse, courseID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress ucp
|
||||||
|
INNER JOIN courses c ON c.id = ucp.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ucp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedCoursesInProgramParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedCoursesInProgram(ctx context.Context, arg CountUserCompletedCoursesInProgramParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedCoursesInProgram, arg.ProgramID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInCourse = `-- name: CountUserCompletedLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInCourseParams struct {
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInCourse(ctx context.Context, arg CountUserCompletedLessonsInCourseParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInCourse, arg.CourseID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInModule = `-- name: CountUserCompletedLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInModuleParams struct {
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInModule(ctx context.Context, arg CountUserCompletedLessonsInModuleParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInModule, arg.ModuleID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInProgram = `-- name: CountUserCompletedLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInProgramParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInProgram(ctx context.Context, arg CountUserCompletedLessonsInProgramParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInProgram, arg.ProgramID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedModulesInCourse = `-- name: CountUserCompletedModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress ump
|
||||||
|
INNER JOIN modules m ON m.id = ump.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ump.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedModulesInCourseParams struct {
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg CountUserCompletedModulesInCourseParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedModulesInCourse, arg.CourseID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
|
||||||
|
SELECT
|
||||||
|
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
|
||||||
|
FROM
|
||||||
|
courses AS c1
|
||||||
|
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
|
||||||
|
AND c2.sort_order = c1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
c1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousCourseInProgram, id)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousLessonInModule = `-- name: GetPreviousLessonInModule :one
|
||||||
|
SELECT
|
||||||
|
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order
|
||||||
|
FROM
|
||||||
|
lessons AS l1
|
||||||
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
|
AND l2.sort_order = l1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
l1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousLessonInModule, id)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousModuleInCourse = `-- name: GetPreviousModuleInCourse :one
|
||||||
|
SELECT
|
||||||
|
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order
|
||||||
|
FROM
|
||||||
|
modules AS m1
|
||||||
|
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
|
||||||
|
AND m2.sort_order = m1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
m1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousModuleInCourse(ctx context.Context, id int64) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousModuleInCourse, id)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousProgram = `-- name: GetPreviousProgram :one
|
||||||
|
SELECT
|
||||||
|
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order
|
||||||
|
FROM
|
||||||
|
programs AS p1
|
||||||
|
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
p1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousProgram(ctx context.Context, id int64) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousProgram, id)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserCourseProgress = `-- name: InsertUserCourseProgress :exec
|
||||||
|
INSERT INTO lms_user_course_progress (user_id, course_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, course_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserCourseProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserCourseProgress(ctx context.Context, arg InsertUserCourseProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserCourseProgress, arg.UserID, arg.CourseID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserLessonProgress = `-- name: InsertUserLessonProgress :exec
|
||||||
|
INSERT INTO lms_user_lesson_progress (user_id, lesson_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, lesson_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserLessonProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserLessonProgress(ctx context.Context, arg InsertUserLessonProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserLessonProgress, arg.UserID, arg.LessonID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserModuleProgress = `-- name: InsertUserModuleProgress :exec
|
||||||
|
INSERT INTO lms_user_module_progress (user_id, module_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, module_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserModuleProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserModuleProgress(ctx context.Context, arg InsertUserModuleProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserModuleProgress, arg.UserID, arg.ModuleID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserProgramProgress = `-- name: InsertUserProgramProgress :exec
|
||||||
|
INSERT INTO lms_user_program_progress (user_id, program_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, program_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserProgramProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserProgramProgress(ctx context.Context, arg InsertUserProgramProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserProgramProgress, arg.UserID, arg.ProgramID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedCourseIDsByUser = `-- name: ListLMSCompletedCourseIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ucp.course_id
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress AS ucp
|
||||||
|
WHERE
|
||||||
|
ucp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ucp.completed_at ASC,
|
||||||
|
ucp.course_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedCourseIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedCourseIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var course_id int64
|
||||||
|
if err := rows.Scan(&course_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, course_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedLessonIDsByUser = `-- name: ListLMSCompletedLessonIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ulp.lesson_id
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress AS ulp
|
||||||
|
WHERE
|
||||||
|
ulp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ulp.completed_at ASC,
|
||||||
|
ulp.lesson_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedLessonIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedLessonIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var lesson_id int64
|
||||||
|
if err := rows.Scan(&lesson_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, lesson_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedModuleIDsByUser = `-- name: ListLMSCompletedModuleIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ump.module_id
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress AS ump
|
||||||
|
WHERE
|
||||||
|
ump.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ump.completed_at ASC,
|
||||||
|
ump.module_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedModuleIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var module_id int64
|
||||||
|
if err := rows.Scan(&module_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, module_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedProgramIDsByUser = `-- name: ListLMSCompletedProgramIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
upp.program_id
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress AS upp
|
||||||
|
WHERE
|
||||||
|
upp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
upp.completed_at ASC,
|
||||||
|
upp.program_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedProgramIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedProgramIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var program_id int64
|
||||||
|
if err := rows.Scan(&program_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, program_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasCourseProgress = `-- name: UserHasCourseProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND course_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasCourseProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasCourseProgress(ctx context.Context, arg UserHasCourseProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasCourseProgress, arg.UserID, arg.CourseID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasLessonProgress = `-- name: UserHasLessonProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND lesson_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasLessonProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasLessonProgress(ctx context.Context, arg UserHasLessonProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasLessonProgress, arg.UserID, arg.LessonID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasModuleProgress = `-- name: UserHasModuleProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND module_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasModuleProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasModuleProgress(ctx context.Context, arg UserHasModuleProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasModuleProgress, arg.UserID, arg.ModuleID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasProgramProgress = `-- name: UserHasProgramProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND program_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasProgramProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasProgramProgress(ctx context.Context, arg UserHasProgramProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasProgramProgress, arg.UserID, arg.ProgramID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
265
gen/db/models.go
265
gen/db/models.go
|
|
@ -23,33 +23,14 @@ type ActivityLog struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Course struct {
|
type Course struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CategoryID int64 `json:"category_id"`
|
ProgramID int64 `json:"program_id"`
|
||||||
Title string `json:"title"`
|
Name string `json:"name"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseCategory struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseSubCategory struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
|
|
@ -69,13 +50,16 @@ type GlobalSetting struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Level struct {
|
type Lesson struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CourseID int64 `json:"course_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
CefrLevel string `json:"cefr_level"`
|
Title string `json:"title"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
VideoUrl pgtype.Text `json:"video_url"`
|
||||||
IsActive bool `json:"is_active"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
Description pgtype.Text `json:"description"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LevelToSubCourse struct {
|
type LevelToSubCourse struct {
|
||||||
|
|
@ -83,14 +67,55 @@ type LevelToSubCourse struct {
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LmsPractice struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserCourseProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserLessonProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserModuleProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserProgramProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
LevelID int64 `json:"level_id"`
|
ProgramID int64 `json:"program_id"`
|
||||||
Title string `json:"title"`
|
CourseID int64 `json:"course_id"`
|
||||||
Description pgtype.Text `json:"description"`
|
Name string `json:"name"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
Description pgtype.Text `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
Icon pgtype.Text `json:"icon"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleToSubCourse struct {
|
type ModuleToSubCourse struct {
|
||||||
|
|
@ -154,6 +179,16 @@ type Permission struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Program struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
type Question struct {
|
type Question struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
|
|
@ -201,7 +236,6 @@ type QuestionSet struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
@ -298,109 +332,6 @@ type ScheduledNotification struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCoursePrerequisite struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration int32 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
IsPublished bool `json:"is_published"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
// Vimeo video ID for videos hosted on Vimeo
|
|
||||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
|
||||||
// Vimeo player embed URL
|
|
||||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
|
||||||
// Vimeo iframe embed HTML code
|
|
||||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
|
||||||
// Vimeo video status: pending, uploading, transcoding, available, error
|
|
||||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
|
||||||
// Video hosting provider: DIRECT or VIMEO
|
|
||||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModule struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ModuleID int64 `json:"module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModuleLesson struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModulePractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModuleVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration pgtype.Int4 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
IsPublished bool `json:"is_published"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
|
||||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
|
||||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
|
||||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
|
||||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionPlan struct {
|
type SubscriptionPlan struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
@ -440,6 +371,15 @@ type TeamMember struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TeamRefreshToken struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
TeamMemberID int64 `json:"team_member_id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
|
Revoked bool `json:"revoked"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
FirstName pgtype.Text `json:"first_name"`
|
FirstName pgtype.Text `json:"first_name"`
|
||||||
|
|
@ -489,35 +429,12 @@ type UserAudioResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserPracticeProgress struct {
|
type UserPracticeProgress struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSubCourseProgress struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ProgressPercentage int16 `json:"progress_percentage"`
|
|
||||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
|
||||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSubCourseVideoProgress struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
VideoID int64 `json:"video_id"`
|
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSubscription struct {
|
type UserSubscription struct {
|
||||||
|
|
|
||||||
|
|
@ -59,26 +59,16 @@ func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg Ge
|
||||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
||||||
INSERT INTO user_practice_progress (
|
INSERT INTO user_practice_progress (
|
||||||
user_id,
|
user_id,
|
||||||
sub_course_id,
|
|
||||||
question_set_id,
|
question_set_id,
|
||||||
completed_at,
|
completed_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
SELECT
|
VALUES (
|
||||||
$1::BIGINT,
|
$1::BIGINT,
|
||||||
CASE
|
$2::BIGINT,
|
||||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
|
||||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
|
||||||
ELSE NULL
|
|
||||||
END,
|
|
||||||
qs.id,
|
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
FROM question_sets qs
|
)
|
||||||
LEFT JOIN sub_modules sm
|
|
||||||
ON qs.owner_type = 'SUB_MODULE'
|
|
||||||
AND qs.owner_id = sm.id
|
|
||||||
WHERE qs.id = $2::BIGINT
|
|
||||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||||
SET completed_at = EXCLUDED.completed_at,
|
SET completed_at = EXCLUDED.completed_at,
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
|
|
|
||||||
210
gen/db/programs.sql.go
Normal file
210
gen/db/programs.sql.go
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: programs.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateProgram = `-- name: CreateProgram :one
|
||||||
|
INSERT INTO programs (name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(p.sort_order)
|
||||||
|
FROM programs AS p), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateProgramParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateProgram, arg.Name, arg.Description, arg.Thumbnail)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteProgram = `-- name: DeleteProgram :exec
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteProgram(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteProgram, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetProgramByID = `-- name: GetProgramByID :one
|
||||||
|
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
FROM programs
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetProgramByID, id)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListAllProgramIDs = `-- name: ListAllProgramIDs :many
|
||||||
|
SELECT
|
||||||
|
p.id
|
||||||
|
FROM
|
||||||
|
programs AS p
|
||||||
|
ORDER BY
|
||||||
|
p.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListAllProgramIDs(ctx context.Context) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListAllProgramIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListPrograms = `-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.sort_order,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.sort_order ASC, p.id ASC
|
||||||
|
LIMIT $1 OFFSET $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListProgramsParams struct {
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProgramsRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]ListProgramsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListPrograms, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListProgramsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListProgramsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.SortOrder,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateProgram = `-- name: UpdateProgram :one
|
||||||
|
UPDATE programs
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::varchar, name),
|
||||||
|
description = COALESCE($2::text, description),
|
||||||
|
thumbnail = COALESCE($3::text, thumbnail),
|
||||||
|
sort_order = coalesce($4::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = $5
|
||||||
|
RETURNING
|
||||||
|
id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateProgramParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateProgram,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ func (q *Queries) GetQuestionSetItemsPaginated(ctx context.Context, arg GetQuest
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
||||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.display_order, qs.intro_video_url
|
||||||
FROM question_sets qs
|
FROM question_sets qs
|
||||||
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
||||||
WHERE qsi.question_id = $1
|
WHERE qsi.question_id = $1
|
||||||
|
|
@ -326,7 +326,6 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,10 @@ INSERT INTO question_sets (
|
||||||
passing_score,
|
passing_score,
|
||||||
shuffle_questions,
|
shuffle_questions,
|
||||||
status,
|
status,
|
||||||
sub_course_video_id,
|
|
||||||
intro_video_url
|
intro_video_url
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateQuestionSetParams struct {
|
type CreateQuestionSetParams struct {
|
||||||
|
|
@ -83,7 +82,6 @@ type CreateQuestionSetParams struct {
|
||||||
PassingScore pgtype.Int4 `json:"passing_score"`
|
PassingScore pgtype.Int4 `json:"passing_score"`
|
||||||
Column10 interface{} `json:"column_10"`
|
Column10 interface{} `json:"column_10"`
|
||||||
Column11 interface{} `json:"column_11"`
|
Column11 interface{} `json:"column_11"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +98,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
||||||
arg.PassingScore,
|
arg.PassingScore,
|
||||||
arg.Column10,
|
arg.Column10,
|
||||||
arg.Column11,
|
arg.Column11,
|
||||||
arg.SubCourseVideoID,
|
|
||||||
arg.IntroVideoUrl,
|
arg.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
var i QuestionSet
|
var i QuestionSet
|
||||||
|
|
@ -119,7 +116,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +133,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||||
AND status = 'PUBLISHED'
|
AND status = 'PUBLISHED'
|
||||||
|
|
@ -163,7 +159,6 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -171,7 +166,7 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE owner_type = $1
|
WHERE owner_type = $1
|
||||||
AND owner_id = $2
|
AND owner_id = $2
|
||||||
|
|
@ -208,7 +203,6 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -223,7 +217,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -246,7 +240,6 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -254,7 +247,7 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE owner_type = $1
|
WHERE owner_type = $1
|
||||||
AND owner_id = $2
|
AND owner_id = $2
|
||||||
|
|
@ -291,7 +284,6 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -308,7 +300,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
||||||
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.display_order, qs.intro_video_url
|
||||||
FROM question_sets qs
|
FROM question_sets qs
|
||||||
WHERE set_type = $1
|
WHERE set_type = $1
|
||||||
AND status != 'ARCHIVED'
|
AND status != 'ARCHIVED'
|
||||||
|
|
@ -339,7 +331,6 @@ type GetQuestionSetsByTypeRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +360,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -383,42 +373,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetSubCourseInitialAssessmentSet = `-- name: GetSubCourseInitialAssessmentSet :one
|
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
|
||||||
FROM question_sets
|
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
|
||||||
AND owner_type = 'SUB_COURSE'
|
|
||||||
AND owner_id = $1
|
|
||||||
AND status = 'PUBLISHED'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSubCourseInitialAssessmentSet(ctx context.Context, ownerID pgtype.Int8) (QuestionSet, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetSubCourseInitialAssessmentSet, ownerID)
|
|
||||||
var i QuestionSet
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.SetType,
|
|
||||||
&i.OwnerType,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.BannerImage,
|
|
||||||
&i.Persona,
|
|
||||||
&i.TimeLimitMinutes,
|
|
||||||
&i.PassingScore,
|
|
||||||
&i.ShuffleQuestions,
|
|
||||||
&i.Status,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetUserPersonasByQuestionSetID = `-- name: GetUserPersonasByQuestionSetID :many
|
const GetUserPersonasByQuestionSetID = `-- name: GetUserPersonasByQuestionSetID :many
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
|
|
@ -519,9 +473,8 @@ SET
|
||||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||||
status = COALESCE($8, status),
|
status = COALESCE($8, status),
|
||||||
intro_video_url = COALESCE($9, intro_video_url),
|
intro_video_url = COALESCE($9, intro_video_url),
|
||||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11
|
WHERE id = $10
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateQuestionSetParams struct {
|
type UpdateQuestionSetParams struct {
|
||||||
|
|
@ -534,7 +487,6 @@ type UpdateQuestionSetParams struct {
|
||||||
ShuffleQuestions bool `json:"shuffle_questions"`
|
ShuffleQuestions bool `json:"shuffle_questions"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,26 +501,7 @@ func (q *Queries) UpdateQuestionSet(ctx context.Context, arg UpdateQuestionSetPa
|
||||||
arg.ShuffleQuestions,
|
arg.ShuffleQuestions,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.IntroVideoUrl,
|
arg.IntroVideoUrl,
|
||||||
arg.SubCourseVideoID,
|
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateQuestionSetVideoLink = `-- name: UpdateQuestionSetVideoLink :exec
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
sub_course_video_id = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateQuestionSetVideoLinkParams struct {
|
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateQuestionSetVideoLink(ctx context.Context, arg UpdateQuestionSetVideoLinkParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateQuestionSetVideoLink, arg.SubCourseVideoID, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
||||||
79
gen/db/team_refresh_tokens.sql.go
Normal file
79
gen/db/team_refresh_tokens.sql.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: team_refresh_tokens.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateTeamRefreshToken = `-- name: CreateTeamRefreshToken :exec
|
||||||
|
INSERT INTO team_refresh_tokens (team_member_id, token, expires_at, revoked, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTeamRefreshTokenParams struct {
|
||||||
|
TeamMemberID int64 `json:"team_member_id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||||
|
Revoked bool `json:"revoked"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTeamRefreshToken(ctx context.Context, arg CreateTeamRefreshTokenParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, CreateTeamRefreshToken,
|
||||||
|
arg.TeamMemberID,
|
||||||
|
arg.Token,
|
||||||
|
arg.ExpiresAt,
|
||||||
|
arg.Revoked,
|
||||||
|
arg.CreatedAt,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetTeamRefreshTokenByToken = `-- name: GetTeamRefreshTokenByToken :one
|
||||||
|
SELECT id, team_member_id, token, expires_at, revoked, created_at
|
||||||
|
FROM team_refresh_tokens
|
||||||
|
WHERE token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetTeamRefreshTokenByToken(ctx context.Context, token string) (TeamRefreshToken, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTeamRefreshTokenByToken, token)
|
||||||
|
var i TeamRefreshToken
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.TeamMemberID,
|
||||||
|
&i.Token,
|
||||||
|
&i.ExpiresAt,
|
||||||
|
&i.Revoked,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const RevokeAllActiveTeamRefreshTokensForMember = `-- name: RevokeAllActiveTeamRefreshTokensForMember :exec
|
||||||
|
UPDATE team_refresh_tokens
|
||||||
|
SET revoked = TRUE
|
||||||
|
WHERE team_member_id = $1
|
||||||
|
AND revoked = FALSE
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) RevokeAllActiveTeamRefreshTokensForMember(ctx context.Context, teamMemberID int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, RevokeAllActiveTeamRefreshTokensForMember, teamMemberID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const RevokeTeamRefreshTokenByToken = `-- name: RevokeTeamRefreshTokenByToken :exec
|
||||||
|
UPDATE team_refresh_tokens
|
||||||
|
SET revoked = TRUE
|
||||||
|
WHERE token = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error {
|
||||||
|
_, err := q.db.Exec(ctx, RevokeTeamRefreshTokenByToken, token)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -140,6 +140,9 @@ type Config struct {
|
||||||
AccountDeletionPurgeEnabled bool
|
AccountDeletionPurgeEnabled bool
|
||||||
AccountDeletionPurgeInterval time.Duration
|
AccountDeletionPurgeInterval time.Duration
|
||||||
AccountDeletionPurgeBatchSize int32
|
AccountDeletionPurgeBatchSize int32
|
||||||
|
InactiveSubModuleContentPurgeEnabled bool
|
||||||
|
InactiveSubModuleContentPurgeInterval time.Duration
|
||||||
|
InactiveSubModuleContentRetentionDays int
|
||||||
DBResetReseedEnabled bool
|
DBResetReseedEnabled bool
|
||||||
DBResetReseedToken string
|
DBResetReseedToken string
|
||||||
DBSeedDir string
|
DBSeedDir string
|
||||||
|
|
@ -565,6 +568,38 @@ func (c *Config) loadEnv() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hard-delete inactive submodule lessons / practices / capstones after a retention period
|
||||||
|
inactiveContentPurge := strings.TrimSpace(os.Getenv("INACTIVE_SUBMODULE_CONTENT_PURGE_ENABLED"))
|
||||||
|
if inactiveContentPurge == "" {
|
||||||
|
c.InactiveSubModuleContentPurgeEnabled = false
|
||||||
|
} else {
|
||||||
|
c.InactiveSubModuleContentPurgeEnabled = inactiveContentPurge == "true" || inactiveContentPurge == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
inactiveContentPurgeInterval := strings.TrimSpace(os.Getenv("INACTIVE_SUBMODULE_CONTENT_PURGE_INTERVAL"))
|
||||||
|
if inactiveContentPurgeInterval == "" {
|
||||||
|
c.InactiveSubModuleContentPurgeInterval = 24 * time.Hour
|
||||||
|
} else {
|
||||||
|
interval, err := time.ParseDuration(inactiveContentPurgeInterval)
|
||||||
|
if err != nil || interval <= 0 {
|
||||||
|
c.InactiveSubModuleContentPurgeInterval = 24 * time.Hour
|
||||||
|
} else {
|
||||||
|
c.InactiveSubModuleContentPurgeInterval = interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
retentionDaysStr := strings.TrimSpace(os.Getenv("INACTIVE_SUBMODULE_CONTENT_RETENTION_DAYS"))
|
||||||
|
if retentionDaysStr == "" {
|
||||||
|
c.InactiveSubModuleContentRetentionDays = 30
|
||||||
|
} else {
|
||||||
|
days, err := strconv.Atoi(retentionDaysStr)
|
||||||
|
if err != nil || days < 1 {
|
||||||
|
c.InactiveSubModuleContentRetentionDays = 30
|
||||||
|
} else {
|
||||||
|
c.InactiveSubModuleContentRetentionDays = days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dangerous DB reset+reseed endpoint configuration
|
// Dangerous DB reset+reseed endpoint configuration
|
||||||
// Enabled by default and does not require .env variables.
|
// Enabled by default and does not require .env variables.
|
||||||
// Optional token can still be set programmatically if needed.
|
// Optional token can still be set programmatically if needed.
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,18 @@ const (
|
||||||
ActionIssueCreated ActivityAction = "ISSUE_CREATED"
|
ActionIssueCreated ActivityAction = "ISSUE_CREATED"
|
||||||
ActionIssueStatusUpdated ActivityAction = "ISSUE_STATUS_UPDATED"
|
ActionIssueStatusUpdated ActivityAction = "ISSUE_STATUS_UPDATED"
|
||||||
ActionIssueDeleted ActivityAction = "ISSUE_DELETED"
|
ActionIssueDeleted ActivityAction = "ISSUE_DELETED"
|
||||||
|
ActionProgramCreated ActivityAction = "PROGRAM_CREATED"
|
||||||
|
ActionProgramUpdated ActivityAction = "PROGRAM_UPDATED"
|
||||||
|
ActionProgramDeleted ActivityAction = "PROGRAM_DELETED"
|
||||||
|
ActionModuleCreated ActivityAction = "MODULE_CREATED"
|
||||||
|
ActionModuleUpdated ActivityAction = "MODULE_UPDATED"
|
||||||
|
ActionModuleDeleted ActivityAction = "MODULE_DELETED"
|
||||||
|
ActionLessonCreated ActivityAction = "LESSON_CREATED"
|
||||||
|
ActionLessonUpdated ActivityAction = "LESSON_UPDATED"
|
||||||
|
ActionLessonDeleted ActivityAction = "LESSON_DELETED"
|
||||||
|
ActionPracticeCreated ActivityAction = "PRACTICE_CREATED"
|
||||||
|
ActionPracticeUpdated ActivityAction = "PRACTICE_UPDATED"
|
||||||
|
ActionPracticeDeleted ActivityAction = "PRACTICE_DELETED"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceType string
|
type ResourceType string
|
||||||
|
|
@ -62,6 +74,10 @@ const (
|
||||||
ResourceQuestion ResourceType = "QUESTION"
|
ResourceQuestion ResourceType = "QUESTION"
|
||||||
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
||||||
ResourceIssue ResourceType = "ISSUE"
|
ResourceIssue ResourceType = "ISSUE"
|
||||||
|
ResourceProgram ResourceType = "PROGRAM"
|
||||||
|
ResourceModule ResourceType = "MODULE"
|
||||||
|
ResourceLesson ResourceType = "LESSON"
|
||||||
|
ResourcePractice ResourceType = "PRACTICE"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityLog struct {
|
type ActivityLog struct {
|
||||||
|
|
|
||||||
33
internal/domain/course.go
Normal file
33
internal/domain/course.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DefaultCEFRCourseNames are the standard course names seeded for each program (migration 000048).
|
||||||
|
// Creating a course via the API may use any of these or a custom name.
|
||||||
|
var DefaultCEFRCourseNames = []string{"A1", "A2", "B1", "B2", "C1", "C2"}
|
||||||
|
|
||||||
|
// Course belongs to a Program.
|
||||||
|
type Course struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCourseInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCourseInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,169 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type SubCourseLevel string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SubCourseLevelBeginner SubCourseLevel = "BEGINNER"
|
|
||||||
SubCourseLevelIntermediate SubCourseLevel = "INTERMEDIATE"
|
|
||||||
SubCourseLevelAdvanced SubCourseLevel = "ADVANCED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubCourseSubLevel string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SubCourseSubLevelA1 SubCourseSubLevel = "A1"
|
|
||||||
SubCourseSubLevelA2 SubCourseSubLevel = "A2"
|
|
||||||
SubCourseSubLevelA3 SubCourseSubLevel = "A3"
|
|
||||||
SubCourseSubLevelB1 SubCourseSubLevel = "B1"
|
|
||||||
SubCourseSubLevelB2 SubCourseSubLevel = "B2"
|
|
||||||
SubCourseSubLevelB3 SubCourseSubLevel = "B3"
|
|
||||||
SubCourseSubLevelC1 SubCourseSubLevel = "C1"
|
|
||||||
SubCourseSubLevelC2 SubCourseSubLevel = "C2"
|
|
||||||
SubCourseSubLevelC3 SubCourseSubLevel = "C3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContentStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContentStatusDraft ContentStatus = "DRAFT"
|
|
||||||
ContentStatusPublished ContentStatus = "PUBLISHED"
|
|
||||||
ContentStatusInactive ContentStatus = "INACTIVE"
|
|
||||||
ContentStatusArchived ContentStatus = "ARCHIVED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TreeSubCourse struct {
|
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
Level string
|
|
||||||
SubLevel string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TreeCourse struct {
|
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
SubCourses []TreeSubCourse
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseCategory struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
IsActive bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Course struct {
|
|
||||||
ID int64
|
|
||||||
CategoryID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
IntroVideoURL *string
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourse struct {
|
|
||||||
ID int64
|
|
||||||
CourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
DisplayOrder int32
|
|
||||||
Level string
|
|
||||||
SubLevel string
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseVideo struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
VideoURL string
|
|
||||||
Duration int32
|
|
||||||
Resolution *string
|
|
||||||
InstructorID *string
|
|
||||||
Thumbnail *string
|
|
||||||
Visibility *string
|
|
||||||
DisplayOrder int32
|
|
||||||
IsPublished bool
|
|
||||||
PublishDate *time.Time
|
|
||||||
Status string
|
|
||||||
// Vimeo-specific fields
|
|
||||||
VimeoID *string
|
|
||||||
VimeoEmbedURL *string
|
|
||||||
VimeoPlayerHTML *string
|
|
||||||
VimeoStatus *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type VideoHostProvider string
|
|
||||||
|
|
||||||
const (
|
|
||||||
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
|
||||||
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VideoAccessBlock struct {
|
|
||||||
VideoID int64
|
|
||||||
Title string
|
|
||||||
DisplayOrder int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Learning Path types — full nested structure for a course
|
|
||||||
type LearningPathVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
VideoURL string `json:"video_url"`
|
|
||||||
Duration int32 `json:"duration"`
|
|
||||||
Resolution *string `json:"resolution,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
VimeoID *string `json:"vimeo_id,omitempty"`
|
|
||||||
VimeoEmbedURL *string `json:"vimeo_embed_url,omitempty"`
|
|
||||||
VideoHostProvider *string `json:"video_host_provider,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathPractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Persona *string `json:"persona,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
|
||||||
QuestionCount int64 `json:"question_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathPrerequisite struct {
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathSubCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
PrerequisiteCount int64 `json:"prerequisite_count"`
|
|
||||||
VideoCount int64 `json:"video_count"`
|
|
||||||
PracticeCount int64 `json:"practice_count"`
|
|
||||||
Prerequisites []LearningPathPrerequisite `json:"prerequisites"`
|
|
||||||
Videos []LearningPathVideo `json:"videos"`
|
|
||||||
Practices []LearningPathPractice `json:"practices"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPath struct {
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
CourseTitle string `json:"course_title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
SubCourses []LearningPathSubCourse `json:"sub_courses"`
|
|
||||||
}
|
|
||||||
32
internal/domain/lesson.go
Normal file
32
internal/domain/lesson.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Lesson belongs to a Module.
|
||||||
|
type Lesson struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
VideoURL *string `json:"video_url,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLessonInput struct {
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
VideoURL *string `json:"video_url,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLessonInput struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
VideoURL *string `json:"video_url,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
22
internal/domain/lms_access.go
Normal file
22
internal/domain/lms_access.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
// LMSEntityAccess describes learner gating for a program, course, module, or lesson.
|
||||||
|
// It is omitted (nil) for non-learner roles in API responses.
|
||||||
|
// Progress fields count completed lessons vs total lessons in that entity’s scope (lesson: 0 or 1 of 1).
|
||||||
|
type LMSEntityAccess struct {
|
||||||
|
IsAccessible bool `json:"is_accessible"`
|
||||||
|
IsCompleted bool `json:"is_completed"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
CompletedCount int `json:"completed_count"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
ProgressPercent int `json:"progress_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LMSUserProgress lists entity IDs the authenticated user has fully completed
|
||||||
|
// (lessons as marked complete; module/course/program when rollup conditions were met).
|
||||||
|
type LMSUserProgress struct {
|
||||||
|
LessonIDs []int64 `json:"lesson_ids"`
|
||||||
|
ModuleIDs []int64 `json:"module_ids"`
|
||||||
|
CourseIDs []int64 `json:"course_ids"`
|
||||||
|
ProgramIDs []int64 `json:"program_ids"`
|
||||||
|
}
|
||||||
30
internal/domain/module.go
Normal file
30
internal/domain/module.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Module belongs to a Course. program_id is the course’s program (stored for querying; not required from the client on create).
|
||||||
|
type Module struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateModuleInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateModuleInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
47
internal/domain/practice.go
Normal file
47
internal/domain/practice.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ParentKind identifies which hierarchy entity owns a practice (exactly one).
|
||||||
|
type ParentKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ParentKindCourse ParentKind = "COURSE"
|
||||||
|
ParentKindModule ParentKind = "MODULE"
|
||||||
|
ParentKindLesson ParentKind = "LESSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Practice is question-set content (story, persona, tips) scoped to a course, module, or lesson.
|
||||||
|
type Practice struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ParentKind ParentKind `json:"parent_kind"`
|
||||||
|
ParentID int64 `json:"parent_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
StoryDescription *string `json:"story_description,omitempty"`
|
||||||
|
StoryImage *string `json:"story_image,omitempty"`
|
||||||
|
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePracticeInput struct {
|
||||||
|
ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"`
|
||||||
|
ParentID int64 `json:"parent_id" validate:"required,gt=0"`
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
StoryDescription *string `json:"story_description,omitempty"`
|
||||||
|
StoryImage *string `json:"story_image,omitempty"`
|
||||||
|
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
|
||||||
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePracticeInput struct {
|
||||||
|
Title *string `json:"title,omitempty"`
|
||||||
|
StoryDescription *string `json:"story_description,omitempty"`
|
||||||
|
StoryImage *string `json:"story_image,omitempty"`
|
||||||
|
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||||
|
QuestionSetID *int64 `json:"question_set_id,omitempty"`
|
||||||
|
QuickTips *string `json:"quick_tips,omitempty"`
|
||||||
|
}
|
||||||
28
internal/domain/program.go
Normal file
28
internal/domain/program.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Program is the top-level container in the LMS hierarchy (e.g. tracks like Beginner / Intermediate / Advanced).
|
||||||
|
type Program struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProgramInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProgramInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrPrerequisiteNotMet = errors.New("prerequisites not completed")
|
|
||||||
ErrProgressNotFound = errors.New("progress record not found")
|
|
||||||
ErrPrerequisiteExists = errors.New("prerequisite already exists")
|
|
||||||
ErrSelfPrerequisite = errors.New("sub-course cannot be its own prerequisite")
|
|
||||||
ErrSubCourseAlreadyStarted = errors.New("sub-course already started")
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubCoursePrerequisite struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
PrerequisiteSubCourseID int64
|
|
||||||
PrerequisiteTitle string
|
|
||||||
PrerequisiteLevel string
|
|
||||||
PrerequisiteDisplayOrder int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseDependent struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
PrerequisiteSubCourseID int64
|
|
||||||
DependentTitle string
|
|
||||||
DependentLevel string
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProgressStatusNotStarted ProgressStatus = "NOT_STARTED"
|
|
||||||
ProgressStatusInProgress ProgressStatus = "IN_PROGRESS"
|
|
||||||
ProgressStatusCompleted ProgressStatus = "COMPLETED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserSubCourseProgress struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Status ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseWithProgress struct {
|
|
||||||
SubCourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
DisplayOrder int32
|
|
||||||
Level string
|
|
||||||
IsActive bool
|
|
||||||
ProgressStatus ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
UnmetPrerequisitesCount int64
|
|
||||||
IsLocked bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserCourseProgressItem struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Status ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt *time.Time
|
|
||||||
SubCourseTitle string
|
|
||||||
SubCourseLevel string
|
|
||||||
SubCourseDisplayOrder int32
|
|
||||||
}
|
|
||||||
|
|
@ -27,6 +27,7 @@ const (
|
||||||
QuestionSetTypeQuiz QuestionSetType = "QUIZ"
|
QuestionSetTypeQuiz QuestionSetType = "QUIZ"
|
||||||
QuestionSetTypeExam QuestionSetType = "EXAM"
|
QuestionSetTypeExam QuestionSetType = "EXAM"
|
||||||
QuestionSetTypeSurvey QuestionSetType = "SURVEY"
|
QuestionSetTypeSurvey QuestionSetType = "SURVEY"
|
||||||
|
QuestionSetTypeCapstone QuestionSetType = "CAPSTONE"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PracticeAccessBlock struct {
|
type PracticeAccessBlock struct {
|
||||||
|
|
@ -103,7 +104,6 @@ type QuestionSet struct {
|
||||||
PassingScore *int32
|
PassingScore *int32
|
||||||
ShuffleQuestions bool
|
ShuffleQuestions bool
|
||||||
Status string
|
Status string
|
||||||
SubCourseVideoID *int64
|
|
||||||
IntroVideoURL *string
|
IntroVideoURL *string
|
||||||
UserPersonas []UserPersona
|
UserPersonas []UserPersona
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
|
@ -172,7 +172,6 @@ type CreateQuestionSetInput struct {
|
||||||
PassingScore *int32
|
PassingScore *int32
|
||||||
ShuffleQuestions *bool
|
ShuffleQuestions *bool
|
||||||
Status *string
|
Status *string
|
||||||
SubCourseVideoID *int64
|
|
||||||
IntroVideoURL *string
|
IntroVideoURL *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
46
internal/domain/reorder.go
Normal file
46
internal/domain/reorder.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrReorderInvalidIDSet means ordered_ids is not an exact permutation of the current entities in scope.
|
||||||
|
var ErrReorderInvalidIDSet = errors.New("ordered_ids must list every id in this scope exactly once, with no duplicates")
|
||||||
|
|
||||||
|
// ReorderIDsRequest is the body for batch reorder endpoints (drag-and-drop UI).
|
||||||
|
// Send "ordered_ids": [] in display order. Must include every id in that scope (use GET list) when there is at least one entity.
|
||||||
|
type ReorderIDsRequest struct {
|
||||||
|
OrderedIDs []int64 `json:"ordered_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateReorderPermutation checks that ordered contains the same multiset of ids as expected (new order vs current scope).
|
||||||
|
func ValidateReorderPermutation(ordered, expected []int64) error {
|
||||||
|
if len(expected) == 0 {
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: no entities exist in this scope", ErrReorderInvalidIDSet)
|
||||||
|
}
|
||||||
|
if len(ordered) != len(expected) {
|
||||||
|
return fmt.Errorf("%w: want %d ids, got %d", ErrReorderInvalidIDSet, len(expected), len(ordered))
|
||||||
|
}
|
||||||
|
seen := make(map[int64]struct{}, len(ordered))
|
||||||
|
for _, id := range ordered {
|
||||||
|
if _, dup := seen[id]; dup {
|
||||||
|
return fmt.Errorf("%w: duplicate id %d", ErrReorderInvalidIDSet, id)
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
a := append([]int64(nil), expected...)
|
||||||
|
b := append([]int64(nil), ordered...)
|
||||||
|
sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
|
||||||
|
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return fmt.Errorf("%w: id set does not match current scope", ErrReorderInvalidIDSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTeamMemberNotFound = errors.New("team member not found")
|
ErrTeamMemberNotFound = errors.New("team member not found")
|
||||||
ErrTeamMemberEmailExists = errors.New("team member email already exists")
|
ErrTeamMemberEmailExists = errors.New("team member email already exists")
|
||||||
ErrInvalidTeamRole = errors.New("invalid team role")
|
ErrInvalidTeamRole = errors.New("invalid team role")
|
||||||
ErrInvalidTeamMemberStatus = errors.New("invalid team member status")
|
ErrInvalidTeamMemberStatus = errors.New("invalid team member status")
|
||||||
ErrInvalidEmploymentType = errors.New("invalid employment type")
|
ErrInvalidEmploymentType = errors.New("invalid employment type")
|
||||||
ErrTeamMemberEmailNotVerified = errors.New("team member email not verified")
|
ErrTeamMemberEmailNotVerified = errors.New("team member email not verified")
|
||||||
|
ErrTeamRefreshTokenNotFound = errors.New("team refresh token not found")
|
||||||
|
ErrTeamRefreshTokenExpired = errors.New("team refresh token expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
type TeamRole string
|
type TeamRole string
|
||||||
|
|
@ -80,6 +82,16 @@ func (e EmploymentType) IsValid() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TeamRefreshToken is a persisted refresh token for team member sessions (separate from users.refresh_tokens).
|
||||||
|
type TeamRefreshToken struct {
|
||||||
|
ID int64
|
||||||
|
TeamMemberID int64
|
||||||
|
Token string
|
||||||
|
ExpiresAt time.Time
|
||||||
|
Revoked bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type TeamMember struct {
|
type TeamMember struct {
|
||||||
ID int64
|
ID int64
|
||||||
FirstName string
|
FirstName string
|
||||||
|
|
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CourseStore interface {
|
|
||||||
// Course Categories
|
|
||||||
CreateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
name string,
|
|
||||||
) (domain.CourseCategory, error)
|
|
||||||
GetCourseCategoryByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.CourseCategory, error)
|
|
||||||
GetAllCourseCategories(
|
|
||||||
ctx context.Context,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.CourseCategory, int64, error)
|
|
||||||
UpdateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
name *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeleteCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Courses
|
|
||||||
CreateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
) (domain.Course, error)
|
|
||||||
GetCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Course, error)
|
|
||||||
GetCoursesByCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.Course, int64, error)
|
|
||||||
UpdateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeleteCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Sub-courses
|
|
||||||
CreateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
level string,
|
|
||||||
subLevel string,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
GetSubCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
GetSubCoursesByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.SubCourse, int64, error)
|
|
||||||
ListSubCoursesByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.SubCourse, error)
|
|
||||||
ListActiveSubCourses(
|
|
||||||
ctx context.Context,
|
|
||||||
) ([]domain.SubCourse, error)
|
|
||||||
UpdateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
level *string,
|
|
||||||
subLevel *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeactivateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
DeleteSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
|
|
||||||
// Sub-course Videos
|
|
||||||
CreateSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
videoURL string,
|
|
||||||
duration int32,
|
|
||||||
resolution *string,
|
|
||||||
instructorID *string,
|
|
||||||
thumbnail *string,
|
|
||||||
visibility *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
status *string,
|
|
||||||
vimeoID *string,
|
|
||||||
vimeoEmbedURL *string,
|
|
||||||
vimeoPlayerHTML *string,
|
|
||||||
vimeoStatus *string,
|
|
||||||
videoHostProvider *string,
|
|
||||||
) (domain.SubCourseVideo, error)
|
|
||||||
GetSubCourseVideoByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourseVideo, error)
|
|
||||||
GetVideosBySubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
) ([]domain.SubCourseVideo, int64, error)
|
|
||||||
GetPublishedVideosBySubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
) ([]domain.SubCourseVideo, error)
|
|
||||||
GetFirstIncompletePreviousVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int64,
|
|
||||||
videoID int64,
|
|
||||||
) (*domain.VideoAccessBlock, error)
|
|
||||||
MarkVideoCompleted(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int64,
|
|
||||||
videoID int64,
|
|
||||||
) error
|
|
||||||
PublishSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
videoID int64,
|
|
||||||
) error
|
|
||||||
UpdateSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
videoURL *string,
|
|
||||||
duration *int32,
|
|
||||||
resolution *string,
|
|
||||||
visibility *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
status *string,
|
|
||||||
) error
|
|
||||||
ArchiveSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
DeleteSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Vimeo integration
|
|
||||||
UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error
|
|
||||||
GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error)
|
|
||||||
|
|
||||||
// Learning Tree
|
|
||||||
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
|
||||||
|
|
||||||
// Learning Path (full nested structure for a course)
|
|
||||||
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
|
|
||||||
|
|
||||||
// Reorder (drag-and-drop support)
|
|
||||||
ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderCourses(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressionStore interface {
|
|
||||||
// Prerequisites (admin)
|
|
||||||
AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error
|
|
||||||
RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error
|
|
||||||
GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error)
|
|
||||||
GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error)
|
|
||||||
CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error)
|
|
||||||
DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error
|
|
||||||
|
|
||||||
// User progress
|
|
||||||
StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
|
||||||
UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error
|
|
||||||
CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error
|
|
||||||
RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error
|
|
||||||
GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
|
||||||
GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error)
|
|
||||||
GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error)
|
|
||||||
}
|
|
||||||
16
internal/ports/lms_course.go
Normal file
16
internal/ports/lms_course.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CourseStore interface {
|
||||||
|
CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error)
|
||||||
|
GetCourseByID(ctx context.Context, id int64) (domain.Course, error)
|
||||||
|
ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error)
|
||||||
|
ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error)
|
||||||
|
ReorderCoursesInProgram(ctx context.Context, programID int64, orderedIDs []int64) error
|
||||||
|
UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error)
|
||||||
|
DeleteCourse(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
14
internal/ports/lms_lesson.go
Normal file
14
internal/ports/lms_lesson.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LessonStore interface {
|
||||||
|
CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error)
|
||||||
|
GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error)
|
||||||
|
ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error)
|
||||||
|
UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error)
|
||||||
|
DeleteLesson(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
16
internal/ports/lms_module.go
Normal file
16
internal/ports/lms_module.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModuleStore interface {
|
||||||
|
CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error)
|
||||||
|
GetModuleByID(ctx context.Context, id int64) (domain.Module, error)
|
||||||
|
ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error)
|
||||||
|
ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error)
|
||||||
|
ReorderModulesInCourse(ctx context.Context, courseID int64, orderedIDs []int64) error
|
||||||
|
UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error)
|
||||||
|
DeleteModule(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
31
internal/ports/lms_practice.go
Normal file
31
internal/ports/lms_practice.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuestionSetByID is implemented by the questions store.
|
||||||
|
type QuestionSetByID interface {
|
||||||
|
GetQuestionSetByID(ctx context.Context, id int64) (domain.QuestionSet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserByID is implemented by the user store.
|
||||||
|
type UserByID interface {
|
||||||
|
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsPracticeStore interface {
|
||||||
|
// courseID, moduleID, lessonID: exactly one non-nil, matching in.ParentKind / in.ParentID.
|
||||||
|
CreateLmsPractice(
|
||||||
|
ctx context.Context,
|
||||||
|
in domain.CreatePracticeInput,
|
||||||
|
courseID, moduleID, lessonID *int64,
|
||||||
|
) (domain.Practice, error)
|
||||||
|
GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error)
|
||||||
|
ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error)
|
||||||
|
DeleteLmsPractice(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
16
internal/ports/program.go
Normal file
16
internal/ports/program.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgramStore interface {
|
||||||
|
CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error)
|
||||||
|
GetProgramByID(ctx context.Context, id int64) (domain.Program, error)
|
||||||
|
ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error)
|
||||||
|
ListAllProgramIDs(ctx context.Context) ([]int64, error)
|
||||||
|
ReorderPrograms(ctx context.Context, orderedIDs []int64) error
|
||||||
|
UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error)
|
||||||
|
DeleteProgram(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,6 @@ type QuestionStore interface {
|
||||||
GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error)
|
GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error)
|
||||||
GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
|
GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
|
||||||
GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error)
|
GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error)
|
||||||
GetSubCourseInitialAssessmentSet(ctx context.Context, subCourseID int64) (domain.QuestionSet, error)
|
|
||||||
GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error)
|
GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error)
|
||||||
MarkPracticeCompleted(ctx context.Context, userID int64, questionSetID int64) error
|
MarkPracticeCompleted(ctx context.Context, userID int64, questionSetID int64) error
|
||||||
UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error
|
UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package ports
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
@ -30,4 +31,9 @@ type TeamStore interface {
|
||||||
GetTeamMembersByRole(ctx context.Context, role string) ([]domain.TeamMember, error)
|
GetTeamMembersByRole(ctx context.Context, role string) ([]domain.TeamMember, error)
|
||||||
CountTeamMembersByStatus(ctx context.Context) (domain.TeamMemberStats, error)
|
CountTeamMembersByStatus(ctx context.Context) (domain.TeamMemberStats, error)
|
||||||
UpdateTeamMemberEmailVerified(ctx context.Context, memberID int64, verified bool) error
|
UpdateTeamMemberEmailVerified(ctx context.Context, memberID int64, verified bool) error
|
||||||
|
|
||||||
|
RevokeAllActiveTeamRefreshTokensForMember(ctx context.Context, memberID int64) error
|
||||||
|
CreateTeamRefreshToken(ctx context.Context, memberID int64, token string, expiresAt, createdAt time.Time) error
|
||||||
|
GetTeamRefreshTokenByToken(ctx context.Context, token string) (domain.TeamRefreshToken, error)
|
||||||
|
RevokeTeamRefreshTokenByToken(ctx context.Context, token string) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewCourseStore(s *Store) *Store { return s }
|
|
||||||
func NewProgressionStore(s *Store) *Store { return s }
|
|
||||||
|
|
||||||
func (s *Store) CreateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
name string,
|
|
||||||
) (domain.CourseCategory, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.CreateCourseCategory(ctx, dbgen.CreateCourseCategoryParams{
|
|
||||||
Name: name,
|
|
||||||
Column2: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.CourseCategory{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCourseCategoryByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.CourseCategory, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.GetCourseCategoryByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return domain.CourseCategory{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetAllCourseCategories(
|
|
||||||
ctx context.Context,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.CourseCategory, int64, error) {
|
|
||||||
|
|
||||||
rows, err := s.queries.GetAllCourseCategories(ctx, dbgen.GetAllCourseCategoriesParams{
|
|
||||||
Limit: pgtype.Int4{Int32: limit},
|
|
||||||
Offset: pgtype.Int4{Int32: offset},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
categories []domain.CourseCategory
|
|
||||||
totalCount int64
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, row := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
totalCount = row.TotalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = append(categories, domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return categories, totalCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
name *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
var (
|
|
||||||
nameVal string
|
|
||||||
isActiveVal bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if name != nil {
|
|
||||||
nameVal = *name
|
|
||||||
}
|
|
||||||
|
|
||||||
if isActive != nil {
|
|
||||||
isActiveVal = *isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.queries.UpdateCourseCategory(ctx, dbgen.UpdateCourseCategoryParams{
|
|
||||||
Name: nameVal,
|
|
||||||
IsActive: isActiveVal,
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
return s.queries.DeleteCourseCategory(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
|
||||||
return s.queries.ReorderCourseCategories(ctx, dbgen.ReorderCourseCategoriesParams{
|
|
||||||
Ids: ids,
|
|
||||||
Positions: positions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Store) CreateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
) (domain.Course, error) {
|
|
||||||
var descVal, thumbVal, introVideoVal string
|
|
||||||
if description != nil {
|
|
||||||
descVal = *description
|
|
||||||
}
|
|
||||||
if thumbnail != nil {
|
|
||||||
thumbVal = *thumbnail
|
|
||||||
}
|
|
||||||
if introVideoURL != nil {
|
|
||||||
introVideoVal = *introVideoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
row, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
|
|
||||||
CategoryID: categoryID,
|
|
||||||
Title: title,
|
|
||||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
|
||||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
|
||||||
IntroVideoUrl: pgtype.Text{String: introVideoVal, Valid: introVideoURL != nil},
|
|
||||||
Column6: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.Course{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapCourse(row), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Course, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.GetCourseByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Course{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapCourse(row), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCoursesByCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.Course, int64, error) {
|
|
||||||
|
|
||||||
rows, err := s.queries.GetCoursesByCategory(ctx, dbgen.GetCoursesByCategoryParams{
|
|
||||||
CategoryID: categoryID,
|
|
||||||
Limit: pgtype.Int4{Int32: limit},
|
|
||||||
Offset: pgtype.Int4{Int32: offset},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
courses []domain.Course
|
|
||||||
totalCount int64
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, row := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
totalCount = row.TotalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
courses = append(courses, domain.Course{
|
|
||||||
ID: row.ID,
|
|
||||||
CategoryID: row.CategoryID,
|
|
||||||
Title: row.Title,
|
|
||||||
Description: ptrText(row.Description),
|
|
||||||
Thumbnail: ptrText(row.Thumbnail),
|
|
||||||
IntroVideoURL: ptrText(row.IntroVideoUrl),
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return courses, totalCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error {
|
|
||||||
var (
|
|
||||||
titleVal string
|
|
||||||
descriptionVal string
|
|
||||||
thumbnailVal string
|
|
||||||
introVideoVal string
|
|
||||||
isActiveVal bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if title != nil {
|
|
||||||
titleVal = *title
|
|
||||||
}
|
|
||||||
if description != nil {
|
|
||||||
descriptionVal = *description
|
|
||||||
}
|
|
||||||
if thumbnail != nil {
|
|
||||||
thumbnailVal = *thumbnail
|
|
||||||
}
|
|
||||||
if introVideoURL != nil {
|
|
||||||
introVideoVal = *introVideoURL
|
|
||||||
}
|
|
||||||
if isActive != nil {
|
|
||||||
isActiveVal = *isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{
|
|
||||||
Title: titleVal,
|
|
||||||
Description: pgtype.Text{String: descriptionVal, Valid: description != nil},
|
|
||||||
Thumbnail: pgtype.Text{String: thumbnailVal, Valid: thumbnail != nil},
|
|
||||||
IntroVideoUrl: pgtype.Text{String: introVideoVal, Valid: introVideoURL != nil},
|
|
||||||
IsActive: isActiveVal,
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
return s.queries.DeleteCourse(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapCourse(row dbgen.Course) domain.Course {
|
|
||||||
return domain.Course{
|
|
||||||
ID: row.ID,
|
|
||||||
CategoryID: row.CategoryID,
|
|
||||||
Title: row.Title,
|
|
||||||
Description: ptrText(row.Description),
|
|
||||||
Thumbnail: ptrText(row.Thumbnail),
|
|
||||||
IntroVideoURL: ptrText(row.IntroVideoUrl),
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
|
||||||
return s.queries.ReorderCourses(ctx, dbgen.ReorderCoursesParams{
|
|
||||||
Ids: ids,
|
|
||||||
Positions: positions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptrText(t pgtype.Text) *string {
|
|
||||||
if t.Valid {
|
|
||||||
return &t.String
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
87
internal/repository/lms_access.go
Normal file
87
internal/repository/lms_access.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousProgram(ctx context.Context, programID int64) (dbgen.Program, error) {
|
||||||
|
return s.queries.GetPreviousProgram(ctx, programID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousCourseInProgram(ctx context.Context, courseID int64) (dbgen.Course, error) {
|
||||||
|
return s.queries.GetPreviousCourseInProgram(ctx, courseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousModuleInCourse(ctx context.Context, moduleID int64) (dbgen.Module, error) {
|
||||||
|
return s.queries.GetPreviousModuleInCourse(ctx, moduleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousLessonInModule(ctx context.Context, lessonID int64) (dbgen.Lesson, error) {
|
||||||
|
return s.queries.GetPreviousLessonInModule(ctx, lessonID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasProgramProgress(ctx context.Context, userID, programID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasProgramProgress(ctx, dbgen.UserHasProgramProgressParams{UserID: userID, ProgramID: programID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasCourseProgress(ctx context.Context, userID, courseID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasCourseProgress(ctx, dbgen.UserHasCourseProgressParams{UserID: userID, CourseID: courseID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasModuleProgress(ctx context.Context, userID, moduleID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasModuleProgress(ctx, dbgen.UserHasModuleProgressParams{UserID: userID, ModuleID: moduleID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LmsUserLessonProgressInModule returns completed and total lesson counts in a module (for progress UI).
|
||||||
|
func (s *Store) LmsUserLessonProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) {
|
||||||
|
total, err = s.queries.CountLessonsInModule(ctx, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LmsUserLessonProgressInCourse returns completed and total lesson counts in a course (all modules).
|
||||||
|
func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
||||||
|
total, err = s.queries.CountLessonsInCourse(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
||||||
|
CourseID: courseID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LmsUserLessonProgressInProgram returns completed and total lesson counts in a program (all courses).
|
||||||
|
func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) {
|
||||||
|
total, err = s.queries.CountLessonsInProgram(ctx, programID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
116
internal/repository/lms_courses.go
Normal file
116
internal/repository/lms_courses.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func courseToDomain(c dbgen.Course) domain.Course {
|
||||||
|
out := domain.Course{
|
||||||
|
ID: c.ID,
|
||||||
|
ProgramID: c.ProgramID,
|
||||||
|
Name: c.Name,
|
||||||
|
}
|
||||||
|
out.Description = fromPgText(c.Description)
|
||||||
|
out.Thumbnail = fromPgText(c.Thumbnail)
|
||||||
|
out.CreatedAt = c.CreatedAt.Time
|
||||||
|
if c.UpdatedAt.Valid {
|
||||||
|
t := c.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
out.SortOrder = int(c.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
|
||||||
|
c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
Name: input.Name,
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
|
||||||
|
return s.queries.ListCourseIDsByProgram(ctx, programID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) {
|
||||||
|
c, err := s.queries.GetCourseByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||||
|
rows, err := s.queries.ListCoursesByProgramID(ctx, dbgen.ListCoursesByProgramIDParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Course{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Course, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, courseToDomain(dbgen.Course{
|
||||||
|
ID: r.ID,
|
||||||
|
ProgramID: r.ProgramID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Thumbnail: r.Thumbnail,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) {
|
||||||
|
var nameText pgtype.Text
|
||||||
|
if input.Name != nil {
|
||||||
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||||
|
} else {
|
||||||
|
nameText = pgtype.Text{Valid: false}
|
||||||
|
}
|
||||||
|
c, err := s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{
|
||||||
|
ID: id,
|
||||||
|
Name: nameText,
|
||||||
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||||
|
SortOrder: optionalInt4Update(input.SortOrder),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteCourse(ctx, id)
|
||||||
|
}
|
||||||
116
internal/repository/lms_lessons.go
Normal file
116
internal/repository/lms_lessons.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func lessonToDomain(l dbgen.Lesson) domain.Lesson {
|
||||||
|
out := domain.Lesson{
|
||||||
|
ID: l.ID,
|
||||||
|
ModuleID: l.ModuleID,
|
||||||
|
Title: l.Title,
|
||||||
|
}
|
||||||
|
out.VideoURL = fromPgText(l.VideoUrl)
|
||||||
|
out.Thumbnail = fromPgText(l.Thumbnail)
|
||||||
|
out.Description = fromPgText(l.Description)
|
||||||
|
out.CreatedAt = l.CreatedAt.Time
|
||||||
|
if l.UpdatedAt.Valid {
|
||||||
|
t := l.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
out.SortOrder = int(l.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||||
|
l, err := s.queries.CreateLesson(ctx, dbgen.CreateLessonParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
Title: input.Title,
|
||||||
|
VideoUrl: toPgText(input.VideoURL),
|
||||||
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error) {
|
||||||
|
l, err := s.queries.GetLessonByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Lesson{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
|
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Lesson{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Lesson, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lessonToDomain(dbgen.Lesson{
|
||||||
|
ID: r.ID,
|
||||||
|
ModuleID: r.ModuleID,
|
||||||
|
Title: r.Title,
|
||||||
|
VideoUrl: r.VideoUrl,
|
||||||
|
Thumbnail: r.Thumbnail,
|
||||||
|
Description: r.Description,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||||
|
var titleText pgtype.Text
|
||||||
|
if input.Title != nil {
|
||||||
|
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
||||||
|
} else {
|
||||||
|
titleText = pgtype.Text{Valid: false}
|
||||||
|
}
|
||||||
|
l, err := s.queries.UpdateLesson(ctx, dbgen.UpdateLessonParams{
|
||||||
|
ID: id,
|
||||||
|
Title: titleText,
|
||||||
|
VideoUrl: optionalTextUpdate(input.VideoURL),
|
||||||
|
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||||
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
SortOrder: optionalInt4Update(input.SortOrder),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Lesson{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteLesson(ctx, id)
|
||||||
|
}
|
||||||
120
internal/repository/lms_modules.go
Normal file
120
internal/repository/lms_modules.go
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func moduleToDomain(m dbgen.Module) domain.Module {
|
||||||
|
out := domain.Module{
|
||||||
|
ID: m.ID,
|
||||||
|
ProgramID: m.ProgramID,
|
||||||
|
CourseID: m.CourseID,
|
||||||
|
Name: m.Name,
|
||||||
|
}
|
||||||
|
out.Description = fromPgText(m.Description)
|
||||||
|
out.Icon = fromPgText(m.Icon)
|
||||||
|
out.CreatedAt = m.CreatedAt.Time
|
||||||
|
if m.UpdatedAt.Valid {
|
||||||
|
t := m.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
out.SortOrder = int(m.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
||||||
|
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
CourseID: courseID,
|
||||||
|
Name: input.Name,
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
Icon: toPgText(input.Icon),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
|
||||||
|
return s.queries.ListModuleIDsByCourse(ctx, courseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) {
|
||||||
|
m, err := s.queries.GetModuleByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||||
|
rows, err := s.queries.ListModulesByProgramAndCourse(ctx, dbgen.ListModulesByProgramAndCourseParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
CourseID: courseID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Module{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Module, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, moduleToDomain(dbgen.Module{
|
||||||
|
ID: r.ID,
|
||||||
|
ProgramID: r.ProgramID,
|
||||||
|
CourseID: r.CourseID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Icon: r.Icon,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||||
|
var nameText pgtype.Text
|
||||||
|
if input.Name != nil {
|
||||||
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||||
|
} else {
|
||||||
|
nameText = pgtype.Text{Valid: false}
|
||||||
|
}
|
||||||
|
m, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
||||||
|
ID: id,
|
||||||
|
Name: nameText,
|
||||||
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
Icon: optionalTextUpdate(input.Icon),
|
||||||
|
SortOrder: optionalInt4Update(input.SortOrder),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteModule(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteModule(ctx, id)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user