From 8d3adf62747915b9552d9a2397a9915eb8f2f9f1 Mon Sep 17 00:00:00 2001 From: Pavel Date: Mon, 19 Feb 2024 19:33:15 +0300 Subject: [PATCH] first adding --- .gitignore | 21 + dal/dal.go | 116 +++ dal/db_query/queries.sql | 310 +++++++ dal/schema/000001_init.down.sql | 24 + dal/schema/000001_init.up.sql | 120 +++ dal/schema/000002_init.down.sql | 1 + dal/schema/000002_init.up.sql | 1 + dal/schema/000003_init.down.sql | 1 + dal/schema/000003_init.up.sql | 1 + dal/schema/000004_init.down.sql | 2 + dal/schema/000004_init.up.sql | 2 + dal/sqlcgen/db.go | 31 + dal/sqlcgen/models.go | 89 ++ dal/sqlcgen/queries.sql.go | 1404 +++++++++++++++++++++++++++++++ go.mod | 29 + go.sum | 95 +++ healthchecks/healthchecks.go | 18 + model/model.go | 271 ++++++ model/tariff/models.pb.go | 313 +++++++ repository/account/account.go | 309 +++++++ repository/answer/answer.go | 88 ++ repository/question/question.go | 388 +++++++++ repository/quiz/quiz.go | 579 +++++++++++++ repository/result/result.go | 259 ++++++ repository/workers/worker.go | 39 + 25 files changed, 4511 insertions(+) create mode 100644 .gitignore create mode 100644 dal/dal.go create mode 100644 dal/db_query/queries.sql create mode 100644 dal/schema/000001_init.down.sql create mode 100644 dal/schema/000001_init.up.sql create mode 100644 dal/schema/000002_init.down.sql create mode 100644 dal/schema/000002_init.up.sql create mode 100644 dal/schema/000003_init.down.sql create mode 100644 dal/schema/000003_init.up.sql create mode 100644 dal/schema/000004_init.down.sql create mode 100644 dal/schema/000004_init.up.sql create mode 100644 dal/sqlcgen/db.go create mode 100644 dal/sqlcgen/models.go create mode 100644 dal/sqlcgen/queries.sql.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 healthchecks/healthchecks.go create mode 100644 model/model.go create mode 100644 model/tariff/models.pb.go create mode 100644 repository/account/account.go create mode 100644 repository/answer/answer.go create mode 100644 repository/question/question.go create mode 100644 repository/quiz/quiz.go create mode 100644 repository/result/result.go create mode 100644 repository/workers/worker.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ffb511 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +squiz +.idea/ +gen +worker/worker +storer/storer +answerer/answerer \ No newline at end of file diff --git a/dal/dal.go b/dal/dal.go new file mode 100644 index 0000000..e524cf9 --- /dev/null +++ b/dal/dal.go @@ -0,0 +1,116 @@ +package dal + +import ( + "context" + "database/sql" + _ "embed" + "errors" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/lib/pq" + "squiz/client/auth" + "squiz/dal/sqlcgen" + "squiz/repository/account" + "squiz/repository/answer" + "squiz/repository/question" + "squiz/repository/quiz" + "squiz/repository/result" + "squiz/repository/workers" + "time" +) + +var errNextDeclined = errors.New("next is declined") + +type DAL struct { + conn *sql.DB + authClient *auth.AuthClient + queries *sqlcgen.Queries + AccountRepo *account.AccountRepository + AnswerRepo *answer.AnswerRepository + QuestionRepo *question.QuestionRepository + QuizRepo *quiz.QuizRepository + ResultRepo *result.ResultRepository + WorkerRepo *workers.WorkerRepository +} + +func New(ctx context.Context, cred string, authClient *auth.AuthClient) (*DAL, error) { + pool, err := sql.Open("postgres", cred) + if err != nil { + return nil, err + } + + timeoutCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + if err := pool.PingContext(timeoutCtx); err != nil { + return nil, err + } + + queries := sqlcgen.New(pool) + + accountRepo := account.NewAccountRepository(account.Deps{ + Queries: queries, + AuthClient: authClient, + Pool: pool, + }) + + answerRepo := answer.NewAnswerRepository(answer.Deps{ + Queries: queries, + Pool: pool, + }) + + questionRepo := question.NewQuestionRepository(question.Deps{ + Queries: queries, + Pool: pool, + }) + + quizRepo := quiz.NewQuizRepository(quiz.Deps{ + Queries: queries, + Pool: pool, + }) + + resultRepo := result.NewResultRepository(result.Deps{ + Queries: queries, + Pool: pool, + }) + + workerRepo := workers.NewWorkerRepository(workers.Deps{ + Queries: queries, + }) + + return &DAL{ + conn: pool, + authClient: authClient, + queries: queries, + AccountRepo: accountRepo, + AnswerRepo: answerRepo, + QuestionRepo: questionRepo, + QuizRepo: quizRepo, + ResultRepo: resultRepo, + WorkerRepo: workerRepo, + }, nil +} + +func (d *DAL) Close() { + d.conn.Close() +} + +func (d *DAL) Init() error { + driver, err := postgres.WithInstance(d.conn, &postgres.Config{}) + if err != nil { + return err + } + + m, err := migrate.NewWithDatabaseInstance( + "file://dal/schema", + "postgres", driver, + ) + if err != nil { + return err + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + return err + } + return nil +} diff --git a/dal/db_query/queries.sql b/dal/db_query/queries.sql new file mode 100644 index 0000000..78393e1 --- /dev/null +++ b/dal/db_query/queries.sql @@ -0,0 +1,310 @@ +-- name: InsertQuiz :one +INSERT INTO quiz (accountid, + fingerprinting, + repeatable, + note_prevented, + mail_notifications, + unique_answers, + super, + group_id, + name, + description, + config, + status, + limit_answers, + due_to, + time_of_passing, + pausable, + parent_ids, + questions_count, + qid +) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) + RETURNING id, created_at, updated_at, qid; + +-- name: InsertQuestion :one +INSERT INTO question ( + quiz_id, + title, + description, + questiontype, + required, + page, + content, + parent_ids, + updated_at +) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + RETURNING id, created_at, updated_at; + +-- name: DeleteQuestion :one +UPDATE question SET deleted=true WHERE id=$1 RETURNING question.*; + +-- name: DeleteQuizByID :one +UPDATE quiz SET deleted=true WHERE quiz.id=$1 AND accountid=$2 RETURNING quiz.*; + +-- name: CopyQuestion :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +) +SELECT $1, title, description, questiontype, required, + page, content, version, parent_ids +FROM question WHERE question.id=$2 + RETURNING question.id, quiz_id, created_at, updated_at; + +-- name: DuplicateQuestion :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +) +SELECT quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +FROM question WHERE question.id=$1 + RETURNING question.id, quiz_id, created_at, updated_at; + +-- name: MoveToHistory :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids, deleted +) +SELECT quiz_id, title, description, questiontype, required, + page, content, version, parent_ids, true as deleted +FROM question WHERE question.id=$1 + RETURNING question.id, quiz_id, parent_ids; + +-- name: MoveToHistoryQuiz :one +INSERT INTO quiz(deleted, + accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +) +SELECT true as deleted, accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +FROM quiz WHERE quiz.id=$1 AND quiz.accountid=$2 + RETURNING quiz.id, qid, parent_ids; + +-- name: CopyQuiz :one +INSERT INTO quiz( + accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +) +SELECT accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +FROM quiz WHERE quiz.id=$1 AND quiz.accountId=$2 + RETURNING id, qid,created_at, updated_at; + +-- name: CopyQuizQuestions :exec +INSERT INTO question( + quiz_id, title, description, questiontype, required, page, content, version, parent_ids +) +SELECT $2, title, description, questiontype, required, page, content, version, parent_ids +FROM question WHERE question.quiz_id=$1 AND deleted=false; + +-- name: GetQuizHistory :many +SELECT * FROM quiz WHERE quiz.id = $1 AND quiz.accountId = $4 OR quiz.id = ANY( + SELECT unnest(parent_ids) FROM quiz WHERE id = $1 +) ORDER BY quiz.id DESC LIMIT $2 OFFSET $3; + +-- name: GetQuestionHistory :many +SELECT * FROM question WHERE question.id = $1 OR question.id = ANY( + SELECT unnest(parent_ids) FROM question WHERE id = $1 +) ORDER BY question.id DESC LIMIT $2 OFFSET $3; + +-- name: ArchiveQuiz :exec +UPDATE quiz SET archived = true WHERE id=$1 AND accountId=$2; + +-- name: GetPrivilegesByAccountID :many +SELECT id,privilegeID,privilege_name,amount, created_at FROM privileges WHERE account_id = $1; + +-- name: GetAccountWithPrivileges :many +SELECT a.id, a.user_id, a.created_at, a.deleted, + p.id AS privilege_id, p.privilegeID, p.privilege_name, p.amount, p.created_at AS privilege_created_at +FROM account a + LEFT JOIN privileges AS p ON a.id = p.account_id +WHERE a.user_id = $1; + +-- name: GetPrivilegesQuizAccount :many +SELECT + p.privilegeID, + p.privilege_name, + p.amount, + p.created_at, + a.id, + a.email, + qz.config +FROM + privileges AS p + INNER JOIN account AS a ON p.account_id = a.id + INNER JOIN quiz AS qz ON qz.accountid = a.user_id +WHERE + qz.id = $1; + +-- name: CreateAccount :exec +INSERT INTO account (id, user_id, email, created_at, deleted) VALUES ($1, $2, $3, $4, $5); + +-- name: DeletePrivilegeByAccID :exec +DELETE FROM privileges WHERE account_id = $1; + +-- name: DeleteAccountById :exec +DELETE FROM account WHERE id = $1; + +-- name: AccountPagination :many +SELECT a.id, a.user_id, a.created_at, a.deleted +FROM account a ORDER BY a.created_at DESC LIMIT $1 OFFSET $2; + +-- name: UpdatePrivilege :exec +UPDATE privileges SET amount = $1, created_at = $2 WHERE account_id = $3 AND privilegeID = $4; + +-- name: InsertPrivilege :exec +INSERT INTO privileges (privilegeID, account_id, privilege_name, amount, created_at) VALUES ($1, $2, $3, $4, $5); + +-- name: GetQuizByQid :one +SELECT * FROM quiz +WHERE + deleted = false AND + archived = false AND + status = 'start' AND + qid = $1; + +-- name: GetQuestionTitle :one +SELECT title, questiontype FROM question WHERE id = $1; + +-- name: WorkerTimeoutProcess :exec +UPDATE quiz SET status = 'timeout' WHERE deleted = false AND due_to <> 0 AND due_to < EXTRACT(epoch FROM CURRENT_TIMESTAMP); + +-- name: GetQuizById :one +SELECT * FROM quiz WHERE id=$1 AND accountId=$2; + +-- name: InsertPrivilegeWC :exec +UPDATE privileges SET amount = $1, created_at = $2 WHERE account_id = $3 AND privilegeID = $4; + +-- name: GetPrivilegesByAccountIDWC :many +SELECT p.id,p.privilegeID,p.privilege_name,p.amount, p.created_at FROM privileges as p JOIN account as a on p.account_id = a.id WHERE a.user_id = $1; + +-- name: WorkerStatProcess :exec +WITH answer_aggregates AS ( + SELECT + quiz_id, + COUNT(DISTINCT session) AS unique_true_answers_count + FROM + answer + WHERE + result = TRUE + GROUP BY + quiz_id +), + question_aggregates AS ( + SELECT + q.id AS quiz_id, + COUNT(qs.id) AS total_questions + FROM + quiz q + INNER JOIN + question qs ON q.id = qs.quiz_id + WHERE + q.deleted = false + AND q.archived = false + AND qs.deleted = false + GROUP BY + q.id + ), + session_times_aggregates AS ( + SELECT + quiz_id, COUNT(session) as sess, + AVG(extract(epoch FROM session_time)) AS average_session_time + FROM ( + SELECT + quiz_id, + session, + (MAX(created_at) - MIN(created_at)) AS session_time + FROM + answer + GROUP BY + quiz_id, + session + ) AS all_sessions + GROUP BY + quiz_id + ) +UPDATE quiz q +SET + questions_count = COALESCE(qa.total_questions, 0), + answers_count = COALESCE(aa.unique_true_answers_count, 0), + average_time_passing = COALESCE(sta.average_session_time, 0), + sessions_count = COALESCE(sta.sess,0) + FROM + (SELECT * FROM quiz WHERE deleted = FALSE AND archived = FALSE) q_sub +LEFT JOIN answer_aggregates aa ON q_sub.id = aa.quiz_id + LEFT JOIN question_aggregates qa ON q_sub.id = qa.quiz_id + LEFT JOIN session_times_aggregates sta ON q_sub.id = sta.quiz_id +WHERE + q.id = q_sub.id; + +-- name: UpdatePrivilegeAmount :exec +UPDATE privileges SET amount = $1 WHERE id = $2; + +-- name: GetAccAndPrivilegeByEmail :one +SELECT + a.id, + a.user_id, + a.email, + a.created_at, + p.ID, + p.privilegeid, + p.amount, + p.created_at +FROM + account AS a + LEFT JOIN privileges AS p ON a.id = p.account_id +WHERE + a.user_id = $1; + +-- name: DeletePrivilegeByID :exec +DELETE FROM privileges WHERE id = $1; + +-- name: GetQuizConfig :one +SELECT config, accountid FROM quiz WHERE id = $1 AND deleted = false; + +-- name: GetExpiredPrivilege :many +SELECT id, privilegeID, privilege_name, amount, created_at +FROM privileges +WHERE created_at + amount * interval '1 day' < NOW() + AND privilegeid = $1; + +-- name: CheckAndAddDefault :exec +UPDATE privileges +SET amount = $1, created_at = NOW() +WHERE privilege_name = $2 + AND (amount < $3 OR created_at <= NOW() - INTERVAL '1 month'); + +-- name: GetAllAnswersByQuizID :many +SELECT DISTINCT ON(question_id) content, created_at, question_id, id FROM answer WHERE session = $1 ORDER BY question_id ASC, created_at DESC; + +-- name: InsertAnswers :exec +INSERT INTO answer( + content, + quiz_id, + question_id, + fingerprint, + session, + result +) VALUES ($1,$2,$3,$4,$5,$6); + +-- name: GetResultAnswers :many +SELECT DISTINCT on (question_id) id, content, quiz_id, question_id, fingerprint, session,created_at, result, new,deleted FROM answer WHERE session = ( + SELECT session FROM answer WHERE answer.id = $1) ORDER BY question_id, created_at DESC; + +-- name: GetQuestions :many +SELECT id, quiz_id, title, description, questiontype, required, deleted, page, content, version, parent_ids, created_at, updated_at FROM question WHERE quiz_id = $1 AND deleted = FALSE; + +-- name: SoftDeleteResultByID :exec +UPDATE answer SET deleted = TRUE WHERE id = $1 AND deleted = FALSE; + +-- name: CheckResultsOwner :many +SELECT a.id +FROM answer a + JOIN quiz q ON a.quiz_id = q.id +WHERE a.id = ANY($1::bigint[]) AND a.deleted = FALSE AND q.accountid = $2; + +-- name: CheckResultOwner :one +SELECT q.accountid FROM answer a JOIN quiz q ON a.quiz_id = q.id WHERE a.id = $1 AND a.deleted = FALSE; diff --git a/dal/schema/000001_init.down.sql b/dal/schema/000001_init.down.sql new file mode 100644 index 0000000..54966a0 --- /dev/null +++ b/dal/schema/000001_init.down.sql @@ -0,0 +1,24 @@ +-- Drop indexes +DROP INDEX IF EXISTS subquizes; +DROP INDEX IF EXISTS birthtime; +DROP INDEX IF EXISTS groups; +DROP INDEX IF EXISTS timeouted; +DROP INDEX IF EXISTS active ON quiz; +DROP INDEX IF EXISTS questiontype; +DROP INDEX IF EXISTS required; +DROP INDEX IF EXISTS relation; +DROP INDEX IF EXISTS active ON question; + +-- Drop tables +DROP TABLE IF EXISTS privileges; +DROP TABLE IF EXISTS answer; +DROP TABLE IF EXISTS question; +DROP TABLE IF EXISTS quiz; +DROP TABLE IF EXISTS account; + +-- Drop types +DO $$ +BEGIN +DROP TYPE IF EXISTS question_type; +DROP TYPE IF EXISTS quiz_status; +END$$; \ No newline at end of file diff --git a/dal/schema/000001_init.up.sql b/dal/schema/000001_init.up.sql new file mode 100644 index 0000000..9aefa67 --- /dev/null +++ b/dal/schema/000001_init.up.sql @@ -0,0 +1,120 @@ +-- Create types +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'question_type') THEN +CREATE TYPE question_type AS ENUM ( + 'variant', + 'images', + 'varimg', + 'emoji', + 'text', + 'select', + 'date', + 'number', + 'file', + 'page', + 'rating' + ); +END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'quiz_status') THEN +CREATE TYPE quiz_status AS ENUM ( + 'draft', + 'template', + 'stop', + 'start', + 'timeout', + 'offlimit' + ); +END IF; + + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +END$$; + +-- Create tables +CREATE TABLE IF NOT EXISTS account ( + id UUID PRIMARY KEY, + user_id VARCHAR(24), + email VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted BOOLEAN DEFAULT false + ); + +CREATE TABLE IF NOT EXISTS quiz ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + qid uuid DEFAULT uuid_generate_v4(), + accountid varchar(30) NOT NULL, + deleted boolean DEFAULT false, + archived boolean DEFAULT false, + fingerprinting boolean DEFAULT false, + repeatable boolean DEFAULT false, + note_prevented boolean DEFAULT false, + mail_notifications boolean DEFAULT false, + unique_answers boolean DEFAULT false, + super boolean DEFAULT false, + group_id bigint DEFAULT 0, + name varchar(280), + description text, + config text, + status quiz_status DEFAULT 'draft', + limit_answers integer DEFAULT 0, + due_to integer DEFAULT 0, + time_of_passing integer DEFAULT 0, + pausable boolean DEFAULT false, + version smallint DEFAULT 0, + version_comment text DEFAULT '', + parent_ids integer[], + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + questions_count integer DEFAULT 0, + answers_count integer DEFAULT 0, + average_time_passing integer DEFAULT 0 + ); + +CREATE TABLE IF NOT EXISTS question ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + quiz_id bigint NOT NULL, + title varchar(512) NOT NULL, + description text, + questiontype question_type DEFAULT 'text', + required boolean DEFAULT false, + deleted boolean DEFAULT false, + page smallint DEFAULT 0, + content text, + version smallint DEFAULT 0, + parent_ids integer[], + created_at timestamp DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT quiz_relation FOREIGN KEY(quiz_id) REFERENCES quiz(id) + ); + +CREATE TABLE IF NOT EXISTS answer ( + id bigserial UNIQUE NOT NULL PRIMARY KEY, + content text, + quiz_id bigint NOT NULL REFERENCES quiz(id), + question_id bigint NOT NULL REFERENCES question(id), + fingerprint varchar(1024), + session varchar(20), + created_at timestamp DEFAULT CURRENT_TIMESTAMP + ); + +CREATE TABLE IF NOT EXISTS privileges ( + id SERIAL PRIMARY KEY, + privilegeID VARCHAR(50), + account_id UUID, + privilege_name VARCHAR(255), + amount INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES account (id) + ); + +-- Create indexes +CREATE INDEX IF NOT EXISTS active ON question(deleted) WHERE deleted=false; +CREATE INDEX IF NOT EXISTS relation ON question(quiz_id DESC); +CREATE INDEX IF NOT EXISTS required ON question(required DESC); +CREATE INDEX IF NOT EXISTS questiontype ON question(questiontype); +CREATE INDEX IF NOT EXISTS active ON quiz(deleted, archived, status) WHERE deleted = false AND archived = false AND status = 'start'; +CREATE INDEX IF NOT EXISTS timeouted ON quiz(due_to DESC) WHERE deleted = false AND due_to <> 0 AND status <> 'timeout'; +CREATE INDEX IF NOT EXISTS groups ON quiz(super) WHERE super = true; +CREATE INDEX IF NOT EXISTS birthtime ON quiz(created_at DESC); +CREATE INDEX IF NOT EXISTS subquizes ON quiz(group_id DESC) WHERE group_id <> 0; diff --git a/dal/schema/000002_init.down.sql b/dal/schema/000002_init.down.sql new file mode 100644 index 0000000..0aa3d9e --- /dev/null +++ b/dal/schema/000002_init.down.sql @@ -0,0 +1 @@ +ALTER TABLE answer DROP COLUMN IF EXISTS result; diff --git a/dal/schema/000002_init.up.sql b/dal/schema/000002_init.up.sql new file mode 100644 index 0000000..4083153 --- /dev/null +++ b/dal/schema/000002_init.up.sql @@ -0,0 +1 @@ +ALTER TABLE answer ADD COLUMN result BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/dal/schema/000003_init.down.sql b/dal/schema/000003_init.down.sql new file mode 100644 index 0000000..94e352f --- /dev/null +++ b/dal/schema/000003_init.down.sql @@ -0,0 +1 @@ +ALTER TABLE quiz DROP COLUMN IF EXISTS sessions_count; diff --git a/dal/schema/000003_init.up.sql b/dal/schema/000003_init.up.sql new file mode 100644 index 0000000..a292d62 --- /dev/null +++ b/dal/schema/000003_init.up.sql @@ -0,0 +1 @@ +ALTER TABLE quiz ADD COLUMN sessions_count integer; diff --git a/dal/schema/000004_init.down.sql b/dal/schema/000004_init.down.sql new file mode 100644 index 0000000..b6d5ec4 --- /dev/null +++ b/dal/schema/000004_init.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE quiz DROP COLUMN IF EXISTS new; +ALTER TABLE quiz DROP COLUMN IF EXISTS deleted; \ No newline at end of file diff --git a/dal/schema/000004_init.up.sql b/dal/schema/000004_init.up.sql new file mode 100644 index 0000000..9dc5591 --- /dev/null +++ b/dal/schema/000004_init.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE answer ADD COLUMN new BOOLEAN DEFAULT TRUE; +ALTER TABLE answer ADD COLUMN deleted BOOLEAN DEFAULT FALSE; \ No newline at end of file diff --git a/dal/sqlcgen/db.go b/dal/sqlcgen/db.go new file mode 100644 index 0000000..6e5541c --- /dev/null +++ b/dal/sqlcgen/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package sqlcgen + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/dal/sqlcgen/models.go b/dal/sqlcgen/models.go new file mode 100644 index 0000000..4eae354 --- /dev/null +++ b/dal/sqlcgen/models.go @@ -0,0 +1,89 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package sqlcgen + +import ( + "database/sql" + + "github.com/google/uuid" +) + +type Account struct { + ID uuid.UUID `db:"id" json:"id"` + UserID sql.NullString `db:"user_id" json:"user_id"` + Email sql.NullString `db:"email" json:"email"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` +} + +type Answer struct { + ID int64 `db:"id" json:"id"` + Content sql.NullString `db:"content" json:"content"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + QuestionID int64 `db:"question_id" json:"question_id"` + Fingerprint sql.NullString `db:"fingerprint" json:"fingerprint"` + Session sql.NullString `db:"session" json:"session"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + Result sql.NullBool `db:"result" json:"result"` + New sql.NullBool `db:"new" json:"new"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` +} + +type Privilege struct { + ID int32 `db:"id" json:"id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + AccountID uuid.NullUUID `db:"account_id" json:"account_id"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +type Question struct { + ID int64 `db:"id" json:"id"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + Title string `db:"title" json:"title"` + Description sql.NullString `db:"description" json:"description"` + Questiontype interface{} `db:"questiontype" json:"questiontype"` + Required sql.NullBool `db:"required" json:"required"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` + Page sql.NullInt16 `db:"page" json:"page"` + Content sql.NullString `db:"content" json:"content"` + Version sql.NullInt16 `db:"version" json:"version"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +type Quiz struct { + ID int64 `db:"id" json:"id"` + Qid uuid.NullUUID `db:"qid" json:"qid"` + Accountid string `db:"accountid" json:"accountid"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` + Archived sql.NullBool `db:"archived" json:"archived"` + Fingerprinting sql.NullBool `db:"fingerprinting" json:"fingerprinting"` + Repeatable sql.NullBool `db:"repeatable" json:"repeatable"` + NotePrevented sql.NullBool `db:"note_prevented" json:"note_prevented"` + MailNotifications sql.NullBool `db:"mail_notifications" json:"mail_notifications"` + UniqueAnswers sql.NullBool `db:"unique_answers" json:"unique_answers"` + Super sql.NullBool `db:"super" json:"super"` + GroupID sql.NullInt64 `db:"group_id" json:"group_id"` + Name sql.NullString `db:"name" json:"name"` + Description sql.NullString `db:"description" json:"description"` + Config sql.NullString `db:"config" json:"config"` + Status interface{} `db:"status" json:"status"` + LimitAnswers sql.NullInt32 `db:"limit_answers" json:"limit_answers"` + DueTo sql.NullInt32 `db:"due_to" json:"due_to"` + TimeOfPassing sql.NullInt32 `db:"time_of_passing" json:"time_of_passing"` + Pausable sql.NullBool `db:"pausable" json:"pausable"` + Version sql.NullInt16 `db:"version" json:"version"` + VersionComment sql.NullString `db:"version_comment" json:"version_comment"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + QuestionsCount sql.NullInt32 `db:"questions_count" json:"questions_count"` + AnswersCount sql.NullInt32 `db:"answers_count" json:"answers_count"` + AverageTimePassing sql.NullInt32 `db:"average_time_passing" json:"average_time_passing"` + SessionsCount sql.NullInt32 `db:"sessions_count" json:"sessions_count"` +} diff --git a/dal/sqlcgen/queries.sql.go b/dal/sqlcgen/queries.sql.go new file mode 100644 index 0000000..fe88502 --- /dev/null +++ b/dal/sqlcgen/queries.sql.go @@ -0,0 +1,1404 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: queries.sql + +package sqlcgen + +import ( + "context" + "database/sql" + + "github.com/google/uuid" + "github.com/lib/pq" +) + +const accountPagination = `-- name: AccountPagination :many +SELECT a.id, a.user_id, a.created_at, a.deleted +FROM account a ORDER BY a.created_at DESC LIMIT $1 OFFSET $2 +` + +type AccountPaginationParams struct { + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` +} + +type AccountPaginationRow struct { + ID uuid.UUID `db:"id" json:"id"` + UserID sql.NullString `db:"user_id" json:"user_id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` +} + +func (q *Queries) AccountPagination(ctx context.Context, arg AccountPaginationParams) ([]AccountPaginationRow, error) { + rows, err := q.db.QueryContext(ctx, accountPagination, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []AccountPaginationRow + for rows.Next() { + var i AccountPaginationRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.CreatedAt, + &i.Deleted, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const archiveQuiz = `-- name: ArchiveQuiz :exec +UPDATE quiz SET archived = true WHERE id=$1 AND accountId=$2 +` + +type ArchiveQuizParams struct { + ID int64 `db:"id" json:"id"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) ArchiveQuiz(ctx context.Context, arg ArchiveQuizParams) error { + _, err := q.db.ExecContext(ctx, archiveQuiz, arg.ID, arg.Accountid) + return err +} + +const checkAndAddDefault = `-- name: CheckAndAddDefault :exec +UPDATE privileges +SET amount = $1, created_at = NOW() +WHERE privilege_name = $2 + AND (amount < $3 OR created_at <= NOW() - INTERVAL '1 month') +` + +type CheckAndAddDefaultParams struct { + Amount sql.NullInt32 `db:"amount" json:"amount"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount_2 sql.NullInt32 `db:"amount_2" json:"amount_2"` +} + +func (q *Queries) CheckAndAddDefault(ctx context.Context, arg CheckAndAddDefaultParams) error { + _, err := q.db.ExecContext(ctx, checkAndAddDefault, arg.Amount, arg.PrivilegeName, arg.Amount_2) + return err +} + +const checkResultOwner = `-- name: CheckResultOwner :one +SELECT q.accountid FROM answer a JOIN quiz q ON a.quiz_id = q.id WHERE a.id = $1 AND a.deleted = FALSE +` + +func (q *Queries) CheckResultOwner(ctx context.Context, id int64) (string, error) { + row := q.db.QueryRowContext(ctx, checkResultOwner, id) + var accountid string + err := row.Scan(&accountid) + return accountid, err +} + +const checkResultsOwner = `-- name: CheckResultsOwner :many +SELECT a.id +FROM answer a + JOIN quiz q ON a.quiz_id = q.id +WHERE a.id = ANY($1::bigint[]) AND a.deleted = FALSE AND q.accountid = $2 +` + +type CheckResultsOwnerParams struct { + Column1 []int64 `db:"column_1" json:"column_1"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) CheckResultsOwner(ctx context.Context, arg CheckResultsOwnerParams) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, checkResultsOwner, pq.Array(arg.Column1), arg.Accountid) + 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.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const copyQuestion = `-- name: CopyQuestion :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +) +SELECT $1, title, description, questiontype, required, + page, content, version, parent_ids +FROM question WHERE question.id=$2 + RETURNING question.id, quiz_id, created_at, updated_at +` + +type CopyQuestionParams struct { + QuizID int64 `db:"quiz_id" json:"quiz_id"` + ID int64 `db:"id" json:"id"` +} + +type CopyQuestionRow struct { + ID int64 `db:"id" json:"id"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *Queries) CopyQuestion(ctx context.Context, arg CopyQuestionParams) (CopyQuestionRow, error) { + row := q.db.QueryRowContext(ctx, copyQuestion, arg.QuizID, arg.ID) + var i CopyQuestionRow + err := row.Scan( + &i.ID, + &i.QuizID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const copyQuiz = `-- name: CopyQuiz :one +INSERT INTO quiz( + accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +) +SELECT accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +FROM quiz WHERE quiz.id=$1 AND quiz.accountId=$2 + RETURNING id, qid,created_at, updated_at +` + +type CopyQuizParams struct { + ID int64 `db:"id" json:"id"` + Accountid string `db:"accountid" json:"accountid"` +} + +type CopyQuizRow struct { + ID int64 `db:"id" json:"id"` + Qid uuid.NullUUID `db:"qid" json:"qid"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *Queries) CopyQuiz(ctx context.Context, arg CopyQuizParams) (CopyQuizRow, error) { + row := q.db.QueryRowContext(ctx, copyQuiz, arg.ID, arg.Accountid) + var i CopyQuizRow + err := row.Scan( + &i.ID, + &i.Qid, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const copyQuizQuestions = `-- name: CopyQuizQuestions :exec +INSERT INTO question( + quiz_id, title, description, questiontype, required, page, content, version, parent_ids +) +SELECT $2, title, description, questiontype, required, page, content, version, parent_ids +FROM question WHERE question.quiz_id=$1 AND deleted=false +` + +type CopyQuizQuestionsParams struct { + QuizID int64 `db:"quiz_id" json:"quiz_id"` + QuizID_2 int64 `db:"quiz_id_2" json:"quiz_id_2"` +} + +func (q *Queries) CopyQuizQuestions(ctx context.Context, arg CopyQuizQuestionsParams) error { + _, err := q.db.ExecContext(ctx, copyQuizQuestions, arg.QuizID, arg.QuizID_2) + return err +} + +const createAccount = `-- name: CreateAccount :exec +INSERT INTO account (id, user_id, email, created_at, deleted) VALUES ($1, $2, $3, $4, $5) +` + +type CreateAccountParams struct { + ID uuid.UUID `db:"id" json:"id"` + UserID sql.NullString `db:"user_id" json:"user_id"` + Email sql.NullString `db:"email" json:"email"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) error { + _, err := q.db.ExecContext(ctx, createAccount, + arg.ID, + arg.UserID, + arg.Email, + arg.CreatedAt, + arg.Deleted, + ) + return err +} + +const deleteAccountById = `-- name: DeleteAccountById :exec +DELETE FROM account WHERE id = $1 +` + +func (q *Queries) DeleteAccountById(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteAccountById, id) + return err +} + +const deletePrivilegeByAccID = `-- name: DeletePrivilegeByAccID :exec +DELETE FROM privileges WHERE account_id = $1 +` + +func (q *Queries) DeletePrivilegeByAccID(ctx context.Context, accountID uuid.NullUUID) error { + _, err := q.db.ExecContext(ctx, deletePrivilegeByAccID, accountID) + return err +} + +const deletePrivilegeByID = `-- name: DeletePrivilegeByID :exec +DELETE FROM privileges WHERE id = $1 +` + +func (q *Queries) DeletePrivilegeByID(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deletePrivilegeByID, id) + return err +} + +const deleteQuestion = `-- name: DeleteQuestion :one +UPDATE question SET deleted=true WHERE id=$1 RETURNING question.id, question.quiz_id, question.title, question.description, question.questiontype, question.required, question.deleted, question.page, question.content, question.version, question.parent_ids, question.created_at, question.updated_at +` + +func (q *Queries) DeleteQuestion(ctx context.Context, id int64) (Question, error) { + row := q.db.QueryRowContext(ctx, deleteQuestion, id) + var i Question + err := row.Scan( + &i.ID, + &i.QuizID, + &i.Title, + &i.Description, + &i.Questiontype, + &i.Required, + &i.Deleted, + &i.Page, + &i.Content, + &i.Version, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteQuizByID = `-- name: DeleteQuizByID :one +UPDATE quiz SET deleted=true WHERE quiz.id=$1 AND accountid=$2 RETURNING quiz.id, quiz.qid, quiz.accountid, quiz.deleted, quiz.archived, quiz.fingerprinting, quiz.repeatable, quiz.note_prevented, quiz.mail_notifications, quiz.unique_answers, quiz.super, quiz.group_id, quiz.name, quiz.description, quiz.config, quiz.status, quiz.limit_answers, quiz.due_to, quiz.time_of_passing, quiz.pausable, quiz.version, quiz.version_comment, quiz.parent_ids, quiz.created_at, quiz.updated_at, quiz.questions_count, quiz.answers_count, quiz.average_time_passing, quiz.sessions_count +` + +type DeleteQuizByIDParams struct { + ID int64 `db:"id" json:"id"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) DeleteQuizByID(ctx context.Context, arg DeleteQuizByIDParams) (Quiz, error) { + row := q.db.QueryRowContext(ctx, deleteQuizByID, arg.ID, arg.Accountid) + var i Quiz + err := row.Scan( + &i.ID, + &i.Qid, + &i.Accountid, + &i.Deleted, + &i.Archived, + &i.Fingerprinting, + &i.Repeatable, + &i.NotePrevented, + &i.MailNotifications, + &i.UniqueAnswers, + &i.Super, + &i.GroupID, + &i.Name, + &i.Description, + &i.Config, + &i.Status, + &i.LimitAnswers, + &i.DueTo, + &i.TimeOfPassing, + &i.Pausable, + &i.Version, + &i.VersionComment, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + &i.QuestionsCount, + &i.AnswersCount, + &i.AverageTimePassing, + &i.SessionsCount, + ) + return i, err +} + +const duplicateQuestion = `-- name: DuplicateQuestion :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +) +SELECT quiz_id, title, description, questiontype, required, + page, content, version, parent_ids +FROM question WHERE question.id=$1 + RETURNING question.id, quiz_id, created_at, updated_at +` + +type DuplicateQuestionRow struct { + ID int64 `db:"id" json:"id"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *Queries) DuplicateQuestion(ctx context.Context, id int64) (DuplicateQuestionRow, error) { + row := q.db.QueryRowContext(ctx, duplicateQuestion, id) + var i DuplicateQuestionRow + err := row.Scan( + &i.ID, + &i.QuizID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getAccAndPrivilegeByEmail = `-- name: GetAccAndPrivilegeByEmail :one +SELECT + a.id, + a.user_id, + a.email, + a.created_at, + p.ID, + p.privilegeid, + p.amount, + p.created_at +FROM + account AS a + LEFT JOIN privileges AS p ON a.id = p.account_id +WHERE + a.user_id = $1 +` + +type GetAccAndPrivilegeByEmailRow struct { + ID uuid.UUID `db:"id" json:"id"` + UserID sql.NullString `db:"user_id" json:"user_id"` + Email sql.NullString `db:"email" json:"email"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + ID_2 sql.NullInt32 `db:"id_2" json:"id_2"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt_2 sql.NullTime `db:"created_at_2" json:"created_at_2"` +} + +func (q *Queries) GetAccAndPrivilegeByEmail(ctx context.Context, userID sql.NullString) (GetAccAndPrivilegeByEmailRow, error) { + row := q.db.QueryRowContext(ctx, getAccAndPrivilegeByEmail, userID) + var i GetAccAndPrivilegeByEmailRow + err := row.Scan( + &i.ID, + &i.UserID, + &i.Email, + &i.CreatedAt, + &i.ID_2, + &i.Privilegeid, + &i.Amount, + &i.CreatedAt_2, + ) + return i, err +} + +const getAccountWithPrivileges = `-- name: GetAccountWithPrivileges :many +SELECT a.id, a.user_id, a.created_at, a.deleted, + p.id AS privilege_id, p.privilegeID, p.privilege_name, p.amount, p.created_at AS privilege_created_at +FROM account a + LEFT JOIN privileges AS p ON a.id = p.account_id +WHERE a.user_id = $1 +` + +type GetAccountWithPrivilegesRow struct { + ID uuid.UUID `db:"id" json:"id"` + UserID sql.NullString `db:"user_id" json:"user_id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + Deleted sql.NullBool `db:"deleted" json:"deleted"` + PrivilegeID sql.NullInt32 `db:"privilege_id" json:"privilege_id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + PrivilegeCreatedAt sql.NullTime `db:"privilege_created_at" json:"privilege_created_at"` +} + +func (q *Queries) GetAccountWithPrivileges(ctx context.Context, userID sql.NullString) ([]GetAccountWithPrivilegesRow, error) { + rows, err := q.db.QueryContext(ctx, getAccountWithPrivileges, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAccountWithPrivilegesRow + for rows.Next() { + var i GetAccountWithPrivilegesRow + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.CreatedAt, + &i.Deleted, + &i.PrivilegeID, + &i.Privilegeid, + &i.PrivilegeName, + &i.Amount, + &i.PrivilegeCreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllAnswersByQuizID = `-- name: GetAllAnswersByQuizID :many +SELECT DISTINCT ON(question_id) content, created_at, question_id, id FROM answer WHERE session = $1 ORDER BY question_id ASC, created_at DESC +` + +type GetAllAnswersByQuizIDRow struct { + Content sql.NullString `db:"content" json:"content"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + QuestionID int64 `db:"question_id" json:"question_id"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) GetAllAnswersByQuizID(ctx context.Context, session sql.NullString) ([]GetAllAnswersByQuizIDRow, error) { + rows, err := q.db.QueryContext(ctx, getAllAnswersByQuizID, session) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllAnswersByQuizIDRow + for rows.Next() { + var i GetAllAnswersByQuizIDRow + if err := rows.Scan( + &i.Content, + &i.CreatedAt, + &i.QuestionID, + &i.ID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getExpiredPrivilege = `-- name: GetExpiredPrivilege :many +SELECT id, privilegeID, privilege_name, amount, created_at +FROM privileges +WHERE created_at + amount * interval '1 day' < NOW() + AND privilegeid = $1 +` + +type GetExpiredPrivilegeRow struct { + ID int32 `db:"id" json:"id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +func (q *Queries) GetExpiredPrivilege(ctx context.Context, privilegeid sql.NullString) ([]GetExpiredPrivilegeRow, error) { + rows, err := q.db.QueryContext(ctx, getExpiredPrivilege, privilegeid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetExpiredPrivilegeRow + for rows.Next() { + var i GetExpiredPrivilegeRow + if err := rows.Scan( + &i.ID, + &i.Privilegeid, + &i.PrivilegeName, + &i.Amount, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPrivilegesByAccountID = `-- name: GetPrivilegesByAccountID :many +SELECT id,privilegeID,privilege_name,amount, created_at FROM privileges WHERE account_id = $1 +` + +type GetPrivilegesByAccountIDRow struct { + ID int32 `db:"id" json:"id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +func (q *Queries) GetPrivilegesByAccountID(ctx context.Context, accountID uuid.NullUUID) ([]GetPrivilegesByAccountIDRow, error) { + rows, err := q.db.QueryContext(ctx, getPrivilegesByAccountID, accountID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPrivilegesByAccountIDRow + for rows.Next() { + var i GetPrivilegesByAccountIDRow + if err := rows.Scan( + &i.ID, + &i.Privilegeid, + &i.PrivilegeName, + &i.Amount, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPrivilegesByAccountIDWC = `-- name: GetPrivilegesByAccountIDWC :many +SELECT p.id,p.privilegeID,p.privilege_name,p.amount, p.created_at FROM privileges as p JOIN account as a on p.account_id = a.id WHERE a.user_id = $1 +` + +type GetPrivilegesByAccountIDWCRow struct { + ID int32 `db:"id" json:"id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +func (q *Queries) GetPrivilegesByAccountIDWC(ctx context.Context, userID sql.NullString) ([]GetPrivilegesByAccountIDWCRow, error) { + rows, err := q.db.QueryContext(ctx, getPrivilegesByAccountIDWC, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPrivilegesByAccountIDWCRow + for rows.Next() { + var i GetPrivilegesByAccountIDWCRow + if err := rows.Scan( + &i.ID, + &i.Privilegeid, + &i.PrivilegeName, + &i.Amount, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getPrivilegesQuizAccount = `-- name: GetPrivilegesQuizAccount :many +SELECT + p.privilegeID, + p.privilege_name, + p.amount, + p.created_at, + a.id, + a.email, + qz.config +FROM + privileges AS p + INNER JOIN account AS a ON p.account_id = a.id + INNER JOIN quiz AS qz ON qz.accountid = a.user_id +WHERE + qz.id = $1 +` + +type GetPrivilegesQuizAccountRow struct { + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + ID uuid.UUID `db:"id" json:"id"` + Email sql.NullString `db:"email" json:"email"` + Config sql.NullString `db:"config" json:"config"` +} + +func (q *Queries) GetPrivilegesQuizAccount(ctx context.Context, id int64) ([]GetPrivilegesQuizAccountRow, error) { + rows, err := q.db.QueryContext(ctx, getPrivilegesQuizAccount, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPrivilegesQuizAccountRow + for rows.Next() { + var i GetPrivilegesQuizAccountRow + if err := rows.Scan( + &i.Privilegeid, + &i.PrivilegeName, + &i.Amount, + &i.CreatedAt, + &i.ID, + &i.Email, + &i.Config, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getQuestionHistory = `-- name: GetQuestionHistory :many +SELECT id, quiz_id, title, description, questiontype, required, deleted, page, content, version, parent_ids, created_at, updated_at FROM question WHERE question.id = $1 OR question.id = ANY( + SELECT unnest(parent_ids) FROM question WHERE id = $1 +) ORDER BY question.id DESC LIMIT $2 OFFSET $3 +` + +type GetQuestionHistoryParams struct { + ID int64 `db:"id" json:"id"` + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` +} + +func (q *Queries) GetQuestionHistory(ctx context.Context, arg GetQuestionHistoryParams) ([]Question, error) { + rows, err := q.db.QueryContext(ctx, getQuestionHistory, arg.ID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Question + for rows.Next() { + var i Question + if err := rows.Scan( + &i.ID, + &i.QuizID, + &i.Title, + &i.Description, + &i.Questiontype, + &i.Required, + &i.Deleted, + &i.Page, + &i.Content, + &i.Version, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getQuestionTitle = `-- name: GetQuestionTitle :one +SELECT title, questiontype FROM question WHERE id = $1 +` + +type GetQuestionTitleRow struct { + Title string `db:"title" json:"title"` + Questiontype interface{} `db:"questiontype" json:"questiontype"` +} + +func (q *Queries) GetQuestionTitle(ctx context.Context, id int64) (GetQuestionTitleRow, error) { + row := q.db.QueryRowContext(ctx, getQuestionTitle, id) + var i GetQuestionTitleRow + err := row.Scan(&i.Title, &i.Questiontype) + return i, err +} + +const getQuestions = `-- name: GetQuestions :many +SELECT id, quiz_id, title, description, questiontype, required, deleted, page, content, version, parent_ids, created_at, updated_at FROM question WHERE quiz_id = $1 AND deleted = FALSE +` + +func (q *Queries) GetQuestions(ctx context.Context, quizID int64) ([]Question, error) { + rows, err := q.db.QueryContext(ctx, getQuestions, quizID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Question + for rows.Next() { + var i Question + if err := rows.Scan( + &i.ID, + &i.QuizID, + &i.Title, + &i.Description, + &i.Questiontype, + &i.Required, + &i.Deleted, + &i.Page, + &i.Content, + &i.Version, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getQuizById = `-- name: GetQuizById :one +SELECT id, qid, accountid, deleted, archived, fingerprinting, repeatable, note_prevented, mail_notifications, unique_answers, super, group_id, name, description, config, status, limit_answers, due_to, time_of_passing, pausable, version, version_comment, parent_ids, created_at, updated_at, questions_count, answers_count, average_time_passing, sessions_count FROM quiz WHERE id=$1 AND accountId=$2 +` + +type GetQuizByIdParams struct { + ID int64 `db:"id" json:"id"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) GetQuizById(ctx context.Context, arg GetQuizByIdParams) (Quiz, error) { + row := q.db.QueryRowContext(ctx, getQuizById, arg.ID, arg.Accountid) + var i Quiz + err := row.Scan( + &i.ID, + &i.Qid, + &i.Accountid, + &i.Deleted, + &i.Archived, + &i.Fingerprinting, + &i.Repeatable, + &i.NotePrevented, + &i.MailNotifications, + &i.UniqueAnswers, + &i.Super, + &i.GroupID, + &i.Name, + &i.Description, + &i.Config, + &i.Status, + &i.LimitAnswers, + &i.DueTo, + &i.TimeOfPassing, + &i.Pausable, + &i.Version, + &i.VersionComment, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + &i.QuestionsCount, + &i.AnswersCount, + &i.AverageTimePassing, + &i.SessionsCount, + ) + return i, err +} + +const getQuizByQid = `-- name: GetQuizByQid :one +SELECT id, qid, accountid, deleted, archived, fingerprinting, repeatable, note_prevented, mail_notifications, unique_answers, super, group_id, name, description, config, status, limit_answers, due_to, time_of_passing, pausable, version, version_comment, parent_ids, created_at, updated_at, questions_count, answers_count, average_time_passing, sessions_count FROM quiz +WHERE + deleted = false AND + archived = false AND + status = 'start' AND + qid = $1 +` + +func (q *Queries) GetQuizByQid(ctx context.Context, qid uuid.NullUUID) (Quiz, error) { + row := q.db.QueryRowContext(ctx, getQuizByQid, qid) + var i Quiz + err := row.Scan( + &i.ID, + &i.Qid, + &i.Accountid, + &i.Deleted, + &i.Archived, + &i.Fingerprinting, + &i.Repeatable, + &i.NotePrevented, + &i.MailNotifications, + &i.UniqueAnswers, + &i.Super, + &i.GroupID, + &i.Name, + &i.Description, + &i.Config, + &i.Status, + &i.LimitAnswers, + &i.DueTo, + &i.TimeOfPassing, + &i.Pausable, + &i.Version, + &i.VersionComment, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + &i.QuestionsCount, + &i.AnswersCount, + &i.AverageTimePassing, + &i.SessionsCount, + ) + return i, err +} + +const getQuizConfig = `-- name: GetQuizConfig :one +SELECT config, accountid FROM quiz WHERE id = $1 AND deleted = false +` + +type GetQuizConfigRow struct { + Config sql.NullString `db:"config" json:"config"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) GetQuizConfig(ctx context.Context, id int64) (GetQuizConfigRow, error) { + row := q.db.QueryRowContext(ctx, getQuizConfig, id) + var i GetQuizConfigRow + err := row.Scan(&i.Config, &i.Accountid) + return i, err +} + +const getQuizHistory = `-- name: GetQuizHistory :many +SELECT id, qid, accountid, deleted, archived, fingerprinting, repeatable, note_prevented, mail_notifications, unique_answers, super, group_id, name, description, config, status, limit_answers, due_to, time_of_passing, pausable, version, version_comment, parent_ids, created_at, updated_at, questions_count, answers_count, average_time_passing, sessions_count FROM quiz WHERE quiz.id = $1 AND quiz.accountId = $4 OR quiz.id = ANY( + SELECT unnest(parent_ids) FROM quiz WHERE id = $1 +) ORDER BY quiz.id DESC LIMIT $2 OFFSET $3 +` + +type GetQuizHistoryParams struct { + ID int64 `db:"id" json:"id"` + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` + Accountid string `db:"accountid" json:"accountid"` +} + +func (q *Queries) GetQuizHistory(ctx context.Context, arg GetQuizHistoryParams) ([]Quiz, error) { + rows, err := q.db.QueryContext(ctx, getQuizHistory, + arg.ID, + arg.Limit, + arg.Offset, + arg.Accountid, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Quiz + for rows.Next() { + var i Quiz + if err := rows.Scan( + &i.ID, + &i.Qid, + &i.Accountid, + &i.Deleted, + &i.Archived, + &i.Fingerprinting, + &i.Repeatable, + &i.NotePrevented, + &i.MailNotifications, + &i.UniqueAnswers, + &i.Super, + &i.GroupID, + &i.Name, + &i.Description, + &i.Config, + &i.Status, + &i.LimitAnswers, + &i.DueTo, + &i.TimeOfPassing, + &i.Pausable, + &i.Version, + &i.VersionComment, + pq.Array(&i.ParentIds), + &i.CreatedAt, + &i.UpdatedAt, + &i.QuestionsCount, + &i.AnswersCount, + &i.AverageTimePassing, + &i.SessionsCount, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getResultAnswers = `-- name: GetResultAnswers :many +SELECT DISTINCT on (question_id) id, content, quiz_id, question_id, fingerprint, session,created_at, result, new,deleted FROM answer WHERE session = ( + SELECT session FROM answer WHERE answer.id = $1) ORDER BY question_id, created_at DESC +` + +func (q *Queries) GetResultAnswers(ctx context.Context, id int64) ([]Answer, error) { + rows, err := q.db.QueryContext(ctx, getResultAnswers, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Answer + for rows.Next() { + var i Answer + if err := rows.Scan( + &i.ID, + &i.Content, + &i.QuizID, + &i.QuestionID, + &i.Fingerprint, + &i.Session, + &i.CreatedAt, + &i.Result, + &i.New, + &i.Deleted, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertAnswers = `-- name: InsertAnswers :exec +INSERT INTO answer( + content, + quiz_id, + question_id, + fingerprint, + session, + result +) VALUES ($1,$2,$3,$4,$5,$6) +` + +type InsertAnswersParams struct { + Content sql.NullString `db:"content" json:"content"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + QuestionID int64 `db:"question_id" json:"question_id"` + Fingerprint sql.NullString `db:"fingerprint" json:"fingerprint"` + Session sql.NullString `db:"session" json:"session"` + Result sql.NullBool `db:"result" json:"result"` +} + +func (q *Queries) InsertAnswers(ctx context.Context, arg InsertAnswersParams) error { + _, err := q.db.ExecContext(ctx, insertAnswers, + arg.Content, + arg.QuizID, + arg.QuestionID, + arg.Fingerprint, + arg.Session, + arg.Result, + ) + return err +} + +const insertPrivilege = `-- name: InsertPrivilege :exec +INSERT INTO privileges (privilegeID, account_id, privilege_name, amount, created_at) VALUES ($1, $2, $3, $4, $5) +` + +type InsertPrivilegeParams struct { + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` + AccountID uuid.NullUUID `db:"account_id" json:"account_id"` + PrivilegeName sql.NullString `db:"privilege_name" json:"privilege_name"` + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` +} + +func (q *Queries) InsertPrivilege(ctx context.Context, arg InsertPrivilegeParams) error { + _, err := q.db.ExecContext(ctx, insertPrivilege, + arg.Privilegeid, + arg.AccountID, + arg.PrivilegeName, + arg.Amount, + arg.CreatedAt, + ) + return err +} + +const insertPrivilegeWC = `-- name: InsertPrivilegeWC :exec +UPDATE privileges SET amount = $1, created_at = $2 WHERE account_id = $3 AND privilegeID = $4 +` + +type InsertPrivilegeWCParams struct { + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + AccountID uuid.NullUUID `db:"account_id" json:"account_id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` +} + +func (q *Queries) InsertPrivilegeWC(ctx context.Context, arg InsertPrivilegeWCParams) error { + _, err := q.db.ExecContext(ctx, insertPrivilegeWC, + arg.Amount, + arg.CreatedAt, + arg.AccountID, + arg.Privilegeid, + ) + return err +} + +const insertQuestion = `-- name: InsertQuestion :one +INSERT INTO question ( + quiz_id, + title, + description, + questiontype, + required, + page, + content, + parent_ids, + updated_at +) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) + RETURNING id, created_at, updated_at +` + +type InsertQuestionParams struct { + QuizID int64 `db:"quiz_id" json:"quiz_id"` + Title string `db:"title" json:"title"` + Description sql.NullString `db:"description" json:"description"` + Questiontype interface{} `db:"questiontype" json:"questiontype"` + Required sql.NullBool `db:"required" json:"required"` + Page sql.NullInt16 `db:"page" json:"page"` + Content sql.NullString `db:"content" json:"content"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +type InsertQuestionRow struct { + ID int64 `db:"id" json:"id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` +} + +func (q *Queries) InsertQuestion(ctx context.Context, arg InsertQuestionParams) (InsertQuestionRow, error) { + row := q.db.QueryRowContext(ctx, insertQuestion, + arg.QuizID, + arg.Title, + arg.Description, + arg.Questiontype, + arg.Required, + arg.Page, + arg.Content, + pq.Array(arg.ParentIds), + arg.UpdatedAt, + ) + var i InsertQuestionRow + err := row.Scan(&i.ID, &i.CreatedAt, &i.UpdatedAt) + return i, err +} + +const insertQuiz = `-- name: InsertQuiz :one +INSERT INTO quiz (accountid, + fingerprinting, + repeatable, + note_prevented, + mail_notifications, + unique_answers, + super, + group_id, + name, + description, + config, + status, + limit_answers, + due_to, + time_of_passing, + pausable, + parent_ids, + questions_count, + qid +) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) + RETURNING id, created_at, updated_at, qid +` + +type InsertQuizParams struct { + Accountid string `db:"accountid" json:"accountid"` + Fingerprinting sql.NullBool `db:"fingerprinting" json:"fingerprinting"` + Repeatable sql.NullBool `db:"repeatable" json:"repeatable"` + NotePrevented sql.NullBool `db:"note_prevented" json:"note_prevented"` + MailNotifications sql.NullBool `db:"mail_notifications" json:"mail_notifications"` + UniqueAnswers sql.NullBool `db:"unique_answers" json:"unique_answers"` + Super sql.NullBool `db:"super" json:"super"` + GroupID sql.NullInt64 `db:"group_id" json:"group_id"` + Name sql.NullString `db:"name" json:"name"` + Description sql.NullString `db:"description" json:"description"` + Config sql.NullString `db:"config" json:"config"` + Status interface{} `db:"status" json:"status"` + LimitAnswers sql.NullInt32 `db:"limit_answers" json:"limit_answers"` + DueTo sql.NullInt32 `db:"due_to" json:"due_to"` + TimeOfPassing sql.NullInt32 `db:"time_of_passing" json:"time_of_passing"` + Pausable sql.NullBool `db:"pausable" json:"pausable"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` + QuestionsCount sql.NullInt32 `db:"questions_count" json:"questions_count"` + Qid uuid.NullUUID `db:"qid" json:"qid"` +} + +type InsertQuizRow struct { + ID int64 `db:"id" json:"id"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at" json:"updated_at"` + Qid uuid.NullUUID `db:"qid" json:"qid"` +} + +func (q *Queries) InsertQuiz(ctx context.Context, arg InsertQuizParams) (InsertQuizRow, error) { + row := q.db.QueryRowContext(ctx, insertQuiz, + arg.Accountid, + arg.Fingerprinting, + arg.Repeatable, + arg.NotePrevented, + arg.MailNotifications, + arg.UniqueAnswers, + arg.Super, + arg.GroupID, + arg.Name, + arg.Description, + arg.Config, + arg.Status, + arg.LimitAnswers, + arg.DueTo, + arg.TimeOfPassing, + arg.Pausable, + pq.Array(arg.ParentIds), + arg.QuestionsCount, + arg.Qid, + ) + var i InsertQuizRow + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Qid, + ) + return i, err +} + +const moveToHistory = `-- name: MoveToHistory :one +INSERT INTO question( + quiz_id, title, description, questiontype, required, + page, content, version, parent_ids, deleted +) +SELECT quiz_id, title, description, questiontype, required, + page, content, version, parent_ids, true as deleted +FROM question WHERE question.id=$1 + RETURNING question.id, quiz_id, parent_ids +` + +type MoveToHistoryRow struct { + ID int64 `db:"id" json:"id"` + QuizID int64 `db:"quiz_id" json:"quiz_id"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` +} + +func (q *Queries) MoveToHistory(ctx context.Context, id int64) (MoveToHistoryRow, error) { + row := q.db.QueryRowContext(ctx, moveToHistory, id) + var i MoveToHistoryRow + err := row.Scan(&i.ID, &i.QuizID, pq.Array(&i.ParentIds)) + return i, err +} + +const moveToHistoryQuiz = `-- name: MoveToHistoryQuiz :one +INSERT INTO quiz(deleted, + accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +) +SELECT true as deleted, accountid, archived,fingerprinting,repeatable,note_prevented,mail_notifications,unique_answers,name,description,config, + status,limit_answers,due_to,time_of_passing,pausable,version,version_comment,parent_ids,questions_count,answers_count,average_time_passing, super, group_id +FROM quiz WHERE quiz.id=$1 AND quiz.accountid=$2 + RETURNING quiz.id, qid, parent_ids +` + +type MoveToHistoryQuizParams struct { + ID int64 `db:"id" json:"id"` + Accountid string `db:"accountid" json:"accountid"` +} + +type MoveToHistoryQuizRow struct { + ID int64 `db:"id" json:"id"` + Qid uuid.NullUUID `db:"qid" json:"qid"` + ParentIds []int32 `db:"parent_ids" json:"parent_ids"` +} + +func (q *Queries) MoveToHistoryQuiz(ctx context.Context, arg MoveToHistoryQuizParams) (MoveToHistoryQuizRow, error) { + row := q.db.QueryRowContext(ctx, moveToHistoryQuiz, arg.ID, arg.Accountid) + var i MoveToHistoryQuizRow + err := row.Scan(&i.ID, &i.Qid, pq.Array(&i.ParentIds)) + return i, err +} + +const softDeleteResultByID = `-- name: SoftDeleteResultByID :exec +UPDATE answer SET deleted = TRUE WHERE id = $1 AND deleted = FALSE +` + +func (q *Queries) SoftDeleteResultByID(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, softDeleteResultByID, id) + return err +} + +const updatePrivilege = `-- name: UpdatePrivilege :exec +UPDATE privileges SET amount = $1, created_at = $2 WHERE account_id = $3 AND privilegeID = $4 +` + +type UpdatePrivilegeParams struct { + Amount sql.NullInt32 `db:"amount" json:"amount"` + CreatedAt sql.NullTime `db:"created_at" json:"created_at"` + AccountID uuid.NullUUID `db:"account_id" json:"account_id"` + Privilegeid sql.NullString `db:"privilegeid" json:"privilegeid"` +} + +func (q *Queries) UpdatePrivilege(ctx context.Context, arg UpdatePrivilegeParams) error { + _, err := q.db.ExecContext(ctx, updatePrivilege, + arg.Amount, + arg.CreatedAt, + arg.AccountID, + arg.Privilegeid, + ) + return err +} + +const updatePrivilegeAmount = `-- name: UpdatePrivilegeAmount :exec +UPDATE privileges SET amount = $1 WHERE id = $2 +` + +type UpdatePrivilegeAmountParams struct { + Amount sql.NullInt32 `db:"amount" json:"amount"` + ID int32 `db:"id" json:"id"` +} + +func (q *Queries) UpdatePrivilegeAmount(ctx context.Context, arg UpdatePrivilegeAmountParams) error { + _, err := q.db.ExecContext(ctx, updatePrivilegeAmount, arg.Amount, arg.ID) + return err +} + +const workerStatProcess = `-- name: WorkerStatProcess :exec +WITH answer_aggregates AS ( + SELECT + quiz_id, + COUNT(DISTINCT session) AS unique_true_answers_count + FROM + answer + WHERE + result = TRUE + GROUP BY + quiz_id +), + question_aggregates AS ( + SELECT + q.id AS quiz_id, + COUNT(qs.id) AS total_questions + FROM + quiz q + INNER JOIN + question qs ON q.id = qs.quiz_id + WHERE + q.deleted = false + AND q.archived = false + AND qs.deleted = false + GROUP BY + q.id + ), + session_times_aggregates AS ( + SELECT + quiz_id, COUNT(session) as sess, + AVG(extract(epoch FROM session_time)) AS average_session_time + FROM ( + SELECT + quiz_id, + session, + (MAX(created_at) - MIN(created_at)) AS session_time + FROM + answer + GROUP BY + quiz_id, + session + ) AS all_sessions + GROUP BY + quiz_id + ) +UPDATE quiz q +SET + questions_count = COALESCE(qa.total_questions, 0), + answers_count = COALESCE(aa.unique_true_answers_count, 0), + average_time_passing = COALESCE(sta.average_session_time, 0), + sessions_count = COALESCE(sta.sess,0) + FROM + (SELECT id, qid, accountid, deleted, archived, fingerprinting, repeatable, note_prevented, mail_notifications, unique_answers, super, group_id, name, description, config, status, limit_answers, due_to, time_of_passing, pausable, version, version_comment, parent_ids, created_at, updated_at, questions_count, answers_count, average_time_passing, sessions_count FROM quiz WHERE deleted = FALSE AND archived = FALSE) q_sub +LEFT JOIN answer_aggregates aa ON q_sub.id = aa.quiz_id + LEFT JOIN question_aggregates qa ON q_sub.id = qa.quiz_id + LEFT JOIN session_times_aggregates sta ON q_sub.id = sta.quiz_id +WHERE + q.id = q_sub.id +` + +func (q *Queries) WorkerStatProcess(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, workerStatProcess) + return err +} + +const workerTimeoutProcess = `-- name: WorkerTimeoutProcess :exec +UPDATE quiz SET status = 'timeout' WHERE deleted = false AND due_to <> 0 AND due_to < EXTRACT(epoch FROM CURRENT_TIMESTAMP) +` + +func (q *Queries) WorkerTimeoutProcess(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, workerTimeoutProcess) + return err +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..92f4fd5 --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module penahub.gitlab.yandexcloud.net/backend/quiz/common + +go 1.21 + +require ( + github.com/gofiber/fiber/v2 v2.51.0 + github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/golang/protobuf v1.5.3 + github.com/google/uuid v1.4.0 + github.com/lib/pq v1.10.9 + google.golang.org/protobuf v1.31.0 + penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d +) + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d376331 --- /dev/null +++ b/go.sum @@ -0,0 +1,95 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= +github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ= +github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= +github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d h1:gbaDt35HMDqOK84WYmDIlXMI7rstUcRqNttaT6Kx1do= +penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM= diff --git a/healthchecks/healthchecks.go b/healthchecks/healthchecks.go new file mode 100644 index 0000000..ad0a945 --- /dev/null +++ b/healthchecks/healthchecks.go @@ -0,0 +1,18 @@ +package healthchecks + +import ( + "github.com/gofiber/fiber/v2" +) + +func Liveness(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) +} + +func Readiness(err *error) fiber.Handler { + return func(c *fiber.Ctx) error { + if *err != nil { + return c.SendString((*err).Error()) + } + return c.SendStatus(fiber.StatusOK) + } +} diff --git a/model/model.go b/model/model.go new file mode 100644 index 0000000..6c15df9 --- /dev/null +++ b/model/model.go @@ -0,0 +1,271 @@ +package model + +import ( + "github.com/golang/protobuf/proto" + "penahub.gitlab.yandexcloud.net/backend/penahub_common/privilege" + "time" +) + +const ( + StatusDraft = "draft" + StatusTemplate = "template" + StatusStop = "stop" + StatusStart = "start" + StatusTimeout = "timeout" + StatusOffLimit = "offlimit" + + TypeVariant = "variant" + TypeImages = "images" + TypeVarImages = "varimg" + TypeFile = "file" + TypeText = "text" + TypeEmoji = "emoji" + TypeSelect = "select" + TypeDate = "date" + TypeNumber = "number" + TypePage = "page" + TypeRating = "rating" + TypeResult = "result" +) + +// Quiz is a struct for set up an quiz settings +type Quiz struct { + Id uint64 `json:"id"` + Qid string `json:"qid"` // uuid for secure data get and post + AccountId string `json:"accountid"` // account that created the quiz + + Deleted bool `json:"deleted"` // fake delete field + Archived bool `json:"archived"` // field for archiving quiz + + Fingerprinting bool `json:"fingerprinting"` // field that need for storing device id + Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting + NotePrevented bool `json:"note_prevented"` // note answers even if the quiz was aborted + MailNotifications bool `json:"mail_notifications"` // set true if you want get an email with every quiz passing + UniqueAnswers bool `json:"unique_answers"` // set true if we you mention only last quiz passing + + Name string `json:"name"` + Description string `json:"description"` + Config string `json:"config"` // serialize json with config for page rules + Status string `json:"status"` // status of quiz as enum. see Status const higher + Limit uint64 `json:"limit"` // max count of quiz passing + DueTo uint64 `json:"due_to"` // time when quiz is end + + TimeOfPassing uint64 `json:"time_of_passing"` // amount of seconds for give all appropriate answers for quiz + Pausable bool `json:"pausable"` // true allows to pause the quiz taking + + Version int `json:"version"` + VersionComment string `json:"version_comment"` + ParentIds []int32 `json:"parent_ids"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + + QuestionsCount uint64 `json:"questions_count"` + SessionCount uint64 `json:"session_count"` + PassedCount uint64 `json:"passed_count"` + AverageTime uint64 `json:"average_time"` + + Super bool `json:"super"` + GroupId uint64 `json:"group_id"` +} + +type QuizConfig struct { + Mailing ResultInfo `json:"resultInfo"` +} + +type ResultInfo struct { + When string `json:"when"` // before|after|email + Theme string `json:"theme"` // тема письма + Reply string `json:"reply"` // email для ответов, указывается в создании письма + ReplName string `json:"repl_name"` // имя отправителя +} + +// Question is a struct for implementing question for quiz +type Question struct { + Id uint64 `json:"id"` + QuizId uint64 `json:"quiz_id"` // relation to quiz table + + Title string `json:"title"` // title of question + Description string `json:"description"` // html\text representation of question and question description for answerer + Type string `json:"type"` // type field. enum with constants from consts higher + + Required bool `json:"required"` // answerer must answer this question + Deleted bool `json:"deleted"` // fake deleting field + + Page int `json:"page"` // set page number for question + //serialized json. caption for button type, array of key-value pairs for checkbox and select types, + // placeholder and input title for text and file types + Content string `json:"content"` + + Version int `json:"version"` + //todo check the best choice: question duplication and no statistics about the most unstable question or + //low performance of high complexity join-on-array query + ParentIds []int32 `json:"parent_ids"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Answer record of question answer +type Answer struct { + Id uint64 + + Content string `json:"content"` //serialized json. empty for buttons + + QuestionId uint64 `json:"question_id"` // relation for quiz + QuizId uint64 // relation for quiz + + Fingerprint string // device Id + Session string // xid of session + + Result bool + CreatedAt time.Time + New bool `json:"new"` + Deleted bool +} + +type ResultContent struct { + Text string `json:"text"` + Name string `json:"name"` + Email string `json:"email"` + Phone string `json:"phone"` + Address string `json:"address"` + Telegram string `json:"telegram"` + Wechat string `json:"wechat"` + Viber string `json:"viber"` + Vk string `json:"vk"` + Skype string `json:"skype"` + Whatsup string `json:"whatsup"` + Messenger string `json:"messenger"` + Custom map[string]string `json:"customs"` +} + +type ResultAnswer struct { + Content string + CreatedAt time.Time + QuestionID uint64 + AnswerID uint64 +} + +const skey = "squiz" + +var ( + Privileges = []privilege.Privilege{ + { + PrivilegeID: "quizCnt", + Name: "Количество Заявок", + ServiceKey: skey, + Description: "Количество полных прохождений опросов", + Type: "count", + Value: "заявка", + }, + { + PrivilegeID: "quizUnlimTime", + Name: "Безлимит Опросов", + ServiceKey: skey, + Description: "Количество дней, в течении которых пользование сервисом безлимитно", + Type: "day", + Value: "день", + }, + } +) + +const ( + ServiceKey = "templategen" + PrivilegeTemplateCount = "templateCnt" + PrivilegeTemplateUnlimTime = "templateUnlimTime" + PrivilegeTemplateStorage = "templateStorage" + BasicAmountPrivilegeTemplateCount = 15 + BasicAmountPrivilegeTemplateStorage = 100 +) + +type Tariff struct { + ID string `json:"_id"` + Name string `json:"name"` + UserID string `json:"user_id"` + Price int64 `json:"price"` + IsCustom bool `json:"isCustom"` + Privileges []Privilege `json:"privileges"` + Deleted bool `json:"isDeleted"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt *time.Time `json:"deletedAt,omitempty"` +} + +type Privilege struct { + ID string `json:"_id"` + Amount int64 `json:"amount"` + PrivilegeID string `json:"privilegeId"` + Name string `json:"name"` + ServiceKey string `json:"serviceKey"` + Description string `json:"description"` + Type string `json:"type"` + Value string `json:"value"` + Price float64 `json:"price"` + IsDeleted bool `json:"isDeleted"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt time.Time `json:"deletedAt"` +} + +type CustomerMessage struct { + Privileges []PrivilegeMessage `protobuf:"bytes,1,rep,name=Privileges,proto3" json:"Privileges,omitempty"` + UserID string `protobuf:"bytes,2,opt,name=UserID,proto3" json:"UserID,omitempty"` +} + +type PrivilegeMessage struct { + PrivilegeID string `protobuf:"bytes,1,opt,name=PrivilegeID,proto3" json:"PrivilegeID,omitempty"` + ServiceKey string `protobuf:"bytes,2,opt,name=ServiceKey,proto3" json:"ServiceKey,omitempty"` + Type PrivilegeType `protobuf:"varint,3,opt,name=Type,proto3,enum=broker.PrivilegeType" json:"Type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` + Amount uint64 `protobuf:"varint,5,opt,name=Amount,proto3" json:"Amount,omitempty"` +} + +type PrivilegeType int32 + +func (m *CustomerMessage) Reset() { + *m = CustomerMessage{} +} + +func (m *CustomerMessage) String() string { + return proto.CompactTextString(m) +} + +func (m *CustomerMessage) ProtoMessage() { +} + +type ShortPrivilege struct { + ID string `json:"id"` + PrivilegeID string `json:"privilege_id"` + PrivilegeName string `json:"privilege_name"` + Amount uint64 `json:"amount"` + CreatedAt time.Time `json:"created_at"` +} + +type Account struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Deleted bool `json:"deleted"` + Privileges map[string]ShortPrivilege `json:"privileges"` +} + +type DefaultData struct { + Amount uint64 + PrivilegeID string + UnlimID string +} + +type ShortQuestion struct { + Title string + Content string + Description string +} + +type AnswerExport struct { + Content string `json:"content"` + Id uint64 `json:"id"` + New bool `json:"new"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/model/tariff/models.pb.go b/model/tariff/models.pb.go new file mode 100644 index 0000000..8f18d98 --- /dev/null +++ b/model/tariff/models.pb.go @@ -0,0 +1,313 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v4.23.4 +// source: models.proto + +package tariff + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PrivilegeType int32 + +const ( + PrivilegeType_Full PrivilegeType = 0 + PrivilegeType_Day PrivilegeType = 1 + PrivilegeType_Count PrivilegeType = 2 +) + +// Enum value maps for PrivilegeType. +var ( + PrivilegeType_name = map[int32]string{ + 0: "Full", + 1: "Day", + 2: "Count", + } + PrivilegeType_value = map[string]int32{ + "Full": 0, + "Day": 1, + "Count": 2, + } +) + +func (x PrivilegeType) Enum() *PrivilegeType { + p := new(PrivilegeType) + *p = x + return p +} + +func (x PrivilegeType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PrivilegeType) Descriptor() protoreflect.EnumDescriptor { + return file_models_proto_enumTypes[0].Descriptor() +} + +func (PrivilegeType) Type() protoreflect.EnumType { + return &file_models_proto_enumTypes[0] +} + +func (x PrivilegeType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PrivilegeType.Descriptor instead. +func (PrivilegeType) EnumDescriptor() ([]byte, []int) { + return file_models_proto_rawDescGZIP(), []int{0} +} + +type PrivilegeMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PrivilegeID string `protobuf:"bytes,1,opt,name=PrivilegeID,proto3" json:"PrivilegeID,omitempty"` + ServiceKey string `protobuf:"bytes,2,opt,name=ServiceKey,proto3" json:"ServiceKey,omitempty"` + Type PrivilegeType `protobuf:"varint,3,opt,name=Type,proto3,enum=tariff.PrivilegeType" json:"Type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"` + Amount uint64 `protobuf:"varint,5,opt,name=Amount,proto3" json:"Amount,omitempty"` +} + +func (x *PrivilegeMessage) Reset() { + *x = PrivilegeMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_models_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PrivilegeMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PrivilegeMessage) ProtoMessage() {} + +func (x *PrivilegeMessage) ProtoReflect() protoreflect.Message { + mi := &file_models_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PrivilegeMessage.ProtoReflect.Descriptor instead. +func (*PrivilegeMessage) Descriptor() ([]byte, []int) { + return file_models_proto_rawDescGZIP(), []int{0} +} + +func (x *PrivilegeMessage) GetPrivilegeID() string { + if x != nil { + return x.PrivilegeID + } + return "" +} + +func (x *PrivilegeMessage) GetServiceKey() string { + if x != nil { + return x.ServiceKey + } + return "" +} + +func (x *PrivilegeMessage) GetType() PrivilegeType { + if x != nil { + return x.Type + } + return PrivilegeType_Full +} + +func (x *PrivilegeMessage) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *PrivilegeMessage) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type TariffMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Privileges []*PrivilegeMessage `protobuf:"bytes,1,rep,name=Privileges,proto3" json:"Privileges,omitempty"` + UserID string `protobuf:"bytes,2,opt,name=UserID,proto3" json:"UserID,omitempty"` +} + +func (x *TariffMessage) Reset() { + *x = TariffMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_models_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TariffMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TariffMessage) ProtoMessage() {} + +func (x *TariffMessage) ProtoReflect() protoreflect.Message { + mi := &file_models_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TariffMessage.ProtoReflect.Descriptor instead. +func (*TariffMessage) Descriptor() ([]byte, []int) { + return file_models_proto_rawDescGZIP(), []int{1} +} + +func (x *TariffMessage) GetPrivileges() []*PrivilegeMessage { + if x != nil { + return x.Privileges + } + return nil +} + +func (x *TariffMessage) GetUserID() string { + if x != nil { + return x.UserID + } + return "" +} + +var File_models_proto protoreflect.FileDescriptor + +var file_models_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, + 0x74, 0x61, 0x72, 0x69, 0x66, 0x66, 0x22, 0xad, 0x01, 0x0a, 0x10, 0x50, 0x72, 0x69, 0x76, 0x69, + 0x6c, 0x65, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x50, + 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x49, 0x44, 0x12, 0x1e, 0x0a, + 0x0a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x29, 0x0a, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x61, + 0x72, 0x69, 0x66, 0x66, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, + 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x61, 0x0a, 0x0d, 0x54, 0x61, 0x72, 0x69, 0x66, 0x66, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x69, + 0x6c, 0x65, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x61, + 0x72, 0x69, 0x66, 0x66, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44, 0x2a, 0x2d, 0x0a, 0x0d, 0x50, 0x72, 0x69, + 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, + 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x61, 0x79, 0x10, 0x01, 0x12, 0x09, 0x0a, + 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x10, 0x02, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x74, 0x61, + 0x72, 0x69, 0x66, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_models_proto_rawDescOnce sync.Once + file_models_proto_rawDescData = file_models_proto_rawDesc +) + +func file_models_proto_rawDescGZIP() []byte { + file_models_proto_rawDescOnce.Do(func() { + file_models_proto_rawDescData = protoimpl.X.CompressGZIP(file_models_proto_rawDescData) + }) + return file_models_proto_rawDescData +} + +var file_models_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_models_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_models_proto_goTypes = []interface{}{ + (PrivilegeType)(0), // 0: tariff.PrivilegeType + (*PrivilegeMessage)(nil), // 1: tariff.PrivilegeMessage + (*TariffMessage)(nil), // 2: tariff.TariffMessage +} +var file_models_proto_depIdxs = []int32{ + 0, // 0: tariff.PrivilegeMessage.Type:type_name -> tariff.PrivilegeType + 1, // 1: tariff.TariffMessage.Privileges:type_name -> tariff.PrivilegeMessage + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_models_proto_init() } +func file_models_proto_init() { + if File_models_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_models_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PrivilegeMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_models_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TariffMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_models_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_models_proto_goTypes, + DependencyIndexes: file_models_proto_depIdxs, + EnumInfos: file_models_proto_enumTypes, + MessageInfos: file_models_proto_msgTypes, + }.Build() + File_models_proto = out.File + file_models_proto_rawDesc = nil + file_models_proto_goTypes = nil + file_models_proto_depIdxs = nil +} diff --git a/repository/account/account.go b/repository/account/account.go new file mode 100644 index 0000000..9cef6e8 --- /dev/null +++ b/repository/account/account.go @@ -0,0 +1,309 @@ +package account + +import ( + "context" + "database/sql" + "fmt" + "github.com/google/uuid" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/model" + "squiz/client/auth" + "strconv" +) + +type Deps struct { + Queries *sqlcgen.Queries + AuthClient *auth.AuthClient + Pool *sql.DB +} + +type AccountRepository struct { + queries *sqlcgen.Queries + authClient *auth.AuthClient + pool *sql.DB +} + +func NewAccountRepository(deps Deps) *AccountRepository { + return &AccountRepository{ + queries: deps.Queries, + authClient: deps.AuthClient, + pool: deps.Pool, + } +} + +// test + +func (r *AccountRepository) GetAccountByID(ctx context.Context, userID string) (model.Account, error) { + userIDSql := sql.NullString{String: userID, Valid: userID != ""} + + accountRows, err := r.queries.GetAccountWithPrivileges(ctx, userIDSql) + if err != nil { + return model.Account{}, err + } + + var account model.Account + privileges := make(map[string]model.ShortPrivilege) + + for _, row := range accountRows { + if account.ID == "" { + account.ID = row.ID.String() + account.UserID = row.UserID.String + account.CreatedAt = row.CreatedAt.Time + account.Deleted = row.Deleted.Bool + } + + if row.PrivilegeID.Valid { + privilege := model.ShortPrivilege{ + ID: fmt.Sprintf("%d", row.PrivilegeID.Int32), + PrivilegeID: row.Privilegeid.String, + PrivilegeName: row.PrivilegeName.String, + Amount: uint64(row.Amount.Int32), + CreatedAt: row.PrivilegeCreatedAt.Time, + } + privileges[privilege.PrivilegeName] = privilege + } + } + + if account.ID == "" { + return model.Account{}, sql.ErrNoRows + } + + account.Privileges = privileges + return account, nil +} + +// test + +func (r *AccountRepository) GetPrivilegesByAccountID(ctx context.Context, userID string) ([]model.ShortPrivilege, error) { + userIDSql := sql.NullString{String: userID, Valid: userID != ""} + + privilegeRows, err := r.queries.GetPrivilegesByAccountIDWC(ctx, userIDSql) + if err != nil { + return nil, err + } + + var privileges []model.ShortPrivilege + + for _, row := range privilegeRows { + privilege := model.ShortPrivilege{ + ID: fmt.Sprintf("%d", row.ID), + PrivilegeID: row.Privilegeid.String, + PrivilegeName: row.PrivilegeName.String, + Amount: uint64(row.Amount.Int32), + CreatedAt: row.CreatedAt.Time, + } + privileges = append(privileges, privilege) + } + + return privileges, nil +} + +// todo test +func (r *AccountRepository) CreateAccount(ctx context.Context, data *model.Account) error { + email, err := r.authClient.GetUserEmail(data.UserID) + if err != nil { + return err + } + + data.ID = uuid.NewString() + + err = r.queries.CreateAccount(ctx, sqlcgen.CreateAccountParams{ + ID: uuid.MustParse(data.ID), + UserID: sql.NullString{String: data.UserID, Valid: data.UserID != ""}, + Email: sql.NullString{String: email, Valid: email != ""}, + CreatedAt: sql.NullTime{Time: data.CreatedAt, Valid: !data.CreatedAt.IsZero()}, + Deleted: sql.NullBool{Bool: data.Deleted, Valid: true}, + }) + + if err != nil { + return fmt.Errorf("failed to create account: %w", err) + } + + for _, privilege := range data.Privileges { + err := r.queries.InsertPrivilege(ctx, sqlcgen.InsertPrivilegeParams{ + Privilegeid: sql.NullString{String: privilege.PrivilegeID, Valid: privilege.PrivilegeID != ""}, + AccountID: uuid.NullUUID{UUID: uuid.MustParse(data.ID), Valid: true}, + PrivilegeName: sql.NullString{String: privilege.PrivilegeName, Valid: privilege.PrivilegeName != ""}, + Amount: sql.NullInt32{Int32: int32(privilege.Amount), Valid: true}, + CreatedAt: sql.NullTime{Time: privilege.CreatedAt, Valid: !privilege.CreatedAt.IsZero()}, + }) + + if err != nil { + return fmt.Errorf("failed to insert privilege: %w", err) + } + } + + return nil +} + +func (r *AccountRepository) DeleteAccount(ctx context.Context, accountID string) error { + tx, err := r.pool.BeginTx(ctx, nil) + if err != nil { + return err + } + + err = r.queries.DeleteAccountById(ctx, uuid.MustParse(accountID)) + if err != nil { + tx.Rollback() + return err + } + + err = r.queries.DeletePrivilegeByAccID(ctx, uuid.NullUUID{UUID: uuid.MustParse(accountID), Valid: true}) + if err != nil { + tx.Rollback() + return err + } + + return tx.Commit() +} + +// test + +func (r *AccountRepository) GetAccounts(ctx context.Context, limit uint64, offset uint64) ([]model.Account, uint64, error) { + rows, err := r.queries.AccountPagination(ctx, sqlcgen.AccountPaginationParams{ + Limit: int32(limit), + Offset: int32(offset), + }) + if err != nil { + return nil, 0, err + } + + var accounts []model.Account + + for _, row := range rows { + account := model.Account{ + ID: row.ID.String(), + UserID: row.UserID.String, + CreatedAt: row.CreatedAt.Time, + Deleted: row.Deleted.Bool, + } + + accounts = append(accounts, account) + } + + return accounts, uint64(len(accounts)), nil +} + +// test + +func (r *AccountRepository) UpdatePrivilege(ctx context.Context, privilege *model.ShortPrivilege, accountID string) error { + err := r.queries.UpdatePrivilege(ctx, sqlcgen.UpdatePrivilegeParams{ + Amount: sql.NullInt32{Int32: int32(privilege.Amount), Valid: true}, + CreatedAt: sql.NullTime{Time: privilege.CreatedAt, Valid: !privilege.CreatedAt.IsZero()}, + AccountID: uuid.NullUUID{UUID: uuid.MustParse(accountID), Valid: true}, + Privilegeid: sql.NullString{String: privilege.PrivilegeID, Valid: privilege.PrivilegeID != ""}, + }) + + if err != nil { + return err + } + + return nil +} + +// test + +func (r *AccountRepository) InsertPrivilege(ctx context.Context, privilege *model.ShortPrivilege, accountID string) error { + err := r.queries.InsertPrivilegeWC(ctx, sqlcgen.InsertPrivilegeWCParams{ + Amount: sql.NullInt32{Int32: int32(privilege.Amount), Valid: true}, + CreatedAt: sql.NullTime{Time: privilege.CreatedAt, Valid: !privilege.CreatedAt.IsZero()}, + AccountID: uuid.NullUUID{UUID: uuid.MustParse(accountID), Valid: true}, + Privilegeid: sql.NullString{String: privilege.PrivilegeID, Valid: privilege.PrivilegeID != ""}, + }) + + if err != nil { + return err + } + + return nil +} + +// test + +func (r *AccountRepository) GetExpired(ctx context.Context, privilegeID string) ([]model.ShortPrivilege, error) { + rows, err := r.queries.GetExpiredPrivilege(ctx, sql.NullString{String: privilegeID, Valid: privilegeID != ""}) + if err != nil { + return nil, err + } + + var expiredRecords []model.ShortPrivilege + + for _, row := range rows { + privilege := model.ShortPrivilege{ + ID: fmt.Sprintf("%d", row.ID), + PrivilegeID: row.Privilegeid.String, + PrivilegeName: row.PrivilegeName.String, + Amount: uint64(row.Amount.Int32), + CreatedAt: row.CreatedAt.Time, + } + expiredRecords = append(expiredRecords, privilege) + + } + + return expiredRecords, nil +} + +func (r *AccountRepository) CheckAndAddDefault(ctx context.Context, amount uint64, privilegeID string, zeroAmount uint64) error { + err := r.queries.CheckAndAddDefault(ctx, sqlcgen.CheckAndAddDefaultParams{ + Amount: sql.NullInt32{Int32: int32(amount), Valid: true}, + PrivilegeName: sql.NullString{String: privilegeID, Valid: privilegeID != ""}, + Amount_2: sql.NullInt32{Int32: int32(zeroAmount), Valid: true}, + }) + + if err != nil { + return fmt.Errorf("error executing SQL query: %v", err) + } + + return nil +} + +// test + +func (r *AccountRepository) DeletePrivilegeByID(ctx context.Context, id string) error { + intID, err := strconv.Atoi(id) + if err != nil { + return fmt.Errorf("failed to convert id to integer: %v", err) + } + + return r.queries.DeletePrivilegeByID(ctx, int32(intID)) +} + +// test + +func (r *AccountRepository) UpdatePrivilegeAmount(ctx context.Context, ID string, newAmount uint64) error { + intID, err := strconv.Atoi(ID) + if err != nil { + return fmt.Errorf("failed to convert id to integer: %v", err) + } + + err = r.queries.UpdatePrivilegeAmount(ctx, sqlcgen.UpdatePrivilegeAmountParams{ + Amount: sql.NullInt32{Int32: int32(newAmount), Valid: true}, + ID: int32(intID), + }) + + if err != nil { + return err + } + + return nil +} + +// test + +func (r *AccountRepository) GetAccAndPrivilegeByEmail(ctx context.Context, email string) (model.Account, []model.ShortPrivilege, error) { + var account model.Account + var privileges []model.ShortPrivilege + + row, err := r.queries.GetAccAndPrivilegeByEmail(ctx, sql.NullString{String: email, Valid: true}) + if err != nil { + return account, privileges, err + } + + account.ID = row.ID.String() + account.UserID = row.UserID.String + account.Email = row.Email.String + account.CreatedAt = row.CreatedAt.Time + + if row.ID_2.Valid { + privilege := model.ShortPrivilege{ + ID: fmt.Sprint(row.ID_2.Int32), + PrivilegeID: row.Privilegeid.String, + Amount: uint64(row.Amount.Int32), + CreatedAt: row.CreatedAt_2.Time, + } + privileges = append(privileges, privilege) + } + + return account, privileges, nil +} diff --git a/repository/answer/answer.go b/repository/answer/answer.go new file mode 100644 index 0000000..13eae89 --- /dev/null +++ b/repository/answer/answer.go @@ -0,0 +1,88 @@ +package answer + +import ( + "context" + "database/sql" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/model" +) + +type Deps struct { + Queries *sqlcgen.Queries + Pool *sql.DB +} + +type AnswerRepository struct { + queries *sqlcgen.Queries + pool *sql.DB +} + +func NewAnswerRepository(deps Deps) *AnswerRepository { + return &AnswerRepository{ + queries: deps.Queries, + pool: deps.Pool, + } +} + +// test + +func (r *AnswerRepository) CreateAnswers(ctx context.Context, answers []model.Answer, session, fp string, quizID uint64) ([]uint64, []error) { + var ( + answered []uint64 + errs []error + ) + + tx, err := r.pool.BeginTx(ctx, nil) + if err != nil { + return nil, []error{err} + } + + for _, ans := range answers { + params := sqlcgen.InsertAnswersParams{ + Content: sql.NullString{String: ans.Content, Valid: true}, + QuizID: int64(quizID), + QuestionID: int64(ans.QuestionId), + Fingerprint: sql.NullString{String: fp, Valid: true}, + Session: sql.NullString{String: session, Valid: true}, + Result: sql.NullBool{Bool: ans.Result, Valid: true}, + } + + err := r.queries.InsertAnswers(ctx, params) + if err != nil { + errs = append(errs, err) + } else { + answered = append(answered, ans.QuestionId) + } + } + + err = tx.Commit() + if err != nil { + errs = append(errs, err) + return nil, errs + } + + return answered, nil +} + +// test + +func (r *AnswerRepository) GetAllAnswersByQuizID(ctx context.Context, session string) ([]model.ResultAnswer, error) { + var results []model.ResultAnswer + + rows, err := r.queries.GetAllAnswersByQuizID(ctx, sql.NullString{String: session, Valid: true}) + if err != nil { + return nil, err + } + + for _, row := range rows { + + resultAnswer := model.ResultAnswer{ + Content: row.Content.String, + CreatedAt: row.CreatedAt.Time, + QuestionID: uint64(row.QuestionID), + AnswerID: uint64(row.ID), + } + + results = append(results, resultAnswer) + } + + return results, nil +} diff --git a/repository/question/question.go b/repository/question/question.go new file mode 100644 index 0000000..a9fc113 --- /dev/null +++ b/repository/question/question.go @@ -0,0 +1,388 @@ +package question + +import ( + "context" + "database/sql" + "errors" + "fmt" + "github.com/lib/pq" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/model" + "strings" + "sync" + "time" +) + +type Deps struct { + Queries *sqlcgen.Queries + Pool *sql.DB +} + +type QuestionRepository struct { + queries *sqlcgen.Queries + pool *sql.DB +} + +func NewQuestionRepository(deps Deps) *QuestionRepository { + return &QuestionRepository{ + queries: deps.Queries, + pool: deps.Pool, + } +} + +// test + +func (r *QuestionRepository) CreateQuestion(ctx context.Context, record *model.Question) error { + params := sqlcgen.InsertQuestionParams{ + QuizID: int64(record.QuizId), + Title: record.Title, + Description: sql.NullString{String: record.Description, Valid: true}, + Questiontype: record.Type, + Required: sql.NullBool{Bool: record.Required, Valid: true}, + Page: sql.NullInt16{Int16: int16(record.Page), Valid: true}, + Content: sql.NullString{String: record.Content, Valid: true}, + ParentIds: record.ParentIds, + UpdatedAt: sql.NullTime{Time: time.Now(), Valid: true}, + } + + data, err := r.queries.InsertQuestion(ctx, params) + if err != nil { + return err + } + + record.Id = uint64(data.ID) + record.CreatedAt = data.CreatedAt.Time + record.UpdatedAt = data.UpdatedAt.Time + + return nil +} + +// test + +// GetQuestionList function for get data page from db +func (r *QuestionRepository) GetQuestionList( + ctx context.Context, + limit, offset, from, to, quizId uint64, + deleted, required bool, + search, qType string) ([]model.Question, uint64, error) { + query := ` + SELECT que.* FROM question as que JOIN quiz as qui on que.quiz_id = ANY(qui.parent_ids) or que.quiz_id = qui.id + %s + ORDER BY que.page, que.created_at ASC + LIMIT $%d OFFSET $%d; +` + + queryCnt := `SELECT count(1) FROM question as que JOIN quiz as qui on que.quiz_id = ANY(qui.parent_ids) or que.quiz_id = qui.id %s;` + + var ( + whereClause []string + data []interface{} + ) + + if quizId != 0 { + data = append(data, quizId) + whereClause = append(whereClause, fmt.Sprintf("qui.id = $%d", len(data))) + } + if from != 0 { + data = append(data, from) + whereClause = append(whereClause, fmt.Sprintf("que.created_at >= to_timestamp($%d)", len(data))) + } + if to != 0 { + data = append(data, to) + whereClause = append(whereClause, fmt.Sprintf("que.created_at <= to_timestamp($%d)", len(data))) + } + + if deleted { + whereClause = append(whereClause, fmt.Sprintf("que.deleted = true")) + } else { + whereClause = append(whereClause, fmt.Sprintf("que.deleted = false")) + } + + if required { + whereClause = append(whereClause, fmt.Sprintf("que.required = true")) + } + if qType != "" { + data = append(data, qType) + whereClause = append(whereClause, fmt.Sprintf("que.questiontype = $%d", len(data))) + } + if search != "" { + data = append(data, search) + whereClause = append(whereClause, fmt.Sprintf("to_tsvector('russian', que.title) @@ to_tsquery('russian', $%d)", len(data))) + } + + data = append(data, limit, offset) + if len(whereClause) != 0 { + query = fmt.Sprintf(query, + fmt.Sprintf(" WHERE %s ", strings.Join(whereClause, " AND ")), + len(data)-1, len(data)) + queryCnt = fmt.Sprintf(queryCnt, fmt.Sprintf(" WHERE %s ", strings.Join(whereClause, " AND "))) + } else { + query = fmt.Sprintf(query, "", 1, 2) + queryCnt = fmt.Sprintf(queryCnt, "") + } + + var ( + qerr, cerr error + count uint64 + result []model.Question + ) + + fmt.Println("QUESTIONS", queryCnt, query, data, data[:len(data)-2]) + + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + defer wg.Done() + + rows, err := r.pool.QueryContext(ctx, query, data...) + if err != nil { + qerr = err + return + } + + defer rows.Close() + + var piece model.Question + pIds := pq.Int32Array{} + for rows.Next() { + if err := rows.Scan( + &piece.Id, + &piece.QuizId, + &piece.Title, + &piece.Description, + &piece.Type, + &piece.Required, + &piece.Deleted, + &piece.Page, + &piece.Content, + &piece.Version, + &pIds, + &piece.CreatedAt, + &piece.UpdatedAt, + ); err != nil { + qerr = err + return + } + piece.ParentIds = pIds + result = append(result, piece) + } + }() + + go func() { + defer wg.Done() + + var ( + err error + rows *sql.Rows + ) + + if len(data) == 2 { + rows, err = r.pool.QueryContext(ctx, queryCnt) + } else { + rows, err = r.pool.QueryContext(ctx, queryCnt, data[:len(data)-2]...) + } + if err != nil { + cerr = err + return + } + + defer rows.Close() + + if ok := rows.Next(); !ok { + cerr = errors.New("can not next count") + } + + if err := rows.Scan(&count); err != nil { + cerr = err + } + }() + + wg.Wait() + + if cerr != nil { + return nil, 0, cerr + } + if qerr != nil { + return nil, 0, qerr + } + + return result, count, nil +} + +// test + +// UpdateQuestion set new data for question +func (r *QuestionRepository) UpdateQuestion(ctx context.Context, record model.Question) error { + query := `UPDATE question + SET %s + WHERE id=$1;` + + var values []string + + if record.Title != "" { + values = append(values, fmt.Sprintf(` title='%s' `, record.Title)) + } + + if record.Description != "" { + values = append(values, fmt.Sprintf(` description='%s' `, record.Description)) + } + + if record.Type != "" { + values = append(values, fmt.Sprintf(` questiontype='%s' `, record.Type)) + } + + values = append(values, fmt.Sprintf(`required=%t `, record.Required), fmt.Sprintf(` version=%d `, record.Version)) + + if record.Content != "" { + values = append(values, fmt.Sprintf(` content='%s' `, record.Content)) + } + + if record.Page != -1 { + values = append(values, fmt.Sprintf(` page=%d `, record.Page)) + } + _, err := r.pool.ExecContext(ctx, fmt.Sprintf(query, strings.Join(values, ",")), record.Id) + + return err +} + +// test + +// DeleteQuestion set question deleted and return deleted question +func (r *QuestionRepository) DeleteQuestion(ctx context.Context, id uint64) (model.Question, error) { + + row, err := r.queries.DeleteQuestion(ctx, int64(id)) + if err != nil { + return model.Question{}, err + } + + result := model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + Title: row.Title, + Description: row.Description.String, + Type: string(row.Questiontype.([]byte)), + Required: row.Required.Bool, + Deleted: row.Deleted.Bool, + Page: int(row.Page.Int16), + Content: row.Content.String, + Version: int(row.Version.Int16), + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + + return result, nil +} + +// test + +// MoveToHistoryQuestion insert deleted duplicate of question +func (r *QuestionRepository) MoveToHistoryQuestion(ctx context.Context, id uint64) (model.Question, error) { + row, err := r.queries.MoveToHistory(ctx, int64(id)) + if err != nil { + return model.Question{}, err + } + + result := model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + ParentIds: row.ParentIds, + } + + return result, nil +} + +// test + +// CopyQuestion method for duplication of question or to copy question to another quiz +func (r *QuestionRepository) CopyQuestion(ctx context.Context, id, quizId uint64) (model.Question, error) { + var record model.Question + + if quizId == 0 { + row, err := r.queries.DuplicateQuestion(ctx, int64(id)) + if err != nil { + return model.Question{}, err + } + + record = model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + } else { + row, err := r.queries.CopyQuestion(ctx, sqlcgen.CopyQuestionParams{ + QuizID: int64(quizId), + ID: int64(id), + }) + if err != nil { + return model.Question{}, err + } + + record = model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + } + + return record, nil +} + +// test + +// QuestionHistory method for obtaining question history from the database +func (r *QuestionRepository) QuestionHistory(ctx context.Context, id, limit, offset uint64) ([]model.Question, error) { + rows, err := r.queries.GetQuestionHistory(ctx, sqlcgen.GetQuestionHistoryParams{ + ID: int64(id), + Limit: int32(limit), + Offset: int32(offset), + }) + if err != nil { + return nil, err + } + + var result []model.Question + for _, row := range rows { + record := model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + Title: row.Title, + Description: row.Description.String, + Type: string(row.Questiontype.([]byte)), + Required: row.Required.Bool, + Deleted: row.Deleted.Bool, + Page: int(row.Page.Int16), + Content: row.Content.String, + Version: int(row.Version.Int16), + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + result = append(result, record) + } + + return result, nil +} + +func (r *QuestionRepository) GetMapQuestions(ctx context.Context, allAnswers []model.ResultAnswer) (map[uint64]string, error) { + questionMap := make(map[uint64]string) + + for _, answer := range allAnswers { + + title, questionType, err := r.GetQuestionTitleByID(ctx, answer.QuestionID) + if err != nil { + return nil, err + } + if questionType != model.TypeResult { + questionMap[answer.AnswerID] = title + } + } + + return questionMap, nil +} + +// test + +func (r *QuestionRepository) GetQuestionTitleByID(ctx context.Context, questionID uint64) (string, string, error) { + row, err := r.queries.GetQuestionTitle(ctx, int64(questionID)) + if err != nil { + return "", "", err + } + + return row.Title, string(row.Questiontype.([]byte)), nil +} diff --git a/repository/quiz/quiz.go b/repository/quiz/quiz.go new file mode 100644 index 0000000..d570968 --- /dev/null +++ b/repository/quiz/quiz.go @@ -0,0 +1,579 @@ +package quiz + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "github.com/google/uuid" + "github.com/lib/pq" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/model" + "strings" + "sync" +) + +type Deps struct { + Queries *sqlcgen.Queries + Pool *sql.DB +} + +type QuizRepository struct { + queries *sqlcgen.Queries + pool *sql.DB +} + +func NewQuizRepository(deps Deps) *QuizRepository { + return &QuizRepository{ + queries: deps.Queries, + pool: deps.Pool, + } +} + +// test + +func (r *QuizRepository) CreateQuiz(ctx context.Context, record *model.Quiz) error { + if record.Qid == "" { + record.Qid = uuid.NewString() + } + + params := sqlcgen.InsertQuizParams{ + Accountid: record.AccountId, + Fingerprinting: sql.NullBool{Bool: record.Fingerprinting, Valid: true}, + Repeatable: sql.NullBool{Bool: record.Repeatable, Valid: true}, + NotePrevented: sql.NullBool{Bool: record.NotePrevented, Valid: true}, + MailNotifications: sql.NullBool{Bool: record.MailNotifications, Valid: true}, + UniqueAnswers: sql.NullBool{Bool: record.UniqueAnswers, Valid: true}, + Super: sql.NullBool{Bool: record.Super, Valid: true}, + GroupID: sql.NullInt64{Int64: int64(record.GroupId), Valid: true}, + Name: sql.NullString{String: record.Name, Valid: true}, + Description: sql.NullString{String: record.Description, Valid: true}, + Config: sql.NullString{String: record.Config, Valid: true}, + Status: record.Status, + LimitAnswers: sql.NullInt32{Int32: int32(record.Limit), Valid: true}, + DueTo: sql.NullInt32{Int32: int32(record.DueTo), Valid: true}, + TimeOfPassing: sql.NullInt32{Int32: int32(record.TimeOfPassing), Valid: true}, + Pausable: sql.NullBool{Bool: record.Pausable, Valid: true}, + ParentIds: record.ParentIds, + QuestionsCount: sql.NullInt32{Int32: int32(record.QuestionsCount), Valid: true}, + Qid: uuid.NullUUID{UUID: uuid.MustParse(record.Qid), Valid: true}, + } + + data, err := r.queries.InsertQuiz(ctx, params) + if err != nil { + return err + } + + record.Id = uint64(data.ID) + record.CreatedAt = data.CreatedAt.Time + record.UpdatedAt = data.UpdatedAt.Time + record.Qid = data.Qid.UUID.String() + + return nil +} + +type GetQuizListDeps struct { + Limit, Offset, From, To, Group uint64 + Deleted, Archived, Super bool + Search, Status, AccountId string +} + +// test + +// GetQuizList function for get data page from db +func (r *QuizRepository) GetQuizList( + ctx context.Context, + deps GetQuizListDeps) ([]model.Quiz, uint64, error) { + query := ` + SELECT * FROM quiz + %s + ORDER BY created_at DESC + LIMIT $1 OFFSET $2; +` + + queryCnt := `SELECT count(1) FROM quiz %s;` + + var ( + whereClause []string + data []interface{} + ) + + whereClause = append(whereClause, fmt.Sprintf(`accountid = '%s'`, deps.AccountId)) + if deps.From != 0 { + data = append(data, deps.From) + whereClause = append(whereClause, fmt.Sprintf("created_at >= to_timestamp($%d)", len(data))) + } + if deps.To != 0 { + data = append(data, deps.To) + whereClause = append(whereClause, fmt.Sprintf("created_at <= to_timestamp($%d)", len(data))) + } + if deps.Deleted { + whereClause = append(whereClause, fmt.Sprintf("deleted = true")) + } else { + whereClause = append(whereClause, fmt.Sprintf("deleted = false")) + } + if deps.Archived { + whereClause = append(whereClause, fmt.Sprintf("archived = true")) + } else { + whereClause = append(whereClause, fmt.Sprintf("archived = false")) + } + + if deps.Super { + whereClause = append(whereClause, fmt.Sprintf("super = true")) + } + + if deps.Group > 0 { + whereClause = append(whereClause, fmt.Sprintf("group_id = %d", deps.Group)) + } + + if deps.Status != "" { + data = append(data, deps.Status) + whereClause = append(whereClause, fmt.Sprintf("status = $%d", len(data))) + } + if deps.Search != "" { + data = append(data, deps.Search) + whereClause = append(whereClause, fmt.Sprintf("to_tsvector('russian', name) @@ to_tsquery('russian', $%d)", len(data))) + } + + if len(whereClause) != 0 { + query = fmt.Sprintf(query, fmt.Sprintf(" WHERE %s ", strings.Join(whereClause, " AND "))) + queryCnt = fmt.Sprintf(queryCnt, fmt.Sprintf(" WHERE %s ", strings.Join(whereClause, " AND "))) + } else { + query = fmt.Sprintf(query, "") + queryCnt = fmt.Sprintf(queryCnt, "") + } + + var ( + qerr, cerr error + count uint64 + result []model.Quiz + ) + + data = append(data, deps.Limit, deps.Offset) + wg := sync.WaitGroup{} + wg.Add(2) + + go func() { + defer wg.Done() + fmt.Println("Q1", query, deps.Limit, deps.Offset) + rows, err := r.pool.QueryContext(ctx, query, deps.Limit, deps.Offset) + if err != nil { + qerr = err + return + } + + defer rows.Close() + + var piece model.Quiz + pIds := pq.Int32Array{} + for rows.Next() { + if err := rows.Scan( + &piece.Id, + &piece.Qid, + &piece.AccountId, + &piece.Deleted, + &piece.Archived, + &piece.Fingerprinting, + &piece.Repeatable, + &piece.NotePrevented, + &piece.MailNotifications, + &piece.UniqueAnswers, + &piece.Super, + &piece.GroupId, + &piece.Name, + &piece.Description, + &piece.Config, + &piece.Status, + &piece.Limit, + &piece.DueTo, + &piece.TimeOfPassing, + &piece.Pausable, + &piece.Version, + &piece.VersionComment, + &pIds, + &piece.CreatedAt, + &piece.UpdatedAt, + &piece.QuestionsCount, + &piece.PassedCount, + &piece.AverageTime, + &piece.SessionCount, + ); err != nil { + qerr = err + return + } + piece.ParentIds = pIds + result = append(result, piece) + } + }() + + go func() { + defer wg.Done() + + fmt.Println("Q2", queryCnt) + var ( + err error + rows *sql.Rows + ) + if len(data) == 2 { + rows, err = r.pool.QueryContext(ctx, queryCnt) + } else { + rows, err = r.pool.QueryContext(ctx, queryCnt, data[:len(data)-2]...) + } + if err != nil { + cerr = err + return + } + + defer rows.Close() + + if !rows.Next() { + cerr = errors.New("can not next count") + } + + if err := rows.Scan(&count); err != nil { + cerr = err + } + }() + + wg.Wait() + fmt.Println("res", result, count, "!", cerr, "?", qerr) + if cerr != nil { + return nil, 0, cerr + } + if qerr != nil { + return nil, 0, qerr + } + return result, count, nil +} + +// test + +// GetQuizByQid method for obtain quiz model by secured id +func (r *QuizRepository) GetQuizByQid(ctx context.Context, qid string) (model.Quiz, error) { + fmt.Println("QUID", ` +SELECT * FROM quiz +WHERE + deleted = false AND + archived = false AND + status = 'start' AND + qid = $1; +`) + rows, err := r.pool.QueryContext(ctx, ` +SELECT * FROM quiz +WHERE + deleted = false AND + archived = false AND + status = 'start' AND + qid = $1; +`, qid) + if err != nil { + return model.Quiz{}, err + } + defer rows.Close() + + if !rows.Next() { + return model.Quiz{}, rows.Err() + } + + var piece model.Quiz + pIds := pq.Int32Array{} + if err := rows.Scan( + &piece.Id, + &piece.Qid, + &piece.AccountId, + &piece.Deleted, + &piece.Archived, + &piece.Fingerprinting, + &piece.Repeatable, + &piece.NotePrevented, + &piece.MailNotifications, + &piece.UniqueAnswers, + &piece.Super, + &piece.GroupId, + &piece.Name, + &piece.Description, + &piece.Config, + &piece.Status, + &piece.Limit, + &piece.DueTo, + &piece.TimeOfPassing, + &piece.Pausable, + &piece.Version, + &piece.VersionComment, + &pIds, + &piece.CreatedAt, + &piece.UpdatedAt, + &piece.QuestionsCount, + &piece.PassedCount, + &piece.AverageTime, + &piece.SessionCount, + ); err != nil { + return model.Quiz{}, err + } + + piece.ParentIds = pIds + + return piece, nil +} + +// test + +func (r *QuizRepository) DeleteQuiz(ctx context.Context, accountId string, id uint64) (model.Quiz, error) { + row, err := r.queries.DeleteQuizByID(ctx, sqlcgen.DeleteQuizByIDParams{ + ID: int64(id), + Accountid: accountId, + }) + + if err != nil { + return model.Quiz{}, err + } + + piece := model.Quiz{ + Id: uint64(row.ID), + Qid: row.Qid.UUID.String(), + AccountId: row.Accountid, + Deleted: row.Deleted.Bool, + Archived: row.Archived.Bool, + Fingerprinting: row.Fingerprinting.Bool, + Repeatable: row.Repeatable.Bool, + NotePrevented: row.NotePrevented.Bool, + MailNotifications: row.MailNotifications.Bool, + UniqueAnswers: row.UniqueAnswers.Bool, + Super: row.Super.Bool, + GroupId: uint64(row.GroupID.Int64), + Name: row.Name.String, + Description: row.Description.String, + Config: row.Config.String, + Status: string(row.Status.([]byte)), + Limit: uint64(row.LimitAnswers.Int32), + DueTo: uint64(row.DueTo.Int32), + TimeOfPassing: uint64(row.TimeOfPassing.Int32), + Pausable: row.Pausable.Bool, + Version: int(row.Version.Int16), + VersionComment: row.VersionComment.String, + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + QuestionsCount: uint64(row.QuestionsCount.Int32), + PassedCount: uint64(row.AnswersCount.Int32), + AverageTime: uint64(row.AverageTimePassing.Int32), + SessionCount: uint64(row.SessionsCount.Int32), + } + + return piece, nil +} + +// test + +// MoveToHistoryQuiz insert deleted duplicate of quiz +func (r *QuizRepository) MoveToHistoryQuiz(ctx context.Context, id uint64, accountId string) (model.Quiz, error) { + row, err := r.queries.MoveToHistoryQuiz(ctx, sqlcgen.MoveToHistoryQuizParams{ + ID: int64(id), + Accountid: accountId, + }) + + if err != nil { + return model.Quiz{}, err + } + + result := model.Quiz{ + Id: uint64(row.ID), + Qid: row.Qid.UUID.String(), + ParentIds: row.ParentIds, + } + + return result, nil +} + +// test + +// UpdateQuiz set new data for quiz +func (r *QuizRepository) UpdateQuiz(ctx context.Context, accountId string, record model.Quiz) error { + query := `UPDATE quiz + SET %s + WHERE id=$1 AND accountid=$2;` + + var values []string + + if record.Name != "" { + values = append(values, fmt.Sprintf(` name='%s' `, record.Name)) + } + + if record.Description != "" { + values = append(values, fmt.Sprintf(` description='%s' `, record.Description)) + } + + if record.Status != "" { + values = append(values, fmt.Sprintf(` status='%s' `, record.Status)) + } + + values = append(values, fmt.Sprintf(`group_id=%d `, record.GroupId), fmt.Sprintf(` version=%d `, record.Version)) + + if record.Config != "" { + values = append(values, fmt.Sprintf(` config='%s' `, record.Config)) + } + + fmt.Println("UPQUI", fmt.Sprintf(query, strings.Join(values, ","))) + _, err := r.pool.ExecContext(ctx, fmt.Sprintf(query, strings.Join(values, ",")), record.Id, accountId) + + return err +} + +// test + +// CopyQuiz method for copy quiz with all of his questions +func (r *QuizRepository) CopyQuiz(ctx context.Context, accountId string, id uint64) (model.Quiz, error) { + row, err := r.queries.CopyQuiz(ctx, sqlcgen.CopyQuizParams{ + ID: int64(id), + Accountid: accountId, + }) + + if err != nil { + return model.Quiz{}, err + } + + result := model.Quiz{ + Id: uint64(row.ID), + Qid: row.Qid.UUID.String(), + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + + err = r.queries.CopyQuizQuestions(ctx, sqlcgen.CopyQuizQuestionsParams{ + QuizID: int64(id), + QuizID_2: row.ID, + }) + if err != nil { + return model.Quiz{}, err + } + + return result, nil +} + +type QuizHistoryDeps struct { + Id, Limit, Offset uint64 + AccountId string +} + +// test + +// QuizHistory method for obtain quiz history from db +func (r *QuizRepository) QuizHistory(ctx context.Context, deps QuizHistoryDeps) ([]model.Quiz, error) { + rows, err := r.queries.GetQuizHistory(ctx, sqlcgen.GetQuizHistoryParams{ + ID: int64(deps.Id), + Limit: int32(deps.Limit), + Offset: int32(deps.Offset), + Accountid: deps.AccountId, + }) + + if err != nil { + return nil, err + } + + var result []model.Quiz + for _, row := range rows { + piece := model.Quiz{ + Id: uint64(row.ID), + Qid: row.Qid.UUID.String(), + AccountId: row.Accountid, + Deleted: row.Deleted.Bool, + Archived: row.Archived.Bool, + Fingerprinting: row.Fingerprinting.Bool, + Repeatable: row.Repeatable.Bool, + NotePrevented: row.NotePrevented.Bool, + MailNotifications: row.MailNotifications.Bool, + UniqueAnswers: row.UniqueAnswers.Bool, + Super: row.Super.Bool, + GroupId: uint64(row.GroupID.Int64), + Name: row.Name.String, + Description: row.Description.String, + Config: row.Config.String, + Status: string(row.Status.([]byte)), + Limit: uint64(row.LimitAnswers.Int32), + DueTo: uint64(row.DueTo.Int32), + TimeOfPassing: uint64(row.TimeOfPassing.Int32), + Pausable: row.Pausable.Bool, + Version: int(row.Version.Int16), + VersionComment: row.VersionComment.String, + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + QuestionsCount: uint64(row.QuestionsCount.Int32), + PassedCount: uint64(row.AnswersCount.Int32), + AverageTime: uint64(row.AverageTimePassing.Int32), + SessionCount: uint64(row.SessionsCount.Int32), + } + + result = append(result, piece) + } + + return result, nil +} + +func (r *QuizRepository) ArchiveQuiz(ctx context.Context, accountId string, id uint64) error { + err := r.queries.ArchiveQuiz(ctx, sqlcgen.ArchiveQuizParams{ + ID: int64(id), + Accountid: accountId, + }) + + if err != nil { + return err + } + + return nil +} + +// test + +func (r *QuizRepository) GetQuizById(ctx context.Context, accountId string, id uint64) (*model.Quiz, error) { + row, err := r.queries.GetQuizById(ctx, sqlcgen.GetQuizByIdParams{ + ID: int64(id), + Accountid: accountId, + }) + + if err != nil { + return nil, err + } + + piece := model.Quiz{ + Id: uint64(row.ID), + Qid: row.Qid.UUID.String(), + AccountId: row.Accountid, + Deleted: row.Deleted.Bool, + Archived: row.Archived.Bool, + Fingerprinting: row.Fingerprinting.Bool, + Repeatable: row.Repeatable.Bool, + NotePrevented: row.NotePrevented.Bool, + MailNotifications: row.MailNotifications.Bool, + UniqueAnswers: row.UniqueAnswers.Bool, + Super: row.Super.Bool, + GroupId: uint64(row.GroupID.Int64), + Name: row.Name.String, + Description: row.Description.String, + Config: row.Config.String, + Status: string(row.Status.([]byte)), + Limit: uint64(row.LimitAnswers.Int32), + DueTo: uint64(row.DueTo.Int32), + TimeOfPassing: uint64(row.TimeOfPassing.Int32), + Pausable: row.Pausable.Bool, + Version: int(row.Version.Int16), + VersionComment: row.VersionComment.String, + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + QuestionsCount: uint64(row.QuestionsCount.Int32), + PassedCount: uint64(row.AnswersCount.Int32), + AverageTime: uint64(row.AverageTimePassing.Int32), + SessionCount: uint64(row.SessionsCount.Int32), + } + + return &piece, nil +} + +// test + +func (r *QuizRepository) GetQuizConfig(ctx context.Context, quizID uint64) (model.QuizConfig, string, error) { + row, err := r.queries.GetQuizConfig(ctx, int64(quizID)) + + if err != nil { + return model.QuizConfig{}, "", err + } + + var config model.QuizConfig + if err := json.Unmarshal([]byte(row.Config.String), &config); err != nil { + return model.QuizConfig{}, "", err + } + + return config, row.Accountid, nil +} diff --git a/repository/result/result.go b/repository/result/result.go new file mode 100644 index 0000000..358248d --- /dev/null +++ b/repository/result/result.go @@ -0,0 +1,259 @@ +package result + +import ( + "context" + "database/sql" + "fmt" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/model" + "strconv" + "strings" + "time" +) + +type Deps struct { + Queries *sqlcgen.Queries + Pool *sql.DB +} + +type ResultRepository struct { + queries *sqlcgen.Queries + pool *sql.DB +} + +func NewResultRepository(deps Deps) *ResultRepository { + return &ResultRepository{ + queries: deps.Queries, + pool: deps.Pool, + } +} + +type GetQuizResDeps struct { + To, From time.Time + New bool + Page uint64 + Limit uint64 +} + +// test + +func (r *ResultRepository) GetQuizResults(ctx context.Context, quizID uint64, reqExport GetQuizResDeps, payment bool) ([]model.AnswerExport, uint64, error) { + var results []model.AnswerExport + + queryBase := "FROM answer WHERE quiz_id = $1 AND result = TRUE AND deleted = FALSE" + queryParams := []interface{}{quizID} + + if !reqExport.From.IsZero() { + queryBase += " AND created_at >= $" + strconv.Itoa(len(queryParams)+1) + queryParams = append(queryParams, reqExport.From) + } + if !reqExport.To.IsZero() { + queryBase += " AND created_at <= $" + strconv.Itoa(len(queryParams)+1) + queryParams = append(queryParams, reqExport.To) + } + + if reqExport.New { + queryBase += " AND new = $" + strconv.Itoa(len(queryParams)+1) + queryParams = append(queryParams, reqExport.New) + } + + offset := reqExport.Page * reqExport.Limit + mainQuery := "SELECT content, id, new, created_at " + queryBase + " ORDER BY created_at DESC LIMIT $" + strconv.Itoa(len(queryParams)+1) + " OFFSET $" + strconv.Itoa(len(queryParams)+2) + queryParams = append(queryParams, reqExport.Limit, offset) + + rows, err := r.pool.QueryContext(ctx, mainQuery, queryParams...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + for rows.Next() { + var answer model.AnswerExport + if err := rows.Scan(&answer.Content, &answer.Id, &answer.New, &answer.CreatedAt); err != nil { + return nil, 0, err + } + + if !payment { + answer.Content = "***" + } + + results = append(results, answer) + } + + totalCountQuery := "SELECT COUNT(*) " + queryBase + var totalCount uint64 + if err := r.pool.QueryRowContext(ctx, totalCountQuery, queryParams[:len(queryParams)-2]...).Scan(&totalCount); err != nil { + return nil, 0, err + } + + return results, totalCount, nil +} + +// test + +func (r *ResultRepository) UpdateAnswersStatus(ctx context.Context, accountID string, answersIDs []uint64) error { + idsPlaceholder := make([]string, len(answersIDs)) + params := make([]interface{}, len(answersIDs)+1) + params[0] = accountID + + for i, id := range answersIDs { + params[i+1] = id + idsPlaceholder[i] = fmt.Sprintf("$%d", i+2) + } + + placeholders := strings.Join(idsPlaceholder, ", ") + sqlStatement := ` + UPDATE answer AS ans + SET new = false + FROM quiz + WHERE ans.quiz_id = quiz.id + AND quiz.accountid = $1 + AND ans.new = true + AND ans.id IN (` + placeholders + ")" + + _, err := r.pool.ExecContext(ctx, sqlStatement, params...) + return err +} + +// test + +func (r *ResultRepository) GetQuizResultsCSV(ctx context.Context, quizID uint64, reqExport GetQuizResDeps) ([]model.Answer, error) { + var results []model.Answer + + mainQuery := `SELECT DISTINCT ON (a.question_id, a.session) + a.id, a.content, a.question_id, a.quiz_id, a.fingerprint, a.session, a.result, a.created_at, a.new, a.deleted + FROM + answer a + WHERE + a.quiz_id = $1 AND a.deleted = FALSE` + + queryParams := []interface{}{quizID} + + if !reqExport.From.IsZero() || !reqExport.To.IsZero() { + mainQuery += ` AND a.created_at BETWEEN $2 AND $3` + queryParams = append(queryParams, reqExport.From, reqExport.To) + } + + if reqExport.New { + mainQuery += ` AND a.new = $` + strconv.Itoa(len(queryParams)+1) + queryParams = append(queryParams, reqExport.New) + } + + mainQuery += ` AND EXISTS ( + SELECT 1 FROM answer as a2 + WHERE a2.session = a.session AND a2.result = TRUE AND a2.quiz_id = a.quiz_id + )` + + mainQuery += ` ORDER BY a.question_id, a.session, a.created_at DESC, a.result DESC` + + rows, err := r.pool.QueryContext(ctx, mainQuery, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var answer model.Answer + if err := rows.Scan(&answer.Id, &answer.Content, &answer.QuestionId, &answer.QuizId, &answer.Fingerprint, &answer.Session, &answer.Result, &answer.CreatedAt, &answer.New, &answer.Deleted); err != nil { + return nil, err + } + results = append(results, answer) + } + + return results, nil +} + +// test + +func (r *ResultRepository) CheckResultsOwner(ctx context.Context, answersIDs []int64, accountID string) ([]uint64, error) { + answerIDs, err := r.queries.CheckResultsOwner(ctx, sqlcgen.CheckResultsOwnerParams{ + Column1: answersIDs, + Accountid: accountID, + }) + if err != nil { + return nil, err + } + + var answers []uint64 + for _, answerID := range answerIDs { + answers = append(answers, uint64(answerID)) + } + + return answers, nil +} + +func (r *ResultRepository) SoftDeleteResultByID(ctx context.Context, answerID uint64) error { + err := r.queries.SoftDeleteResultByID(ctx, int64(answerID)) + if err != nil { + return err + } + + return nil +} + +// test + +func (r *ResultRepository) GetQuestions(ctx context.Context, quizID uint64) ([]model.Question, error) { + rows, err := r.queries.GetQuestions(ctx, int64(quizID)) + if err != nil { + return nil, err + } + + var questions []model.Question + + for _, row := range rows { + question := model.Question{ + Id: uint64(row.ID), + QuizId: uint64(row.QuizID), + Title: row.Title, + Description: row.Description.String, + Type: string(row.Questiontype.([]byte)), + Required: row.Required.Bool, + Deleted: row.Deleted.Bool, + Page: int(row.Page.Int16), + Content: row.Content.String, + Version: int(row.Version.Int16), + ParentIds: row.ParentIds, + CreatedAt: row.CreatedAt.Time, + UpdatedAt: row.UpdatedAt.Time, + } + + questions = append(questions, question) + } + + return questions, nil +} + +// test + +func (r *ResultRepository) GetResultAnswers(ctx context.Context, answerID uint64) ([]model.Answer, error) { + rows, err := r.queries.GetResultAnswers(ctx, int64(answerID)) + if err != nil { + return nil, err + } + + var answers []model.Answer + + for _, row := range rows { + answer := model.Answer{ + Id: uint64(row.ID), + Content: row.Content.String, + QuestionId: uint64(row.QuestionID), + QuizId: uint64(row.QuizID), + Fingerprint: row.Fingerprint.String, + Session: row.Session.String, + Result: row.Result.Bool, + CreatedAt: row.CreatedAt.Time, + New: row.New.Bool, + Deleted: row.Deleted.Bool, + } + + answers = append(answers, answer) + } + + return answers, nil +} + +// test + +func (r *ResultRepository) CheckResultOwner(ctx context.Context, answerID uint64, accountID string) (bool, error) { + ownerAccountID, err := r.queries.CheckResultOwner(ctx, int64(answerID)) + if err != nil { + return false, err + } + + return ownerAccountID == accountID, nil +} diff --git a/repository/workers/worker.go b/repository/workers/worker.go new file mode 100644 index 0000000..13861f9 --- /dev/null +++ b/repository/workers/worker.go @@ -0,0 +1,39 @@ +package workers + +import ( + "context" + "penahub.gitlab.yandexcloud.net/backend/quiz/common/dal/sqlcgen" +) + +type Deps struct { + Queries *sqlcgen.Queries +} + +type WorkerRepository struct { + queries *sqlcgen.Queries +} + +func NewWorkerRepository(deps Deps) *WorkerRepository { + return &WorkerRepository{ + queries: deps.Queries, + } +} + +// test + +func (r *WorkerRepository) WorkerStatProcess(ctx context.Context) error { + err := r.queries.WorkerStatProcess(ctx) + if err != nil { + return err + } + + return nil +} + +// test + +func (r *WorkerRepository) WorkerTimeoutProcess(ctx context.Context) error { + err := r.queries.WorkerTimeoutProcess(ctx) + if err != nil { + return err + } + return nil +}