first adding

This commit is contained in:
Pavel 2024-02-19 19:33:15 +03:00
parent 7ed0a3c6fe
commit 8d3adf6274
25 changed files with 4511 additions and 0 deletions

21
.gitignore vendored Normal file

@ -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

116
dal/dal.go Normal file

@ -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
}

310
dal/db_query/queries.sql Normal file

@ -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;

@ -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$$;

@ -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;

@ -0,0 +1 @@
ALTER TABLE answer DROP COLUMN IF EXISTS result;

@ -0,0 +1 @@
ALTER TABLE answer ADD COLUMN result BOOLEAN DEFAULT FALSE;

@ -0,0 +1 @@
ALTER TABLE quiz DROP COLUMN IF EXISTS sessions_count;

@ -0,0 +1 @@
ALTER TABLE quiz ADD COLUMN sessions_count integer;

@ -0,0 +1,2 @@
ALTER TABLE quiz DROP COLUMN IF EXISTS new;
ALTER TABLE quiz DROP COLUMN IF EXISTS deleted;

@ -0,0 +1,2 @@
ALTER TABLE answer ADD COLUMN new BOOLEAN DEFAULT TRUE;
ALTER TABLE answer ADD COLUMN deleted BOOLEAN DEFAULT FALSE;

31
dal/sqlcgen/db.go Normal file

@ -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,
}
}

89
dal/sqlcgen/models.go Normal file

@ -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"`
}

1404
dal/sqlcgen/queries.sql.go Normal file

File diff suppressed because it is too large Load Diff

29
go.mod Normal file

@ -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
)

95
go.sum Normal file

@ -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=

@ -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)
}
}

271
model/model.go Normal file

@ -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"`
}

313
model/tariff/models.pb.go Normal file

@ -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
}

@ -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
}

@ -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
}

@ -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
}

579
repository/quiz/quiz.go Normal file

@ -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
}

259
repository/result/result.go Normal file

@ -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
}

@ -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
}